Collapse caller gen to single batch of 10 with anti-collision rule

Previously _build_backgrounds ran two parallel Sonnet calls of 6
callers each, with zero cross-batch awareness. Each batch independently
gravitated to the same quirky archetypes (Coast to Coast weirdos,
specific hobbies) and produced clustered rosters — one recent show
had two BBQ competitors, two taxidermists, and two conspiracy-pattern
callers across 10 slots.

Collapses to a single Sonnet call generating all 10 at once so the
model can self-diversify across the full roster. The BATCH_SYSTEM_PROMPT
also gets an explicit ANTI-COLLISION RULE with concrete forbidden
examples (no two BBQ competitors, no two taxidermists, no two mystery-
signal callers, etc.) and an instruction to scan output for collisions
before finalizing.

max_tokens bumped 8000 -> 16000 to accommodate the larger response.
Wall-clock cost: ~30s -> ~60s, acceptable for the diversity gain.
Verified on a fresh reset: 10 fully differentiated callers, zero
archetype collisions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 02:05:17 -06:00
co-authored by Claude Opus 4.6
parent 8a20e679f6
commit 1c7ac334b4
2 changed files with 16 additions and 18 deletions
+10 -16
View File
@@ -822,22 +822,16 @@ class Session:
"recent_caller_summaries": self._get_recent_summaries(),
"voice_roster": voice_roster,
}
# Split into two parallel calls: call 1 includes regulars + 5 walk-ins,
# call 2 is 6 pure walk-ins. Halves generation time by parallelizing.
ctx_a = {**base_ctx, "regulars_included": regulars_for_tonight, "caller_count": 6}
ctx_b = {**base_ctx, "regulars_included": [], "caller_count": 6}
batches = await asyncio.gather(
caller_gen.generate_batch(ctx_a),
caller_gen.generate_batch(ctx_b),
return_exceptions=True,
)
identities: list = []
for i, result in enumerate(batches):
if isinstance(result, Exception):
print(f"[Background] Batch {i+1} failed: {result}")
continue
identities.extend(result)
# Single batch of 10 so sonnet sees the full roster and can enforce
# the anti-collision rule across all callers. Previously two parallel
# batches of 6 were generated blind to each other, which produced
# recurring archetype clusters (e.g. two BBQ callers, two taxidermists).
ctx = {**base_ctx, "regulars_included": regulars_for_tonight, "caller_count": 10}
try:
identities = await caller_gen.generate_batch(ctx)
except Exception as e:
print(f"[Background] Batch failed: {e}")
identities = []
# Build regular lookup tables
regular_names = {r["name"] for r in regulars_for_tonight}
+6 -2
View File
@@ -62,7 +62,11 @@ BATCH_SYSTEM_PROMPT = """You are writing a roster of callers for Luke's late-nig
CREATIVE RANGE: Your callers must span the emotional range of Howard Stern (chaos, strong characters), Coast to Coast AM (earnest weirdos, sincere believers), Loveline (real problems, real advice-seeking), Delilah (emotional vulnerability, connection), and Opie and Anthony (sharp, irreverent, specific people).
Maximum character distance between callers. No two callers should feel like siblings. Do not default to sitcom plots. Real humans are specific and strange. Give each caller details that could only belong to them.
Maximum character distance between callers. No two callers should feel like siblings.
ANTI-COLLISION RULE THIS IS NON-NEGOTIABLE: All callers in this roster must be clearly differentiated. No two callers may share the same hobby, obsession, profession archetype, or story theme. Specifically forbidden within a single roster: two BBQ competitors, two taxidermists, two amateur-radio or mystery-signal callers, two callers with ex-spouse drama, two conspiracy/pattern-seers, two retired military, two grandmothers-of-many, two callers calling about a weird neighbor, two callers with religious-object stories. Each caller's defining "thing" — their hook, their obsession, the specific topic they're calling about must appear exactly once in the roster. Before finalizing, scan your output and swap any collisions.
Do not default to sitcom plots. Real humans are specific and strange. Give each caller details that could only belong to them.
You will output strict JSON with a "callers" array. Each caller has exactly these fields: name, age, voice_suggestion, location, identity, situation, reason_calling, opening_line, secret_want, specific_details (array of 2-3 strings), emotional_register."""
@@ -113,7 +117,7 @@ async def generate_batch(ctx: dict) -> list[CallerIdentity]:
"model": BATCH_MODEL,
"messages": [{"role": "user", "content": prompt}],
"response_format": {"type": "json_object"},
"max_tokens": 8000,
"max_tokens": 16000,
"temperature": 0.9,
},
)