From a75c14e5368d4578a4a66db4bd677d91a73a60a1 Mon Sep 17 00:00:00 2001 From: m3taversal Date: Sun, 22 Mar 2026 16:57:47 +0000 Subject: [PATCH] =?UTF-8?q?epimetheus:=20auto-learning=20trigger=20?= =?UTF-8?q?=E2=80=94=20bot=20self-writes=20learnings=20from=20corrections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- telegram/bot.py | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 62ec68a..b51c537 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -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: