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_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:
|
||||||
|
|||||||
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
|
# Logging level for the plugin
|
||||||
conf.registerGlobalValue(
|
conf.registerGlobalValue(
|
||||||
Chat,
|
Chat,
|
||||||
|
|||||||
120
plugin.py
120
plugin.py
@ -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:
|
||||||
|
|||||||
@ -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 2–3 short lines, like a human typing follow-ups. Never more.
|
- 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.
|
- 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.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user