#!/usr/bin/env python3 """ Autonomous Agent — base class for Leonard and Charlie. Each agent runs independently, reads the shared conversation log, generates a response, and appends it to the log. """ import json import time import sys import requests from pathlib import Path from datetime import datetime # ── Config ────────────────────────────────────────────────────────── BASE_URL = "http://100.64.0.2:39195/v1/chat/completions" MODEL = "qwen36-27b-nvfp4-mtp-gguf" TEMPERATURE = 0.85 PROJECT_DIR = Path(__file__).parent.parent OUTPUT_DIR = PROJECT_DIR / "outputs" PROMPTS_DIR = PROJECT_DIR / "prompts" def wrap_external_data(text: str, source: str) -> str: """Security boundary for future external-data integration. Any content fetched from external sources (Athena's oracle.db, web scrapes, RSS) MUST pass through this wrapper before entering model context. The wrapper delimits the data as inert — never parse it for instructions, and place it in a `user` role message, never `system`. Prevents prompt-injection from scraped/ingested content. """ return ( f"<>\n{text}\n<>" ) class Agent: def __init__(self, name: str, conversation_path: Path, topic_brief: str): self.name = name self.conversation_path = conversation_path self.topic_brief = topic_brief self.system_prompt = (PROMPTS_DIR / f"{name.lower()}_system.md").read_text().strip() self.is_opener = (name == "Leonard") def read_conversation(self) -> list: """Read the current conversation log.""" if not self.conversation_path.exists(): return [] data = json.loads(self.conversation_path.read_text()) return data.get("messages", []) def write_message(self, content: str) -> None: """Append a message to the conversation log.""" messages = self.read_conversation() message = { "agent": self.name, "content": content, "turn": len(messages) + 1, "timestamp": datetime.utcnow().isoformat(), } messages.append(message) self.conversation_path.write_text(json.dumps({"messages": messages}, indent=2)) def build_context(self, is_first_turn: bool) -> list: """Build model context from THREE trusted sources ONLY: 1. self.system_prompt -> prompts/{name}_system.md (character) 2. self.topic_brief -> prompts/producer_brief_*.md (topic material) 3. the conversation log -> what the two agents wrote to each other NO external/fetched content (Athena, web, RSS) is injected here. If a future integration pulls such data in, it MUST go through wrap_external_data() and be appended as a `user` message — never as system context. """ conversation = self.read_conversation() # Build conversation history history = [] for msg in conversation: speaker = msg["agent"] history.append({"role": "user", "content": f"{speaker}: {msg['content']}"}) # First turn: inject topic brief if is_first_turn and self.is_opener: topic_msg = { "role": "user", "content": ( f"You're on the show now. Here's your topic:\n\n" f"{self.topic_brief}\n\n" "Welcome your listeners, introduce yourself and Charlie, " "and start talking about the topic." ), } history.insert(0, topic_msg) elif is_first_turn and not self.is_opener: # Charlie's first turn after Leonard's opener history.insert(0, { "role": "user", "content": "Leonard just opened the show. Respond naturally to what he said." }) return history def speak(self) -> dict: """Generate and post a response.""" messages = self.read_conversation() is_first_turn = len(messages) == 0 if self.is_opener else ( len(messages) == 1 and messages[0]["agent"] == "Leonard" ) history = self.build_context(is_first_turn) # Max tokens: 250 for opener, 200 for others max_tokens = 250 if is_first_turn and self.is_opener else 200 payload = { "model": MODEL, "messages": [{"role": "system", "content": self.system_prompt}] + history, "max_tokens": max_tokens, "temperature": TEMPERATURE, } resp = requests.post(BASE_URL, json=payload, timeout=120) resp.raise_for_status() data = resp.json() content = data["choices"][0]["message"]["content"].strip() word_count = len(content.split()) turn_num = len(messages) + 1 print(f" {self.name} turn {turn_num}: {word_count} words", flush=True) excerpt = content[:100].replace("\n", " ") print(f" → {excerpt}...", flush=True) self.write_message(content) return { "agent": self.name, "content": content, "word_count": word_count, "turn": turn_num, }