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:
@@ -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"],
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user