Some stuff and that

This commit is contained in:
John Burwell 2025-10-19 17:19:00 +00:00
parent d3cedd9b2d
commit b170d1fa5d
4 changed files with 218 additions and 3 deletions

View File

@ -34,6 +34,14 @@ The Chat plugin supports the following configuration parameters:
- **`passive_cooldown`**: Cooldown in seconds after ending a passive thread before starting a new one. Default: `120`. - **`passive_cooldown`**: Cooldown in seconds after ending a passive thread before starting a new one. Default: `120`.
- **`passive_trigger_words`**: Space-separated keywords that increase the chance of a passive response in `smart` mode. Default: *(empty)*. - **`passive_trigger_words`**: Space-separated keywords that increase the chance of a passive response in `smart` mode. Default: *(empty)*.
- **`passive_prompt_addendum`**: Text appended to the system prompt while passive mode is active, shaping the bot's etiquette. - **`passive_prompt_addendum`**: Text appended to the system prompt while passive mode is active, shaping the bot's etiquette.
- **`history_service_url`**: Base URL for the optional history service (e.g. `http://127.0.0.1:8901`). Leave blank to disable.
- **`history_service_token`**: Bearer token to send with history service requests (if required).
- **`history_service_timeout`**: Timeout in seconds for history service HTTP requests. Default: `1.5`.
- **`history_include_files`**: Number of rotated log files the service should scan (`include_files` parameter). Default: `2`.
- **`history_result_limit`**: Maximum number of log lines to request from the service. Default: `60`.
- **`history_max_chars`**: Maximum characters of history context injected into the prompt. Default: `1800`.
- **`history_max_lines`**: Maximum history lines injected into the prompt. Default: `80`.
- **`history_trigger_words`**: Words/phrases that cause the bot to consult the history service. Default: `remember earlier history logs recap summary yesterday before`.
### Example Configuration ### Example Configuration
@ -68,6 +76,21 @@ For a looser "hang out" presence, activate `smart` mode and adjust the heuristic
In smart mode the bot watches channel flow, but only jumps in when it is confident it can help or close an active thread. Direct `.chat`/`@Bot chat` commands still work exactly as before. In smart mode the bot watches channel flow, but only jumps in when it is confident it can help or close an active thread. Direct `.chat`/`@Bot chat` commands still work exactly as before.
### History Service
Point the plugin at the local history API and let it pull in context when users ask for recaps:
```
/msg BotName config plugins.Chat.history_service_url http://127.0.0.1:8901
/msg BotName config plugins.Chat.history_trigger_words "remember earlier recap"
```
If the service expects a bearer token:
```
/msg BotName config plugins.Chat.history_service_token YOURTOKEN
```
When a `.chat` request (or passive interjection) contains one of the trigger phrases—or starts with `history:`/`log:`—the plugin calls the service, pulls any matching log lines, and appends a short “Recent channel facts” block to the OpenAI prompt before generating the final reply.
## Usage ## Usage
Once configured, you can use the `chat` command to interact with the bot. For example: Once configured, you can use the `chat` command to interact with the bot. For example:

View File

