Some stuff and that
This commit is contained in:
parent
d3cedd9b2d
commit
b170d1fa5d
23
README.md
23
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:
|
||||
|
||||
74
config.py
74
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,
|
||||
|
||||
120
plugin.py
120
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:
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user