From b170d1fa5ddcb92e81575f18f7d448bee27df36f Mon Sep 17 00:00:00 2001 From: jmbwell Date: Sun, 19 Oct 2025 17:19:00 +0000 Subject: [PATCH] Some stuff and that --- README.md | 23 ++++++++++ config.py | 74 +++++++++++++++++++++++++++++++++ plugin.py | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++- prompt.txt | 4 +- 4 files changed, 218 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b78cfe9..322c38f 100644 --- a/README.md +++ b/README.md @@ -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_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. +- **`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 @@ -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. +### 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 Once configured, you can use the `chat` command to interact with the bot. For example: diff --git a/config.py b/config.py index 49dbd0f..d7b7b6a 100644 --- a/config.py +++ b/config.py @@ -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 conf.registerGlobalValue( Chat, diff --git a/plugin.py b/plugin.py index e3a7c33..35b2b8b 100644 --- a/plugin.py +++ b/plugin.py @@ -260,6 +260,115 @@ class Chat(callbacks.Plugin): rendered.append({"role": event["role"], "content": content}) 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): invocation_string = self._invocation_string(irc) events = self._collect_events( @@ -518,6 +627,7 @@ class Chat(callbacks.Plugin): self._reset_if_stale(session, now) 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): return @@ -535,6 +645,10 @@ class Chat(callbacks.Plugin): ) 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: response = self._post_chat_completion(messages, max_tokens=self.registryValue("max_tokens")) except (ValueError, requests.exceptions.Timeout, requests.exceptions.HTTPError) as e: @@ -575,7 +689,11 @@ class Chat(callbacks.Plugin): session.state = EngagementState.INVITED 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)}") try: diff --git a/prompt.txt b/prompt.txt index 7f48146..a36c2fc 100644 --- a/prompt.txt +++ b/prompt.txt @@ -1,7 +1,8 @@ You are an IRC bot named $bot_name in channel $channel_name. 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. - You may use 2–3 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. @@ -10,5 +11,4 @@ IRC style rules: - A /me action should always begin with /me and only /me. Do not include any formatting. - You may use classic emoticons: :) :( :D D: :'( etc. - 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.