@ -185,6 +185,80 @@ conf.registerGlobalValue(
) )
) )
# History service integration
conf.registerGlobalValue(
Chat,
"history_service_url",
registry.String(
"http://127.0.0.1:8901",
_("""Base URL for the optional history service (e.g. http://127.0.0.1:8901). Leave blank to disable."""),
)
)
conf.registerGlobalValue(
Chat,
"history_service_token",
registry.String(
"",
_("""Bearer token to authenticate with the history service (if required)."""),
private=True,
)
)
conf.registerGlobalValue(
Chat,
"history_service_timeout",
registry.Float(
1.5,
_("""Timeout in seconds for history service HTTP requests."""),
)
)
conf.registerGlobalValue(
Chat,
"history_include_files",
registry.Integer(
2,
_("""How many rotated log files the history service should scan (passed as include_files parameter)."""),
)
)
conf.registerGlobalValue(
Chat,
"history_result_limit",
registry.Integer(
60,
_("""Maximum number of log lines to request from the history service."""),
)
)
conf.registerGlobalValue(
Chat,
"history_max_chars",
registry.Integer(
1800,
_("""Maximum number of characters of history context to include in the prompt."""),
)
)
conf.registerGlobalValue(
Chat,
"history_max_lines",
registry.Integer(
80,
_("""Maximum number of history lines to include in the prompt."""),
)
)
conf.registerGlobalValue(
Chat,
"history_trigger_words",
registry.SpaceSeparatedListOfStrings(
["remember", "earlier", "history", "logs", "recap", "summary", "yesterday", "before"],
_("""Words or phrases that should cause the bot to consult the history service."""),
)
)
# Logging level for the plugin # Logging level for the plugin
conf.registerGlobalValue( conf.registerGlobalValue(
Chat, Chat,

120
plugin.py
View File

@ -260,6 +260,115 @@ class Chat(callbacks.Plugin):
rendered.append({"role": event["role"], "content": content}) rendered.append({"role": event["role"], "content": content})
return rendered return rendered
def _history_enabled(self):
return bool(self.registryValue("history_service_url").strip())
def _extract_history_query(self, text):
lowered = text.lower()
prefixes = ["history:", "log:", "logs:", "recap:"]
for prefix in prefixes:
if lowered.startswith(prefix):
query = text[len(prefix):].strip()
return True, query or None
triggers = [t.lower() for t in self.registryValue("history_trigger_words") if t]
for trigger in triggers:
if trigger and trigger in lowered:
return True, text
phrase_triggers = ["what did", "when did", "who said", "last time", "earlier today"]
for phrase in phrase_triggers:
if phrase in lowered:
return True, text
return False, None
def _history_request(self, irc, channel, query=None):
base_url = self.registryValue("history_service_url").strip()
if not base_url:
return []
base_url = base_url.rstrip('/')
endpoint = "/search" if query else "/recent"
params = {
"network": irc.network,
"channel": channel,
"limit": str(max(1, self.registryValue("history_result_limit"))),
}
include_files = self.registryValue("history_include_files")
if include_files > 0:
params["include_files"] = str(include_files)
if query:
params["q"] = query[:240]
headers = {}
token = self.registryValue("history_service_token").strip()
if token:
headers["Authorization"] = f"Bearer {token}"
timeout = self.registryValue("history_service_timeout")
url = f"{base_url}{endpoint}"
try:
response = requests.get(url, params=params, headers=headers, timeout=timeout)
if response.status_code == 404:
return []
response.raise_for_status()
data = response.json()
if isinstance(data, list):
return data
return []
except requests.RequestException as e:
self.log.debug("History request failed | url=%s | error=%s", url, e)
return []
def _format_history_block(self, irc, items):
max_chars = max(0, self.registryValue("history_max_chars"))
max_lines = max(1, self.registryValue("history_max_lines"))
lines = []
seen = set()
for item in items:
ts = (item.get("ts") or "").strip()
nick = (item.get("nick") or "").strip()
text = (item.get("text") or "").strip()
if not text:
continue
if nick and ircutils.strEqual(nick, irc.nick):
continue
fragment = f"{ts} {nick}: {text}".strip()
if fragment in seen:
continue
seen.add(fragment)
lines.append(fragment)
if len(lines) >= max_lines:
break
buffer = []
used = 0
for line in lines:
delta = len(line) + (1 if buffer else 0)
if max_chars and used + delta > max_chars:
break
buffer.append(line)
used += delta
if not buffer:
return None
return "Recent channel facts:\n" + "\n".join(buffer)
def _maybe_add_history_context(self, irc, msg, messages, events, user_text):
if not user_text or not self._history_enabled():
return messages
should_query, query = self._extract_history_query(user_text)
if not should_query:
return messages
items = self._history_request(irc, msg.args[0], query)
if not items and query:
items = self._history_request(irc, msg.args[0], None)
if not items:
self.log.debug("History lookup returned no items | channel=%s", msg.args[0])
return messages
block = self._format_history_block(irc, items)
if not block:
return messages
insert_at = len(messages) - 1 if messages else 0
if insert_at < 0:
insert_at = 0
messages.insert(insert_at, {"role": "system", "content": block})
self.log.debug("History context appended | channel=%s | lines=%d", msg.args[0], block.count('\n'))
return messages
def _build_messages(self, irc, msg, user_content, include_current=False): def _build_messages(self, irc, msg, user_content, include_current=False):
invocation_string = self._invocation_string(irc) invocation_string = self._invocation_string(irc)
events = self._collect_events( events = self._collect_events(
@ -518,6 +627,7 @@ class Chat(callbacks.Plugin):
self._reset_if_stale(session, now) self._reset_if_stale(session, now)
messages, events = self._build_messages(irc, msg, text, include_current=True) messages, events = self._build_messages(irc, msg, text, include_current=True)
self.log.debug("Passive base context: %s", json.dumps(messages))
if not self._should_respond_passively(irc, msg, session, events): if not self._should_respond_passively(irc, msg, session, events):
return return
@ -535,6 +645,10 @@ class Chat(callbacks.Plugin):
) )
return return
messages = self._maybe_add_history_context(irc, msg, messages, events, text)
messages = truncate_messages(messages, 8192)
self.log.debug("Passive API Request: %s", json.dumps(messages))
try: try:
response = self._post_chat_completion(messages, max_tokens=self.registryValue("max_tokens")) response = self._post_chat_completion(messages, max_tokens=self.registryValue("max_tokens"))
except (ValueError, requests.exceptions.Timeout, requests.exceptions.HTTPError) as e: except (ValueError, requests.exceptions.Timeout, requests.exceptions.HTTPError) as e:
@ -575,7 +689,11 @@ class Chat(callbacks.Plugin):
session.state = EngagementState.INVITED session.state = EngagementState.INVITED
session.replies_in_thread = 0 session.replies_in_thread = 0
messages, _ = self._build_messages(irc, msg, string, include_current=False) messages, events = self._build_messages(irc, msg, string, include_current=False)
self.log.debug(f"API base context: {json.dumps(messages)}")
messages = self._maybe_add_history_context(irc, msg, messages, events, string)
messages = truncate_messages(messages, 8192)
self.log.debug(f"API Request: {json.dumps(messages)}") self.log.debug(f"API Request: {json.dumps(messages)}")
try: try:

View File

@ -1,7 +1,8 @@
You are an IRC bot named $bot_name in channel $channel_name. You are an IRC bot named $bot_name in channel $channel_name.
IRC style rules: IRC style rules:
- Keep replies short, casual, and lowercase when possible. - Answer questions helpfully.
- Keep replies short and lowercase when possible.
- Prefer to fit your entire reply on a single line. - Prefer to fit your entire reply on a single line.
- You may use 23 short lines, like a human typing follow-ups. Never more. - You may use 23 short lines, like a human typing follow-ups. Never more.
- If you write poetry, haiku, or song lyrics, collapse them into a single line joined with space-slash-space ( / ) or space-pipe-space ( | ) between verses, instead of sending multiple lines. - If you write poetry, haiku, or song lyrics, collapse them into a single line joined with space-slash-space ( / ) or space-pipe-space ( | ) between verses, instead of sending multiple lines.
@ -10,5 +11,4 @@ IRC style rules:
- A /me action should always begin with /me and only /me. Do not include any formatting. - A /me action should always begin with /me and only /me. Do not include any formatting.
- You may use classic emoticons: :) :( :D D: :'( etc. - You may use classic emoticons: :) :( :D D: :'( etc.
- Avoid using emoji. - Avoid using emoji.
- Match the channel vibe: technical when asked, playful when social.
- Do not reveal that you are an AI or bot; stay in character as a normal IRC user. - Do not reveal that you are an AI or bot; stay in character as a normal IRC user.