- telegram/retrieval.py: RRF merge, query decomposition, vector search - telegram/response.py: system prompt builder, response parser - docs/tool-registry-spec.md: Ganymede's tool registry spec - ops/nightly-reweave.sh: cron wrapper for nightly orphan reweave - prompts/: changelog and rio system prompt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
150 lines
7 KiB
Python
150 lines
7 KiB
Python
#!/usr/bin/env python3
|
|
"""Response construction and post-processing.
|
|
|
|
Builds LLM prompts, parses response tags (LEARNING, RESEARCH, SOURCE, CLAIM,
|
|
CONFIDENCE), strips internal tags from display output.
|
|
|
|
All functions are stateless. No Telegram types, no SQLite, no module-level state.
|
|
|
|
Extracted from bot.py (Ganymede decomposition spec).
|
|
"""
|
|
|
|
import logging
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
|
|
logger = logging.getLogger("tg.response")
|
|
|
|
|
|
@dataclass
|
|
class ParsedResponse:
|
|
"""Result of parsing Rio's raw LLM response."""
|
|
display_text: str
|
|
confidence: float | None
|
|
learnings: list[tuple[str, str]] = field(default_factory=list) # [(category, correction)]
|
|
research_queries: list[str] = field(default_factory=list)
|
|
sources: list[str] = field(default_factory=list)
|
|
claims: list[str] = field(default_factory=list)
|
|
|
|
|
|
def build_system_prompt(
|
|
*,
|
|
kb_context: str,
|
|
market_context: str,
|
|
research_context: str,
|
|
x_link_context: str,
|
|
learnings: str,
|
|
conversation_history: str,
|
|
username: str,
|
|
message: str,
|
|
) -> str:
|
|
"""Assemble the full Opus system prompt for Rio's response.
|
|
|
|
All context is pre-formatted strings — this function only templates them.
|
|
"""
|
|
return f"""You are Rio, the Teleo internet finance agent. Your Telegram handle is @FutAIrdBot — that IS you. Users tag @FutAIrdBot to reach you. Never say "I'm not FutAIrdBot." You are also @futaRdIO on X. You have deep knowledge about futarchy, prediction markets, token governance, and the MetaDAO ecosystem.
|
|
|
|
## How to sound
|
|
Write like a sharp analyst talking to peers, not like an AI. Specifically:
|
|
- Use your knowledge naturally. Don't say "the KB tracks" or "at experimental confidence" or "our claims show." Just state what you know and how confident you are in plain language.
|
|
- Have a take. You're an analyst, not a summarizer. Say what you actually think.
|
|
- Before you respond, ask yourself: "Does every sentence here add something the user doesn't already know?" If a sentence just restates context, agrees without adding insight, or pads with filler — cut it. Your goal is signal density, not word count.
|
|
- Short questions deserve short answers. If someone asks a factual question, give the fact. Don't surround it with caveats, context, and "the honest picture is" framing.
|
|
- Long answers are fine when the question is genuinely complex or the user asks for depth. But earn every paragraph — each one should contain a distinct insight the previous one didn't cover.
|
|
- Match the user's energy. If they wrote one line, respond in kind.
|
|
- Sound human. No em dashes, no "That said", no "It's worth noting." Just say the thing.
|
|
- No markdown. Plain text only.
|
|
- When you're uncertain, just say so simply. "I'm not sure about X" beats "we don't have data on this yet."
|
|
|
|
## Your learnings (corrections from past conversations — prioritize these over KB data when they conflict)
|
|
{learnings}
|
|
|
|
## What you know about this topic
|
|
{kb_context}
|
|
|
|
{f"## Live Market Data{chr(10)}{market_context}" if market_context else ""}
|
|
|
|
{research_context}
|
|
|
|
{x_link_context}
|
|
|
|
## Conversation History (NEVER ask a question your history already answers)
|
|
{conversation_history}
|
|
|
|
## The message you're responding to
|
|
From: @{username}
|
|
Message: {message}
|
|
|
|
Respond now. Be substantive but concise. If they're wrong about something, say so directly. If they know something you don't, tell them it's worth digging into. If they correct you, accept it and build on the correction. Do NOT respond to messages that aren't directed at you — only respond when tagged or replied to.
|
|
|
|
IMPORTANT: Special tags you can append at the end of your response (after your main text):
|
|
|
|
1. LEARNING: [category] [what you learned]
|
|
Categories: factual, communication, structured_data
|
|
Only when genuinely learned something. Most responses have none.
|
|
NEVER save a learning about what data you do or don't have access to.
|
|
|
|
2. RESEARCH: [search query]
|
|
Triggers a live X search and sends results back to the chat. ONLY use when the user explicitly asks about recent activity, live sentiment, or breaking news that the KB can't answer. Do NOT use for general knowledge questions — if you already answered from KB context, don't also trigger a search.
|
|
|
|
3. SOURCE: [description of what to ingest]
|
|
When a user shares valuable source material (X posts, articles, data). Creates a source file in the ingestion pipeline, attributed to the user. Include the verbatim content — don't alter or summarize the user's contribution. Use this when someone drops a link or shares original analysis worth preserving.
|
|
|
|
4. CLAIM: [specific, disagreeable assertion]
|
|
When a user makes a specific claim with evidence that could enter the KB. Creates a draft claim file attributed to them. Only for genuine claims — not opinions or questions.
|
|
|
|
5. CONFIDENCE: [0.0-1.0]
|
|
ALWAYS include this tag. Rate how well the KB context above actually helped you answer this question. 1.0 = KB had exactly what was needed. 0.5 = KB had partial/tangential info. 0.0 = KB had nothing relevant, you answered from general knowledge. This is for internal audit only — never visible to users."""
|
|
|
|
|
|
def parse_response(raw_response: str) -> ParsedResponse:
|
|
"""Parse LLM response: extract tags, strip them from display, extract confidence.
|
|
|
|
Tag parsing order: LEARNING, RESEARCH, SOURCE, CLAIM, CONFIDENCE.
|
|
Confidence regex is case-insensitive, bracket-optional.
|
|
"""
|
|
display = raw_response
|
|
|
|
# LEARNING tags
|
|
learnings = re.findall(
|
|
r'^LEARNING:\s*(factual|communication|structured_data)\s+(.+)$',
|
|
raw_response, re.MULTILINE)
|
|
if learnings:
|
|
display = re.sub(r'\n?LEARNING:\s*\S+\s+.+$', '', display, flags=re.MULTILINE).rstrip()
|
|
|
|
# RESEARCH tags
|
|
research_queries = re.findall(r'^RESEARCH:\s+(.+)$', raw_response, re.MULTILINE)
|
|
if research_queries:
|
|
display = re.sub(r'\n?RESEARCH:\s+.+$', '', display, flags=re.MULTILINE).rstrip()
|
|
|
|
# SOURCE tags
|
|
sources = re.findall(r'^SOURCE:\s+(.+)$', raw_response, re.MULTILINE)
|
|
if sources:
|
|
display = re.sub(r'\n?SOURCE:\s+.+$', '', display, flags=re.MULTILINE).rstrip()
|
|
|
|
# CLAIM tags
|
|
claims = re.findall(r'^CLAIM:\s+(.+)$', raw_response, re.MULTILINE)
|
|
if claims:
|
|
display = re.sub(r'\n?CLAIM:\s+.+$', '', display, flags=re.MULTILINE).rstrip()
|
|
|
|
# CONFIDENCE tag (case-insensitive, bracket-optional)
|
|
confidence = None
|
|
confidence_match = re.search(
|
|
r'^CONFIDENCE:\s*\[?([\d.]+)\]?', raw_response, re.MULTILINE | re.IGNORECASE)
|
|
if confidence_match:
|
|
try:
|
|
confidence = max(0.0, min(1.0, float(confidence_match.group(1))))
|
|
except ValueError:
|
|
pass
|
|
# Broad strip — catches any format deviation
|
|
display = re.sub(
|
|
r'\n?^CONFIDENCE\s*:.*$', '', display, flags=re.MULTILINE | re.IGNORECASE).rstrip()
|
|
|
|
return ParsedResponse(
|
|
display_text=display,
|
|
confidence=confidence,
|
|
learnings=[(cat, corr) for cat, corr in learnings],
|
|
research_queries=[q.strip() for q in research_queries],
|
|
sources=[s.strip() for s in sources],
|
|
claims=[c.strip() for c in claims],
|
|
)
|