Skip to main content
Lesson 14 of 18 · Realtime API and Voice Agents

Build a complete voice agent

Lesson 14 of 18 Updated November 3, 2026 Developer
On this page
  1. Build a complete voice agent
  2. Agent architecture
  3. The VoiceAgent class
  4. Audio capture module
  5. Event processing module
  6. Tool management module
  7. Launch the agent
  8. Key takeaways

Updated November 2, 2026 — Audio models. Several models in this course are deprecated: tts-1, tts-1-hd, and gpt-4o-mini-tts will stop on January 6, 2027 (successor: gpt-realtime-2.1-mini, which the OpenAI TTS guide states is not supported by /v1/audio/speech: speech synthesis migration moves to the Realtime API); gpt-realtime, gpt-4o-realtime, and legacy audio models on January 20, 2027 (gpt-realtime-2.1, gpt-realtime-2.1-mini, gpt-audio-1.5); whisper-1, gpt-4o-transcribe, and /v1/audio/translations on February 26, 2027 (gpt-transcribe, gpt-live-transcribe, v1/realtime/translations). For full-duplex voice, GPT-Live 1 has been available in the API since September 10, 2026.

Corrected November 3, 2026. Input transcription used gpt-live-transcribe, which only works in a transcription session and supports neither server_vad nor semantic_vad. In a conversation session like this one, where VAD validates each turn, the documentation specifies gpt-transcribe, which transcribes validated turns and uses previous turns as context. Sources: Realtime transcription · GPT-Transcribe · GPT-Live-Transcribe.

Build a complete voice agent

This lesson brings together all the concepts covered so far to build a fully functional end-to-end voice agent. You will create an assistant that listens to the user, understands their request, executes actions, and responds by voice—all in real time via the Realtime API.

Agent architecture

Four modules, and above all one constraint that drives the entire architecture: everything must happen at once. The microphone captures while the model responds, events arrive while audio plays, and the user can interrupt at any moment. A voice agent is not a question-and-answer loop; it is a system where multiple streams advance in parallel—which is why we rely heavily on asyncio in what follows.

Audio
Capture and playback
Session
WebSocket and config
Tools
Business actions
State
Conversation management

The VoiceAgent class

The class gathers the state that these parallel streams must share: the connection, the current session, and the response currently playing. Precisely because multiple tasks access this state simultaneously, an object is preferable to a set of global variables—when you need to cancel an ongoing response, you must know one exists.

import asyncio
import json
import base64
import websockets
import sounddevice as sd
import numpy as np
import pyaudio

class VoiceAgent:
    """Complete voice agent based on the Realtime API."""

    def __init__(self, api_key: str, instructions: str, tools: list = None):
        self.api_key = api_key
        self.instructions = instructions
        self.tools = tools or []
        self.ws = None
        self.is_playing = False
        self.sample_rate = 24000
        self.chunk_size = 2400  # 100ms

    async def connect(self):
        """Establishes the connection and configures the session."""
        url = "wss://api.openai.com/v1/realtime?model=gpt-realtime-2.1"
        headers = {
            "Authorization": f"Bearer {self.api_key}"
        }

        self.ws = await websockets.connect(url, additional_headers=headers)

        # Wait for session.created
        event = json.loads(await self.ws.recv())
        print(f"Session: {event['session']['id']}")

        # Configure
        await self.ws.send(json.dumps({
            "type": "session.update",
            "session": {
                "type": "realtime",
                "output_modalities": ["audio"],
                "instructions": self.instructions,
                "audio": {
                    "input": {
                        "format": {"type": "audio/pcm", "rate": 24000},
                        "transcription": {"model": "gpt-transcribe", "languages": ["fr"]},
                        "turn_detection": {
                            "type": "server_vad",
                            "threshold": 0.5,
                            "silence_duration_ms": 600
                        }
                    },
                    "output": {
                        "format": {"type": "audio/pcm", "rate": 24000},
                        "voice": "marin"
                    }
                },
                "tools": self.tools,
                "tool_choice": "auto"
            }
        }))

        await self.ws.recv()  # session.updated
        print("Voice agent ready.")

    async def start(self):
        """Starts the voice agent."""
        await self.connect()
        await asyncio.gather(
            self._capture_audio(),
            self._process_events()
        )

