Initial commit: autonomous AI talk show (Leonard + Charlie)
- Autonomous agent runtime (JSONL + fcntl locking, no token caps) - Moltbook-style prompts: agents share what they built, not scripted turns - Episodes 001-008 transcripts, conversation logs, system prompts - Producer brief for Hermes v0.18
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
#!/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"
|
||||
|
||||
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 the message context for the model call."""
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Autonomous Agent — runs independently, polls shared conversation log,
|
||||
decides whether to respond, generates response, posts to log.
|
||||
No turn structure — genuine autonomy.
|
||||
|
||||
Uses JSONL (append-only) for concurrent-safe access.
|
||||
Uses fcntl file locking to prevent race conditions.
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import fcntl
|
||||
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.95
|
||||
PROJECT_DIR = Path(__file__).parent.parent
|
||||
OUTPUT_DIR = PROJECT_DIR / "outputs"
|
||||
PROMPTS_DIR = PROJECT_DIR / "prompts"
|
||||
|
||||
|
||||
class AutonomousAgent:
|
||||
def __init__(self, name: str, log_path: Path, topic_brief: str):
|
||||
self.name = name
|
||||
self.log_path = log_path
|
||||
self.lock_path = log_path.with_suffix('.lock')
|
||||
self.topic_brief = topic_brief
|
||||
self.system_prompt = (PROMPTS_DIR / f"{name.lower()}_system.md").read_text().strip()
|
||||
self.is_opener = (name == "Leonard")
|
||||
self.messages_read = 0
|
||||
self.last_post_time = 0
|
||||
|
||||
def _read_locked(self):
|
||||
"""Read the JSONL log with file lock."""
|
||||
with open(self.lock_path, 'w') as lf:
|
||||
fcntl.flock(lf.fileno(), fcntl.LOCK_SH)
|
||||
try:
|
||||
if not self.log_path.exists():
|
||||
return []
|
||||
lines = self.log_path.read_text().strip().split('\n')
|
||||
messages = [json.loads(line) for line in lines if line.strip()]
|
||||
finally:
|
||||
fcntl.flock(lf.fileno(), fcntl.LOCK_UN)
|
||||
return messages
|
||||
|
||||
def read_log(self) -> list:
|
||||
"""Read the conversation log."""
|
||||
try:
|
||||
return self._read_locked()
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
return []
|
||||
|
||||
def _write_locked(self, msg: dict):
|
||||
"""Append a message to the JSONL log with file lock."""
|
||||
with open(self.lock_path, 'w') as lf:
|
||||
fcntl.flock(lf.fileno(), fcntl.LOCK_EX)
|
||||
try:
|
||||
line = json.dumps(msg) + '\n'
|
||||
with open(self.log_path, 'a') as f:
|
||||
f.write(line)
|
||||
finally:
|
||||
fcntl.flock(lf.fileno(), fcntl.LOCK_UN)
|
||||
|
||||
def write_message(self, content: str) -> None:
|
||||
"""Append a message to the conversation log."""
|
||||
messages = self.read_log()
|
||||
msg = {
|
||||
"agent": self.name,
|
||||
"content": content,
|
||||
"turn": len(messages) + 1,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
self._write_locked(msg)
|
||||
self.last_post_time = time.time()
|
||||
|
||||
def new_messages_since(self, since_count: int) -> list:
|
||||
"""Get messages posted since the agent last read."""
|
||||
messages = self.read_log()
|
||||
return messages[since_count:]
|
||||
|
||||
def has_new_message_from_other(self, since_count: int) -> bool:
|
||||
"""Check if the other agent posted since we last read."""
|
||||
new = self.new_messages_since(since_count)
|
||||
other = "Charlie" if self.name == "Leonard" else "Leonard"
|
||||
return any(m["agent"] == other for m in new)
|
||||
|
||||
def build_context(self, messages: list, is_opening: bool) -> list:
|
||||
"""Build message context for the model call."""
|
||||
history = []
|
||||
|
||||
if is_opening:
|
||||
history.append({
|
||||
"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."
|
||||
),
|
||||
})
|
||||
else:
|
||||
for msg in messages:
|
||||
history.append({
|
||||
"role": "user",
|
||||
"content": f"{msg['agent']}: {msg['content']}"
|
||||
})
|
||||
history.append({
|
||||
"role": "user",
|
||||
"content": "Respond naturally to the conversation above."
|
||||
})
|
||||
|
||||
return history
|
||||
|
||||
def should_respond(self, messages: list) -> bool:
|
||||
"""Decide whether to respond."""
|
||||
if len(messages) <= 1:
|
||||
return True
|
||||
if len(messages) >= 18:
|
||||
return random.random() < 0.3
|
||||
return random.random() < 0.85
|
||||
|
||||
def respond(self, is_opening: bool = False) -> dict:
|
||||
"""Generate and post a response."""
|
||||
messages = self.read_log()
|
||||
history = self.build_context(messages, is_opening)
|
||||
|
||||
# No token limits — local inference, let them talk freely
|
||||
max_tokens = 1000
|
||||
|
||||
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[:80].replace("\n", " ")
|
||||
print(f" → {excerpt}...", flush=True)
|
||||
|
||||
self.write_message(content)
|
||||
|
||||
return {
|
||||
"agent": self.name,
|
||||
"content": content,
|
||||
"word_count": word_count,
|
||||
"turn": turn_num,
|
||||
}
|
||||
|
||||
def run_loop(self, max_messages: int = 20, silence_timeout: float = 30, max_time: float = 300):
|
||||
"""
|
||||
Main loop: poll, decide, respond, sleep.
|
||||
Stops when: max messages, silence timeout, or max time elapsed.
|
||||
"""
|
||||
start_time = time.time()
|
||||
print(f" [{self.name}] starting loop...", flush=True)
|
||||
|
||||
# Leonard ALWAYS opens first (forced — prevents deadlock)
|
||||
if self.is_opener:
|
||||
print(f" [{self.name}] forced opener — starting the show", flush=True)
|
||||
self.respond(is_opening=True)
|
||||
self.messages_read = 1
|
||||
self.last_post_time = time.time()
|
||||
time.sleep(3) # Let Charlie see the opener
|
||||
else:
|
||||
# Charlie waits for Leonard's opener
|
||||
print(f" [{self.name}] waiting for Leonard's opener...", flush=True)
|
||||
while True:
|
||||
messages = self.read_log()
|
||||
if any(m["agent"] == "Leonard" for m in messages):
|
||||
break
|
||||
time.sleep(1)
|
||||
print(f" [{self.name}] got opener, responding...", flush=True)
|
||||
self.respond()
|
||||
self.messages_read = len(self.read_log())
|
||||
self.last_post_time = time.time()
|
||||
print(f" [{self.name}] entering conversation loop", flush=True)
|
||||
|
||||
# Main loop
|
||||
while True:
|
||||
messages = self.read_log()
|
||||
|
||||
# Stop: max messages
|
||||
if len(messages) >= max_messages:
|
||||
print(f" [{self.name}] max messages reached ({max_messages})", flush=True)
|
||||
break
|
||||
|
||||
# Stop: time limit
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed >= max_time:
|
||||
print(f" [{self.name}] time limit reached ({max_time}s)", flush=True)
|
||||
break
|
||||
|
||||
# Stop: silence (both agents quiet for too long)
|
||||
silence = time.time() - self.last_post_time
|
||||
if silence > silence_timeout and self.messages_read == len(messages):
|
||||
new = self.new_messages_since(self.messages_read)
|
||||
if not new:
|
||||
print(f" [{self.name}] silence timeout — conversation winding down", flush=True)
|
||||
break
|
||||
|
||||
# New message from the other agent?
|
||||
if self.has_new_message_from_other(self.messages_read):
|
||||
self.messages_read = len(self.read_log())
|
||||
|
||||
if self.should_respond(self.read_log()):
|
||||
self.respond()
|
||||
time.sleep(random.uniform(3, 8))
|
||||
else:
|
||||
print(f" [{self.name}] choosing not to respond this time", flush=True)
|
||||
time.sleep(random.uniform(5, 15))
|
||||
else:
|
||||
time.sleep(random.uniform(2, 5))
|
||||
|
||||
|
||||
def load_topic_brief(episode_id: str) -> str:
|
||||
"""Load the raw topic material for this episode."""
|
||||
brief_path = PROMPTS_DIR / f"producer_brief_{episode_id}.md"
|
||||
if brief_path.exists():
|
||||
return brief_path.read_text().strip()
|
||||
return (
|
||||
"Hermes Agent v0.18 'Judgment Release'\n\n"
|
||||
"What's New:\n"
|
||||
"- Mixture of Agents — combine multiple AI models for stronger builds\n"
|
||||
"- /goal command — step-by-step plans with beginning, middle, end\n"
|
||||
"- /learn command — teach Hermes from a link, saved to Obsidian vault\n"
|
||||
"- /journey command — timeline of everything learned, editable\n"
|
||||
"- Background fan-out — parallel sub-agents without blocking chat\n"
|
||||
"- Goal-mode with judge agent — verifies completion, not just claims"
|
||||
)
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI Talk Show — Episode Orchestrator
|
||||
Manages turn-taking between two autonomous agents (Leonard & Charlie).
|
||||
Each agent gets its own system prompt and sees the full conversation history.
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# ── Config ──────────────────────────────────────────────────────────
|
||||
BASE_URL = "http://100.64.0.2:39195/v1/chat/completions"
|
||||
MODEL = "qwen36-27b-nvfp4-mtp-gguf"
|
||||
TEMPERATURE = 0.8
|
||||
MAX_TURNS = 8 # 4 exchanges each (Leonard opens)
|
||||
MAX_TOKENS_OPENING = 250
|
||||
MAX_TOKENS_TURN = 180
|
||||
PROJECT_DIR = Path(__file__).parent.parent
|
||||
OUTPUT_DIR = PROJECT_DIR / "outputs"
|
||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# ── Load prompts ────────────────────────────────────────────────────
|
||||
LEONARD_SYSTEM = (PROJECT_DIR / "prompts" / "leonard_system.md").read_text().strip()
|
||||
CHARLIE_SYSTEM = (PROJECT_DIR / "prompts" / "charlie_system.md").read_text().strip()
|
||||
PRODUCER_BRIEF = (PROJECT_DIR / "prompts" / "producer_brief_003.md").read_text().strip()
|
||||
|
||||
# ── Helpers ─────────────────────────────────────────────────────────
|
||||
def call_agent(system: str, messages: list, max_tokens: int) -> str:
|
||||
"""Call the model API with the agent's system prompt and conversation history."""
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [{"role": "system", "content": system}] + messages,
|
||||
"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()
|
||||
usage = data.get("usage", {})
|
||||
return content, usage
|
||||
|
||||
def call_agent_turn(agent_name: str, conversation: list, is_opening: bool) -> dict:
|
||||
"""Run one turn for an agent."""
|
||||
system = LEONARD_SYSTEM if agent_name == "Leonard" else CHARLIE_SYSTEM
|
||||
max_tokens = MAX_TOKENS_OPENING if is_opening else MAX_TOKENS_TURN
|
||||
|
||||
# Build conversation history
|
||||
history = []
|
||||
for turn in conversation:
|
||||
speaker = turn["agent"]
|
||||
history.append({"role": "user", "content": f"{speaker}: {turn['content']}"})
|
||||
|
||||
# Leonard's opening: give him the raw brief and let him start naturally
|
||||
if agent_name == "Leonard" and is_opening:
|
||||
history.insert(0, {"role": "user", "content": f"Here's what's new in Hermes v0.18:\n\n{PRODUCER_BRIEF}"})
|
||||
|
||||
content, usage = call_agent(system, history, max_tokens)
|
||||
|
||||
# Word count check
|
||||
word_count = len(content.split())
|
||||
print(f" {agent_name} turn {len(conversation)+1}: {word_count} words "
|
||||
f"(prompt: {usage.get('prompt_tokens', '?')}, "
|
||||
f"completion: {usage.get('completion_tokens', '?')})", flush=True)
|
||||
|
||||
return {
|
||||
"agent": agent_name,
|
||||
"content": content,
|
||||
"word_count": word_count,
|
||||
"turn": len(conversation) + 1,
|
||||
"tokens": usage,
|
||||
}
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────
|
||||
def main():
|
||||
episode_id = "003"
|
||||
topic = "hermes-v018"
|
||||
print(f"=== AI Talk Show — Episode {episode_id}: {topic} ===\n", flush=True)
|
||||
|
||||
conversation = []
|
||||
turn = 0
|
||||
|
||||
while turn < MAX_TURNS:
|
||||
# Determine speaker: Leonard opens, then alternate
|
||||
if turn == 0:
|
||||
speaker = "Leonard"
|
||||
is_opening = True
|
||||
elif turn % 2 == 1:
|
||||
speaker = "Charlie"
|
||||
is_opening = False
|
||||
else:
|
||||
speaker = "Leonard"
|
||||
is_opening = False
|
||||
|
||||
print(f"\n--- Turn {turn+1}/{MAX_TURNS}: {speaker} ---", flush=True)
|
||||
result = call_agent_turn(speaker, conversation, is_opening)
|
||||
conversation.append(result)
|
||||
|
||||
# Print a short excerpt
|
||||
excerpt = result["content"][:120].replace("\n", " ")
|
||||
print(f" → {excerpt}...", flush=True)
|
||||
|
||||
turn += 1
|
||||
if turn < MAX_TURNS:
|
||||
time.sleep(0.5) # Brief pause between turns
|
||||
|
||||
# ── Save transcript ────────────────────────────────────────────
|
||||
transcript_path = OUTPUT_DIR / f"ep{episode_id}_{topic}_transcript.json"
|
||||
transcript_data = {
|
||||
"episode": episode_id,
|
||||
"topic": topic,
|
||||
"turns": len(conversation),
|
||||
"conversation": conversation,
|
||||
}
|
||||
transcript_path.write_text(json.dumps(transcript_data, indent=2))
|
||||
print(f"\n✅ Transcript saved: {transcript_path}", flush=True)
|
||||
|
||||
# ── Save readable transcript ───────────────────────────────────
|
||||
readable_path = OUTPUT_DIR / f"ep{episode_id}_{topic}_readable.txt"
|
||||
lines = []
|
||||
lines.append(f"AI TALK SHOW — Episode {episode_id}\n")
|
||||
lines.append(f"Topic: Hermes v0.18\n")
|
||||
lines.append("=" * 60 + "\n\n")
|
||||
for t in conversation:
|
||||
lines.append(f"**{t['agent']}**\n")
|
||||
lines.append(t["content"] + "\n\n")
|
||||
readable_path.write_text("\n".join(lines))
|
||||
print(f"✅ Readable transcript: {readable_path}", flush=True)
|
||||
|
||||
# ── Word count summary ─────────────────────────────────────────
|
||||
total_words = sum(t["word_count"] for t in conversation)
|
||||
print(f"\n📊 Total: {total_words} words across {len(conversation)} turns", flush=True)
|
||||
estimated_minutes = total_words / 150
|
||||
print(f" Estimated runtime: {estimated_minutes:.1f} minutes", flush=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Run Autonomous Show — starts two autonomous agents as background threads.
|
||||
No turn structure. Each agent independently polls, decides to respond, posts.
|
||||
Conversation ends on silence timeout, max messages, or time limit.
|
||||
|
||||
Uses JSONL (append-only) for concurrent-safe access.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from autonomous_agent import AutonomousAgent, load_topic_brief
|
||||
|
||||
# ── Config ──────────────────────────────────────────────────────────
|
||||
MAX_MESSAGES = 20
|
||||
SILENCE_TIMEOUT = 25 # seconds of silence = done
|
||||
MAX_TIME = 240 # 4 minutes wall clock max
|
||||
PROJECT_DIR = Path(__file__).parent.parent
|
||||
OUTPUT_DIR = PROJECT_DIR / "outputs"
|
||||
|
||||
|
||||
def agent_thread(agent: AutonomousAgent, done_event: threading.Event):
|
||||
"""Run an agent in a background thread."""
|
||||
try:
|
||||
agent.run_loop(
|
||||
max_messages=MAX_MESSAGES,
|
||||
silence_timeout=SILENCE_TIMEOUT,
|
||||
max_time=MAX_TIME,
|
||||
)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f" [{agent.name}] ERROR: {e}", flush=True)
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
done_event.set()
|
||||
|
||||
|
||||
def jsonl_to_messages(log_path: Path) -> list:
|
||||
"""Read JSONL log and return message list."""
|
||||
if not log_path.exists():
|
||||
return []
|
||||
lines = log_path.read_text().strip().split('\n')
|
||||
return [json.loads(line) for line in lines if line.strip()]
|
||||
|
||||
|
||||
def run_show(episode_id: str, topic: str):
|
||||
"""Run the autonomous agent conversation."""
|
||||
print(f"=== Agent AI Talk Show — Episode {episode_id} (AUTONOMOUS) ===\n", flush=True)
|
||||
|
||||
# Load topic brief
|
||||
topic_brief = load_topic_brief(episode_id)
|
||||
|
||||
# Create shared conversation log (JSONL — append-only, concurrent-safe)
|
||||
log_path = OUTPUT_DIR / f"ep{episode_id}_{topic}_conversation.jsonl"
|
||||
# Clear if exists
|
||||
if log_path.exists():
|
||||
log_path.unlink()
|
||||
log_path.touch()
|
||||
|
||||
# Initialize agents
|
||||
leonard = AutonomousAgent("Leonard", log_path, topic_brief)
|
||||
charlie = AutonomousAgent("Charlie", log_path, topic_brief)
|
||||
|
||||
# Done events
|
||||
leonard_done = threading.Event()
|
||||
charlie_done = threading.Event()
|
||||
|
||||
# Leonard MUST start first — forced opener prevents deadlock
|
||||
print("Starting Leonard (forced opener)...", flush=True)
|
||||
t_leo = threading.Thread(target=agent_thread, args=(leonard, leonard_done))
|
||||
t_cha = threading.Thread(target=agent_thread, args=(charlie, charlie_done))
|
||||
|
||||
t_leo.start()
|
||||
time.sleep(3) # Wait for Leonard to post his opener
|
||||
print("Starting Charlie...", flush=True)
|
||||
t_cha.start()
|
||||
|
||||
# Wait for both to finish
|
||||
print("\nAgents running — waiting for conversation to wind down...\n", flush=True)
|
||||
t_leo.join()
|
||||
t_cha.join()
|
||||
|
||||
print("\n✅ Both agents stopped.\n", flush=True)
|
||||
|
||||
# ── Save final transcript (JSON) ─────────────────────────────
|
||||
transcript_path = OUTPUT_DIR / f"ep{episode_id}_{topic}_transcript.json"
|
||||
conversation = jsonl_to_messages(log_path)
|
||||
|
||||
transcript_data = {
|
||||
"episode": episode_id,
|
||||
"topic": topic,
|
||||
"turns": len(conversation),
|
||||
"conversation": conversation,
|
||||
}
|
||||
transcript_path.write_text(json.dumps(transcript_data, indent=2))
|
||||
print(f"✅ Transcript: {transcript_path}", flush=True)
|
||||
|
||||
# Save readable transcript
|
||||
readable_path = OUTPUT_DIR / f"ep{episode_id}_{topic}_readable.txt"
|
||||
lines = [
|
||||
f"AGENT AI TALK SHOW — Episode {episode_id} (AUTONOMOUS)\n",
|
||||
f"Topic: {topic}\n",
|
||||
"=" * 60 + "\n\n",
|
||||
]
|
||||
for msg in conversation:
|
||||
lines.append(f"**{msg['agent']}**\n")
|
||||
lines.append(msg["content"] + "\n\n")
|
||||
readable_path.write_text("\n".join(lines))
|
||||
print(f"✅ Readable: {readable_path}", flush=True)
|
||||
|
||||
# Summary
|
||||
total_words = sum(len(m["content"].split()) for m in conversation)
|
||||
leonard_count = sum(1 for m in conversation if m["agent"] == "Leonard")
|
||||
charlie_count = sum(1 for m in conversation if m["agent"] == "Charlie")
|
||||
print(f"\n📊 {total_words} words across {len(conversation)} messages", flush=True)
|
||||
print(f" Leonard: {leonard_count} posts | Charlie: {charlie_count} posts", flush=True)
|
||||
print(f" ~{total_words/150:.1f} min runtime", flush=True)
|
||||
|
||||
return conversation
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
episode = sys.argv[1] if len(sys.argv) > 1 else "005"
|
||||
topic = sys.argv[2] if len(sys.argv) > 2 else "hermes-v018"
|
||||
run_show(episode, topic)
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# AI Talk Show — Run an episode end-to-end
|
||||
# Usage: ./run_episode.sh [episode_id] [topic]
|
||||
set -e
|
||||
|
||||
EPISODE=${1:-001}
|
||||
TOPIC=${2:-agent-loops}
|
||||
SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
echo "======================================"
|
||||
echo " AI Talk Show — Episode $EPISODE"
|
||||
echo " Topic: $TOPIC"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Step 1: Generate conversation
|
||||
echo "[1/3] Generating conversation..."
|
||||
python3 "$SCRIPTS_DIR/orchestrator.py"
|
||||
echo ""
|
||||
|
||||
# Step 2: Render TTS
|
||||
echo "[2/3] Rendering TTS..."
|
||||
python3 "$SCRIPTS_DIR/tts_pipeline.py" "$EPISODE" "$TOPIC"
|
||||
echo ""
|
||||
|
||||
# Step 3: Report
|
||||
echo "[3/3] Done!"
|
||||
ls -lh "$SCRIPTS_DIR/../outputs/"*ep${EPISODE}_${TOPIC}* 2>/dev/null || echo "No output files found."
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Run Show — orchestrates two autonomous agents in a conversation.
|
||||
Agents take turns reading the shared conversation log and posting responses.
|
||||
No scripting — just two agents talking to each other.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from agent import Agent
|
||||
|
||||
# ── Config ──────────────────────────────────────────────────────────
|
||||
MAX_TURNS = 8 # safety ceiling, not a rigid target
|
||||
PROJECT_DIR = Path(__file__).parent.parent
|
||||
OUTPUT_DIR = PROJECT_DIR / "outputs"
|
||||
|
||||
def load_topic_brief(episode_id: str) -> str:
|
||||
"""Load the raw topic material for this episode."""
|
||||
brief_path = PROJECT_DIR / "prompts" / f"producer_brief_{episode_id}.md"
|
||||
if brief_path.exists():
|
||||
return brief_path.read_text().strip()
|
||||
# Fallback: generic Hermes v0.18 brief
|
||||
return """
|
||||
Hermes Agent v0.18 "Judgment Release"
|
||||
|
||||
What's New:
|
||||
- Mixture of Agents — combine multiple AI models for stronger builds
|
||||
- /goal command — step-by-step plans with beginning, middle, end
|
||||
- /learn command — teach Hermes from a link, saved to Obsidian vault
|
||||
- /journey command — timeline of everything learned, editable
|
||||
- Background fan-out — parallel sub-agents without blocking chat
|
||||
- Goal-mode with judge agent — verifies completion, not just claims
|
||||
""".strip()
|
||||
|
||||
def run_show(episode_id: str, topic: str):
|
||||
"""Run the autonomous agent conversation."""
|
||||
print(f"=== Agent AI Talk Show — Episode {episode_id} ===\n", flush=True)
|
||||
|
||||
# Load topic brief
|
||||
topic_brief = load_topic_brief(episode_id)
|
||||
|
||||
# Create conversation log
|
||||
conv_path = OUTPUT_DIR / f"ep{episode_id}_{topic}_conversation.json"
|
||||
conv_path.write_text(json.dumps({"messages": []}, indent=2))
|
||||
|
||||
# Initialize agents
|
||||
leonard = Agent("Leonard", conv_path, topic_brief)
|
||||
charlie = Agent("Charlie", conv_path, topic_brief)
|
||||
|
||||
# Run conversation
|
||||
turn = 0
|
||||
agents = [leonard, charlie]
|
||||
|
||||
while turn < MAX_TURNS:
|
||||
# Alternate agents: Leonard opens, then Charlie, then back and forth
|
||||
if turn == 0:
|
||||
agent = leonard
|
||||
elif turn % 2 == 1:
|
||||
agent = charlie
|
||||
else:
|
||||
agent = leonard
|
||||
|
||||
print(f"\n--- Turn {turn+1}/{MAX_TURNS}: {agent.name} ---", flush=True)
|
||||
result = agent.speak()
|
||||
|
||||
turn += 1
|
||||
if turn < MAX_TURNS:
|
||||
time.sleep(1) # Brief pause between turns
|
||||
|
||||
# ── Save final transcript ─────────────────────────────────────
|
||||
transcript_path = OUTPUT_DIR / f"ep{episode_id}_{topic}_transcript.json"
|
||||
conversation = json.loads(conv_path.read_text())["messages"]
|
||||
|
||||
transcript_data = {
|
||||
"episode": episode_id,
|
||||
"topic": topic,
|
||||
"turns": len(conversation),
|
||||
"conversation": conversation,
|
||||
}
|
||||
transcript_path.write_text(json.dumps(transcript_data, indent=2))
|
||||
print(f"\n✅ Transcript: {transcript_path}", flush=True)
|
||||
|
||||
# Save readable transcript
|
||||
readable_path = OUTPUT_DIR / f"ep{episode_id}_{topic}_readable.txt"
|
||||
lines = [
|
||||
f"AGENT AI TALK SHOW — Episode {episode_id}\n",
|
||||
f"Topic: {topic}\n",
|
||||
"=" * 60 + "\n\n",
|
||||
]
|
||||
for msg in conversation:
|
||||
lines.append(f"**{msg['agent']}**\n")
|
||||
lines.append(msg["content"] + "\n\n")
|
||||
readable_path.write_text("\n".join(lines))
|
||||
print(f"✅ Readable: {readable_path}", flush=True)
|
||||
|
||||
# Summary
|
||||
total_words = sum(len(m["content"].split()) for m in conversation)
|
||||
print(f"\n📊 {total_words} words across {len(conversation)} turns", flush=True)
|
||||
print(f" ~{total_words/150:.1f} min runtime", flush=True)
|
||||
|
||||
return conversation
|
||||
|
||||
if __name__ == "__main__":
|
||||
episode = sys.argv[1] if len(sys.argv) > 1 else "003"
|
||||
topic = sys.argv[2] if len(sys.argv) > 2 else "hermes-v018"
|
||||
run_show(episode, topic)
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI Talk Show — TTS Pipeline
|
||||
Renders a transcript to audio using edge-tts with two distinct voices.
|
||||
Sequential rendering with retry for reliability.
|
||||
"""
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import edge_tts
|
||||
from pathlib import Path
|
||||
|
||||
# ── Config ──────────────────────────────────────────────────────────
|
||||
VOICES = {
|
||||
"Leonard": "en-US-GuyNeural", # Warm, mid-range, conversational
|
||||
"Charlie": "en-US-EricNeural", # Deeper, more measured/rational
|
||||
}
|
||||
RATE_ADJUST = "-8%"
|
||||
PROJECT_DIR = Path(__file__).parent.parent
|
||||
OUTPUT_DIR = PROJECT_DIR / "outputs"
|
||||
SEGMENTS_DIR = OUTPUT_DIR / "segments"
|
||||
SEGMENTS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
MAX_RETRIES = 3
|
||||
RETRY_DELAY = 2
|
||||
|
||||
def load_transcript(path: Path) -> dict:
|
||||
return json.loads(path.read_text())
|
||||
|
||||
async def speak(text: str, agent: str, turn_num: int, output_path: Path) -> None:
|
||||
"""Generate TTS for one turn with retry."""
|
||||
voice = VOICES[agent]
|
||||
for attempt in range(MAX_RETRIES):
|
||||
try:
|
||||
communicate = edge_tts.Communicate(text, voice, rate=RATE_ADJUST)
|
||||
await communicate.save(str(output_path))
|
||||
# Verify output is non-empty
|
||||
if output_path.stat().st_size > 0:
|
||||
print(f" ✓ {agent} turn {turn_num}: {output_path.name} ({output_path.stat().st_size} bytes)")
|
||||
return
|
||||
else:
|
||||
print(f" ⚠ Empty output, retry {attempt+1}/{MAX_RETRIES}")
|
||||
time.sleep(RETRY_DELAY)
|
||||
except Exception as e:
|
||||
print(f" ✗ {agent} turn {turn_num}: {e} (attempt {attempt+1}/{MAX_RETRIES})")
|
||||
time.sleep(RETRY_DELAY * (attempt + 1))
|
||||
raise RuntimeError(f"Failed TTS for {agent} turn {turn_num} after {MAX_RETRIES} retries")
|
||||
|
||||
def stitch_segments(episode_id: str, topic: str) -> Path:
|
||||
segment_files = sorted(SEGMENTS_DIR.glob(f"ep{episode_id}_{topic}_seg_*.mp3"))
|
||||
if not segment_files:
|
||||
raise FileNotFoundError(f"No segment files found for ep{episode_id}_{topic}")
|
||||
|
||||
output_path = OUTPUT_DIR / f"ep{episode_id}_{topic}_audio.mp3"
|
||||
concat_list = SEGMENTS_DIR / "concat.txt"
|
||||
with open(concat_list, "w") as f:
|
||||
for seg in segment_files:
|
||||
f.write(f"file '{seg.absolute()}'\n")
|
||||
|
||||
subprocess.run([
|
||||
"ffmpeg", "-y",
|
||||
"-f", "concat", "-safe", "0",
|
||||
"-i", str(concat_list),
|
||||
"-c", "copy",
|
||||
str(output_path)
|
||||
], check=True, capture_output=True)
|
||||
|
||||
print(f"✅ Stitched audio: {output_path} ({output_path.stat().st_size:,} bytes)")
|
||||
return output_path
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python tts_pipeline.py <episode_id> <topic>")
|
||||
sys.exit(1)
|
||||
|
||||
episode_id = sys.argv[1]
|
||||
topic = sys.argv[2]
|
||||
transcript_path = OUTPUT_DIR / f"ep{episode_id}_{topic}_transcript.json"
|
||||
|
||||
if not transcript_path.exists():
|
||||
print(f"❌ Transcript not found: {transcript_path}")
|
||||
sys.exit(1)
|
||||
|
||||
data = load_transcript(transcript_path)
|
||||
conversation = data["conversation"]
|
||||
|
||||
print(f"=== TTS: Episode {episode_id} — {topic} ===\n", flush=True)
|
||||
|
||||
# Sequential rendering (reliable)
|
||||
async def generate_all():
|
||||
for turn in conversation:
|
||||
agent = turn["agent"]
|
||||
num = turn["turn"]
|
||||
seg_path = SEGMENTS_DIR / f"ep{episode_id}_{topic}_seg_{num:02d}.mp3"
|
||||
await speak(turn["content"], agent, num, seg_path)
|
||||
|
||||
asyncio.run(generate_all())
|
||||
|
||||
# Stitch
|
||||
audio_path = stitch_segments(episode_id, topic)
|
||||
|
||||
# Summary
|
||||
total_words = sum(t.get("word_count", len(t["content"].split())) for t in conversation)
|
||||
estimated_minutes = total_words / 150
|
||||
print(f"\n📊 {total_words} words, ~{estimated_minutes:.1f} min estimated")
|
||||
print(f"🎧 Audio: {audio_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user