Skip to content

feat: v4 voice overhaul — grounded knowledge, in-call scheduling, hardened ops - #6

Open
ssevera1 wants to merge 2 commits into
mainfrom
feat/v4-voice-overhaul
Open

feat: v4 voice overhaul — grounded knowledge, in-call scheduling, hardened ops#6
ssevera1 wants to merge 2 commits into
mainfrom
feat/v4-voice-overhaul

Conversation

@ssevera1

@ssevera1 ssevera1 commented Sep 2, 2026

Copy link
Copy Markdown
Owner

What

Live call loop moves to Grok speech-to-speech (grok-voice-latest) with a generalized in-call tool runtime: get_availability / book_tentative against 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 via git merge-tree); main only 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.

…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.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. nownow + 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:3anthropic==0.42.0anthropic>=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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant