Add sample caller dialogue generation script + 10 validation samples
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""Generate 10 sample caller dialogues for user validation before cutover.
|
||||
|
||||
5 with Silas batch (if lore exists), 5 walk-ins. Writes transcripts to docs/samples/.
|
||||
"""
|
||||
import asyncio
|
||||
import random
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
from backend.config import settings
|
||||
from backend.main import (
|
||||
BLACKLISTED_VOICES,
|
||||
INWORLD_FEMALE_VOICES,
|
||||
INWORLD_MALE_VOICES,
|
||||
get_caller_prompt_slim,
|
||||
)
|
||||
from backend.services import caller_gen, regulars_v2
|
||||
|
||||
DIALOG_MODEL = "anthropic/claude-haiku-4.5"
|
||||
|
||||
HOST_REACTIONS = [
|
||||
"Yeah?",
|
||||
"Go on.",
|
||||
"Mm-hmm.",
|
||||
"Wait — really?",
|
||||
"Hold on, back up.",
|
||||
"Is that right?",
|
||||
"How'd you end up there?",
|
||||
"And then what?",
|
||||
"What'd she say?",
|
||||
"So what are you gonna do about it?",
|
||||
"That's — okay, keep going.",
|
||||
"I mean, what do you want me to tell you?",
|
||||
]
|
||||
|
||||
|
||||
async def dialog_turn(client: httpx.AsyncClient, system_prompt: str, conversation: list) -> str:
|
||||
last_err = None
|
||||
for attempt in range(3):
|
||||
try:
|
||||
resp = await client.post(
|
||||
"https://openrouter.ai/api/v1/chat/completions",
|
||||
headers={"Authorization": f"Bearer {settings.openrouter_api_key}"},
|
||||
json={
|
||||
"model": DIALOG_MODEL,
|
||||
"messages": [{"role": "system", "content": system_prompt}] + conversation,
|
||||
"max_tokens": 300,
|
||||
"temperature": 0.9,
|
||||
},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()["choices"][0]["message"]["content"].strip()
|
||||
except (httpx.ReadTimeout, httpx.HTTPStatusError) as e:
|
||||
last_err = e
|
||||
print(f" [retry {attempt+1}/3] dialog_turn failed: {type(e).__name__}")
|
||||
raise last_err
|
||||
|
||||
|
||||
async def main():
|
||||
out_dir = Path("docs/samples")
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
voice_roster = [
|
||||
n for n in INWORLD_MALE_VOICES + INWORLD_FEMALE_VOICES
|
||||
if n not in BLACKLISTED_VOICES
|
||||
]
|
||||
|
||||
date_str = datetime.now().strftime("%A, %B %d, %Y")
|
||||
|
||||
# Batch 1: include Silas (if lore exists)
|
||||
regulars = regulars_v2.load_all_active_regulars()
|
||||
if regulars:
|
||||
ctx_with_silas = {
|
||||
"date": date_str,
|
||||
"weather": "cool desert night",
|
||||
"headlines": [],
|
||||
"recent_caller_summaries": [],
|
||||
"regulars_included": [
|
||||
{"name": r.name, "lore": r.lore_body, "arc_state": r.arc_state}
|
||||
for r in regulars
|
||||
],
|
||||
"caller_count": 5,
|
||||
"voice_roster": voice_roster,
|
||||
}
|
||||
print("[Batch 1] Generating 5 callers including Silas...")
|
||||
silas_batch = await caller_gen.generate_batch(ctx_with_silas)
|
||||
print(f"[Batch 1] Got {len(silas_batch)} callers: {[c.name for c in silas_batch]}")
|
||||
else:
|
||||
print("[Batch 1] No regulars found — skipping")
|
||||
silas_batch = []
|
||||
|
||||
# Batch 2: walk-ins
|
||||
ctx_walkins = {
|
||||
"date": date_str,
|
||||
"weather": "cool desert night",
|
||||
"headlines": [],
|
||||
"recent_caller_summaries": [],
|
||||
"regulars_included": [],
|
||||
"caller_count": 5,
|
||||
"voice_roster": voice_roster,
|
||||
}
|
||||
print("[Batch 2] Generating 5 walk-in callers...")
|
||||
walkin_batch = await caller_gen.generate_batch(ctx_walkins)
|
||||
print(f"[Batch 2] Got {len(walkin_batch)} callers: {[c.name for c in walkin_batch]}")
|
||||
|
||||
all_callers = silas_batch + walkin_batch
|
||||
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
for idx, caller in enumerate(all_callers, 1):
|
||||
print(f"[Dialog {idx}/{len(all_callers)}] {caller.name}...")
|
||||
cdict = {
|
||||
"name": caller.name,
|
||||
"identity": caller.identity,
|
||||
"situation": caller.situation,
|
||||
"reason_calling": caller.reason_calling,
|
||||
"secret_want": caller.secret_want,
|
||||
"specific_details": caller.specific_details,
|
||||
}
|
||||
system_prompt = get_caller_prompt_slim(cdict)
|
||||
conversation = [{"role": "assistant", "content": caller.opening_line}]
|
||||
|
||||
for _ in range(4):
|
||||
host_line = random.choice(HOST_REACTIONS)
|
||||
conversation.append({"role": "user", "content": host_line})
|
||||
reply = await dialog_turn(client, system_prompt, conversation)
|
||||
conversation.append({"role": "assistant", "content": reply})
|
||||
|
||||
transcript_lines = [
|
||||
f"CALLER ({caller.name}, {caller.age}, {caller.location}): "
|
||||
f"{conversation[0]['content']}"
|
||||
]
|
||||
for i in range(1, len(conversation), 2):
|
||||
transcript_lines.append(f"LUKE: {conversation[i]['content']}")
|
||||
if i + 1 < len(conversation):
|
||||
transcript_lines.append(f"CALLER: {conversation[i+1]['content']}")
|
||||
|
||||
slug = caller.name.replace(" ", "_").lower()
|
||||
fname = out_dir / f"sample_{slug}.txt"
|
||||
fname.write_text(
|
||||
f"=== {caller.name} ({caller.age}, {caller.location}) ===\n"
|
||||
f"voice: {caller.voice_resolved}\n"
|
||||
f"emotional_register: {caller.emotional_register}\n"
|
||||
f"secret_want: {caller.secret_want}\n\n"
|
||||
+ "\n".join(transcript_lines)
|
||||
+ "\n"
|
||||
)
|
||||
print(f"Wrote {fname}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user