epimetheus: auto-learning trigger — bot self-writes learnings from corrections

Opus decides what to learn. Prompt instructs: append LEARNING: [category] [description]
at end of response when genuinely learning something new. Bot parses the line,
strips it from displayed response, calls _save_learning() to persist.

Zero additional API calls (Rhea's design). The model already has full context.
Categories: factual, communication, structured_data.
Most responses have no LEARNING line — only fires on genuine corrections.

Pentagon-Agent: Epimetheus <3D35839A-7722-4740-B93D-51157F7D5E70>
This commit is contained in:
m3taversal 2026-03-22 16:57:47 +00:00
parent a11eca90e3
commit a75c14e536

View file

@ -52,7 +52,8 @@ BOT_TOKEN_FILE = "/opt/teleo-eval/secrets/telegram-bot-token"
OPENROUTER_KEY_FILE = "/opt/teleo-eval/secrets/openrouter-key"
PIPELINE_DB = "/opt/teleo-eval/pipeline/pipeline.db"
KB_READ_DIR = "/opt/teleo-eval/workspaces/main" # For KB retrieval (clean main branch)
ARCHIVE_DIR = "/opt/teleo-eval/workspaces/main" # For archiving sources (push_main_with_retry)
ARCHIVE_DIR = "/opt/teleo-eval/telegram-archives" # Write outside worktree to avoid read-only errors
MAIN_WORKTREE = "/opt/teleo-eval/workspaces/main" # For git operations only
LEARNINGS_FILE = "/opt/teleo-eval/workspaces/main/agents/rio/learnings.md" # Agent memory (Option D)
LOG_FILE = "/opt/teleo-eval/logs/telegram-bot.log"
@ -173,7 +174,7 @@ def _git_commit_archive(archive_path, filename: str):
"""Commit archived source to git so it survives git clean. (Rio review: data loss bug)"""
import subprocess
try:
cwd = ARCHIVE_DIR
cwd = MAIN_WORKTREE
subprocess.run(["git", "add", str(archive_path)], cwd=cwd, timeout=10,
capture_output=True, check=False)
result = subprocess.run(
@ -247,7 +248,7 @@ def _save_learning(correction: str, category: str = "factual"):
# Commit + push
import subprocess
cwd = ARCHIVE_DIR
cwd = MAIN_WORKTREE
subprocess.run(["git", "add", LEARNINGS_FILE], cwd=cwd, timeout=10,
capture_output=True, check=False)
subprocess.run(
@ -335,7 +336,7 @@ async def handle_research(msg, query: str, user):
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
slug = re.sub(r"[^a-z0-9]+", "-", query[:60].lower()).strip("-")
filename = f"{date_str}-x-research-{slug}.md"
source_path = Path(ARCHIVE_DIR) / "inbox" / "queue" / filename
source_path = Path(ARCHIVE_DIR) / filename
source_path.parent.mkdir(parents=True, exist_ok=True)
# Build consolidated source file
@ -548,7 +549,10 @@ Message: {text}
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.
If the user corrects a factual error or teaches you something new, note it internally you can save important corrections to your learnings file for future conversations."""
IMPORTANT: If you learn something from this exchange a correction, a new rule, new data append a LEARNING line at the very end of your response. Format:
LEARNING: [category] [what you learned]
Categories: factual, communication, structured_data
Only when you genuinely learned something. Most responses have NO learning line."""
# Call Opus
response = await call_openrouter(RESPONSE_MODEL, prompt, max_tokens=1024)
@ -557,8 +561,20 @@ If the user corrects a factual error or teaches you something new, note it inter
await msg.reply_text("Processing error — I'll get back to you.")
return
# Post response
await msg.reply_text(response)
# Parse LEARNING lines before posting (Rhea: zero-cost self-write trigger)
display_response = response
learning_lines = re.findall(r'^LEARNING:\s*(factual|communication|structured_data)\s+(.+)$',
response, re.MULTILINE)
if learning_lines:
# Strip LEARNING lines from displayed response
display_response = re.sub(r'\nLEARNING:\s*\S+\s+.+$', '', response, flags=re.MULTILINE).rstrip()
# Save each learning
for category, correction in learning_lines:
_save_learning(correction.strip(), category.strip())
logger.info("Auto-learned [%s]: %s", category, correction[:80])
# Post response (without LEARNING lines)
await msg.reply_text(display_response)
# Update conversation state: reset window, store history (Ganymede+Rhea)
if user:
@ -623,7 +639,7 @@ def _archive_exchange(user_text: str, rio_response: str, user, msg,
slug = re.sub(r"[^a-z0-9]+", "-", user_text[:50].lower()).strip("-")
filename = f"{date_str}-telegram-{username}-{slug}.md"
archive_path = Path(ARCHIVE_DIR) / "inbox" / "queue" / filename
archive_path = Path(ARCHIVE_DIR) / filename
archive_path.parent.mkdir(parents=True, exist_ok=True)
# Extract rationale (the user's text minus the @mention and URL)
@ -670,15 +686,11 @@ tags: [telegram, ownership-community]
**Intake tier:** {intake_tier} {'fast-tracked, contributor provided reasoning' if intake_tier == 'directed' else 'standard processing'}
**Triage:** Conversation may contain [CLAIM], [ENTITY], or [EVIDENCE] for extraction.
"""
# Write file unlocked — additive, no coordination needed (Ganymede: decouple)
# Write to telegram-archives/ (outside worktree — no read-only errors)
# A cron moves files into inbox/queue/ and commits them
archive_path.write_text(content)
logger.info("Archived exchange to %s (tier: %s, urls: %d)",
filename, intake_tier, len(urls or []))
# Commit with lock — deferred on timeout, file persists on disk
try:
_git_commit_archive(archive_path, filename)
except Exception:
logger.warning("Commit deferred for %s — file on disk, next cycle picks it up", filename)
except Exception as e:
logger.error("Failed to archive exchange: %s", e)
@ -789,7 +801,7 @@ def _archive_window(window: list[dict], tag: str):
slug = re.sub(r"[^a-z0-9]+", "-", window[0]["text"][:40].lower()).strip("-")
filename = f"{date_str}-telegram-{first_user}-{slug}.md"
archive_path = Path(ARCHIVE_DIR) / "inbox" / "queue" / filename
archive_path = Path(ARCHIVE_DIR) / filename
archive_path.parent.mkdir(parents=True, exist_ok=True)
# Build conversation content
@ -822,14 +834,10 @@ tags: [telegram, ownership-community]
**Triage:** [{tag}] classified by batch triage
**Participants:** {', '.join(f'@{u}' for u in contributors)}
"""
# Write file unlocked (Ganymede: decouple write from lock)
# Write to telegram-archives/ (outside worktree)
archive_path.write_text(content)
logger.info("Archived window [%s]: %s (%d msgs, %d participants)",
tag, filename, len(window), len(contributors))
try:
_git_commit_archive(archive_path, filename)
except Exception:
logger.warning("Commit deferred for %s — file on disk", filename)
except TimeoutError:
logger.warning("Failed to archive window: worktree lock timeout")
except Exception as e: