feat: v4 voice overhaul — grounded knowledge, in-call scheduling, hardened ops - #6
feat: v4 voice overhaul — grounded knowledge, in-call scheduling, hardened ops#6ssevera1 wants to merge 2 commits into
Conversation
…dened ops Live loop stays Grok speech-to-speech (grok-voice-latest, configurable voice, default eve; cumulative-transcript handling) with a generalized tool runtime in the bridge. In-call tools: get_availability and book_tentative against Microsoft Graph (Exchange Online; device-code sign-in reusing the mailbox's existing app registration; Google backend available behind a facade), notify_owner (live Telegram), lookup_opportunity (read-only career-ops lookup), end_call. Facts come from a compiled knowledge pack validated against the source CV (numbers must appear in sources; banned phrasings rejected; wrapper preamble trimmed; claude CLI fallback when no API key). The hand-written resume facts and the spoken-passphrase Gateway mode are deleted. Post-call, Claude Opus 5 produces a structured JSON extract driving the Telegram report, call history, and a career-ops inbox note; finalization is exactly-once (the status callback raced the stream cleanup). The media-stream WebSocket requires a single-use per-call token, delivered as a TwiML Parameter because Twilio drops query strings. Launcher: ngrok or cloudflared quick-tunnel fallback, re-points the Twilio number every start; logon task plus a 15-minute watchdog that relaunches when local or public health fails. All test fixtures are fictional. 84 tests.
There was a problem hiding this comment.
Reviewed the whole diff. The architecture is sound and the security posture genuinely improves — removing the "open my gateway" passphrase mode (agent/prompts.py), the single-use <Parameter>-delivered media-stream token (voice/stream_tokens.py + voice/twilio_webhook.py:193-203), the exactly-once _finalize_call flag set before the first await, HTML-escaping in notifications/telegram.py send_live, and dispatch() never raising into a live call are all done right. Scope matches the title. One real bug blocks approval, plus a few smaller items.
Blocking — the slot scan runs a day past the free/busy data it checks against (possible double-booking).
agent/scheduling.py:82 scans range(r.horizon_days + 1), i.e. day offsets 0 through 7 — eight calendar days. But voice/tools.py:114 fetches busy intervals with days=max(days, rules.horizon_days), i.e. now → now + 7 days. Day offset 7's bookable window runs 09:00–20:00 local, while the fetched window ends at now's time-of-day on that date, so anything on the last scanned day after that instant was never checked for conflicts.
Failure scenario: a recruiter calls Monday 09:00. Days 1–6 are fully booked (the exact situation this feature exists for), so the day-spreading pick in agent/scheduling.py:100-113 falls through to day offset 7 — next Monday. Next Monday 14:00 already has a meeting on the calendar, but it is outside the fetched free/busy range, so it isn't in busy, Sophie offers it, and book_tentative writes a real hold on top of the existing event. test_fully_booked_horizon_gives_no_slots doesn't catch this because its busy block spans a full month.
Fix either side of the seam — range(r.horizon_days) in propose_slots, or fetch days=rules.horizon_days + 1 in _get_availability. Worth a test that blocks days 1–6 and asserts a busy interval on the final scanned day is still respected.
voice/tools.py:113 — the advertised days_ahead parameter is inert. It's parsed and clamped to 14, and it widens the free/busy fetch, but propose_slots always uses rules.horizon_days (7), so a model asking for 14 days out gets the same 7-day scan. Either thread it through as replace(rules, horizon_days=days) or drop it from the tool schema so the model isn't offered a knob that does nothing.
test_voice_v4.py:122 — dead assertion. assert sent and "Dana Fields" in sent[0] and "delete it to decline" in sent[0].lower() or sent parses as (A and B and C) or sent, which collapses to just sent being truthy. The content checks never run; the test would pass on an empty or wrong Telegram body. Parenthesize or split into separate asserts.
requirements.txt:3 — anthropic==0.42.0 → anthropic>=1.2.0. Every other first-party pin in this file is exact (fastapi==, pydantic==, pydantic-settings==). An open upper bound on the SDK whose messages.create shape agent/llm.py and scripts/build_knowledge.py both depend on means a future major can break a fresh install without any change here. Suggest pinning to the specific version this was developed against.
Non-blocking nits: agent/scheduling.py:41 hardcodes "Central" in Slot.spoken() while SchedulingRules.timezone is configurable — they'll disagree for anyone who changes it. scripts/watchdog.ps1:31 stops all cloudflared processes, not just the one this stack started.
What
Live call loop moves to Grok speech-to-speech (
grok-voice-latest) with a generalized in-call tool runtime:get_availability/book_tentativeagainst Microsoft Graph (Google backend available behind a facade),notify_owner(live Telegram),lookup_opportunity(read-only career-ops lookup),end_call.Sophie's facts about the owner now come from a compiled, validated knowledge pack (
scripts/build_knowledge.py) checked against the source CV, not hand-written prompt claims. The old hand-written resume facts and the spoken-passphrase Gateway mode are removed.Post-call, Claude Opus 5 produces a structured JSON extract driving the Telegram report, call history, and a career-ops inbox note. Finalization is exactly-once (the status callback previously raced stream cleanup). The media-stream WebSocket now requires a single-use per-call token delivered as a TwiML Parameter, since Twilio drops query strings on that path.
Ops: launcher falls back through ngrok / cloudflared quick-tunnel and re-points the Twilio number on every start; a logon task plus a 15-minute watchdog relaunches on local or public health failure.
84 tests added/updated across
test_voice_v4.py,test_knowledge.py,test_scheduling.py,test_integrations.py. All fixtures are fictional.Why
Design doc (
design/2026-08-30-v4-voice-overhaul.md) was approved 2026-08-30. This branch implements it in full.Notes
No merge conflicts with
main(checked viagit merge-tree);mainonly advanced with unrelated CI-tooling commits since this branch diverged. Opening this now to put the already-implemented, already-approved work through the normal review/CI pipeline instead of leaving it parked on a branch with no PR.