Audio capture module

    async def _capture_audio(self):
        """Captures microphone input and sends it to the Realtime API."""
        loop = asyncio.get_event_loop()
        queue = asyncio.Queue()

        def callback(indata, frames, time_info, status):
            pcm = (indata[:, 0] * 32767).astype(np.int16)
            loop.call_soon_threadsafe(queue.put_nowait, pcm)

        stream = sd.InputStream(
            samplerate=self.sample_rate,
            channels=1,
            dtype="float32",
            blocksize=self.chunk_size,
            callback=callback
        )

        with stream:
            while True:
                pcm_data = await queue.get()
                audio_b64 = base64.b64encode(pcm_data.tobytes()).decode()
                await self.ws.send(json.dumps({
                    "type": "input_audio_buffer.append",
                    "audio": audio_b64
                }))

Event processing module

    async def _process_events(self):
        """Processes all events from the server."""
        pa = pyaudio.PyAudio()
        player = pa.open(
            format=pyaudio.paInt16, channels=1,
            rate=self.sample_rate, output=True
        )

        try:
            async for raw in self.ws:
                event = json.loads(raw)
                t = event["type"]

                if t == "response.output_audio.delta":
                    audio = base64.b64decode(event["delta"])
                    player.write(audio)
                    self.is_playing = True

                elif t == "response.output_audio.done":
                    self.is_playing = False

                elif t == "response.output_audio_transcript.delta":
                    print(event["delta"], end="", flush=True)

                elif t == "input_audio_buffer.speech_started":
                    if self.is_playing:
                        await self.ws.send(json.dumps({
                            "type": "response.cancel"
                        }))
                        self.is_playing = False

                elif t == "response.done":
                    await self._handle_response_done(event)

                elif t == "error":
                    print(f"\nError: {event['error']['message']}")

        finally:
            player.close()
            pa.terminate()

Tool management module

This module transforms the agent from a simple voice chatbot into an assistant capable of taking action. The difference from classic function calling lies in the timing constraint: the user is on the line with no visual progress indicator. If execution takes more than a second or two, have the agent speak while it works—“Let me check that for you right now”—because silence in a voice conversation sounds like a breakdown.

    async def _handle_response_done(self, event):
        """Handles function calls in the response."""
        response = event["response"]
        function_calls = [
            o for o in response.get("output", [])
            if o["type"] == "function_call"
        ]

        if not function_calls:
            print()  # New line after transcription
            return

        for call in function_calls:
            name = call["name"]
            args = json.loads(call["arguments"])
            call_id = call["call_id"]

            print(f"\n→ {name}({args})")

            # Execute the function (adapt to your business logic)
            result = await self._execute_function(name, args)

            await self.ws.send(json.dumps({
                "type": "conversation.item.create",
                "item": {
                    "type": "function_call_output",
                    "call_id": call_id,
                    "output": json.dumps(result)
                }
            }))

        # Ask the model to continue
        await self.ws.send(json.dumps({"type": "response.create"}))

    async def _execute_function(self, name: str, args: dict) -> dict:
        """Dispatches the function call."""
        # Implement according to your business logic
        return {"error": f"Function {name} not implemented"}

Launch the agent

async def main():
    agent = VoiceAgent(
        api_key="sk-...",
        instructions=(
            "You are a voice assistant for Corsen AI. "
            "You help users manage their appointments "
            "and get information about our services. "
            "Respond in French, concisely and professionally."
        ),
        tools=[
            {
                "type": "function",
                "name": "check_availability",
                "description": "Checks availability for a time slot",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "date": {"type": "string"},
                        "service": {"type": "string"}
                    },
                    "required": ["date"]
                }
            }
        ]
    )

    await agent.start()

asyncio.run(main())

Key takeaways

  • A complete voice agent combines four modules: audio, session, tools, and state management
  • The VoiceAgent class encapsulates all logic behind a simple interface
  • asyncio.gather enables parallel microphone capture and event processing
  • Handling interruptions (canceling a response when the user speaks) is essential
  • Tools transform the agent from a simple voice chatbot into an assistant capable of action