Feb 2025 - Mar 2025
JobVoice
Real-time AI interview simulator with a 3-agent voice system.
JobVoice runs a live mock interview through three coordinated voice agents that hand off context to each other, fact-check the candidate against their CV and the job description in real time, and stay safe under adversarial prompts.
Architecture
The problem. A realistic mock interview needs several specialized turns (asking, probing, fact-checking) that still feel like one coherent conversation, all over a live voice stream and safe against manipulation.
Three agents, one thread. Three voice agents pass context to each other through handoff so the conversation stays continuous, while in-session RAG checks the candidate's answers against their CV and the job description in real time.
Safe under adversarial input. The agent I/O is wrapped in an injection guard of deterministic tool-call guards and prompt-leak detection, gated by a 50-prompt adversarial audit before it ships.
Observable end to end. A W3C traceparent is propagated across Next.js, Firestore, and the voice agent, and an LLM eval harness gates CI on p95 latency budgets, per-session cost, and quality drift.
What I built
- Built a 3-agent voice system unified by context-carrying handoff, with in-session RAG fact-checking the CV and JD claims live.
- Hardened against prompt injection with deterministic tool-call guards and prompt-leak detection, gated by a 50-prompt audit.
- Instrumented end-to-end OpenTelemetry tracing, propagating a W3C traceparent across Next.js, Firestore, and the voice agent.
- Engineered an LLM eval harness gating CI on quality drift, with p95 latency budgets per stage and per-session cost telemetry.
Under the hood
The stack, the data structures, and the decisions that matter, for engineers.
The three-agent panel and hand-off
The panel is three livekit-agents Agent subclasses (behavioral, technical, system-design), each a persona with its own ElevenLabs voice. A transfer_to_* function tool returns the next Agent instance and the SDK swaps it in place, forwarding the shared chat context so conversation history survives the hand-off. The final agent calls end_interview, which sets a module-level asyncio.Event the entrypoint watches alongside the session task. The worker forks a subprocess per session, so module-level state is safely per-call.