Move caller dialog to Sonnet 4.6, add SearXNG for Devon, and fix stale model ids

Working-tree changes that had accumulated without being committed. The test
updates matter most: tests/test_caller_gen.py was left behind when caller_gen
started requiring voice and age in regulars_included, so the committed tree had
a failing suite that only passed locally.

- Caller dialog moves from Haiku 4.5 to Sonnet 4.6 (~$1/show to ~$3-4/show)
- Grok pinned to x-ai/grok-4.3; grok-4, grok-4-fast and grok-4.1-fast were
  retired from OpenRouter, and llm.py swallows the 404 and returns empty text,
  so a retired id makes callers go silent with nothing in the logs
- Devon's web_search now runs against SearXNG on the NAS
- Assorted TTS, audio, news, cost tracker and control-panel changes
- CLAUDE.md updated to match

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 04:21:55 -05:00
co-authored by Claude Opus 5
parent 2901e5f4fb
commit 44ef13336e
21 changed files with 731 additions and 186 deletions
+10 -3
View File
@@ -63,8 +63,15 @@ def test_resolve_voice_case_insensitive():
def test_resolve_voice_falls_back_when_no_match():
from backend.services.caller_gen import resolve_voice
roster = ["Marcus", "Dennis"]
# Deterministic fallback: return first from roster
assert resolve_voice("Santiago", roster) == "Marcus"
# Random fallback, so hallucinated voices don't all collapse onto roster[0]
assert resolve_voice("Santiago", roster) in roster
def test_resolve_voice_fallback_spreads_across_roster():
from backend.services.caller_gen import resolve_voice
roster = ["Marcus", "Dennis", "Priya", "Edward"]
picked = {resolve_voice("Santiago", roster) for _ in range(60)}
assert len(picked) > 1, "fallback collapsed onto a single voice"
def test_resolve_voice_empty_suggestion_falls_back():
@@ -103,7 +110,7 @@ def test_build_batch_prompt_includes_silas_lore_when_present():
"weather": "...",
"headlines": [],
"recent_caller_summaries": [],
"regulars_included": [{"name": "Silas", "lore": "Silas leads a small desert cult...", "arc_state": "seeking new members"}],
"regulars_included": [{"name": "Silas", "voice": "Sebastian", "age": 52, "lore": "Silas leads a small desert cult...", "arc_state": "seeking new members"}],
"caller_count": 12,
"voice_roster": ["Marcus"],
}
+32
View File
@@ -22,4 +22,36 @@ def test_prompt_includes_identity_and_situation():
assert "NEVER use asterisks" in prompt
assert "NEVER use parenthetical stage directions" in prompt
assert "Mix short punchy replies with longer ones" in prompt
assert "YOU CAN BE MOVED" in prompt
assert "NEVER restate the same dilemma" in prompt
assert len(prompt) < 3500
def test_prompt_includes_opening_line():
caller = {
"name": "Tina",
"identity": "A nurse who just got off a 16-hour shift",
"situation": "Found her ex's wedding invitation in her mailbox",
"reason_calling": "She's not sure if she should go",
"secret_want": "Wants someone to tell her she's over him",
"opening_line": "Luke, I literally just got home from work and there's this gold envelope sitting on my kitchen counter.",
"specific_details": ["invitation was hand-addressed", "wedding is in two weeks"],
}
prompt = get_caller_prompt(caller)
assert "gold envelope" in prompt
assert "FIRST message" in prompt
assert "listening for" in prompt.lower() or "been listening" in prompt.lower()
def test_prompt_without_opening_line():
caller = {
"name": "Ray",
"identity": "A retired mechanic",
"situation": "Neighbor's dog dug up something weird",
"reason_calling": "He thinks it might be human bones",
"secret_want": "Doesn't want to call the cops on his neighbor",
"specific_details": ["bones were wrapped in a tarp"],
}
prompt = get_caller_prompt(caller)
assert "FIRST message" not in prompt
assert "planned opening" not in prompt.lower()
+43
View File
@@ -64,6 +64,49 @@ def test_session_get_show_history_summary():
assert "EARLIER IN THE SHOW" in summary
def test_show_history_reactions_constant_is_usable():
from backend.main import SHOW_HISTORY_REACTIONS
assert SHOW_HISTORY_REACTIONS, "generic reaction pool must not be empty"
assert all(isinstance(r, str) and r.strip() for r in SHOW_HISTORY_REACTIONS)
# Each one is interpolated as "...and you {reaction}." so it must not
# carry its own leading/trailing punctuation or an unfilled placeholder.
for r in SHOW_HISTORY_REACTIONS:
assert not r.endswith("."), r
assert "{" not in r, r
def test_build_specific_reaction_falls_back_when_record_has_no_details():
"""A CallRecord with neither key_details nor situation_summary hits the
generic branch — this used to raise NameError mid-show."""
from backend.main import SHOW_HISTORY_REACTIONS
s = Session()
bare = CallRecord(
caller_type="ai", caller_name="Jasmine",
summary="Talked about her boss", transcript=[],
)
reaction = s._build_specific_reaction({}, bare)
assert reaction in SHOW_HISTORY_REACTIONS
def test_get_show_history_never_raises_when_reaction_branch_fires(monkeypatch):
"""Force the reaction branch every time so the fallback path is covered
deterministically rather than at its ~15% random rate."""
import backend.main as main
monkeypatch.setattr(main.random, "random", lambda: 0.0)
s = Session()
s.call_history.append(CallRecord(
caller_type="real", caller_name="Dave",
summary="Called about his wife leaving", transcript=[],
))
summary = s.get_show_history()
assert "DAVE" in summary
assert "and you " in summary
def test_session_reset_clears_history():
s = Session()
s.call_history.append(CallRecord(