← Back to work

Feb 2025 - Mar 2025

JobVoice

Real-time AI interview simulator with a 3-agent voice system.

Next.jsLiveKitRAGOpenTelemetry

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.

Built with
Next.js 16React 19LiveKit Agents 1.xPythonGroq (Llama 3.3 70B)Deepgram nova-2ElevenLabsSilero VADLlamaIndex + fastembedFirestoreOpenTelemetry
01 / 06

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.