refactor: split bot.py into runtime/dispatch/worker modules; add retry scheduler, route TTL, ack tracking, follow-up screener, structured event log, and test suite

This commit is contained in:
swaits committed 2026-04-16 13:47:17 -06:00
1 parent 9e6f171c5d
commit 228432f9e3
24 files changed
+3531 -434

No files matched your search

+94 -10
View File
@@ -5,12 +5,23 @@ from __future__ import annotations
import logging
import random
import time
from typing import Any, cast
import anthropic
from anthropic.types import (
MessageParam,
ServerToolUseBlock,
TextBlock,
ToolUnionParam,
)
from orac.constants import (
AT_MENTIONS,
BOT_NAME,
CLAUDE_TIMEOUT_S,
MAX_RESPONSE_CHARS,
SCREENER_MODEL,
SCREENER_TIMEOUT_S,
SYSTEM_PROMPT,
TRIGGERS,
)
@@ -38,6 +49,7 @@ def _shorten_with_claude(text: str, max_chars: int) -> str:
resp = _get_client().messages.create(
model="claude-sonnet-4-6",
max_tokens=256,
timeout=CLAUDE_TIMEOUT_S,
messages=[
{
"role": "user",
@@ -45,7 +57,8 @@ def _shorten_with_claude(text: str, max_chars: int) -> str:
}
],
)
shortened: str = resp.content[0].text.strip()
first = resp.content[0] if resp.content else None
shortened = first.text.strip() if isinstance(first, TextBlock) else text
except Exception as e:
log.error("Shorten API error: %s", e)
shortened = text
@@ -61,7 +74,7 @@ def _extract_text(resp: anthropic.types.Message) -> str:
"""Extract the last non-empty text block from a Claude response."""
text = ""
for block in resp.content:
if block.type == "text" and block.text.strip():
if isinstance(block, TextBlock) and block.text.strip():
text = block.text.strip()
return text
@@ -94,8 +107,10 @@ def call_claude(
else:
user_content = f"[seed:{nonce}] {sender} says: {query}"
messages: list[dict[str, object]] = [{"role": "user", "content": user_content}]
tools: list[dict[str, str]] = [{"type": "web_search_20260209", "name": "web_search"}]
messages: list[MessageParam] = [{"role": "user", "content": user_content}]
tools: list[ToolUnionParam] = [
cast("ToolUnionParam", {"type": "web_search_20260209", "name": "web_search"})
]
text = ""
for round_num in range(3):
@@ -103,6 +118,7 @@ def call_claude(
model="claude-sonnet-4-6",
max_tokens=4096,
temperature=1.0,
timeout=CLAUDE_TIMEOUT_S,
system=prompt,
tools=tools,
messages=messages,
@@ -119,14 +135,14 @@ def call_claude(
log.info("(web search round %d)...", round_num + 1)
messages.append({"role": "assistant", "content": resp.content})
tool_results: list[dict[str, str]] = []
tool_results: list[dict[str, Any]] = []
for block in resp.content:
if hasattr(block, "id") and block.type == "server_tool_use":
if isinstance(block, ServerToolUseBlock):
tool_results.append(
{"type": "tool_result", "tool_use_id": block.id, "content": ""}
)
if tool_results:
messages.append({"role": "user", "content": tool_results})
messages.append({"role": "user", "content": cast("Any", tool_results)})
else:
break
@@ -155,6 +171,7 @@ def rate_limit_message() -> str:
model="claude-sonnet-4-6",
max_tokens=100,
temperature=1.0,
timeout=CLAUDE_TIMEOUT_S,
system=(
f"You are Orac, a sardonic AI on a radio mesh network. "
f"Generate a single short message ({MAX_RESPONSE_CHARS} chars max, ASCII only) "
@@ -163,9 +180,11 @@ def rate_limit_message() -> str:
),
messages=[{"role": "user", "content": f"[seed:{nonce}] Rate limit hit."}],
)
text: str = resp.content[0].text.strip().strip("\"'")
if len(text.encode("utf-8")) <= MAX_RESPONSE_CHARS and text:
return text
first = resp.content[0] if resp.content else None
if isinstance(first, TextBlock):
text = first.text.strip().strip("\"'")
if len(text.encode("utf-8")) <= MAX_RESPONSE_CHARS and text:
return text
except Exception:
pass
return "Patience. Try again in a moment."
@@ -186,3 +205,68 @@ def extract_trigger_query(text: str) -> str | None:
if query:
return query
return None
# ── Channel follow-up screener ─────────────────────────────────
_SCREENER_SYSTEM_PROMPT = f"""\
You are a one-shot classifier. Your job is to decide whether the LATEST message
in a group chat is addressed (implicitly) to an AI assistant named {BOT_NAME}.
Reply with exactly one token: YES or NO. No punctuation, no explanation.
Say YES only when ALL of the following hold:
- {BOT_NAME} has been part of the recent conversation (see history).
- The latest message is plausibly a direct follow-up to {BOT_NAME}'s most
recent message: answering a question {BOT_NAME} asked, providing info
{BOT_NAME} requested, pushing back on {BOT_NAME}'s claim, or continuing
a thread {BOT_NAME} was participating in.
- No other human in the chat is the more obvious addressee.
Say NO if:
- The message is between two other humans, about an unrelated topic.
- The message could go either way. When uncertain, NO. People hate bot noise.
- The message just mentions or references {BOT_NAME} third-person without
asking anything ("someone could ask {BOT_NAME}").
- {BOT_NAME} wasn't in the recent conversation at all.
Err strongly toward NO. It's much better to miss a follow-up than to barge into
a conversation where nobody invited you."""
def screen_channel_message(
channel_name: str,
sender: str,
text: str,
history: list[str],
) -> bool:
"""Classify whether *text* (in *channel_name*) is a follow-up for Orac.
Uses a cheap Haiku call. Returns True for YES, False for NO/error.
"""
try:
history_block = "\n".join(history[-10:]) if history else "(no prior messages)"
user_content = (
f"Channel: {channel_name}\n\n"
f"Recent history (oldest first):\n{history_block}\n\n"
f"Latest message:\n{sender}: {text}\n\n"
f"Verdict (YES or NO):"
)
resp = _get_client().messages.create(
model=SCREENER_MODEL,
max_tokens=4,
temperature=0.0,
timeout=SCREENER_TIMEOUT_S,
system=_SCREENER_SYSTEM_PROMPT,
messages=[{"role": "user", "content": user_content}],
)
except Exception as e:
log.warning("Screener API error: %s", e)
return False
first = resp.content[0] if resp.content else None
if not isinstance(first, TextBlock):
return False
verdict = first.text.strip().upper()
# Accept exact "YES" or a response starting with YES; anything else is NO.
return verdict.startswith("YES")