# Model Context Protocol (MCP)
Source: https://visionagents.ai/ai-technologies/model-context-protocol
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/overview) is an open standard that defines how LLMs interact with external tools and services. Vision Agents supports MCP for function calling and connecting to external tool servers.
## How Vision Agents Uses MCP
Vision Agents provides two ways to give your agent access to tools:
1. **Function Registration** — Decorate Python functions to make them callable by the LLM
2. **MCP Servers** — Connect to local or remote servers that expose tools
Both approaches work with any LLM that supports function calling (OpenAI, Gemini, etc.) and with Realtime models.
## Function Registration
Use `@llm.register_function()` to make any Python function available to your agent:
```python theme={null}
from vision_agents.plugins import openai
llm = openai.LLM(model="gpt-5.4")
@llm.register_function(description="Get current weather for a location")
async def get_weather(location: str) -> dict:
return {"location": location, "temperature": "22°C", "condition": "Sunny"}
# The LLM will automatically call get_weather() when relevant
response = await llm.simple_response("What's the weather like in London?")
```
## MCP Servers
Connect to MCP servers for access to external services:
```python theme={null}
from vision_agents.core.mcp import MCPServerRemote, MCPServerLocal
from vision_agents.core.agents import Agent
# Remote server (HTTP)
github_server = MCPServerRemote(
url="https://api.githubcopilot.com/mcp/",
headers={"Authorization": f"Bearer {token}"}
)
# Local server (stdio)
local_server = MCPServerLocal(command="uv run my_mcp_server.py")
# Connect to your agent
agent = Agent(
edge=getstream.Edge(),
llm=openai.LLM(model="gpt-5.4"),
agent_user=User(name="Assistant", id="agent"),
mcp_servers=[github_server, local_server]
)
```
When you add MCP servers to an agent, tools are automatically discovered and registered with the LLM.
## When to Use Each Approach
| Approach | Best For |
| ------------------------- | --------------------------------------------------- |
| **Function Registration** | Simple tools, inline logic, prototyping |
| **MCP Servers** | External APIs, shared tooling, complex integrations |
You can combine both — register local functions while also connecting MCP servers.
For detailed examples including parameter types, custom function names, and real-world patterns, see the [MCP and Function Calling Guide](/guides/mcp-tool-calling).
# Speech To Speech (STS)
Source: https://visionagents.ai/ai-technologies/speech-to-speech
Speech-to-Speech (STS), also known as **Realtime** in the SDK, is a single provider that handles listening, reasoning, and speaking over one connection. Realtime models include built-in [STT](/ai-technologies/speech-to-text), [TTS](/ai-technologies/text-to-speech), and [turn detection](/ai-technologies/turn-detection) — you configure `llm=.Realtime()` without separate speech plugins.
```mermaid theme={null}
flowchart TB
Participants[Call participants] --> Edge[Edge transport]
Edge --> Agent[Agent RealtimeInferenceFlow]
Agent --> Realtime[Realtime API]
Realtime --> Agent
Agent --> Edge
Edge --> Participants
```
You still need `edge`, `agent_user`, `instructions`, API keys, and call lifecycle setup. See [Voice Agents](/introduction/voice-agents) for the full guide.
## How It Works
When the LLM is a Realtime model, the Agent selects `RealtimeInferenceFlow`:
* Call audio is sent to the Realtime API
* Model audio and transcripts are published back to the call
* STT, TTS, and turn detection plugins are automatically disabled
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful voice assistant.",
llm=gemini.Realtime(), # no stt/tts/turn_detection needed
)
```
## Realtime vs Custom Pipeline
| Mode | Best for | You choose |
| ------------------- | ---------------------------- | ---------------------------------- |
| **Realtime** | Fastest path, lowest latency | One provider for speech in and out |
| **Custom Pipeline** | Full control over each stage | STT, LLM, and TTS independently |
Use a [custom pipeline](/introduction/voice-agents#custom-pipeline-mode) when you want to mix providers — for example Deepgram STT, Gemini LLM, and Inworld TTS.
## Providers
| Provider | Integration page |
| ----------- | ------------------------------------------------- |
| OpenAI | [OpenAI Realtime](/integrations/realtime/openai) |
| Gemini | [Gemini Live](/integrations/realtime/gemini) |
| Qwen | [Qwen OMNI](/integrations/realtime/qwen) |
| AWS Bedrock | [AWS Bedrock](/integrations/realtime/aws-bedrock) |
| xAI | [xAI](/integrations/realtime/xai) |
| Inworld | [Inworld](/integrations/realtime/inworld) |
## Next Steps
Realtime setup and provider swapping
Barge-in and turn timing
Tools with realtime models
Mix STT, LLM, and TTS providers
# Speech To Text (STT)
Source: https://visionagents.ai/ai-technologies/speech-to-text
Speech-to-Text (STT) converts spoken audio into text. In a custom voice pipeline, STT sits between the call and the LLM: user speaks → **STT** transcribes → turn signal → **LLM** → **TTS**. See [Voice Agents](/introduction/voice-agents#custom-pipeline-mode) for the full wiring guide.
```mermaid theme={null}
flowchart LR
Audio[Spoken audio] --> STT[STT plugin]
STT --> Text[Transcript text]
```
## Quick Start
```bash theme={null}
uv add "vision-agents[deepgram,inworld,getstream]"
```
Add keys to your `.env`:
```bash theme={null}
STREAM_API_KEY=...
STREAM_API_SECRET=...
DEEPGRAM_API_KEY=...
```
```python theme={null}
from dotenv import load_dotenv
from vision_agents.core import Agent, User
from vision_agents.plugins import deepgram, gemini, getstream, inworld
load_dotenv()
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful voice assistant.",
llm=gemini.LLM(),
stt=deepgram.STT(eager_turn_detection=True),
tts=inworld.TTS(),
)
```
The SDK captures audio from your Stream call and feeds it to the STT provider in real time. As people speak, you receive live transcripts.
```mermaid theme={null}
flowchart TB
Participants[Call participants] --> Edge[Edge transport]
Edge --> Agent[Agent audio input]
Agent --> STT[STT plugin]
STT --> Partial[Partial transcripts]
STT --> Final[Final transcripts]
STT --> Turns[TurnStarted / TurnEnded]
Final --> Events[UserTranscriptEvent]
Turns --> Pipeline[LLM turn timing]
```
## Providers
| Plugin | Turn detection | Latency model | Integration |
| ------------ | -------------------- | ------------------------- | ---------------------------------------------- |
| Deepgram | Yes (eager optional) | Streaming | [Deepgram](/integrations/stt/deepgram) |
| ElevenLabs | Yes (VAD commit) | Streaming | [ElevenLabs](/integrations/stt/elevenlabs) |
| Cartesia | Yes (eager default) | Streaming | [Cartesia](/integrations/stt/cartesia) |
| AssemblyAI | Yes | Streaming | [AssemblyAI](/integrations/stt/assemblyai) |
| Sarvam | Yes | Streaming | [Sarvam](/integrations/stt/sarvam) |
| Fast-Whisper | No | Local batch (\~2s buffer) | [Fast-Whisper](/integrations/stt/fast-whisper) |
| Wizper | No | Cloud (Fal.ai) | [Wizper](/integrations/stt/wizper) |
| Fish | No | Batch | [Fish](/integrations/stt/fish) |
| Mistral | No | Streaming | [Mistral](/integrations/stt/mistral) |
**Fast-Whisper** runs locally on your machine. **Wizper** uses Fal.ai cloud — it is not a local model.
STT plugins without built-in turn detection need an external [turn detection](/ai-technologies/turn-detection) plugin, or the agent falls back to using the final transcript as the end-of-turn signal.
## Transcripts
STT providers emit partial, replacement, and final transcripts. Subscribe to agent events to handle them in your application:
```python theme={null}
from vision_agents.core.agents.events import UserTranscriptEvent
@agent.events.subscribe
async def on_transcript(event: UserTranscriptEvent):
print(event.text)
```
## Next Steps
Wire STT into a full voice pipeline
When the agent starts and stops listening
# Text To Speech (TTS)
Source: https://visionagents.ai/ai-technologies/text-to-speech
Text-to-Speech (TTS) transforms written words into spoken audio, allowing your applications to "speak" to users naturally. With the Vision Agents, you can easily add voice capabilities to your video calls and applications, creating experiences where text becomes lifelike speech in real-time.
## How does text to speech work?
TTS converts text to PCM audio. In a voice agent, text usually comes from the LLM response; you can also call `agent.say()` to speak text directly without the LLM.
```mermaid theme={null}
flowchart LR
Text[Input text] --> TTS[TTS plugin]
TTS --> Audio[PCM audio]
```
## Using Text-To-Speech with Stream
The Vision Agents SDK routes synthesized audio from the TTS plugin into your Stream call via the agent's outbound audio track.
```mermaid theme={null}
flowchart TB
LLM[LLM response text] --> TTS[TTS plugin]
Say[agent.say text] --> TTS
TTS --> Track[Agent audio track]
Track --> Edge[Edge transport]
Edge --> Participants[Call participants]
```
## Worked example
Let's walk through a real-world example to see how TTS works in your application.
Imagine you're building a customer support system where callers get placed in a queue. Here's how TTS makes this experience feel personal and professional:
**The Scenario**: A customer calls your support line and gets placed in a queue.
**What Happens**:
1. Your system detects the caller and generates a friendly message: "Thank you for calling TechCorp Support. Your estimated wait time is 5 minutes."
2. Instead of showing this as text on screen (which the caller can't see), your TTS plugin converts it to natural speech that sounds like a real person.
3. The voice speaks directly to the caller through the Stream call, creating an immediate human connection.
4. As the queue updates, new messages are automatically spoken: "Your wait time is now 3 minutes" or "We're connecting you to an agent now."
**The Result**: Instead of a silent, frustrating wait, customers get a conversational experience that feels like they're being personally attended to, even when they're waiting in line.
# Turn Detection
Source: https://visionagents.ai/ai-technologies/turn-detection
Turn Detection identifies when a speaker has finished their conversational turn and it's appropriate for an AI to respond. It solves a critical problem in voice AI: respond too early and you interrupt the speaker; wait too long and the conversation feels awkward.
## Two Paths in Vision Agents
Turn detection works differently depending on your setup:
```
Custom pipeline: Call audio → STT or TurnDetector → turn signals → LLM
Realtime mode: Call audio ↔ Realtime API
```
### 1. Provider-built-in (STT output stream)
Many STT plugins emit `TurnStarted` and `TurnEnded` signals on their output stream. Turn detection happens inside the provider API — no separate plugin needed.
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import deepgram, gemini, getstream, inworld
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful voice assistant.",
llm=gemini.LLM(),
stt=deepgram.STT(eager_turn_detection=True), # built-in turns; eager = lower latency
tts=inworld.TTS(),
)
```
### 2. External plugin
When your STT plugin has no built-in turn detection, add a `TurnDetector` plugin:
```python theme={null}
from vision_agents.plugins import smart_turn
agent = Agent(
...,
stt=fast_whisper.STT(),
turn_detection=smart_turn.TurnDetection(),
)
```
External plugins use Silero VAD plus neural models to predict turn completion — see [Smart Turn](/integrations/turn-detection/smart-turn) and [Vogent](/integrations/turn-detection/vogent).
## Turn Detection vs VAD
| | VAD | Turn Detection |
| ---------------- | --------------------------- | ---------------------------------------- |
| **Question** | "Is someone speaking?" | "Has the speaker finished?" |
| **Output** | Speech start/end timestamps | `TurnStarted` / `TurnEnded` turn signals |
| **Intelligence** | Simple audio analysis | Provider API or neural turn models |
| **Best for** | Detecting presence | Knowing when to respond |
## STT Plugins with Built-in Turn Detection
| STT Plugin | Turn Detection |
| ------------------------------------------ | ---------------------------------------------------------------------- |
| [Deepgram](/integrations/stt/deepgram) | Always on; set `eager_turn_detection=True` for speculative early turns |
| [ElevenLabs](/integrations/stt/elevenlabs) | Built-in via VAD commit strategy |
| [Cartesia](/integrations/stt/cartesia) | Built-in; eager by default |
| [AssemblyAI](/integrations/stt/assemblyai) | Built-in |
| [Sarvam](/integrations/stt/sarvam) | Built-in via VAD events |
STT plugins without turn detection (Fast-Whisper, Wizper, Fish, Mistral, AWS) need an external plugin or fall back to using the final transcript as the end-of-turn signal.
## External Turn Detection Plugins
| Plugin | Description |
| ----------------------------------------------------- | ------------------------------------------------------------------------ |
| [Smart Turn](/integrations/turn-detection/smart-turn) | Combines Silero VAD, Whisper features, and neural turn completion models |
| [Vogent](/integrations/turn-detection/vogent) | Neural turn detection with high accuracy prediction |
For Realtime APIs (OpenAI, Gemini, AWS Bedrock, Qwen, xAI, Inworld), turn detection is built-in at the model level — no separate plugin needed. See [Voice Agents — Realtime Mode](/introduction/voice-agents#realtime-mode).
When an STT plugin provides built-in turn detection (`stt.turn_detection` is `True`), the `Agent` automatically ignores any external `TurnDetector` plugin to prevent conflicts.
## SDK Behavior
* **`TurnStarted`** — triggers barge-in interrupt in the transcribing flow
* **`TurnEnded(eager=True)`** — starts speculative LLM work for lower latency
* **`TurnEnded(eager=False)`** — confirms the turn is complete
* Pipeline signals (`TurnStarted`/`TurnEnded` on the STT output stream) are distinct from agent events (`UserTurnStartedEvent`, `UserTurnEndedEvent`)
## Next Steps
Setup, tuning, and troubleshooting
Wire turn detection into a pipeline
Configure the Smart Turn plugin
Alternative turn detection option
# Agent Class
Source: https://visionagents.ai/core/agent-core
The `Agent` class is the central orchestrator that brings together all other components in the Vision Agents framework. It manages the conversation flow, handles real-time audio/video processing, coordinates responses, and integrates with external tools via MCP (Model Context Protocol) servers. It is the main interface for building AI-powered video and voice applications. It supports both traditional STT/TTS workflows and modern realtime speech-to-speech models, making it flexible for various use cases.
Do not reuse an `Agent` instance. Create a new agent for each call. Calling
`join()` twice on the same agent raises `RuntimeError`.
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import openai, deepgram, elevenlabs, getstream
# Traditional STT/TTS mode
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="AI Assistant", id="agent"),
instructions="You're a helpful AI assistant",
llm=openai.LLM(model="gpt-5.4"),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
)
# Realtime mode
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="AI Assistant", id="agent"),
instructions="You're a helpful AI assistant",
llm=openai.Realtime(model="gpt-realtime", voice="marin"),
)
```
## Constructor Parameters
* **`edge`** (`EdgeTransport`): The edge network provider for video & audio transport (you can choose any provider here)
* **`llm`** (`LLM | AudioLLM | VideoLLM`): The language model — can be a text LLM, audio-capable (AudioLLM), video-capable (VideoLLM), or a combined Realtime model
* **`agent_user`** (`User`): The agent's user information (name, id, etc.)
**Optional Parameters**
* **`instructions`** (`str`): System instructions for the agent. Supports `@file.md` references to load instructions from markdown files (default: "Keep your replies short and dont use special characters.")
* **`stt`** (`Optional[STT]`): Speech-to-text service (automatically disabled in realtime mode)
* **`tts`** (`Optional[TTS]`): Text-to-speech service (automatically disabled in realtime mode)
* **`turn_detection`** (`Optional[TurnDetector]`): Turn detection service for managing conversation turns. Automatically disabled in realtime mode. Ignored automatically if the configured STT plugin already provides built-in turn detection (when `stt.turn_detection` is `True`, e.g. `elevenlabs.STT`, `deepgram.STT`)
* **`processors`** (`Optional[List[Processor]]`): List of processors for video/audio processing
* **`avatar`** (`Optional[Avatar]`): Avatar provider for lip-synced video output (for example `anam.Avatar(...)`)
* **`mcp_servers`** (`Optional[List[MCPBaseServer]]`): MCP servers for external tool access
* **`options`** (`Optional[AgentOptions]`): Configuration options — see [AgentOptions](#agentoptions) below
* **`broadcast_metrics`** (`bool`): Broadcast agent metrics to call participants via custom events (default: `False`)
* **`broadcast_metrics_interval`** (`float`): Interval in seconds between metrics broadcasts (default: `5.0`)
* **`multi_speaker_filter`** (`Optional[AudioFilter]`): Filter for multi-speaker audio routing. Defaults to `FirstSpeakerWinsFilter`, which locks onto the first active speaker and drops other participants' audio until silence timeout or disconnect. Only activates when two or more participants have active audio tracks — see [Multiple Speakers](/guides/multiple-speakers)
* **`tracer`** (`Tracer`): OpenTelemetry tracer for distributed tracing (default: `trace.get_tracer("agents")`)
* **`profiler`** (`Optional[Profiler]`): Performance profiler for the agent and its plugins
### AgentOptions
| Field | Type | Default | Description |
| ----------- | ----- | --------------------- | ---------------------------------------------------------------- |
| `model_dir` | `str` | System temp directory | Directory for downloaded model files (VAD, turn detection, etc.) |
### Mode Validation
When an `AudioLLM` is configured (currently Realtime models), STT, TTS, and turn detection are automatically disabled with a warning log. In non-realtime mode, the agent requires at least an audio processing path (STT, TTS, or turn detection) or video processors. Video-only agents without an LLM are allowed when video processors are configured.
## Core Lifecycle Methods
**`async create_call(call_type: str, call_id: str) -> Call`**
Creates a call on the edge provider. Calls `authenticate()` automatically.
**`async join(call: Call, participant_wait_timeout: Optional[float] = 10.0) -> AsyncIterator[None]`**
Joins a video call. Must be called as an async context manager.
The agent can join the call only once. Once the call ends, the agent closes itself. `join()` always calls `close()` in its `finally` block.
**Parameters**
* **`call`** (`Call`): the call to join.
* **`participant_wait_timeout`** (`Optional[float]`): timeout in seconds to wait for other participants to join before proceeding.\
If `0`, do not wait at all. If `None`, wait forever.\
Default - `10.0`. Delegates to `wait_for_participant()`.
```python theme={null}
call = await agent.create_call(call_type="default", call_id="my-call")
async with agent.join(call):
await agent.simple_response("Say hi.")
await agent.finish() # Wait for call to end
```
**`async wait_for_participant(timeout: Optional[float] = None) -> None`**
Waits for at least one participant to join. Default `None` waits forever.
**`async finish()`**
Waits for the call to end gracefully. Subscribes to the call ended event. Returns immediately if not joined or already closed.
**`async close()`**
Cleans up all connections and resources. Safe to call multiple times. Called automatically when `join()` context exits.
**`async authenticate()`**
Authenticates the agent user with the edge provider. Idempotent — safe to call multiple times. Called automatically from `join()` and `create_call()`, so you usually don't need to call it directly.
### Response Methods
**`async simple_response(text: str, participant: Optional[Participant] = None, interrupt: bool = True)`**
Sends a text prompt to the active inference flow. The LLM generates a response which is then spoken through TTS (or realtime audio output). Use `interrupt=True` when you want this request to preempt an in-flight response.
```python theme={null}
await agent.simple_response("Hello, how can I help you?", interrupt=True)
```
**`async say(text: str, interrupt: bool = False)`**
Makes the agent speak text directly, bypassing the LLM. Works in transcribing (STT + LLM + TTS) mode only — the text is synthesized via TTS. In Realtime mode, `say()` logs a warning and does not produce audio (conversation history may still be updated). Use `interrupt=True` to stop current output before speaking.
```python theme={null}
await agent.say("Welcome to the call!", interrupt=False)
```
### Monitoring Methods
**`idle_for() -> float`**
Returns the number of seconds the agent has been alone on the call (all human participants have left). Returns `0.0` while other participants are present or before join.
**`on_call_for() -> float`**
Returns the number of seconds since the agent joined the call.
### Observability
**`metrics`** — `AgentMetrics` property for performance data. See [Telemetry](/core/telemetry).
**`async send_metrics_event(event_type="agent_metrics", fields=None)`** — Broadcast metrics to call participants.
**`async send_custom_event(data: dict)`** — Push custom data to call participants via the edge provider.
## MCP Integration
The Agent supports Model Context Protocol (MCP) for external tool integration. MCP servers connect when the agent joins a call:
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.core.mcp import MCPServerRemote
github_server = MCPServerRemote(
url="https://api.githubcopilot.com/mcp/",
headers={"Authorization": f"Bearer {github_pat}"}
)
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
llm=...,
mcp_servers=[github_server],
)
```
MCP tools are automatically registered with the LLM's function registry and can be called during conversations. See the [MCP guide](/guides/mcp-tool-calling) for more.
## Event System
The `Agent` makes it easy for developers to quickly subscribe and listen to events happening across all components. The event system merges all events across the plugin and core allowing you to listen to events in a single place using their respective type.
### Core Events
* **Agent Lifecycle Events**: `UserTurnStartedEvent`, `UserTurnEndedEvent`, `UserTranscriptEvent`, `AgentTurnStartedEvent`, `AgentTurnEndedEvent`, `AgentJoinedCallEvent`, `AgentLeftCallEvent`, `AgentFinishEvent` (emitted only when `finish()` is cancelled, not on normal call end)
* **Edge / Call Events**: `ParticipantJoinedEvent`, `ParticipantLeftEvent`, `CallEndedEvent`, `TrackAddedEvent`, `TrackRemovedEvent`, `AudioReceivedEvent`
* **LLM Events**: `LLMResponseFinalEvent`, `LLMErrorEvent`
* **Tool Events**: `ToolStartEvent`, `ToolEndEvent`
* **Realtime Events**: `RealtimeConnectedEvent`, `RealtimeDisconnectedEvent`
* **STT Events**: `STTConnectedEvent`, `STTDisconnectedEvent`, `STTErrorEvent`
* **TTS Events**: `TTSSynthesisStartEvent`, `TTSSynthesisCompleteEvent`, `TTSConnectedEvent`, `TTSDisconnectedEvent`, `TTSErrorEvent`
See [Events Reference](/reference/events-reference) for full field details.
### Event Subscription
```python theme={null}
from vision_agents.core.edge.events import AudioReceivedEvent
@agent.events.subscribe
async def on_audio_received(event: AudioReceivedEvent):
# Handle audio data
pass
```
You can also use `agent.subscribe(function)` as an alternative to the decorator.
## Debugging with local video files
For testing and debugging video processing without a live camera, you can use a local video file as the video source. This is useful for reproducible testing and development.
Video override only works when `publish_video` is `True` (requires an avatar or
a video publisher processor). Set the path **before** calling `join()`.
### Using the CLI
Pass the `--video-track-override` option when running your agent:
```bash theme={null}
uv run agent.py run --video-track-override=/path/to/video.mp4
```
### Using the API
```python theme={null}
from vision_agents.core import Agent, User
agent = Agent(...)
agent.set_video_track_override_path("/path/to/video.mp4")
```
When a video override is set, the local video file plays in a loop at 30 FPS instead of any incoming video tracks from call participants. The track lifecycle remains intact (starts when a user joins, stops when they leave).
# Avatar Class
Source: https://visionagents.ai/core/avatar-core
Avatars consume the agent's audio output and produce a synced video and audio feed of a virtual character. They run in passthrough mode: the avatar owns the agent's outbound video and audio tracks, and its output never feeds back into the LLM or any video processors.
## Class Hierarchy
The `vision_agents.core.avatars` module exports two classes:
| Class | Purpose |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `Avatar` | Abstract base class; consumes the agent's audio output and publishes synced video and audio. |
| `AVSynchronizer` | Utility that owns paired audio/video tracks and delays video frames to match the audio buffer depth, keeping lip-sync accurate. |
All three built-in implementations (LiveAvatar, Anam, LemonSlice) build on `AVSynchronizer` for output, so it's the recommended building block for custom avatars too.
## Lifecycle
The agent drives the avatar through a fixed lifecycle:
1. **`Agent.__init__`** queries `video_output()` and calls `attach_audio_input(stream)`, handing the avatar the inference flow's audio output stream.
2. **`Agent.join()`** calls `await avatar.start()`, which opens the provider connection and begins consuming the input stream.
3. **While running**, the avatar drains `input_audio_stream`, forwards audio to the provider, and exposes lip-synced video via `video_output()` and audio via `audio_output()`.
4. **`Agent.close()`** calls `await avatar.close()` for teardown.
When an avatar is set, the agent publishes `avatar.audio_output()` as outbound audio instead of the TTS stream directly — TTS still synthesises, the avatar lip-syncs and republishes.
## Abstract Methods
Subclasses must implement all four:
| Method | Description |
| ---------------- | ---------------------------------------------------------------------- |
| `video_output()` | Return the outbound `aiortc.VideoStreamTrack` published to the call. |
| `audio_output()` | Return the outbound `AudioOutputStream` published to the call. |
| `async start()` | Open the provider connection and begin consuming `input_audio_stream`. |
| `async close()` | Tear down the provider connection and cancel any consumer tasks. |
Subclasses may also implement an `interrupt()` method to stop the in-flight utterance at the provider during barge-in.
## Properties & Helpers
| Member | Description |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `provider_name` | Class attribute identifying the provider (used in events and metrics). |
| `events` | `EventManager` for emitting avatar-specific events. |
| `metrics` | `MetricsCollector` for recording avatar metrics. |
| `input_audio_stream` | The agent's audio output stream attached via `attach_audio_input`. Raises `ValueError` if accessed before attach. |
| `attach_audio_input(stream)` | Called by the agent to hand off its audio output stream. Override to customise how audio is consumed. |
## AVSynchronizer
`AVSynchronizer` is a utility class that solves the lip-sync problem: provider video and audio arrive on separate streams, and pushing them straight onto the outbound WebRTC tracks usually drifts. It owns a paired `audio_output` and `video_output`, delays each video frame by the current audio buffer depth, and paces frames at the configured fps (overriding aiortc's hardcoded 30 fps).
```python theme={null}
from vision_agents.core.avatars import AVSynchronizer
sync = AVSynchronizer(
width=1920,
height=1080,
fps=30,
max_queue_size=30, # typically int(fps * buffer_seconds)
)
```
| Member | Description |
| -------------------------- | -------------------------------------------------------------------------------------- |
| `video_output` | The `QueuedVideoTrack` to expose from `Avatar.video_output()`. |
| `audio_output` | The `AudioOutputStream` to expose from `Avatar.audio_output()`. |
| `async write_video(frame)` | Queue an `av.VideoFrame` from the provider, delayed by the current audio buffer depth. |
| `async write_audio(pcm)` | Write a `PcmData` chunk from the provider to the audio track. |
| `async flush()` | Discard pending video frames and flush buffered audio (use on interrupt). |
| `close()` | Close the underlying audio stream. |
## Building a Custom Avatar
A minimal subclass wraps an `AVSynchronizer`, exposes its tracks, and pumps provider frames into it from a consumer task started in `start()`:
```python theme={null}
import asyncio
from vision_agents.core.avatars import Avatar, AVSynchronizer
from vision_agents.core.agents.inference import AudioOutputStream
from getstream.video.rtc.track_util import PcmData
import av
class MyAvatar(Avatar):
provider_name = "my_avatar"
def __init__(self, width: int = 1280, height: int = 720, fps: int = 30) -> None:
super().__init__()
self._sync = AVSynchronizer(width=width, height=height, fps=fps)
self._task: asyncio.Task | None = None
def video_output(self):
return self._sync.video_output
def audio_output(self) -> AudioOutputStream:
return self._sync.audio_output
async def start(self) -> None:
# open provider connection, then pump agent audio into it
self._task = asyncio.create_task(self._consume(self.input_audio_stream))
async def close(self) -> None:
if self._task:
self._task.cancel()
self._sync.close()
async def _consume(self, stream: AudioOutputStream) -> None:
async for chunk in stream:
# send chunk.data to the provider; for each response frame:
# await self._sync.write_video(frame) # av.VideoFrame
# await self._sync.write_audio(pcm) # PcmData
...
```
## Usage
Pass an avatar to the agent at initialisation:
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import deepgram, gemini, getstream, liveavatar
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
llm=gemini.LLM("gemini-3-flash-preview"),
tts=deepgram.TTS(),
stt=deepgram.STT(),
avatar=liveavatar.Avatar(),
)
```
## Available Implementations
Real-time interactive avatars by HeyGen with WebSocket lip-sync.
Anam's avatar SDK with configurable dimensions and frame rate.
LemonSlice avatars delivered over LiveKit.
# LLM Class
Source: https://visionagents.ai/core/llm-core
The LLM component handles text generation and conversation logic. It supports both traditional request-response patterns and real-time streaming.
The base interface provides `simple_response()` for generating responses from text input, includes function calling capabilities with automatic tool execution, and manages conversation context. Multiple providers are supported including OpenAI, Anthropic, Google, and others.
## Class Hierarchy
The LLM system uses a class hierarchy to support different modalities:
| Class | Description |
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| `LLM` | Abstract base class for text-only language models |
| `AudioLLM` | Extends `LLM` with `simple_audio_response()` for speech-to-speech processing. Does not require separate STT/TTS services |
| `VideoLLM` | Extends `LLM` with `watch_video_track()` / `stop_watching_video_track()` for consuming video input |
| `OmniLLM` | Combines `AudioLLM` + `VideoLLM` for models supporting both audio and video |
| `Realtime` | Extends `OmniLLM` for realtime streaming models (e.g., OpenAI Realtime, Gemini Live) with persistent connections |
```python theme={null}
# Traditional mode — text LLM with separate STT/TTS
agent = Agent(
llm=openai.LLM(model="gpt-5.4"),
stt=deepgram.STT(),
tts=elevenlabs.TTS()
)
# Realtime mode — handles speech natively, no STT/TTS needed
agent = Agent(
llm=openai.Realtime()
)
```
Each LLM follows our philosophy of "thin wrapping". Out of the box, developers can pass in their own client to the LLMs or interact with the native APIs directly including full support for passing native method args.
The LLMs can be combined with other features such as processors to provide realtime feedback on the world around you. It can also be used in a simple voice only mode as shown in the previous example.
For models running in the non-realtime mode, a TTS service and STT service must be provided. These models will convert the user's speech to text which is then passed to the model. The model response is then converted into a voice output.
## Function Calling
The LLM base class includes a built-in function registry for tool use. Register functions as tools that the LLM can call during conversations:
```python theme={null}
@agent.llm.register_function(
name="get_weather",
description="Get the current weather for a location"
)
async def get_weather(location: str) -> str:
return f"The weather in {location} is sunny, 72°F"
```
Registered functions are automatically provided to the LLM as available tools. When the LLM decides to call a tool, the framework executes it with concurrency control (max 8 concurrent calls) and a per-tool timeout (default 30s), then feeds the result back to the LLM.
MCP servers registered via the `mcp_servers` parameter on the Agent also register their tools through the same function registry.
## Chat Completions API Support
Many open-source models follow the OpenAI Chat Completions API format. Whether you're experimenting with Kimi, Deepseek or Mistral, they call can be accessed by changing the base API url of the OpenAI SDK and setting an API key obtained from their respective dashboards.
To support this, Vision Agent's ships with support for both the OpenAI Response API (used by GPT 5 and the default) as well as the Chat Completions API with streaming. To use, you must have the OpenAI plugin installed in your project.
**Example**
```python theme={null}
from vision_agents.plugins import deepgram, elevenlabs, getstream, openai
async def create_agent(**kwargs) -> Agent:
# Initialize the Baseten VLM
llm = openai.ChatCompletionsVLM(
api_key="API-KEY",
model="qwen3vl",
base_url="https://model-vq0nkx7w.api.baseten.co/development/sync/v1", # Replace with your model's hosted URL
)
# Create an agent with video understanding capabilities
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Video Assistant", id="agent"),
instructions="You're a helpful video AI assistant. Analyze the video frames and respond to user questions about what you see.",
llm=llm,
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
processors=[],
)
return agent
```
We offer both `ChatCompletionsLLM` and `ChatCompletionsVLM` interfaces. The VLM interface will automatically forward the user's video feed as frames to the model. The above example demonstrates this using Qwen3-VL running on Baseten.
## VLM Support
Models such as [Moondream](/integrations/vision/moondream), [TwelveLabs Pegasus](/integrations/vision/twelvelabs), Qwen 3 and others offer powerful APIs for visual reasoning and understanding. These models operate as a subset of `LLM` called VLM. The frames from the user's video feeds is buffered and sent to the model at a specified interval. Each VLM is unique so be sure to check the docs and model capabilities of each but generally, each VLM also requires an STT provider and in some cases an TTS provider to vocalise the response (some models like Qwen OMNI has TTS built in).
```python theme={null}
llm = moondream.CloudVLM(
api_key="your-api-key", # or set MOONDREAM_API_KEY env var
mode="vqa", # or "caption" for image captioning
)
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Vision Assistant", id="agent"),
llm=llm,
tts=elevenlabs.TTS(),
stt=deepgram.STT(),
)
```
# Overview
Source: https://visionagents.ai/core/overview
Modular architecture for real-time voice and video AI agents
New to Vision Agents? Start with the [Overview](/introduction/overview) and [Quickstart](/introduction/quickstart). This section is API reference — read it after your first agent runs.
The SDK is built around a modular architecture where each component can be independently configured and extended. At its heart is the `Agent` class, which orchestrates all other components to create intelligent, interactive experiences. The Agent class is transport agnostic, meaning developers can use Stream's low-latency Video transport by default or bring custom transports.
### Design Philosophy
* **Modularity**: Each component (LLM, STT, TTS, Turn Detection, Processors) can be independently configured
* **Provider Agnostic**: Support for multiple AI providers through standardised interfaces
* **Event-Driven**: Built on an event system for loose coupling and extensibility
* **Low Latency**: Optimized for real-time voice and video interactions
* **Type Safety**: Rich typing throughout for better developer experience
# Processors Class
Source: https://visionagents.ai/core/processors-core
Processors extend the agent's capabilities by analyzing and transforming audio/video streams in real-time. They can access incoming media, publish transformed streams back to the call, and (in plugin implementations) feed detection results or external data to the LLM.
Common use cases:
* **Video analysis** — Pose detection, object recognition, scene understanding
* **Media transformation** — Video effects, avatars, filters
* **State injection** — Plugin-specific patterns for feeding detection results to the LLM (not automatic in core)
## Class Hierarchy
All processors inherit from the abstract `Processor` base class:
| Class | Purpose |
| ------------------------- | ------------------------------------------------------------------------- |
| `Processor` | Abstract base class with `name`, optional `close()`, and `attach_agent()` |
| `VideoProcessor` | Receives video tracks via `process_video()` |
| `VideoPublisher` | Outputs video via `publish_video_track()` |
| `VideoProcessorPublisher` | Receives and outputs video (e.g., annotated frames) |
| `AudioProcessor` | Receives audio via `process_audio()` |
| `AudioPublisher` | Outputs audio via `publish_audio_track()` |
| `AudioProcessorPublisher` | Receives and outputs audio |
## Base Processor
All processors must implement `name`. The `close()` method is inherited from `Component` with a no-op default — override it to release resources. The `attach_agent()` method is optional.
```python theme={null}
from vision_agents.core.processors import Processor
class MyProcessor(Processor):
name = "my_processor"
async def close(self) -> None:
"""Clean up resources when the agent stops."""
pass
def attach_agent(self, agent: "Agent") -> None:
"""Called once when the processor is registered with an agent.
Use this to access agent.events for custom event registration.
"""
self._agent = agent
```
## Video Processor
Receives video tracks from participants. The agent provides a shared `VideoForwarder` that distributes frames to all processors.
```python theme={null}
from typing import Optional
import aiortc
from vision_agents.core.processors import VideoProcessor
from vision_agents.core.utils.video_forwarder import VideoForwarder
class MyVideoProcessor(VideoProcessor):
name = "my_video_processor"
def __init__(self):
self._forwarder: Optional[VideoForwarder] = None
async def process_video(
self,
track: aiortc.VideoStreamTrack,
participant_id: Optional[str],
shared_forwarder: Optional[VideoForwarder] = None,
) -> None:
"""Called when a participant publishes video."""
self._forwarder = shared_forwarder
self._forwarder.add_frame_handler(
self._on_frame, fps=5.0, name="my_handler"
)
async def _on_frame(self, frame):
# Process the frame
pass
async def stop_processing(self) -> None:
"""Called when video tracks are removed."""
if self._forwarder:
await self._forwarder.remove_frame_handler(self._on_frame)
self._forwarder = None
async def close(self) -> None:
await self.stop_processing()
```
## Video Publisher
Outputs a video track to the call (e.g., AI-generated video or avatars).
```python theme={null}
import aiortc
from vision_agents.core.processors import VideoPublisher
from vision_agents.core.utils.video_track import QueuedVideoTrack
class MyVideoPublisher(VideoPublisher):
name = "my_video_publisher"
def __init__(self):
self._track = QueuedVideoTrack(width=1280, height=720, fps=30)
def publish_video_track(self) -> aiortc.VideoStreamTrack:
"""Return the track to publish."""
return self._track
async def close(self) -> None:
self._track.stop()
```
## Video Processor + Publisher
For processors that receive video and output transformed frames (e.g., object detection with annotations).
```python theme={null}
import av
from typing import Optional
import aiortc
from vision_agents.core.processors import VideoProcessorPublisher
from vision_agents.core.utils.video_forwarder import VideoForwarder
from vision_agents.core.utils.video_track import QueuedVideoTrack
class AnnotationProcessor(VideoProcessorPublisher):
name = "annotation_processor"
def __init__(self, fps: int = 30):
self.fps = fps
self._forwarder: Optional[VideoForwarder] = None
self._track = QueuedVideoTrack()
async def process_video(
self,
track: aiortc.VideoStreamTrack,
participant_id: Optional[str],
shared_forwarder: Optional[VideoForwarder] = None,
) -> None:
self._forwarder = shared_forwarder
self._forwarder.add_frame_handler(
self._process_frame, fps=float(self.fps), name="annotator"
)
async def _process_frame(self, frame: av.VideoFrame):
# Transform the frame
annotated = self._annotate(frame)
await self._track.add_frame(annotated)
def _annotate(self, frame: av.VideoFrame) -> av.VideoFrame:
# Add annotations to frame
return frame
def publish_video_track(self) -> aiortc.VideoStreamTrack:
return self._track
async def stop_processing(self) -> None:
if self._forwarder:
await self._forwarder.remove_frame_handler(self._process_frame)
self._forwarder = None
async def close(self) -> None:
await self.stop_processing()
self._track.stop()
```
## Audio Processor
Receives audio data from participants. Audio is delivered as `PcmData` chunks.
```python theme={null}
from getstream.video.rtc import PcmData
from vision_agents.core.processors import AudioProcessor
class MyAudioProcessor(AudioProcessor):
name = "my_audio_processor"
async def process_audio(self, audio_data: PcmData) -> None:
"""Process incoming audio.
Args:
audio_data: Contains samples, sample_rate, channels, and participant info.
"""
samples = audio_data.samples
participant = audio_data.participant
# Process audio...
async def close(self) -> None:
pass
```
## Audio Publisher
Outputs an audio track to the call.
```python theme={null}
import aiortc
from vision_agents.core.processors import AudioPublisher
class MyAudioPublisher(AudioPublisher):
name = "my_audio_publisher"
def publish_audio_track(self) -> aiortc.AudioStreamTrack:
"""Return the audio track to publish."""
return self._audio_track
async def close(self) -> None:
pass
```
## Usage
Pass processors to the agent at initialization:
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import getstream, openai
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="AI Assistant", id="agent"),
llm=openai.Realtime(),
processors=[your_processor],
)
```
For complete examples including YOLO pose detection and object detection, see [Building Video Processors](/guides/video-processors).
# Realtime Class
Source: https://visionagents.ai/core/realtime-core
The Realtime component provides end-to-end speech-to-speech communication, combining STT, LLM, and TTS functionality in a single, optimized interface. It delivers ultra-low latency speech processing, direct audio streaming without intermediate text conversion, and support for multiple modalities (audio, video, text).
## When to Use Realtime
Use a **Realtime** LLM when you want the lowest latency voice interactions. The model handles speech recognition, response generation, and speech synthesis natively—no separate STT or TTS services required.
Use the **traditional STT → LLM → TTS** pipeline when you need custom voices (e.g., Cartesia, ElevenLabs), specific transcription providers, or models that don't support realtime audio.
## Supported Providers
* [OpenAI Realtime](/integrations/realtime/openai) — WebRTC-based, supports video
* [Gemini Live](/integrations/realtime/gemini) — WebSocket-based, supports video
* [AWS Nova](/integrations/realtime/aws-bedrock) — WebSocket-based
* [Qwen Omni](/integrations/realtime/qwen) — Native audio support
## Basic Usage
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import openai, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="AI Assistant", id="agent"),
instructions="You're a helpful voice assistant",
llm=openai.Realtime(model="gpt-realtime", voice="marin"),
processors=[]
)
```
## Agent methods with realtime
```python theme={null}
await agent.simple_response("What do you see in the video?", interrupt=True)
```
Use `agent.simple_response(...)` to inject text prompts. `agent.say()` is not supported with Realtime LLMs — use `simple_response()` instead. You usually do not call realtime audio methods directly from app code.
## Properties
| Property | Type | Description |
| ------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `connected` | `bool` | `True` if the realtime session is active |
| `fps` | `int` | Video frames per second sent to the model (default: 1) |
| `session_id` | `str` | UUID identifying the current session |
| `epoch` | `int` | Monotonic interruption counter. Increments each time `interrupt()` is called, allowing stale audio output events to be identified and dropped. |
## Realtime methods
### `interrupt()`
Increments the epoch counter so that any in-flight audio output from a previous response is detected as stale and discarded by the Agent. The Agent calls this automatically on barge-in.
## Events
The Realtime class emits a small set of events for connection state:
| Event | Description |
| --------------------------- | --------------------------------------------------------- |
| `RealtimeConnectedEvent` | Connection established with session config & capabilities |
| `RealtimeDisconnectedEvent` | Connection closed (includes `reason` and `clean` flag) |
For conversation events, subscribe to the agent-level events — `UserTranscriptEvent` fires in both classic STT and realtime modes:
```python theme={null}
from vision_agents.core.agents.events import UserTranscriptEvent
@agent.events.subscribe
async def on_user_speech(event: UserTranscriptEvent):
print(f"User said: {event.text}")
```
See [Events Reference](/reference/events-reference) for the full event surface, including LLM, tool, and error events.
For provider-specific parameters and configuration, see the integration docs for [OpenAI](/integrations/realtime/openai), [Gemini](/integrations/realtime/gemini), [AWS Bedrock](/integrations/realtime/aws-bedrock), or [Qwen](/integrations/realtime/qwen).
# Speech-to-Text and Text-to-Speech Class
Source: https://visionagents.ai/core/stt-tts-core
LLMs not running using a `Realtime` model requires some help to convert the user's speech and LLM responses into something the user can speak to and hear. To achieve this, the `Agent` class exposes two parameters `tts` and `stt` allowing developers to pass in any text-to-speech and speech-to-text service they like. Using this method, the output voices can be configured, the transcription rate can be adjusted and more.
Internally, the Agent class handles the management between these services and things such as setting up the audio track for the STT providers as an example.
### STT (Speech-to-Text)
STT components convert audio input into text for processing by the LLM. All implementations follow a standardised interface with consistent event emission.
These components process real-time audio with `PcmData` objects from `getstream.video.rtc.track_util`, provide partial transcript support for responsive UI, and include comprehensive error handling and connection management. Multiple providers are supported including Deepgram, ElevenLabs, Fast Whisper, and others.
The Agent starts STT automatically during `join()`. Plugin authors may override `start()` to initialize provider connections; do not call `start()` manually or twice — a second call raises `ValueError`.
Some STT providers include built-in turn detection (indicated by the `turn_detection` property). When this is the case, the Agent automatically skips any separately configured `TurnDetector` to avoid conflicts.
#### STT Methods
| Method | Description |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `start()` | Initialize connections and resources. Called automatically by the Agent on join; override in plugins |
| `process_audio(pcm_data, participant)` | Process an audio frame (\~20ms chunks) |
| `clear()` | Clear any pending audio or internal state |
| `close()` | Clean up resources |
#### STT Events
| Event | Description |
| ---------------------- | ------------------------------------------------- |
| `STTConnectedEvent` | STT connection established |
| `STTDisconnectedEvent` | STT connection closed (with `reason` and `clean`) |
| `STTErrorEvent` | Temporary, recoverable error |
Final user transcripts surface on the agent as `UserTranscriptEvent` (from `vision_agents.core.agents.events`) — not as a separate STT event — so the same handler works in both classic STT and realtime modes. See [Events Reference](/reference/events-reference).
### TTS (Text-to-Speech)
TTS components convert LLM responses into audio output. They handle audio synthesis and streaming to the output track.
These components provide streaming audio synthesis for low latency, multiple voice options and customisation, audio format standardisation using `PcmData` and `AudioFormat` from `getstream.video.rtc.track_util`, and support for providers like ElevenLabs, Cartesia, and others.
#### TTS Methods
| Method | Description |
| ----------------------------------- | --------------------------------------------------------------------- |
| `stream_audio(text, ...)` | Plugin implementer API — convert text to `PcmData` or byte chunks |
| `send_iter(text, participant=None)` | Convert text to speech and yield `TTSOutputChunk` items |
| `stop_audio()` | Clear the audio queue and stop current playback |
| `interrupt()` | Increment the interruption epoch and cancel stale in-flight synthesis |
| `close()` | Clean up resources |
Output is standardized as `PcmData` through the inference pipeline; the Agent re-chunks audio to 20ms frames for the outbound track.
#### TTS Events
| Event | Description |
| --------------------------- | ------------------------------------------------------------------------------------------------- |
| `TTSSynthesisStartEvent` | Synthesis has begun for a text input |
| `TTSSynthesisCompleteEvent` | Synthesis finished (includes metrics like `synthesis_time_ms`, `chunk_count`, `real_time_factor`) |
| `TTSConnectedEvent` | TTS connection established |
| `TTSDisconnectedEvent` | TTS connection closed (with `reason` and `clean`) |
| `TTSErrorEvent` | Temporary, recoverable error |
#### Interruption support
The TTS base class exposes an `epoch` property and an `interrupt()` method for handling barge-in scenarios:
| Member | Type | Description |
| ------------- | ------- | --------------------------------------------------------------------------------------------------- |
| `epoch` | `int` | Monotonic counter that increments on each interruption. Used to identify stale audio events. |
| `interrupt()` | `async` | Increments the epoch and stops the current audio synthesis. Stale events are automatically dropped. |
You usually do not need to call `interrupt()` manually — `agent.simple_response(..., interrupt=True)` and `agent.say(..., interrupt=True)` route interruption through the active inference flow.
# Telemetry & Metrics
Source: https://visionagents.ai/core/telemetry
Vision Agents provides built-in observability through [OpenTelemetry](https://opentelemetry.io/). Collect metrics and traces across all components to monitor performance, latency, and errors in your agents.
## Quick Start
To enable metrics collection, configure OpenTelemetry:
```python theme={null}
# 1. Configure OpenTelemetry
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.exporter.prometheus import PrometheusMetricReader
from prometheus_client import start_http_server
start_http_server(9464)
reader = PrometheusMetricReader()
metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))
# 2. Now import and create your agent
from vision_agents.core import Agent, User
from vision_agents.plugins import deepgram, getstream, openai
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Metrics Agent", id="agent"),
llm=openai.LLM(model="gpt-4o"),
stt=deepgram.STT(),
tts=deepgram.TTS(),
)
```
Metrics are now available at `http://localhost:9464/metrics`.
## MetricsCollector
The `MetricsCollector` records metrics via normalized `on_*()` hooks that plugins call directly. Each `Agent` creates a root collector and merges child collectors from STT, TTS, LLM, and other components during initialization, so metrics collection is enabled by default.
If no OpenTelemetry providers are configured, metrics are no-ops and have no performance impact.
For new integrations, prefer the collector's normalized `on_*` metric hooks and `agent.metrics` over provider-specific event classes.
Plugins record metrics through hooks such as:
* **LLM** — `on_llm_response`, `on_tool_call`, `on_llm_error`
* **STT** — `on_stt_transcript`, `on_stt_error`
* **TTS** — `on_tts_synthesis`, `on_tts_error`
* **Turn Detection** — `on_turn_ended`
* **Realtime LLM** — `on_realtime_audio_input/output`, `on_realtime_user/agent_transcription`
* **VLM** — `on_vlm_inference`, `on_vlm_error`
* **Video Processors** — `on_video_detection`
### Metric Attributes
All metrics include contextual attributes:
| Attribute | Description |
| ------------ | -------------------------------------------- |
| `provider` | The plugin name (e.g., `openai`, `deepgram`) |
| `model` | Model identifier when available |
| `error_type` | Exception class name for error metrics |
| `error_code` | Error code when available |
## Metrics Reference
All metrics use the `vision_agents.core` meter namespace.
### STT Metrics
| Metric | Type | Unit | Description |
| ----------------------- | --------- | ---- | ------------------------------------- |
| `stt.latency.ms` | Histogram | ms | Processing latency for speech-to-text |
| `stt.audio_duration.ms` | Histogram | ms | Duration of audio processed |
| `stt.errors` | Counter | — | Total STT errors |
### TTS Metrics
| Metric | Type | Unit | Description |
| ----------------------- | --------- | ---- | ----------------------------- |
| `tts.latency.ms` | Histogram | ms | Synthesis latency |
| `tts.audio_duration.ms` | Histogram | ms | Duration of synthesized audio |
| `tts.characters` | Counter | — | Characters synthesized |
| `tts.errors` | Counter | — | Total TTS errors |
### LLM Metrics
| Metric | Type | Unit | Description |
| ---------------------------- | --------- | ---- | -------------------------------------- |
| `llm.latency.ms` | Histogram | ms | Response latency (request to complete) |
| `llm.time_to_first_token.ms` | Histogram | ms | Time to first token (streaming) |
| `llm.tokens.input` | Counter | — | Input/prompt tokens consumed |
| `llm.tokens.output` | Counter | — | Output/completion tokens generated |
| `llm.tool_calls` | Counter | — | Tool/function calls executed |
| `llm.tool_latency.ms` | Histogram | ms | Tool execution latency |
| `llm.errors` | Counter | — | Total LLM errors |
### Turn Detection Metrics
| Metric | Type | Unit | Description |
| -------------------------- | --------- | ---- | --------------------------------- |
| `turn.duration.ms` | Histogram | ms | Duration of detected speech turns |
| `turn.trailing_silence.ms` | Histogram | ms | Silence duration before turn end |
### Realtime LLM Metrics
For speech-to-speech models like OpenAI Realtime:
| Metric | Type | Unit | Description |
| ----------------------------------- | --------- | ----- | ----------------------------- |
| `realtime.sessions` | Counter | — | Sessions started |
| `realtime.session_duration.ms` | Histogram | ms | Session duration |
| `realtime.audio.input.bytes` | Counter | bytes | Audio bytes sent to LLM |
| `realtime.audio.output.bytes` | Counter | bytes | Audio bytes received from LLM |
| `realtime.audio.input.duration.ms` | Counter | ms | Audio duration sent |
| `realtime.audio.output.duration.ms` | Counter | ms | Audio duration received |
| `realtime.responses` | Counter | — | Complete responses received |
| `realtime.transcriptions.user` | Counter | — | User speech transcriptions |
| `realtime.transcriptions.agent` | Counter | — | Agent speech transcriptions |
| `realtime.errors` | Counter | — | Realtime errors |
### VLM / Vision Metrics
| Metric | Type | Unit | Description |
| -------------------------- | --------- | ---- | --------------------------- |
| `vlm.inference.latency.ms` | Histogram | ms | VLM inference latency |
| `vlm.inferences` | Counter | — | Inference requests |
| `vlm.tokens.input` | Counter | — | Input tokens (text + image) |
| `vlm.tokens.output` | Counter | — | Output tokens |
| `vlm.errors` | Counter | — | VLM errors |
### Video Processor Metrics
| Metric | Type | Unit | Description |
| ----------------------------- | --------- | ---- | ------------------------ |
| `video.frames.processed` | Counter | — | Frames processed |
| `video.processing.latency.ms` | Histogram | ms | Frame processing latency |
| `video.detections` | Counter | — | Objects/items detected |
## AgentMetrics
For in-process metrics without external infrastructure, access aggregated metrics directly from the agent:
```python theme={null}
# After running your agent
metrics = agent.metrics
# STT
print(f"Average STT latency: {metrics.stt_latency_ms__avg.value()} ms")
print(f"Total audio processed: {metrics.stt_audio_duration_ms__total.value()} ms")
# TTS
print(f"Average TTS latency: {metrics.tts_latency_ms__avg.value()} ms")
print(f"Characters synthesized: {metrics.tts_characters__total.value()}")
# LLM
print(f"Average LLM latency: {metrics.llm_latency_ms__avg.value()} ms")
print(f"Input tokens: {metrics.llm_input_tokens__total.value()}")
print(f"Output tokens: {metrics.llm_output_tokens__total.value()}")
print(f"Tool calls: {metrics.llm_tool_calls__total.value()}")
```
### Available AgentMetrics
| Metric | Type | Description |
| ------------------------------------------ | ------- | -------------------------------- |
| `stt_latency_ms__avg` | Average | Average STT processing latency |
| `stt_audio_duration_ms__total` | Counter | Total audio duration processed |
| `tts_latency_ms__avg` | Average | Average TTS synthesis latency |
| `tts_audio_duration_ms__total` | Counter | Total synthesized audio duration |
| `tts_characters__total` | Counter | Total characters synthesized |
| `llm_latency_ms__avg` | Average | Average LLM response latency |
| `llm_time_to_first_token_ms__avg` | Average | Average time to first token |
| `llm_input_tokens__total` | Counter | Total input tokens |
| `llm_output_tokens__total` | Counter | Total output tokens |
| `llm_tool_calls__total` | Counter | Total tool calls |
| `llm_tool_latency_ms__avg` | Average | Average tool execution latency |
| `turn_duration_ms__avg` | Average | Average turn duration |
| `turn_trailing_silence_ms__avg` | Average | Average trailing silence |
| `realtime_audio_input_bytes__total` | Counter | Total audio bytes sent |
| `realtime_audio_output_bytes__total` | Counter | Total audio bytes received |
| `realtime_audio_input_duration_ms__total` | Counter | Total input audio duration |
| `realtime_audio_output_duration_ms__total` | Counter | Total output audio duration |
| `realtime_user_transcriptions__total` | Counter | Total user transcriptions |
| `realtime_agent_transcriptions__total` | Counter | Total agent transcriptions |
| `vlm_inference_latency_ms__avg` | Average | Average VLM inference latency |
| `vlm_inferences__total` | Counter | Total VLM inferences |
| `vlm_input_tokens__total` | Counter | Total VLM input tokens |
| `vlm_output_tokens__total` | Counter | Total VLM output tokens |
| `video_frames_processed__total` | Counter | Total frames processed |
| `video_processing_latency_ms__avg` | Average | Average frame processing latency |
## Prometheus Setup
Export metrics to Prometheus for monitoring dashboards and alerting.
**Step 1 — Install the exporter**
```bash theme={null}
uv add opentelemetry-exporter-prometheus prometheus-client
```
**Step 2 — Configure OpenTelemetry**
```python theme={null}
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.exporter.prometheus import PrometheusMetricReader
from prometheus_client import start_http_server
# Start HTTP server for Prometheus scraping
start_http_server(port=9464)
# Configure OpenTelemetry
reader = PrometheusMetricReader()
metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))
```
**Step 3 — Create and run your agent**
```python theme={null}
from vision_agents.core import Agent, AgentLauncher, Runner
agent = Agent(...)
# MetricsCollector is automatically attached
# Run with CLI
Runner(AgentLauncher(create_agent=..., join_call=...)).cli()
```
View metrics at `http://localhost:9464/metrics`.
## Tracing with Jaeger
Trace requests across components for debugging latency issues.
**Step 1 — Install the exporter**
```bash theme={null}
uv add opentelemetry-sdk opentelemetry-exporter-otlp
```
**Step 2 — Configure tracing**
```python theme={null}
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
resource = Resource.create({"service.name": "my-agent"})
provider = TracerProvider(resource=resource)
exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
```
**Step 3 — Run Jaeger**
```bash theme={null}
docker run --rm -it \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 -p 4317:4317 -p 4318:4318 \
jaegertracing/all-in-one:1.51
```
View traces at `http://localhost:16686`.
## Complete Example
```python theme={null}
"""Prometheus metrics example with Vision Agents."""
# Configure OpenTelemetry
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.exporter.prometheus import PrometheusMetricReader
from prometheus_client import start_http_server
start_http_server(9464)
reader = PrometheusMetricReader()
metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))
# Now import agents
from vision_agents.core import Agent, User, AgentLauncher, Runner
from vision_agents.plugins import deepgram, getstream, gemini, elevenlabs
async def create_agent(**kwargs) -> Agent:
return Agent(
edge=getstream.Edge(),
agent_user=User(name="Metrics Agent", id="agent"),
instructions="You're a helpful voice assistant.",
llm=gemini.LLM("gemini-flash-lite-latest"),
tts=elevenlabs.TTS(),
stt=deepgram.STT(),
)
async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
# MetricsCollector is automatically attached to the agent
call = await agent.create_call(call_type, call_id)
async with agent.join(call):
await agent.simple_response("Hello! Metrics are being collected.")
await agent.finish()
# Print summary after call
m = agent.metrics
print(f"LLM latency: {m.llm_latency_ms__avg.value():.1f} ms")
print(f"Tokens: {m.llm_input_tokens__total.value()} in / {m.llm_output_tokens__total.value()} out")
if __name__ == "__main__":
Runner(AgentLauncher(create_agent=create_agent, join_call=join_call)).cli()
```
Run with:
```bash theme={null}
uv run agent.py run --call-type default --call-id test
```
Metrics available at `http://localhost:9464/metrics`.
## Example Prometheus Queries
OpenTelemetry metric names use dots (e.g., `llm.latency.ms`). Prometheus converts these to underscores when scraping (e.g., `llm_latency_ms`).
**Average LLM latency over time:**
```promql theme={null}
rate(llm_latency_ms_sum[5m]) / rate(llm_latency_ms_count[5m])
```
**Total tokens used:**
```promql theme={null}
sum(llm_tokens_input) + sum(llm_tokens_output)
```
**Error rate:**
```promql theme={null}
rate(llm_errors_total[5m])
```
## Best Practices
**Configure OpenTelemetry** - Set up providers to enable metric collection. If no providers are configured, metrics are no-ops.
**MetricsCollector is automatic** - Each Agent automatically creates a MetricsCollector internally. If no OpenTelemetry provider is configured, metrics are no-ops with no performance impact.
**Use AgentMetrics for simple logging** - Access `agent.metrics` directly for in-process metrics without external infrastructure.
**Add resource attributes** - Include service name and environment in your metrics:
```python theme={null}
from opentelemetry.sdk.resources import Resource
resource = Resource.create({
"service.name": "my-agent",
"service.version": "1.0.0",
"deployment.environment": "production",
})
provider = MeterProvider(resource=resource, metric_readers=[reader])
```
**Set up alerting on:**
* LLM latency > 2000ms (p95)
* Error rate > 1%
* Token usage anomalies
## Next Steps
* [Kubernetes Deployment](/guides/kubernetes-deployment) - Helm chart with Prometheus and Grafana out of the box
* [Built-in HTTP Server](/guides/http-server) - Console mode and HTTP server for session management
# Expressive Voice Narrator
Source: https://visionagents.ai/examples/cartesia-narrator
Build a storytelling agent with expressive speech using Cartesia's Sonic 3 TTS
Check out the complete Cartesia Narrator example in our GitHub repository
In this example, we build a storytelling narrator agent using [Cartesia's Sonic 3 TTS](https://cartesia.ai/?utm_medium=partner\&utm_source=getstream) with Vision Agents. The agent narrates stories with highly expressive speech, leveraging Cartesia's audio markup tags to customize the output with emotions, pauses, and vocal effects.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## What You Will Build
* Listen to story topics via voice input using [Cartesia Ink STT](https://cartesia.ai/?utm_medium=partner\&utm_source=getstream)
* Generate creative narratives using [OpenAI's GPT-4o-mini](https://openai.com/)
* Speak with highly expressive and customizable voice using [Cartesia's Sonic 3 TTS](https://cartesia.ai/?utm_medium=partner\&utm_source=getstream)
* Use audio markup tags for enhanced speech control (emotions, pauses, emphasis)
* Run on [Stream's](https://getstream.io/) low-latency edge network
## Next Steps
Explore Cartesia's TTS configuration and audio markup
Real-time virtual try-on with Decart's Lucy-2 model
# Live Sports Commentator
Source: https://visionagents.ai/examples/football-commentator
Build a real-time AI sports commentator using object detection and realtime models
Check out the complete Football Commentator example in our GitHub repository
In this example, we build a real-time sports commentator that combines [Roboflow's RF-DETR](https://github.com/roboflow/rf-detr) for player and ball detection with realtime models from [Gemini](https://ai.google.dev/) and [OpenAI](https://platform.openai.com/docs/guides/realtime). The system annotates video with bounding boxes, detects game events (like when the ball reappears after fast action), and triggers AI commentary. This example also explores the current limitations of realtime video understanding and showcases Vision Agents' ability to hot-swap between providers with minimal code changes.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## What You Will Build
* Detect players and the ball in real time using [Roboflow RF-DETR](https://roboflow.com/model/rf-detr)
* Annotate live video with bounding boxes and detection overlays
* Trigger AI commentary based on game events (ball reappearance, fast plays)
* Compare [Gemini Live](https://ai.google.dev/) vs [OpenAI Realtime](https://platform.openai.com/docs/guides/realtime) with a one-line swap
* Run on [Stream's](https://getstream.io/) low-latency edge network
## Next Steps
Explore Roboflow detection models and configuration
See another video processing example with YOLO pose detection
# AI Golf Coach
Source: https://visionagents.ai/examples/golf-coach
Build a real-time golf coaching agent with YOLO pose detection and voice feedback
Check out the complete AI Golf Coach example in our GitHub repository
Build a real-time golf coach that watches your swing via camera and gives spoken feedback. It combines [Ultralytics YOLO](https://docs.ultralytics.com/tasks/pose/) pose detection to analyze body position with [Gemini Live](https://gemini.google/overview/gemini-live/) for real-time coaching — all running on [Stream's](https://getstream.io/) low-latency edge network. This pattern applies to any video coaching use case: sports training, physical therapy, workout guidance, or drone monitoring.
Complete the [Quickstart](/introduction/quickstart) first. This example adds a **video processor** on top of a Realtime LLM.
## What You Will Build
* Analyze golf swings in real time using [YOLO](https://www.ultralytics.com/yolo) pose detection
* Process video at configurable FPS with [Gemini Live](https://ai.google.dev/) or [OpenAI Realtime](https://platform.openai.com/docs/guides/realtime)
* Deliver spoken coaching feedback based on body position and movement
* Hot-swap between AI providers with a one-line config change
## Prerequisites
```bash theme={null}
STREAM_API_KEY=
STREAM_API_SECRET=
GOOGLE_API_KEY=
```
If using OpenAI instead of Gemini, also add `OPENAI_API_KEY`.
## Run the example
Clone the repo and install dependencies from the root:
```bash theme={null}
git clone git@github.com:GetStream/Vision-Agents.git
cd Vision-Agents
uv sync
```
Create a `.env` file at the repo root with your Stream and Gemini API keys.
From the example directory:
```bash theme={null}
cd examples/02_golf_coach_example
uv run golf_coach_example.py run
```
The CLI opens a browser demo with your camera. Position yourself so the camera can see your full body, then perform a golf swing. The agent analyzes your form and gives spoken feedback.
## How it works
The agent combines a Realtime LLM with a YOLO pose processor:
```python theme={null}
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="AI golf coach"),
instructions="Read @golf_coach.md",
llm=gemini.Realtime(fps=3),
processors=[
ultralytics.YOLOPoseProcessor(model_path="yolo26n-pose.pt")
],
)
```
1. **Video capture** — the user's camera feeds video to the agent
2. **Pose detection** — YOLO analyzes each frame and extracts body position data
3. **LLM processing** — Gemini Realtime receives video frames at 3 FPS
4. **Feedback** — the agent speaks coaching guidance based on `golf_coach.md` instructions
The `fps=3` parameter controls how many frames per second are sent to the model. Higher FPS gives more detail but uses more tokens.
## Customize
* **Change FPS**: `gemini.Realtime(fps=5)` for lower cost, `fps=10` for more detail
* **Switch to OpenAI**: replace with `openai.Realtime(fps=3)` in `agent.py`
* **Edit coaching style**: modify `golf_coach.md` in the example directory
* **Different YOLO models**: use `YOLOProcessor` for object detection instead of pose estimation
## Next Steps
VLMs, processors, and realtime video patterns
Build custom detection and analysis pipelines
Use Roboflow object detection for multi-object tracking
Explore YOLO model options and configuration
# Phone Support Agent
Source: https://visionagents.ai/examples/phone-and-rag
Build voice agents that answer phone calls with RAG-powered knowledge retrieval
Check out the complete Phone & RAG example in our GitHub repository
Builds on the [Twilio Phone Agent](/examples/twilio-phone-agent) example — complete ngrok and Twilio webhook setup there first. This tutorial adds RAG so your phone agent can answer questions from a knowledge base using [Gemini FileSearch](https://ai.google.dev/gemini-api/docs/file-search), [TurboPuffer](https://turbopuffer.com/), or Qdrant.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## What You Will Build
* Answer inbound phone calls with an AI agent that retrieves knowledge in real time
* Make outbound calls programmatically for tasks like booking reservations
* Swap RAG backends with a single environment variable
* Handle bidirectional audio via Twilio Media Streams over WebSocket
## Prerequisites
Complete the [Twilio Phone Agent](/examples/twilio-phone-agent) setup first (`.env`, ngrok, Twilio webhook). Add these variables depending on your RAG backend:
```bash theme={null}
# Required for all backends
STREAM_API_KEY=
STREAM_API_SECRET=
GOOGLE_API_KEY=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
NGROK_URL=your-subdomain.ngrok-free.app
# TurboPuffer only
TURBO_PUFFER_KEY=
```
## Add RAG to your phone agent
Set `RAG_BACKEND` when running the inbound example:
| Backend | `RAG_BACKEND` | Notes |
| ----------------- | ------------------ | ----------------------------------------------------------- |
| Gemini FileSearch | `gemini` (default) | Simplest setup — automatic chunking and embedding |
| TurboPuffer | `turbopuffer` | Hybrid vector + keyword search; requires `TURBO_PUFFER_KEY` |
| Qdrant | `qdrant` | Hybrid search with function calling |
See the [RAG guide](/guides/rag) for backend details.
The example ships with sample docs in `examples/03_phone_and_rag_example/knowledge/` covering Stream Chat, Video, and Feeds APIs. Add your own `.md` files to this directory or replace them with product documentation.
From the example directory:
```bash theme={null}
cd examples/03_phone_and_rag_example
RAG_BACKEND=gemini NGROK_URL=your-subdomain.ngrok-free.app \
uv run inbound_phone_and_rag_example.py
```
Swap `RAG_BACKEND=turbopuffer` or `RAG_BACKEND=qdrant` to try other backends.
Call your Twilio number and ask questions the knowledge base can answer, for example:
* "What APIs does Stream offer for chat?"
* "Tell me about Stream Video features."
* "How does Stream Feeds work?"
The agent retrieves relevant chunks before responding.
## How RAG connects to the phone agent
The inbound script (`inbound_phone_and_rag_example.py`) wires RAG into the agent based on `RAG_BACKEND`:
* **Gemini** — uses `gemini.GeminiFilesearchRAG` with File Search tools on the LLM
* **TurboPuffer / Qdrant** — registers a search function the LLM calls during conversation
Phone plumbing is unchanged from the [Twilio Phone Agent](/examples/twilio-phone-agent): Twilio webhooks start the media stream, `attach_phone_to_call` bridges audio to Stream, and the agent runs inside the Stream call.
For outbound calls with RAG, extend `outbound_phone_example.py` using the same RAG initialization pattern from the inbound script.
## Next Steps
Learn more about RAG backends and knowledge retrieval
Plugin API reference and components
Deploy your phone bot with the built-in HTTP server
Move from local dev to production
# AI Meeting Copilot
Source: https://visionagents.ai/examples/sales-assistant
Build a real-time sales assistant that listens to meetings and surfaces coaching suggestions
Check out the complete Sales Assistant example in our GitHub repository
In this example, we build a real-time AI meeting copilot that silently listens to your microphone and system audio during sales calls, interviews, or meetings. It transcribes the conversation with speaker diarization using [AssemblyAI](https://www.assemblyai.com/), analyzes the dialogue with [Gemini](https://ai.google.dev/), and surfaces coaching suggestions on a translucent macOS overlay — invisible to other participants. The agent can be extended with RAG and custom knowledge bases to tailor suggestions to your product, company playbook, or deal context.
The project has two components: a Python agent backend built with Vision Agents, and a companion [macOS overlay app](https://github.com/GetStream/vision-agents-sales-assistant-demo) built with Flutter that captures audio and displays suggestions.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## What You Will Build
* Silently listen to meeting audio (microphone + system audio) in real time
* Transcribe conversations with speaker diarization using [AssemblyAI](https://www.assemblyai.com/)
* Generate contextual coaching suggestions with [Gemini](https://ai.google.dev/)
* Display suggestions on a translucent macOS overlay via [Stream Chat](https://getstream.io/chat/)
* Run the agent as an HTTP server that the companion app connects to
## Next Steps
Learn how the agent server handles session management
Explore AssemblyAI's STT with speaker diarization
# Smart Security Camera
Source: https://visionagents.ai/examples/security-camera
Build a security camera with face recognition, package detection, and automated theft alerts
Check out the complete Security Camera example in our GitHub repository
In this example, we build a real-time security monitoring system using [face\_recognition](https://github.com/ageitgey/face_recognition) for identifying visitors, [YOLOv11](https://docs.ultralytics.com/models/yolo11/) for detecting packages, and [Gemini](https://ai.google.dev/) for conversational AI — all running on [Stream's](https://getstream.io/) edge network. The agent tracks visitors, monitors packages, generates "wanted posters" when packages disappear, and answers natural language questions like "How many people visited?" or "What happened while I was away?"
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## What You Will Build
* Detect and recognize faces in real time with [face\_recognition](https://github.com/ageitgey/face_recognition), including named face registration
* Monitor packages using a custom [YOLOv11](https://docs.ultralytics.com/models/yolo11/) detection model
* Detect package theft and automatically generate wanted posters
* Post alerts to [X (Twitter)](https://developer.x.com) with suspect photos
* Ask the AI questions about security activity via voice ("Who visited?", "Any packages delivered?")
* Run on [Stream's](https://getstream.io/) low-latency edge network
## Next Steps
Explore YOLO model options for object detection
Docker setup and environment configuration
# Voice Agent Starter
Source: https://visionagents.ai/examples/simple-agent
Build a conversational voice AI agent that listens, thinks, and responds in real time
Check out the complete Simple Agent example in our GitHub repository
Build a custom STT → LLM → TTS voice agent with [Gemini](https://ai.google.dev/) for reasoning, [Deepgram](https://deepgram.com/) for speech recognition, and [ElevenLabs](https://elevenlabs.io/) for natural-sounding responses. The agent joins a video call, handles voice conversation, and can observe the camera feed.
Complete the [Quickstart](/introduction/quickstart) first. This example uses a **custom pipeline** (not Realtime mode) — see [Voice Agents](/introduction/voice-agents) for the same pattern with additional providers.
## What You Will Build
* Listen to user speech and convert it to text with [Deepgram](https://deepgram.com/) STT
* Process conversations using [Gemini](https://ai.google.dev/) with function calling (weather tool)
* Respond with natural-sounding speech via [ElevenLabs](https://elevenlabs.io/) TTS
* Run on [Stream's](https://getstream.io/) low-latency edge network
## Prerequisites
API keys for Stream, Gemini, Deepgram, and ElevenLabs. Free tiers are available from each provider.
```bash theme={null}
STREAM_API_KEY=
STREAM_API_SECRET=
GOOGLE_API_KEY=
DEEPGRAM_API_KEY=
ELEVENLABS_API_KEY=
```
## Run the example
Clone the repo and install dependencies from the root:
```bash theme={null}
git clone git@github.com:GetStream/Vision-Agents.git
cd Vision-Agents
uv sync
```
Create a `.env` file at the repo root with your API keys (see Prerequisites above).
From the example directory:
```bash theme={null}
cd examples/01_simple_agent_example
uv run simple_agent_example.py run
```
The CLI opens a browser demo. Join the call and speak to the agent. Ask about the weather to trigger the registered `get_weather` function.
## How it works
The agent uses a custom pipeline instead of a Realtime model:
```python theme={null}
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="My happy AI friend", id="agent"),
instructions=INSTRUCTIONS,
llm=setup_llm(), # gemini.LLM() with @register_function
tts=elevenlabs.TTS(model_id="eleven_flash_v2_5"),
stt=deepgram.STT(eager_turn_detection=True),
)
```
Audio flows: user speaks → **Deepgram STT** transcribes → **Gemini LLM** generates a response (or calls a tool) → **ElevenLabs TTS** speaks it back.
Deepgram's `eager_turn_detection=True` reduces latency by starting LLM inference before the user fully stops speaking.
## Customize
* **Swap providers**: any STT, LLM, and TTS plugin works — see [Integrations](/integrations/introduction-to-integrations).
* **Use Realtime instead**: replace the pipeline with `llm=gemini.Realtime()` and remove `stt` and `tts` — same pattern as the [Quickstart](/introduction/quickstart).
* **Add processors**: pass items to `processors=[]` for video analysis — see [AI Golf Coach](/examples/golf-coach).
## Next Steps
Add video processing with YOLO pose detection
Custom pipelines and function calling
Swap in any of 35+ supported AI providers
# Twilio Phone Agent
Source: https://visionagents.ai/examples/twilio-phone-agent
Step-by-step guide to inbound and outbound phone calls with Twilio Media Streams
Both `outbound_phone_example.py` and `inbound_phone_and_rag_example.py` live in this folder
Build inbound and outbound phone agents with [Twilio](https://www.twilio.com/) Media Streams, [Stream](https://getstream.io/) edge transport, and Gemini. This tutorial covers phone plumbing only — no RAG. For knowledge retrieval on calls, continue to [Phone Support Agent (RAG)](/examples/phone-and-rag).
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## What You Will Build
* Make an outbound call programmatically (e.g. call your cell to test audio)
* Answer inbound calls on your Twilio number with a voice AI agent
* Handle bidirectional audio via Twilio Media Streams over WebSocket
* Bridge phone audio into a Stream call with `attach_phone_to_call`
For optimal latency, deploy in **US-east**. Local development adds round-trip
latency through ngrok and your machine.
## Prerequisites
Create a `.env` file at the [Vision Agents](https://github.com/GetStream/Vision-Agents) repo root:
```bash theme={null}
STREAM_API_KEY=
STREAM_API_SECRET=
GOOGLE_API_KEY=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
```
You also need [ngrok](https://ngrok.com/) and a Twilio phone number. See the [Twilio integration](/integrations/telephony/twilio) for webhook and API details.
## Run the example
Clone the repo and install dependencies from the root:
```bash theme={null}
git clone git@github.com:GetStream/Vision-Agents.git
cd Vision-Agents
uv sync
```
Expose port 8000 so Twilio can reach your local server:
```bash theme={null}
ngrok http 8000
```
Copy the HTTPS hostname (without `https://`) — you'll use it as `NGROK_URL`.
In the [Twilio Console](https://console.twilio.com/):
1. Go to **Phone Numbers → Manage → Active numbers**
2. Select your number (or buy one)
3. Under **Voice Configuration**, set **A call comes in** to **Webhook**
4. Enter `https:///twilio/voice` with method **HTTP POST**
See [Twilio integration](/integrations/telephony/twilio) for how the webhook handler works.
In a new terminal, from the example directory:
```bash theme={null}
cd examples/03_phone_and_rag_example
NGROK_URL=your-subdomain.ngrok-free.app uv run outbound_phone_example.py \
--from +15551234567 \
--to +15557654321
```
Replace with your Twilio number (`--from`) and a destination you can answer (`--to`, often your cell). This starts the HTTP server and initiates the outbound call.
With ngrok and your Twilio webhook still configured, start the inbound server:
```bash theme={null}
cd examples/03_phone_and_rag_example
NGROK_URL=your-subdomain.ngrok-free.app uv run inbound_phone_and_rag_example.py
```
RAG is optional at this stage — the agent runs with Gemini even without extra RAG configuration.
Dial your Twilio number from any phone. You should hear the AI agent answer and respond in real time.
## How it works
Twilio uses [TwiML](https://www.twilio.com/docs/voice/twiml) to control calls. The voice webhook returns a `` response that pipes audio to your WebSocket:
1. **`POST /twilio/voice`** — validates the Twilio signature, registers the call in `TwilioCallRegistry`, returns TwiML with a media stream URL
2. **`WS /twilio/media/{call_id}/{token}`** — accepts the WebSocket, runs `TwilioMediaStream`, validates the token
3. **`attach_phone_to_call`** — bridges Twilio mulaw audio ↔ the Stream call where your agent runs
The inbound script uses `ProxyHeadersMiddleware` so signature validation works when ngrok terminates HTTPS.
## Next Steps
Add Gemini FileSearch or TurboPuffer knowledge retrieval
Plugin API reference and components
Provider overview and learning path
Alternative telephony provider
# Video Call Moderator
Source: https://visionagents.ai/examples/video-moderator
Build a real-time video moderator that detects, censors, and escalates with verbal warnings
Check out the complete Moderation example in our GitHub repository
In this example, we build a real-time video moderation agent that detects offensive gestures using a custom [Roboflow](https://roboflow.com/) model running locally, censors them with a Gaussian blur, and issues escalating verbal warnings — ultimately kicking the user from the call on the third offense. The agent uses [Gemini Flash Lite](https://ai.google.dev/) for language and [Deepgram](https://deepgram.com/) for speech, processing video at 15 FPS with local inference for low-latency detection.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## What You Will Build
* Detect offensive content in real time using a custom [Roboflow](https://roboflow.com/) model with local inference
* Censor detected content with Gaussian blur applied directly to the video stream
* Issue escalating verbal warnings via [Gemini Flash Lite](https://ai.google.dev/) and [Deepgram](https://deepgram.com/) TTS
* Automatically kick users from the call on the third offense via the [Stream](https://getstream.io/) API
* Run detection locally with no cloud round-trip per frame for low latency
## Next Steps
Train or find your own detection model on Roboflow
Docker setup and environment configuration
# Live Video Try-On
Source: https://visionagents.ai/examples/visual-storyteller
Build a real-time virtual try-on agent with Decart's Lucy-2 model
Check out the complete virtual try-on example in our GitHub repository
Build a real-time virtual try-on agent using [Vision Agents](https://visionagents.ai/) and [Decart](https://decart.ai/). Powered by Decart's **Lucy-2** real-time model (`lucy_2_rt`), the agent listens for voice requests and restyles your video feed so you appear to be wearing different outfits — driven by both a text prompt and a reference image.
Lucy-2 is purpose-built for virtual try-on and costume-swap use cases. It accepts a reference image alongside a prompt, enabling accurate outfit transfer onto the user's live video. Prompt and image updates are applied atomically via `update_state`, so the output video never shows a half-updated frame.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## What you will build
* Listen to voice input and swap outfits in real time
* Use [Decart](https://decart.ai/) Lucy-2 to restyle your video feed with both a prompt and a reference image
* Atomically swap costumes via `processor.update_state(prompt=..., image=...)`
* Fall back to prompt-only changes for freeform outfit requests
* Speak with an expressive voice using [ElevenLabs](https://elevenlabs.io/)
* Run on [Stream's](https://getstream.io/) low-latency edge network
## Next steps
Explore Decart's video restyling and try-on capabilities
See another storytelling example with Cartesia's expressive TTS
# Phone Calling
Source: https://visionagents.ai/guides/calling
Connect Vision Agents to PSTN phone calls. A telephony provider handles the phone network; your FastAPI server receives webhooks, streams audio over WebSocket, and bridges it into a Stream call where your agent runs.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Choose a provider
Media Streams, TwiML, built-in webhook helpers — default in our examples
Call Control API, bidirectional media streaming
## How phone agents work
Every phone integration follows the same pattern, regardless of provider:
1. **Webhook** — the provider notifies your server of an incoming call (or you initiate an outbound call via REST API)
2. **Register** — create a call entry in a provider registry with a secret token
3. **Stream URL** — tell the provider to open a WebSocket media stream to your server
4. **Bridge** — `attach_phone_to_call` connects phone audio ↔ a Stream call where your agent listens and responds
```mermaid theme={null}
flowchart LR
subgraph twilio [Twilio]
T1[Voice webhook] --> T2[TwiML Connect Stream]
T2 --> T3[Media Streams WS]
end
subgraph telnyx [Telnyx]
X1[Call Control webhook] --> X2[Answer or Dial API]
X2 --> X3[Media Streaming WS]
end
T3 --> Bridge[attach_phone_to_call]
X3 --> Bridge
Bridge --> Stream[Stream call plus Agent]
```
## Shared building blocks
Both telephony plugins expose the same core primitives:
| Building block | Role |
| ---------------------- | ---------------------------------------------------------------------------------- |
| Call registry | Tracks active calls, assigns tokens, optional async `prepare` to warm up the agent |
| Media stream | WebSocket handler that receives/sends phone audio |
| `attach_phone_to_call` | Bridges provider audio ↔ Stream WebRTC participant |
| Tokenized media URL | Provider connects to `wss:////media/{call_id}/{token}` |
Provider-specific differences:
| | Twilio | Telnyx |
| ---------------- | ------------------------------------ | ---------------------------------------------- |
| Answer mechanism | Return TwiML with `` | Call Control Answer/Dial API with `stream_url` |
| Webhook auth | `verify_twilio_signature` (built-in) | Ed25519 verification (see plugin examples) |
| Audio encoding | mulaw 8 kHz | PCMU/PCMA 8 kHz or L16 16 kHz |
## Prerequisites
Before starting any phone example:
* [Stream](https://getstream.io/try-for-free/) API key and secret
* A telephony provider account with at least one phone number
* [ngrok](https://ngrok.com/) (or another HTTPS tunnel) for local development
* A public webhook URL pointing at your FastAPI server
**Prefer managed hosting?** [Stream Voice AI](https://getstream.io/video/voice-ai/) runs production voice agents on Stream's global edge, phone numbers, web and mobile clients, co-located STT/LLM/TTS, and built-in observability. [Join the waitlist](https://getstream.io/video/voice-ai/) for early access.
## Learning path
Follow this order:
1. **[Twilio Phone Agent](/examples/twilio-phone-agent)** — get your first inbound and outbound call working
2. **[Phone Support Agent (RAG)](/examples/phone-and-rag)** — add knowledge retrieval on top
3. **[Twilio](/integrations/telephony/twilio)** or **[Telnyx](/integrations/telephony/telnyx)** integration pages — API reference when building your own server
Step-by-step phone tutorial
Add knowledge retrieval to calls
## Quick orientation
Run `ngrok http 8000` and note the HTTPS hostname for `NGROK_URL`.
Configure your provider to send call events to your FastAPI endpoints (e.g. `https:///twilio/voice` for Twilio).
Follow the [Twilio Phone Agent](/examples/twilio-phone-agent) tutorial for full commands. Dial your Twilio number to verify end-to-end audio.
## Next Steps
Plugin API reference
Alternative provider
Knowledge retrieval backends
Default edge transport
Voice agent fundamentals
# Memory and Chat
Source: https://visionagents.ai/guides/chat-and-memory
Vision Agents provides a conversation system that maintains context across interactions. The system supports persistent storage through Stream Chat (default) and in-memory storage for development.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Persistent conversations use Stream Chat automatically.
## Persistent Conversations (Default)
By default, agents use `StreamConversation` which persists messages to Stream Chat. No additional setup required.
```python theme={null}
from vision_agents.core import User, Agent, Runner
from vision_agents.core.agents import AgentLauncher
from vision_agents.plugins import getstream, gemini
async def create_agent(**kwargs) -> Agent:
return Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="Remember details from our conversation across sessions.",
llm=gemini.Realtime(),
)
async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
await agent.create_user()
call = await agent.create_call(call_type, call_id)
async with agent.join(call):
# Conversation automatically:
# - Stores user messages from STT
# - Stores agent responses from LLM
# - Persists to Stream Chat
# - Maintains context across sessions
await agent.simple_response("Hello! I'll remember our conversation.")
await agent.finish()
if __name__ == "__main__":
Runner(AgentLauncher(create_agent=create_agent, join_call=join_call)).cli()
```
Messages are streamed to an ephemeral endpoint before persisting, ensuring real-time UI updates without affecting performance.
## In-Memory Conversations
For development and testing, use `InMemoryConversation`:
```python theme={null}
from vision_agents.core.agents.conversation import InMemoryConversation
async def create_agent(**kwargs) -> Agent:
llm = gemini.LLM()
llm.set_conversation(InMemoryConversation("Be friendly", []))
return Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You're a conversational AI assistant.",
llm=llm,
)
```
## Custom Conversation Storage
Implement the `Conversation` abstract base class for custom storage:
```python theme={null}
from vision_agents.core.agents.conversation import Conversation, Message
class CustomConversation(Conversation):
def add_message(self, message: Message, completed: bool = True):
"""Add a message to your custom storage."""
pass
def update_message(self, message_id: str, input_text: str, user_id: str,
replace_content: bool, completed: bool):
"""Update an existing message."""
pass
```
## Message Structure
Each message includes:
| Field | Description |
| ----------- | ------------------------- |
| `content` | Message text |
| `role` | `user` or `assistant` |
| `user_id` | Sender identifier |
| `timestamp` | When the message was sent |
| `id` | Unique message identifier |
## Next Steps
React to conversation events
Add knowledge base access
# Overview
Source: https://visionagents.ai/guides/deploying-overview
From local development to production Kubernetes cluster
Vision Agents can run as a single process on your laptop or as a scaled-out service in Kubernetes. This guide maps the self-hosted path from local dev to production. If you'd rather skip the infra, Stream also offers a [hosted Voice AI platform](https://getstream.io/video/voice-ai/) on the same edge network.
**Prefer managed hosting?** [Stream Voice AI](https://getstream.io/video/voice-ai/) runs production voice agents on Stream's global edge, phone numbers, web and mobile clients, co-located STT/LLM/TTS, and built-in observability. [Join the waitlist](https://getstream.io/video/voice-ai/) for early access.
## The path to production
Start your agent as an HTTP server with session management. This is the foundation for everything that follows.
```bash theme={null}
uv run agent.py serve
```
The server handles session creation, health checks, authentication, and metrics out of the box.
API endpoints, session limits, and authentication
Package your agent into a Docker image for deployment to any environment.
Dockerfiles for CPU and GPU, environment configuration
Running multiple replicas? Add a Redis-backed session registry so any node can manage any session.
Redis session store, custom backends, heartbeat mechanism
The complete setup: Helm chart, health probes, Redis, Prometheus scraping, and a Grafana dashboard.
Step-by-step guide with monitoring included
Track latency, token usage, and errors across all components with OpenTelemetry. Works at any stage, not just Kubernetes.
Metrics reference, Prometheus queries, Jaeger tracing
## Pick your starting point
Not every project needs every step.
| Goal | Start here |
| ----------------------------- | ---------------------------------------------------------------- |
| Managed hosting (phone, web) | [Stream Voice AI waitlist](https://getstream.io/video/voice-ai/) |
| Local development and testing | [HTTP Server](/guides/http-server) |
| Deploy a single container | [Docker Deployment](/guides/deployment) |
| Run multiple replicas | [Horizontal Scaling](/guides/horizontal-scaling) |
| Full production setup | [Kubernetes Deployment](/guides/kubernetes-deployment) |
| Add metrics to any setup | [Telemetry & Metrics](/core/telemetry) |
# Docker Deployment
Source: https://visionagents.ai/guides/deployment
Deploy Vision Agents to production using Docker. For a complete Kubernetes setup with Helm charts, monitoring, and Grafana dashboards, see the [Kubernetes Deployment](/guides/kubernetes-deployment) guide.
**Prefer managed hosting?** [Stream Voice AI](https://getstream.io/video/voice-ai/) runs production voice agents on Stream's global edge, phone numbers, web and mobile clients, co-located STT/LLM/TTS, and built-in observability. [Join the waitlist](https://getstream.io/video/voice-ai/) for early access.
## Key Considerations
| Factor | Recommendation |
| -------------- | ------------------------------------------------------------------------ |
| **Region** | US East for lowest latency (most AI providers default here) |
| **CPU vs GPU** | CPU for most voice agents; GPU only if running local models |
| **Scaling** | Use the [HTTP server](/guides/http-server) for multi-session deployments |
## Docker
Two Dockerfiles are provided:
**CPU** (`Dockerfile`) - Small, fast to build (\~150MB)
```dockerfile theme={null}
FROM python:3.13-slim
WORKDIR /app
RUN pip install uv
COPY pyproject.toml uv.lock agent.py ./
EXPOSE 8080
ENV UV_LINK_MODE=copy
CMD ["sh", "-c", "uv sync --frozen && uv run agent.py serve --host 0.0.0.0 --port 8080"]
```
**GPU** (`Dockerfile.gpu`) - For local model inference (\~8GB)
```dockerfile theme={null}
FROM pytorch/pytorch:2.9.1-cuda12.8-cudnn9-runtime
WORKDIR /app
RUN pip install uv
COPY pyproject.toml uv.lock agent.py ./
EXPOSE 8080
ENV UV_LINK_MODE=copy
CMD ["sh", "-c", "uv sync --frozen && uv run agent.py serve --host 0.0.0.0 --port 8080"]
```
Build for Linux (required for cloud deployment):
```bash theme={null}
docker buildx build --platform linux/amd64 -t vision-agent .
```
Only use the GPU Dockerfile if running local models (Roboflow, local VLMs). Most voice agents use cloud APIs and don't need GPUs. Make sure CUDA drivers are installed and the base image matches your CUDA version.
## Environment Variables
Create a `.env` file with your API keys:
```bash theme={null}
STREAM_API_KEY=your_key
STREAM_API_SECRET=your_secret
DEEPGRAM_API_KEY=your_key
ELEVENLABS_API_KEY=your_key
GOOGLE_API_KEY=your_key
```
For Kubernetes, create a secret:
```bash theme={null}
kubectl create secret generic vision-agent-env --from-env-file=.env
```
## Next Steps
API endpoints, session limits, and authentication
Scale across multiple servers with Redis
Helm chart, Prometheus, and Grafana
OpenTelemetry, Prometheus, and Jaeger setup
# Event System
Source: https://visionagents.ai/guides/event-system
React to what's happening in your agent — participant joins, transcriptions, LLM responses, errors, and more. Subscribe to events using the `@agent.events.subscribe` decorator.
For a complete list of available events, see [Events Reference](/reference/events-reference).
## How Events Work
The event system is built on three properties worth understanding before you write a handler.
**Fire-and-forget dispatch.** When something inside the agent emits an event, `agent.events.send(...)` schedules each handler as its own `asyncio.Task` and returns immediately. The caller does not wait for handlers to finish — so don't rely on a handler having run before the next line of agent code executes.
**Fanout.** All handlers subscribed to a given event type run concurrently as independent tasks. There is no ordering guarantee between handlers, and one slow handler does not delay the others.
**Error isolation.** If a handler raises, the exception is caught and re-emitted as an `ExceptionEvent`; other handlers still run and the agent keeps going. Subscribe to `ExceptionEvent` if you want a single place to log handler failures.
## Subscribing to Events
Use the `@agent.events.subscribe` decorator with a type hint to specify which event you want. Handlers must be async functions:
```python theme={null}
from vision_agents.core.edge.events import ParticipantJoinedEvent
@agent.events.subscribe
async def handle_participant_joined(event: ParticipantJoinedEvent):
await agent.simple_response(f"Hello {event.participant.user_id}!")
```
## Common Events
| Event | When | Import |
| ----------------------------------------------- | -------------------------------- | ---------------------------------- |
| `ParticipantJoinedEvent` | User joins the call | `vision_agents.core.edge.events` |
| `ParticipantLeftEvent` | User leaves the call | `vision_agents.core.edge.events` |
| `UserTranscriptEvent` | Final user transcript ready | `vision_agents.core.agents.events` |
| `UserTurnStartedEvent` / `UserTurnEndedEvent` | User started / stopped speaking | `vision_agents.core.agents.events` |
| `AgentTurnStartedEvent` / `AgentTurnEndedEvent` | Agent started / stopped speaking | `vision_agents.core.agents.events` |
| `LLMResponseFinalEvent` | LLM finishes a response | `vision_agents.core.llm.events` |
| `ToolStartEvent` / `ToolEndEvent` | Function/tool call | `vision_agents.core.llm.events` |
## Example: Greeting Participants
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.core.edge.events import (
ParticipantJoinedEvent,
ParticipantLeftEvent,
)
from vision_agents.plugins import openai, getstream, deepgram, elevenlabs
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You're a helpful voice assistant.",
llm=openai.LLM(model="gpt-5.4"),
tts=elevenlabs.TTS(),
stt=deepgram.STT(),
)
@agent.events.subscribe
async def on_join(event: ParticipantJoinedEvent):
await agent.simple_response(
f"Welcome, {event.participant.user_id}!",
interrupt=True,
)
@agent.events.subscribe
async def on_leave(event: ParticipantLeftEvent):
await agent.say(f"Goodbye, {event.participant.user_id}!", interrupt=False)
```
## Component Events
Subscribe to events from specific components. Each component (LLM, agent lifecycle, TTS, etc.) emits events when it does work:
```python theme={null}
from vision_agents.core.agents.events import UserTranscriptEvent
from vision_agents.core.llm.events import LLMResponseFinalEvent
@agent.events.subscribe
async def on_transcript(event: UserTranscriptEvent):
print(f"User said: {event.text}")
@agent.events.subscribe
async def on_response(event: LLMResponseFinalEvent):
print(f"Agent said: {event.text}")
print(f"Model: {event.model}")
```
### Realtime LLM Events
`UserTranscriptEvent` fires in both classic STT and realtime modes, so use it for the user side regardless of which LLM you're running. For connection-state changes in a realtime session, subscribe to `RealtimeConnectedEvent` / `RealtimeDisconnectedEvent`.
## Tool Execution Events
Monitor function calling with tool events:
```python theme={null}
from vision_agents.core.llm.events import ToolStartEvent, ToolEndEvent
@agent.events.subscribe
async def on_tool_start(event: ToolStartEvent):
print(f"Calling tool: {event.tool_name}")
print(f"Arguments: {event.arguments}")
@agent.events.subscribe
async def on_tool_end(event: ToolEndEvent):
if event.success:
print(f"Tool {event.tool_name} completed in {event.execution_time_ms}ms")
else:
print(f"Tool {event.tool_name} failed: {event.error}")
```
## Error Handling
Each component has its own error event type, and handler exceptions become `ExceptionEvent`s:
```python theme={null}
from vision_agents.core.events.base import ExceptionEvent
from vision_agents.core.stt.events import STTErrorEvent
from vision_agents.core.llm.events import LLMErrorEvent
@agent.events.subscribe
async def on_stt_error(event: STTErrorEvent):
print(f"STT error: {event.error_message}")
@agent.events.subscribe
async def on_llm_error(event: LLMErrorEvent):
print(f"LLM error: {event.error_message}")
print(f"Context: {event.context}")
@agent.events.subscribe
async def on_handler_failure(event: ExceptionEvent):
print(f"Handler {event.handler.__name__} raised: {event.exc}")
```
## Multiple Event Types
Handle related events in one handler using union types:
```python theme={null}
@agent.events.subscribe
async def on_participant_change(
event: ParticipantJoinedEvent | ParticipantLeftEvent
):
action = "joined" if isinstance(event, ParticipantJoinedEvent) else "left"
print(f"{event.participant.user_id} {action}")
```
Use the `|` operator (Python 3.10+) or `Union` from typing for older versions.
## When to Use Events
A few patterns drawn from the real example apps:
**Observing agent behavior.** Subscribe to `UserTranscriptEvent` plus `LLMResponseFinalEvent` to log what the user said and what the agent answered — useful for debugging, replay, or building a transcript UI.
**Reacting to participants.** Greet on `ParticipantJoinedEvent`, persist call duration on `ParticipantLeftEvent`. See the [Greeting Participants](#example-greeting-participants) example above.
**Triggering actions from vision detections.** Subscribe to a video plugin's `DetectionCompletedEvent` (e.g. `roboflow.DetectionCompletedEvent`) to fire an LLM response when something appears on camera. See the [Football Commentator example](/examples/football-commentator) for a debounced version.
**Coordinating with awaitable completion.** For tests or scripted flows, briefly subscribe inside a function and `await asyncio.Event()` to wait for a specific event to fire — for example, awaiting `TTSSynthesisCompleteEvent` before sending the next prompt. For normal app code, prefer the higher-level `agent.simple_response(..., interrupt=...)` and `agent.say(..., interrupt=...)`.
## Best Practices
**Keep handlers focused** — One handler per concern:
```python theme={null}
@agent.events.subscribe
async def log_transcripts(event: UserTranscriptEvent):
logger.info(f"Transcript: {event.text}")
@agent.events.subscribe
async def detect_keywords(event: UserTranscriptEvent):
if "help" in event.text.lower():
await agent.simple_response("How can I help?", interrupt=True)
```
**Don't rely on handler completion order** — Handlers for the same event run concurrently with no ordering guarantee. If one handler depends on state another sets, fold them into a single handler instead.
**Use async handlers** — Event handlers must be async functions. Non-async handlers raise an error at subscribe time.
**Access common event fields** — All events have these base fields:
* `event.type` — Event type identifier (e.g. `"agent.user_transcript"`)
* `event.event_id` — Unique ID for this event instance
* `event.timestamp` — When the event was created (UTC)
* `event.session_id` — Current session identifier
* `event.participant` — Participant the event relates to (when applicable)
## Next Steps
Complete event list
Handle user interruptions
# Horizontal Scaling
Source: https://visionagents.ai/guides/horizontal-scaling
Scale Vision Agents across multiple servers with Redis-backed session management
By default, `AgentLauncher` tracks sessions in local memory. This works for single-node deployments, but breaks when you scale horizontally — each node only knows about its own sessions.
The `SessionRegistry` with a `RedisSessionKVStore` solves this by sharing session state across all nodes via Redis. Any node can query or close sessions running on any other node, and sticky sessions are no longer required.
## Why Multi-Node?
* **Scalability** — Handle more concurrent voice sessions by distributing across nodes
* **Reliability** — If one node goes down, new sessions route to healthy nodes
* **Session visibility** — Any node can query or close sessions running on any other node
## Installation
```bash theme={null}
uv add "vision-agents[redis]"
```
This installs `redis[hiredis]` as an optional dependency.
## Quick Start
```python theme={null}
from vision_agents.core import AgentLauncher, Runner
from vision_agents.core.agents.session_registry import (
RedisSessionKVStore,
SessionRegistry,
)
# 1. Create the Redis-backed store
store = RedisSessionKVStore(url="redis://localhost:6379")
# 2. Create the registry
registry = SessionRegistry(store=store)
# 3. Pass to AgentLauncher
runner = Runner(
AgentLauncher(
create_agent=create_agent,
join_call=join_call,
registry=registry,
)
)
runner.cli()
```
Without a `registry`, `AgentLauncher` falls back to an in-memory store automatically. Existing single-node deployments continue to work with no changes.
## How It Works
1. **Registration** — When `start_session()` is called, the session is registered in the store with a TTL (default 30s)
2. **Heartbeat** — The maintenance loop periodically refreshes the TTL for all active sessions on this node, keeping them alive in the store
3. **Cross-node close** — Calling the close endpoint on any node writes a close-request flag to the store. The node owning that session picks it up during the next maintenance cycle and shuts it down
4. **Expiry** — If a node crashes, its sessions' TTLs expire naturally. Other nodes see those sessions disappear from the registry
The `ttl` value must be significantly higher than `maintenance_interval` to avoid sessions expiring between heartbeats. The default of 30s TTL with 5s maintenance interval provides a comfortable margin.
## Architecture
The system has three layers:
1. **`SessionKVStore`** — Abstract key-value store with TTL support. Two built-in implementations:
* `InMemorySessionKVStore` — Used by default for single-node deployments
* `RedisSessionKVStore` — For multi-node production deployments
2. **`SessionRegistry`** — Facade that manages session lifecycle: registration, heartbeat refresh, cross-node close requests, and expiry detection
3. **`AgentLauncher`** — Accepts an optional `registry` parameter. When provided, the maintenance loop refreshes TTLs and processes close requests from other nodes
## Configuration
### SessionRegistry
| Parameter | Type | Default | Description |
| --------- | ---------------- | ------- | ---------------------------------------------------- |
| `store` | `SessionKVStore` | `None` | Key-value store backend. `None` uses in-memory store |
| `node_id` | `str \| None` | `None` | Unique ID for this node. Auto-generated if `None` |
| `ttl` | `float` | `30.0` | Time-to-live in seconds for session keys |
### RedisSessionKVStore
| Parameter | Type | Default | Description |
| ------------ | ------------- | ------------------ | ------------------------------------------------- |
| `client` | `Redis` | `None` | Existing async Redis client instance |
| `url` | `str \| None` | `None` | Redis connection URL (used if `client` is `None`) |
| `key_prefix` | `str` | `"vision_agents:"` | Prefix for all keys in Redis |
Either `client` or `url` must be provided. Pass `client` to reuse an existing Redis connection pool, or `url` for convenience.
### InMemorySessionKVStore
Used automatically when no store is provided. Suitable for single-node deployments and development.
| Parameter | Type | Default | Description |
| ------------------ | ------- | ------- | ----------------------------------------------- |
| `cleanup_interval` | `float` | `60.0` | Interval in seconds between expired key cleanup |
## Custom Store Backend
The `SessionKVStore` is an abstract class with a simple interface. You can implement your own backend for any key-value store that supports TTL-based expiry (DynamoDB, Memcached, etcd, etc.).
Subclass `SessionKVStore` and implement these abstract methods:
```python theme={null}
from vision_agents.core.agents.session_registry import SessionKVStore
class DynamoDBSessionKVStore(SessionKVStore):
async def start(self) -> None:
"""Open connections. Called once when the registry starts."""
...
async def close(self) -> None:
"""Close connections. Called once when the registry stops."""
...
async def set(
self, key: str, value: bytes, ttl: float, *, only_if_exists: bool = False
) -> None:
"""Store a value with a TTL in seconds.
If only_if_exists is True, silently skip if the key doesn't exist.
"""
...
async def mset(self, items: list[tuple[str, bytes, float]]) -> None:
"""Store multiple (key, value, ttl) tuples."""
...
async def get(self, key: str) -> bytes | None:
"""Retrieve a value, or None if expired/missing."""
...
async def mget(self, keys: list[str]) -> list[bytes | None]:
"""Retrieve multiple values. Return None for missing keys."""
...
async def expire(self, *keys: str, ttl: float) -> None:
"""Refresh TTL on existing keys without changing values."""
...
async def keys(self, prefix: str) -> list[str]:
"""Return all non-expired keys matching the prefix."""
...
async def delete(self, keys: list[str]) -> None:
"""Delete one or more keys. Ignore missing keys."""
...
```
Then pass it to `SessionRegistry`:
```python theme={null}
store = DynamoDBSessionKVStore(table_name="sessions")
registry = SessionRegistry(store=store)
```
The store works with raw bytes — all serialization is handled by `SessionRegistry`. Your implementation only needs to store and retrieve byte values with TTL expiry.
## Next Steps
Docker, Kubernetes, and scaling basics
HTTP server API reference and configuration
Monitor agent performance in production
Complete Helm chart implementation
# Built-in HTTP Server
Source: https://visionagents.ai/guides/http-server
Run agents as an HTTP server with session management, authentication, and real-time metrics
The `Runner` class provides two modes for running your agents:
* a single-agent console mode for development,
* and an HTTP server mode that spawns agents on demand for production deployments.
For a complete working example, see [08\_agent\_server\_example](https://github.com/GetStream/Vision-Agents/tree/main/examples/08_agent_server_example) in the Vision Agents repository.
## Core Components
Running agents as a server requires four components:
1. **`create_agent()`** - A factory function that configures and returns an Agent instance
2. **`join_call()`** - Defines what happens when an agent joins a call
3. **`AgentLauncher`** - Responsible for running and monitoring the agents
4. **`Runner`** - a wrapper on top of `AgentLauncher`, providing CLI commands for console and server modes
## Basic Example
```python theme={null}
import logging
from dotenv import load_dotenv
from vision_agents.core import Agent, AgentLauncher, Runner, User
from vision_agents.plugins import deepgram, elevenlabs, gemini, getstream
load_dotenv()
logging.basicConfig(level=logging.INFO)
async def create_agent(**kwargs) -> Agent:
"""Factory function that creates and configures an agent."""
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful voice assistant.",
llm=gemini.LLM("gemini-flash-lite-latest"),
tts=elevenlabs.TTS(),
stt=deepgram.STT(eager_turn_detection=True),
)
@agent.llm.register_function(description="Get the current weather for a location")
async def get_weather(location: str) -> str:
return f"The weather in {location} is sunny and 72°F."
return agent
async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
"""Called when the agent should join a call."""
call = await agent.create_call(call_type, call_id)
async with agent.join(call):
await agent.simple_response("Hello! How can I help you today?")
await agent.finish()
if __name__ == "__main__":
runner = Runner(AgentLauncher(create_agent=create_agent, join_call=join_call))
runner.cli()
```
## Running the Server
Start the HTTP server with the `serve` command:
```bash theme={null}
uv run serve
```
The server starts on `http://127.0.0.1:8000` by default.
The interactive API documentation can be found at `http://127.0.0.1:8000/docs` (Swagger UI).
### CLI Options
| Option | Default | Description |
| -------------------- | ----------- | ------------------------- |
| `--host` | `127.0.0.1` | Server host |
| `--port` | `8000` | Server port |
| `--agents-log-level` | `INFO` | Log level for agents |
| `--http-log-level` | `INFO` | Log level for HTTP server |
| `--no-splash` | `false` | Disable the splash screen |
```bash theme={null}
uv run agent.py serve --host 0.0.0.0 --port 8000 --agents-log-level DEBUG
```
### Console mode
For development and testing, use console mode to run a single agent:
```bash theme={null}
uv run run
```
| Option | Default | Description |
| ------------------------ | -------------- | -------------------------------------------------- |
| `--call-type` | `default` | Call type for the video call |
| `--call-id` | auto-generated | Call ID for the video call |
| `--debug` | `false` | Enable debug mode |
| `--log-level` | `INFO` | Set the logging level |
| `--no-demo` | `false` | Disable opening the demo UI |
| `--video-track-override` | — | Local video file to play instead of incoming video |
| `--no-splash` | `false` | Disable the splash screen |
The splash screen is only shown in interactive terminals. It is automatically suppressed in non-interactive environments such as CI pipelines and Docker containers. Use `--no-splash` to suppress it explicitly.
## API Endpoints
The server exposes these endpoints:
| Method | Endpoint | Purpose |
| ------ | ------------------------------------------------ | ----------------------------------- |
| POST | `/calls/{call_id}/sessions` | Spawn a new agent for a call |
| DELETE | `/calls/{call_id}/sessions/{session_id}` | Request closure of an agent session |
| POST | `/calls/{call_id}/sessions/{session_id}/close` | Request closure via sendBeacon |
| GET | `/calls/{call_id}/sessions/{session_id}` | Get session information |
| GET | `/calls/{call_id}/sessions/{session_id}/metrics` | Real-time performance metrics |
| GET | `/health` | Liveness check |
| GET | `/ready` | Readiness check |
Close operations (DELETE and POST `/close`) return **HTTP 202 Accepted**. The close request is processed asynchronously — the owning node will shut down the session on its next maintenance cycle.
**Creating a Session:**
```bash theme={null}
curl -X POST http://127.0.0.1:8000/calls/my-call-123/sessions \
-H "Content-Type: application/json" \
-d '{"call_type": "default"}'
```
Response:
```json theme={null}
{
"session_id": "abc-123",
"call_id": "my-call-123",
"session_started_at": "2025-01-15T10:30:00Z"
}
```
**Getting Session Metrics:**
```bash theme={null}
curl http://127.0.0.1:8000/calls/my-call-123/sessions/abc-123/metrics
```
Response:
```json theme={null}
{
"session_id": "abc-123",
"call_id": "my-call-123",
"session_started_at": "2025-01-15T10:30:00Z",
"metrics_generated_at": "2025-01-15T10:35:00Z",
"metrics": {
"llm_latency_ms__avg": 245.5,
"llm_time_to_first_token_ms__avg": 120.3,
"llm_input_tokens__total": 1500,
"llm_output_tokens__total": 800,
"stt_latency_ms__avg": 85.2,
"tts_latency_ms__avg": 95.1
}
}
```
## Configuration with ServeOptions
The HTTP server behavior can be customized using `ServeOptions`:
```python theme={null}
from vision_agents.core import Runner, AgentLauncher, ServeOptions
runner = Runner(
AgentLauncher(create_agent=create_agent, join_call=join_call),
serve_options=ServeOptions(
cors_allow_origins=["https://myapp.com"],
cors_allow_methods=["GET", "POST", "DELETE"],
cors_allow_headers=["Authorization"],
cors_allow_credentials=True,
),
)
```
### CORS Options
| Option | Description | Default |
| ------------------------ | -------------------- | ------- |
| `cors_allow_origins` | Allowed origins | `["*"]` |
| `cors_allow_methods` | Allowed HTTP methods | `["*"]` |
| `cors_allow_headers` | Allowed headers | `["*"]` |
| `cors_allow_credentials` | Allow credentials | `True` |
### Authentication & Permissions
Use authentication and permission callbacks to secure your agent server and control who can start, view, or close sessions.
These callbacks are standard FastAPI dependencies, giving you access to headers, query parameters, and dependency injection.
| Option | Default | Description |
| ------------------- | --------- | -------------------------------------- |
| `can_start_session` | allow all | Permission check for starting sessions |
| `can_close_session` | allow all | Permission check for closing sessions |
| `can_view_session` | allow all | Permission check for viewing sessions |
| `can_view_metrics` | allow all | Permission check for viewing metrics |
#### Permission Callbacks
Each permission callback receives `call_id` from the URL path and can use standard FastAPI dependencies for authentication:
```python theme={null}
from fastapi import Header, HTTPException
async def can_start_session(
call_id: str,
authorization: str = Header(None),
) -> bool:
"""Check if the request is authorized to start a session."""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization required")
user = await validate_token(authorization)
if not user.has_permission("start_session"):
raise HTTPException(status_code=403, detail="Permission denied")
return True
async def can_close_session(
call_id: str,
authorization: str = Header(None),
) -> bool:
"""Check if the request is authorized to close a session."""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization required")
user = await validate_token(authorization)
if not user.can_access_call(call_id):
raise HTTPException(status_code=403, detail="Cannot close this session")
return True
runner = Runner(
AgentLauncher(create_agent=create_agent, join_call=join_call),
serve_options=ServeOptions(
can_start_session=can_start_session,
can_close_session=can_close_session,
),
)
```
### Customizing the Default FastAPI App
The `Runner` exposes its FastAPI instance via `runner.fast_api`, allowing you to add custom routes, middlewares, and other configurations after initialization.
```python theme={null}
from fastapi.middleware.gzip import GZipMiddleware
runner = Runner(AgentLauncher(create_agent=create_agent, join_call=join_call))
# Adding a custom endpoint
@runner.fast_api.get("/custom")
def custom_endpoint():
return {"message": "Custom endpoint"}
# Add custom middleware
runner.fast_api.add_middleware(GZipMiddleware, minimum_size=1000)
```
### Using a Custom FastAPI Instance
For full control over the FastAPI configuration, provide your own instance via `ServeOptions`:
```python theme={null}
from fastapi import FastAPI
app = FastAPI(
title="My Agent Server",
description="Custom agent server with additional features",
version="1.0.0",
)
# Add your own routes before passing to Runner
@app.get("/custom")
def custom_endpoint():
return {"message": "Custom endpoint"}
runner = Runner(
AgentLauncher(create_agent=create_agent, join_call=join_call),
serve_options=ServeOptions(fast_api=app),
)
```
When providing a custom FastAPI app via `ServeOptions(fast_api=app)`, the `Runner` will use it as-is without any configuration.
It will not register the default endpoints (`/calls/{call_id}/sessions/...`, `/health`, `/ready`, etc.) nor apply CORS settings.
You are responsible for assembling the application yourself.
## Session Limits & Resource Management
`AgentLauncher` provides options to control session lifecycle and resource usage:
| Parameter | Type | Default | Description |
| ------------------------------ | --------------- | ------- | -------------------------------------------------------- |
| `max_concurrent_sessions` | `int \| None` | `None` | Maximum concurrent sessions across all calls |
| `max_sessions_per_call` | `int \| None` | `None` | Maximum sessions allowed per call\_id |
| `max_session_duration_seconds` | `float \| None` | `None` | Maximum duration before session is auto-closed |
| `agent_idle_timeout` | `float` | `60.0` | Seconds agent stays alone on call before auto-close |
| `maintenance_interval` | `float` | `5.0` | Interval between maintenance checks for expired sessions |
```python theme={null}
runner = Runner(
AgentLauncher(
create_agent=create_agent,
join_call=join_call,
max_concurrent_sessions=10, # Limit total concurrent agents
max_sessions_per_call=1, # One agent per call
max_session_duration_seconds=3600, # 1 hour max per session
agent_idle_timeout=120.0, # Disconnect after 2 min alone
)
)
```
* **`max_concurrent_sessions`** - Prevents resource exhaustion by capping how many agents can run simultaneously. Useful for cost control and server capacity planning.
* **`max_sessions_per_call`** - Prevents duplicate agents from joining the same call. Set to `1` to ensure only one agent per conversation.
* **`max_session_duration_seconds`** - Automatically terminates long-running sessions. Protects against runaway sessions that could accumulate costs.
* **`agent_idle_timeout`** - Cleans up agents when all other participants have left the call. The agent disconnects after being alone for this duration.
## Using AgentLauncher Without the HTTP Server
The built-in HTTP server is just a thin wrapper around `AgentLauncher`. If you need a different transport — gRPC, WebSocket, message queue, or a custom protocol — you can use `AgentLauncher` directly.
`AgentLauncher` is transport-agnostic. It manages agent lifecycle, session limits, and the session registry. You provide the interface layer on top.
```python theme={null}
from vision_agents.core import AgentLauncher
launcher = AgentLauncher(
create_agent=create_agent,
join_call=join_call,
max_concurrent_sessions=10,
)
# Start the launcher (warmup + maintenance loop)
await launcher.start()
# Start a session — returns an AgentSession
session = await launcher.start_session(call_id="my-call-123", call_type="default")
# Query a session from the registry (works across nodes)
info = await launcher.get_session_info("my-call-123", session.id)
# Request session closure (works across nodes)
await launcher.request_close_session("my-call-123", session.id)
# Close a session running on this node
await launcher.close_session(session.id)
# Stop the launcher (closes all sessions + registry)
await launcher.stop()
```
### AgentLauncher Methods
| Method | Scope | Description |
| -------------------------------------------- | -------- | --------------------------------------------------- |
| `start()` | Local | Initialize launcher, warmup, start maintenance loop |
| `stop()` | Local | Close all sessions and stop the launcher |
| `start_session(call_id, call_type)` | Both | Create agent, join call, register in store |
| `close_session(session_id)` | Local | Close a session running on this node |
| `get_session(session_id)` | Local | Look up a session on this node only |
| `get_session_info(call_id, session_id)` | Registry | Query session info from shared storage |
| `request_close_session(call_id, session_id)` | Registry | Set a close flag for any node to process |
"Local" methods operate on this node's in-memory session map. "Registry" methods read from or write to shared storage, so they work across nodes when a `SessionRegistry` is configured.
### gRPC Example
Here's a sketch of how you might wrap `AgentLauncher` with a gRPC service:
```python theme={null}
import grpc
from vision_agents.core import AgentLauncher
class AgentService(agent_pb2_grpc.AgentServiceServicer):
def __init__(self, launcher: AgentLauncher):
self.launcher = launcher
async def StartSession(self, request, context):
session = await self.launcher.start_session(
call_id=request.call_id,
call_type=request.call_type,
)
return agent_pb2.StartSessionResponse(session_id=session.id)
async def CloseSession(self, request, context):
await self.launcher.request_close_session(
call_id=request.call_id,
session_id=request.session_id,
)
return agent_pb2.CloseSessionResponse()
# Start the launcher, then serve
launcher = AgentLauncher(create_agent=create_agent, join_call=join_call)
await launcher.start()
server = grpc.aio.server()
agent_pb2_grpc.add_AgentServiceServicer_to_server(AgentService(launcher), server)
server.add_insecure_port("[::]:50051")
await server.start()
await server.wait_for_termination()
```
## Scaling to Multiple Nodes
By default, `AgentLauncher` tracks sessions in local memory, which works for single-node deployments. To scale horizontally across multiple servers, you can provide a `SessionRegistry` backed by Redis. This allows any node to query or close sessions running on other nodes, and removes the need for sticky sessions or session affinity.
See the [Horizontal Scaling](/guides/horizontal-scaling) guide for setup instructions.
## Next Steps
Scale across multiple servers with Redis
Docker, Kubernetes, and scaling
Complete working implementation
# Interruption Handling
Source: https://visionagents.ai/guides/interruption-handling
Interruption handling ensures your voice agent responds naturally when users speak over the agent mid-response. Vision Agents handles interruptions automatically.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Automatic Handling
When you configure turn detection, the Agent class automatically:
1. **Detects** when the user starts speaking from STT/turn signals
2. **Interrupts** the active TTS or Realtime LLM
3. **Discards stale audio** from before the interruption
4. **Flushes** the audio track to clear buffered audio
5. **Listens** to the user's new input
6. **Responds** appropriately
No custom event handlers required for basic interruption handling.
### Stale audio protection
After an interruption, the agent ignores any already-generated audio that belongs to the previous response. This prevents "old" speech from leaking into the new turn.
## Realtime APIs
If you're using OpenAI Realtime, Gemini Live, AWS Bedrock, or Qwen, interruption handling is **built-in at the model level**. No turn detection plugin needed.
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import openai, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You're a helpful voice assistant.",
llm=openai.Realtime(), # Built-in interruption handling
)
```
Realtime APIs are recommended for the most natural conversation flow with minimal latency.
## Traditional Pipeline Setup
For the `STT → LLM → TTS` pipeline, you need turn detection. Some STT plugins include it automatically:
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import openai, getstream, elevenlabs
# ElevenLabs STT has built-in VAD turn detection — no extra plugin needed
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You're a helpful voice assistant.",
llm=openai.LLM(model="gpt-5.4"),
stt=elevenlabs.STT(),
tts=elevenlabs.TTS(),
)
```
If your STT plugin does not include turn detection, add a separate plugin:
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import openai, getstream, deepgram, elevenlabs, smart_turn
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You're a helpful voice assistant.",
llm=openai.LLM(model="gpt-5.4"),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
turn_detection=smart_turn.TurnDetection(),
)
```
If you provide both an STT with built-in turn detection and a separate `turn_detection` plugin, the `Agent` automatically ignores the external plugin to prevent conflicts.
## Custom Behavior
For app-triggered responses, make your own calls interruptible too:
```python theme={null}
await agent.say("One moment while I check that.", interrupt=True)
await agent.simple_response("Thanks for waiting.", interrupt=True)
```
You don't need to call `tts.interrupt()` or `llm.interrupt()` yourself during normal conversation. The Agent handles interruptions automatically.
## Tuning Sensitivity
Adjust turn detection parameters to control interruption response:
### More Sensitive (Faster Response)
```python theme={null}
turn_detection = smart_turn.TurnDetection(
buffer_in_seconds=0.5,
confidence_threshold=0.3
)
```
**Use when:** You want immediate response to any sound.
**Trade-off:** May trigger on background noise.
### Less Sensitive (More Deliberate)
```python theme={null}
turn_detection = smart_turn.TurnDetection(
buffer_in_seconds=2.0,
confidence_threshold=0.7
)
```
**Use when:** You want to avoid false positives.
**Trade-off:** Slower to respond to genuine interruptions.
### Recommended Defaults
```python theme={null}
turn_detection = smart_turn.TurnDetection(
buffer_in_seconds=1.5,
confidence_threshold=0.5
)
```
## Best Practices
**Keep responses concise** — Shorter responses mean fewer interruptions:
```python theme={null}
instructions="Keep responses under 2-3 sentences. Be concise."
```
**Acknowledge interruptions** — Add context in your instructions:
```python theme={null}
instructions="If interrupted, briefly acknowledge and address the user's new question."
```
**Filter agent-originated signals** — If you process raw turn signals, ignore events from the agent user:
```python theme={null}
if signal.participant and signal.participant.user_id == "agent":
return
```
## Troubleshooting
| Issue | Solution |
| ---------------------------------------- | ------------------------------------------------------------------------------- |
| Agent doesn't stop when interrupted | Verify `turn_detection` is configured; lower `confidence_threshold` |
| Agent stops too easily (false positives) | Increase `confidence_threshold` to `0.7`; increase `buffer_in_seconds` to `2.0` |
| Delay before responding to interruption | Decrease `buffer_in_seconds` to `0.5`; consider Realtime API |
| Not working at all | Don't use turn detection with Realtime LLMs (they handle it internally) |
## Next Steps
VAD vs Turn Detection concepts
Full parameter reference
# Kubernetes Deployment
Source: https://visionagents.ai/guides/kubernetes-deployment
Deploy Vision Agents to Kubernetes with Helm — step-by-step guide
Deploy a Vision Agent to any Kubernetes cluster using the official Helm chart. This guide walks you through every step — from building a container image to seeing live metrics in Grafana.
## What you'll set up
* Agent running on Kubernetes with health checks
* Redis for session storage (bundled in the chart)
* Prometheus metrics scraping
* Grafana dashboard with live panels (sessions, latency, tokens)
## Prerequisites
| Tool | Install |
| ------------------------ | --------------------------------------------------------------------- |
| **Docker** | [docker.com](https://docs.docker.com/get-docker/) |
| **kubectl** | [kubernetes.io](https://kubernetes.io/docs/tasks/tools/) |
| **Helm** | `brew install helm` or [helm.sh](https://helm.sh/docs/intro/install/) |
| **A Kubernetes cluster** | Any: GKE, EKS, AKS, Minikube, OrbStack, Docker Desktop |
You'll also need API keys for the AI services:
```bash theme={null}
STREAM_API_KEY=...
STREAM_API_SECRET=...
DEEPGRAM_API_KEY=...
ELEVENLABS_API_KEY=...
GOOGLE_API_KEY=...
```
## Step 1: Get the example
Clone the repository and navigate to the deploy example:
```bash theme={null}
git clone https://github.com/GetStream/vision-agents.git
cd vision-agents/examples/07_k8s_deploy_example
```
## Step 2: Create your `.env` file
```bash theme={null}
cp .env.example .env
```
Edit `.env` and fill in your API keys.
## Step 3: Build the Docker image
Generate the lock file and build:
```bash theme={null}
uv lock
docker build -t vision-agent-deploy:latest -f Dockerfile .
```
If you're deploying to a cloud cluster (not local), you'll need to push the image to a container registry:
```bash theme={null}
docker tag vision-agent-deploy:latest YOUR_REGISTRY/vision-agent-deploy:latest
docker push YOUR_REGISTRY/vision-agent-deploy:latest
```
Then set `image.repository` in Step 5 accordingly.
## Step 4: Build Helm dependencies
The chart includes Redis as an optional bundled dependency:
```bash theme={null}
helm dependency build ./helm
```
## Step 5: Install
```bash theme={null}
helm install my-agent ./helm \
--set metrics.enabled=false \
--set grafana.enabled=false \
--set secrets.streamApiKey="$(grep '^STREAM_API_KEY=' .env | cut -d= -f2)" \
--set secrets.streamApiSecret="$(grep '^STREAM_API_SECRET=' .env | cut -d= -f2)" \
--set secrets.deepgramApiKey="$(grep '^DEEPGRAM_API_KEY=' .env | cut -d= -f2)" \
--set secrets.elevenlabsApiKey="$(grep '^ELEVENLABS_API_KEY=' .env | cut -d= -f2)" \
--set secrets.googleApiKey="$(grep '^GOOGLE_API_KEY=' .env | cut -d= -f2)"
```
We disable `metrics` and `grafana` for now because they require Prometheus CRDs (ServiceMonitor). We'll enable them in [Step 8](#step-8-monitoring-prometheus--grafana).
This deploys:
* **Agent** — Deployment with health probes, resource limits, and your API keys
* **Redis** — Standalone instance for session storage
* **Service** — ClusterIP for internal routing
* **Ingress** — External access (disabled by default, enable with `ingress.enabled=true` and set `ingress.host`)
## Step 6: Verify
Wait for pods to be ready:
```bash theme={null}
kubectl get pods -w
```
You should see two pods reach `1/1 Running`:
```
my-agent-redis-master-0 1/1 Running 0 30s
my-agent-vision-agent-xxxxx-xxxxx 1/1 Running 1 35s
```
The agent pod may restart once on first deploy. This happens because the agent tries to connect to Redis at startup, but Redis isn't ready yet. After the restart, Redis is up and everything works normally.
### Test the health endpoint
```bash theme={null}
kubectl port-forward svc/my-agent-vision-agent 8080:8080
```
In another terminal:
```bash theme={null}
curl -s -w "\nHTTP %{http_code}\n" http://localhost:8080/health
```
Expected: `HTTP 200`
### Create a session
```bash theme={null}
curl -s -X POST http://localhost:8080/calls/test-1/sessions \
-H "Content-Type: application/json" \
-d '{"call_type":"default"}' | python3 -m json.tool
```
Expected:
```json theme={null}
{
"session_id": "...",
"call_id": "test-1",
"session_started_at": "..."
}
```
## Step 7: Verify Redis
Check that the session was stored in Redis:
```bash theme={null}
REDIS_PASSWORD=$(kubectl get secret my-agent-redis -o jsonpath='{.data.redis-password}' | base64 -d) kubectl exec my-agent-redis-master-0 -- redis-cli -a "$REDIS_PASSWORD" KEYS '*'
```
Expected output:
```
vision_agents:sessions/test-1/
```
## Step 8: Monitoring (Prometheus + Grafana)
### Install the monitoring stack
This installs Prometheus + Grafana with CRDs for ServiceMonitor:
```bash theme={null}
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install monitoring prometheus-community/kube-prometheus-stack \
--set grafana.adminPassword=admin \
--set prometheus.prometheusSpec.retention=2h \
--set alertmanager.enabled=false
```
### Enable metrics in the agent chart
Now upgrade with metrics and Grafana dashboard enabled:
```bash theme={null}
helm upgrade my-agent ./helm \
--set metrics.additionalLabels.release=monitoring \
--set secrets.streamApiKey="$(grep '^STREAM_API_KEY=' .env | cut -d= -f2)" \
--set secrets.streamApiSecret="$(grep '^STREAM_API_SECRET=' .env | cut -d= -f2)" \
--set secrets.deepgramApiKey="$(grep '^DEEPGRAM_API_KEY=' .env | cut -d= -f2)" \
--set secrets.elevenlabsApiKey="$(grep '^ELEVENLABS_API_KEY=' .env | cut -d= -f2)" \
--set secrets.googleApiKey="$(grep '^GOOGLE_API_KEY=' .env | cut -d= -f2)"
```
`metrics.additionalLabels.release=monitoring` is required so Prometheus discovers the ServiceMonitor. The label must match your Prometheus `serviceMonitorSelector` — for `kube-prometheus-stack`, it's `release: `.
This adds:
* **ServiceMonitor** — tells Prometheus to scrape `/metrics` on the agent pods
* **Grafana Dashboard** — auto-provisioned via ConfigMap sidecar
### Open Grafana
```bash theme={null}
kubectl port-forward svc/monitoring-grafana 3000:80
```
If you see `pod is not running. Current status=Pending`, wait a moment for the Grafana pod to start and retry. The monitoring stack takes a bit longer to initialize than the agent.
Open [http://localhost:3000](http://localhost:3000) — you'll see the Grafana login screen. Enter `admin` / `admin`.
Navigate to **Dashboards → Vision Agent Overview**.
You'll see 7 panels:
| Row | Left | Right |
| --- | --------------------------------------------------- | -------------------------------------------- |
| 1 | Active Sessions | Pipeline Latency (STT / LLM / TTS) |
| 2 | LLM Tokens (input / output) | TTS Characters |
| 3 | Pod CPU Usage (not visible on screenshot) | Pod Memory Usage (not visible on screenshot) |
| 4 | LLM Time to First Token (not visible on screenshot) | — |
### Generate some data
Create test sessions to verify the metrics pipeline works end-to-end. The 3-second delay between sessions ensures Prometheus captures active session counts between its 30-second scrape intervals:
```bash theme={null}
for i in $(seq 1 20); do
curl -s -X POST http://localhost:8080/calls/load-test-$i/sessions \
-H "Content-Type: application/json" \
-d '{"call_type":"default"}'
sleep 3
done
```
The **Pod CPU Usage** and **Pod Memory Usage** panels will show data immediately since they use cluster-level metrics. The application-level panels (Active Sessions, Latency, Tokens) will populate as sessions run through the pipeline.
For sustained, non-zero application metrics, connect a real client via the Stream Video SDK. Test sessions created via curl are short-lived and may complete between Prometheus scrape intervals (30s).
## Configuration reference
### `values.yaml` — key settings
| Setting | Default | Description |
| -------------------------- | --------------------- | ------------------------------------------------------ |
| `replicaCount` | `1` | Number of agent pods |
| `image.repository` | `vision-agent-deploy` | Container image |
| `image.tag` | `latest` | Image tag |
| `containerPort` | `8080` | Application port |
| `redis.deploy.enabled` | `true` | Deploy Redis alongside the agent |
| `redis.auth.enabled` | `true` | Enable Redis authentication (bundled Redis) |
| `redis.url` | `""` | External Redis URL (when `deploy.enabled=false`) |
| `ingress.enabled` | `false` | Create an Ingress resource (requires `host` to be set) |
| `ingress.className` | `""` | Ingress class (nginx, traefik, etc.) |
| `ingress.host` | `""` | Domain name |
| `metrics.enabled` | `true` | Create ServiceMonitor for Prometheus |
| `metrics.additionalLabels` | `{}` | Extra labels on ServiceMonitor |
| `grafana.enabled` | `true` | Deploy Grafana dashboard ConfigMap |
| `gpu.enabled` | `false` | Switch to GPU resources and tolerations |
| `cache.enabled` | `true` | Persistent volume for uv package cache |
| `secrets.existingSecret` | `""` | Use a pre-created Secret instead |
### Using managed Redis (production)
For production, use a managed Redis service instead of the bundled one:
```bash theme={null}
helm install my-agent ./helm \
--set redis.deploy.enabled=false \
--set redis.url="rediss://:AUTH@your-redis-host:6380/0" \
--set secrets.existingSecret=my-api-keys
```
### Using a custom domain
```bash theme={null}
helm install my-agent ./helm \
--set ingress.className=nginx \
--set ingress.host=agent.example.com \
--set ingress.tls[0].secretName=agent-tls \
--set ingress.tls[0].hosts[0]=agent.example.com
```
## Troubleshooting
### Pod crashes immediately — `uv.lock` not found
You need to generate the lock file before building:
```bash theme={null}
uv lock
docker build -t vision-agent-deploy:latest -f Dockerfile .
```
### Server listens on `127.0.0.1` — probes fail
The Dockerfile must use `--host 0.0.0.0`:
```dockerfile theme={null}
CMD ["sh", "-c", "uv sync --frozen && exec uv run deploy_example.py serve --host 0.0.0.0 --port 8080"]
```
Without `--host 0.0.0.0`, the server only accepts connections from inside the container, and Kubernetes health probes can't reach it.
### ServiceMonitor exists but Prometheus doesn't scrape
Prometheus only watches ServiceMonitors with matching labels. Check what your Prometheus expects:
```bash theme={null}
kubectl get prometheus -o jsonpath='{.items[0].spec.serviceMonitorSelector}'
```
Then add the required label:
```bash theme={null}
helm upgrade my-agent ./helm \
--set metrics.additionalLabels.release=monitoring
```
### Grafana dashboard shows "No data"
1. Check Prometheus is scraping: open [http://localhost:9090/targets](http://localhost:9090/targets) (port-forward Prometheus first)
2. Check the metric exists: in Grafana **Explore**, query `ai_demo_active_sessions`
3. If data shows in Explore but not the dashboard — restart Grafana to reload ConfigMaps:
```bash theme={null}
kubectl delete pod -l app.kubernetes.io/name=grafana
```
## Cleanup
Remove everything:
```bash theme={null}
helm uninstall my-agent
helm uninstall monitoring
kubectl delete pvc --all
```
## Next steps
API endpoints, session limits, and CORS configuration
Multi-replica deployment with Redis session registry
OpenTelemetry metrics reference and Prometheus queries
Docker, GPU, and general deployment tips
# MCP and Function Calling
Source: https://visionagents.ai/guides/mcp-tool-calling
Function calling lets your AI agent execute Python functions and access external services during conversations.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
For a conceptual overview of MCP, see [Model Context Protocol](/ai-technologies/model-context-protocol).
```mermaid theme={null}
flowchart TB
subgraph setup [On agent.join]
Reg["@llm.register_function"]
MCP[MCP servers]
Reg --> Registry[LLM function registry]
MCP --> Connect[Connect and discover tools]
Connect --> Registry
end
subgraph runtime [During conversation]
User[User input] --> LLM[LLM]
LLM --> ToolCall{Tool call?}
ToolCall -->|Yes| Execute[Run function or MCP tool]
Execute --> LLM
ToolCall -->|No| Response[Agent response]
end
Registry --> LLM
```
## Registering Functions
Use `@llm.register_function()` to make any Python function callable by the LLM:
```python theme={null}
from vision_agents.plugins import openai
llm = openai.LLM(model="gpt-5.4")
@llm.register_function(description="Get current weather for a location")
async def get_weather(location: str) -> dict:
return {"location": location, "temperature": "22°C", "condition": "Sunny"}
@llm.register_function(description="Calculate the sum of two numbers")
async def calculate_sum(a: int, b: int) -> int:
return a + b
```
Only async functions can be registered. Passing a synchronous function to `@register_function()` raises a `ValueError`.
The LLM automatically calls these functions when relevant:
```python theme={null}
response = await llm.simple_response("What's the weather in London?")
# Calls get_weather("London") and incorporates result
```
### Parameters and Types
Functions support required and optional parameters:
```python theme={null}
@llm.register_function(description="Search for products")
async def search_products(
query: str, # Required
category: str = "all", # Optional with default
max_price: float = 1000.0,
in_stock: bool = True
) -> list:
return [{"name": "Product 1", "price": 29.99}]
```
### Custom Function Names
Override the function name exposed to the LLM:
```python theme={null}
@llm.register_function(
name="check_permissions",
description="Check if a user has specific permissions"
)
async def verify_user_access(user_id: str, permission: str) -> bool:
return True
```
## MCP Servers
MCP servers provide your agent with access to external tools and services.
### Local Servers
Run on your machine via stdio:
```python theme={null}
from vision_agents.core.mcp import MCPServerLocal
local_server = MCPServerLocal(
command="uv run my_mcp_server.py",
session_timeout=300.0
)
```
### Remote Servers
Connect over HTTP:
```python theme={null}
from vision_agents.core.mcp import MCPServerRemote
github_server = MCPServerRemote(
url="https://api.githubcopilot.com/mcp/",
headers={"Authorization": f"Bearer {token}"},
timeout=10.0,
session_timeout=300.0
)
```
## Connecting to Agent
Pass MCP servers to your agent — tools are automatically discovered and registered:
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import openai, getstream
agent = Agent(
edge=getstream.Edge(),
llm=openai.LLM(model="gpt-5.4"),
agent_user=User(name="Assistant", id="agent"),
instructions="You have access to GitHub tools.",
mcp_servers=[github_server]
)
```
### Multiple Servers
```python theme={null}
agent = Agent(
edge=getstream.Edge(),
llm=llm,
agent_user=User(name="Multi-Tool Assistant", id="agent"),
mcp_servers=[github_server, weather_server, database_server]
)
```
## Complete Example
```python theme={null}
import asyncio
import os
from vision_agents.core import Agent, User
from vision_agents.core.mcp import MCPServerRemote
from vision_agents.plugins import openai, getstream
async def main():
github_server = MCPServerRemote(
url="https://api.githubcopilot.com/mcp/",
headers={"Authorization": f"Bearer {os.getenv('GITHUB_PAT')}"},
timeout=10.0
)
llm = openai.LLM(model="gpt-5.4")
@llm.register_function(description="Get current time")
async def get_current_time() -> str:
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
agent = Agent(
edge=getstream.Edge(),
llm=llm,
agent_user=User(name="GitHub Assistant", id="agent"),
instructions="You can access GitHub and tell the time.",
mcp_servers=[github_server]
)
await agent.create_user()
call = agent.edge.client.video.call("default", "mcp-demo")
async with agent.join(call):
await agent.finish()
if __name__ == "__main__":
asyncio.run(main())
```
## Tool Execution Events
The framework emits events when tools execute:
| Event | When | Fields |
| ---------------- | -------------------- | ------------------------------------------------------------- |
| `ToolStartEvent` | Before tool runs | `tool_name`, `arguments`, `tool_call_id` |
| `ToolEndEvent` | After tool completes | `tool_name`, `success`, `result`/`error`, `execution_time_ms` |
## Multi-round tool calling
All LLM plugins support multiple tool-calling rounds. If the model needs to call more tools after seeing results, you can configure the maximum number of rounds (default `3`):
```python theme={null}
# OpenAI Responses API
llm = openai.LLM(model="gpt-5.4", max_tool_rounds=5)
# ChatCompletions, Gemini, xAI, OpenRouter, and other plugins
llm = gemini.LLM(model="gemini-3-flash-preview", tools_max_rounds=5)
```
## Next Steps
MCP concepts
Add function calling to custom LLMs
# Multiple Speakers
Source: https://visionagents.ai/guides/multiple-speakers
Handle calls where multiple human participants talk to the same agent. The framework routes audio per-participant and gates who the agent listens to at any given moment.
## How It Works
When several participants publish audio, the agent maintains a separate audio queue for each one. A **multi-speaker filter** decides whose audio actually reaches the pipeline:
1. Each participant gets their own audio queue.
2. A `FirstSpeakerWinsFilter` (enabled by default) uses Silero VAD to detect speech.
3. The first participant whose VAD score exceeds `speech_threshold` acquires a **lock** — only their audio passes through.
4. Everyone else's audio is dropped until the lock is released.
5. The lock releases when the active speaker goes silent for `silence_release_ms`, or when that participant disconnects.
```
Participant A audio ─┐
├──→ Multi-speaker filter ──→ STT → LLM → TTS
Participant B audio ─┘ (first speaker wins)
```
The filter only activates when **two or more participants have active audio tracks**. Single-speaker calls bypass it entirely with no overhead.
## Configuration
Pass a `multi_speaker_filter` to the `Agent` constructor to customize the behavior:
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.core.utils.audio_filter import FirstSpeakerWinsFilter
from vision_agents.plugins import deepgram, elevenlabs, gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful voice assistant.",
llm=gemini.LLM("gemini-flash-lite-latest"),
tts=elevenlabs.TTS(),
stt=deepgram.STT(),
multi_speaker_filter=FirstSpeakerWinsFilter(
speech_threshold=0.5,
silence_release_ms=1500.0,
),
)
```
Omitting `multi_speaker_filter` (or passing `None`) defaults to `FirstSpeakerWinsFilter()` with the parameters shown above.
## FirstSpeakerWinsFilter Parameters
| Parameter | Type | Default | Description |
| -------------------- | ------- | --------------------------------- | ------------------------------------------------------------------------- |
| `speech_threshold` | `float` | `0.5` | Silero VAD score (0.0–1.0) a participant must exceed to acquire the lock |
| `silence_release_ms` | `float` | `1500.0` | Milliseconds of silence from the active speaker before releasing the lock |
| `model_dir` | `str` | `"/tmp/first_speaker_wins_model"` | Directory for Silero VAD model files |
**Lock lifecycle:**
1. **No lock held** — all audio passes through. The first participant whose VAD score exceeds `speech_threshold` acquires the lock.
2. **Lock held** — only the locked speaker's audio reaches the pipeline. Other participants' audio is dropped without running VAD (no extra cost).
3. **Silence timeout** — if the active speaker goes silent for `silence_release_ms`, the lock is released.
4. **Participant disconnects** — the lock is cleared immediately if that participant held it.
Use the `active_speaker_id` property on the filter to inspect which participant currently holds the lock.
## Realtime Mode
The same filter path applies before `llm.process_audio()` in realtime mode. Lock release is via silence timeout and disconnect only — there are no STT turn signals in realtime.
## Disabling the Filter
Passing `None` still defaults to `FirstSpeakerWinsFilter`. To disable filtering, pass a pass-through implementation:
```python theme={null}
from typing import Optional
from getstream.video.rtc import PcmData
from vision_agents.core.edge.types import Participant
from vision_agents.core.utils.audio_filter import AudioFilter
class PassThroughFilter(AudioFilter):
async def process_audio(
self, pcm: PcmData, participant: Participant
) -> Optional[PcmData]:
return pcm
def clear(self, participant: Optional[Participant] = None) -> None:
pass
agent = Agent(..., multi_speaker_filter=PassThroughFilter())
```
## Building a Custom AudioFilter
Replace the default filter with your own by implementing the `AudioFilter` interface:
```python theme={null}
from typing import Optional
from getstream.video.rtc import PcmData
from vision_agents.core.edge.types import Participant
from vision_agents.core.utils.audio_filter import AudioFilter
class MyCustomFilter(AudioFilter):
async def process_audio(
self, pcm: PcmData, participant: Participant
) -> Optional[PcmData]:
"""Return PcmData to pass the audio through, or None to drop it."""
# Your logic here
return pcm
def clear(self, participant: Optional[Participant] = None) -> None:
"""Called on participant disconnect.
If participant is provided, only clear state for that participant.
If None, clear all state unconditionally.
"""
pass
```
Then pass it to the agent:
```python theme={null}
agent = Agent(
...,
multi_speaker_filter=MyCustomFilter(),
)
```
## Best Practices
**Tune thresholds for your environment** — Lower `speech_threshold` for quiet speakers; raise it to reject background noise. Adjust `silence_release_ms` based on expected pause lengths in your use case.
**Combine with turn detection** — The multi-speaker filter gates *which* speaker's audio reaches the pipeline. [Turn detection](/ai-technologies/turn-detection) determines *when* the speaker has finished. They operate independently — after turn detection fires, the lock may persist until the silence timeout elapses.
## Next Steps
Handle user interruptions
VAD and turn detection concepts
# RAG for Agents
Source: https://visionagents.ai/guides/rag
Give your agents access to documents, URLs, and knowledge bases using Retrieval-Augmented Generation (RAG).
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Options
| Option | Best For | Complexity |
| ---------------------- | ----------------------------------------- | ---------- |
| **Gemini File Search** | Quick setup, automatic chunking/embedding | Simple |
| **TurboPuffer** | Full control, hybrid search, production | More setup |
## Gemini File Search
[Gemini's File Search](https://ai.google.dev/gemini-api/docs/file-search) handles chunking, embedding, and retrieval automatically.
```python theme={null}
from vision_agents.plugins import gemini
# Create and populate a file search store
store = gemini.GeminiFilesearchRAG(name="my-knowledge-base")
await store.create() # Reuses existing store if found
await store.add_directory("./knowledge") # Skips duplicates via content hash
# Use with Gemini LLM
llm = gemini.LLM(
model="gemini-3-flash-preview",
tools=[gemini.tools.FileSearch(store)]
)
```
**Features:**
* Store reuse (finds existing stores by name)
* Content deduplication via SHA-256 hash
* Concurrent batch uploads
## TurboPuffer
[TurboPuffer](https://turbopuffer.com/) provides hybrid search combining vector (semantic) and BM25 (keyword) search with Reciprocal Rank Fusion.
```python theme={null}
from vision_agents.plugins import turbopuffer, gemini
# Initialize with hybrid search
rag = turbopuffer.TurboPufferRAG(
namespace="my-knowledge",
chunk_size=10000,
chunk_overlap=200,
)
await rag.add_directory("./knowledge")
# Register as function for LLM
llm = gemini.LLM("gemini-3-flash-preview")
@llm.register_function(description="Search the knowledge base")
async def search_knowledge(query: str) -> str:
return await rag.search(query, top_k=5, mode="hybrid")
```
## RAG Pipeline Overview
For custom implementations, a typical RAG pipeline involves:
1. **Document gathering** — URLs, folders, PDFs, external APIs
2. **Parsing** — Convert to text (markdownify, BeautifulSoup, OCR)
3. **Chunking** — Split into retrievable pieces (fixed size, semantic, recursive)
4. **Embedding** — Convert text to vectors ([MTEB leaderboard](https://huggingface.co/spaces/mteb/leaderboard))
5. **Vector storage** — Store embeddings for similarity search
6. **Hybrid search** — Combine vector + full-text search ([TurboPuffer guide](https://turbopuffer.com/docs/hybrid))
7. **Reranking** — Score and filter results before passing to LLM
## Comparison
| Feature | Gemini File Search | TurboPuffer |
| -------- | -------------------- | ---------------------------- |
| Setup | Simple | More setup |
| Chunking | Automatic | Configurable |
| Search | Managed | Hybrid (vector + BM25) |
| Control | Less | Full control |
| Cost | Included with Gemini | Separate service |
| Best for | Prototypes | Production with custom needs |
## Next Steps
Phone + RAG implementation
TurboPuffer plugin reference
# Testing agents
Source: https://visionagents.ai/guides/testing
Verify agent behavior with text-only tests using pytest
The `vision_agents.testing` module provides a lightweight testing layer for verifying agent behavior — tool calls, arguments, responses, and intent — without spinning up audio/video infrastructure.
This framework uses familiar pytest patterns. No custom test runner required.
## Installation
The testing module is included with Vision Agents:
```sh theme={null}
uv add vision-agents
```
Configure pytest for async support in `pytest.ini`:
```ini theme={null}
[pytest]
asyncio_mode = auto
```
## Core concepts
| Component | Purpose |
| ---------------------------- | -------------------------------------------------------- |
| `TestSession` | Async context manager that wraps an LLM for testing |
| `TestResponse` | Result of a conversation turn with events and assertions |
| `LLMJudge` | Evaluates agent responses against target intents |
| `TestSession.mock_functions` | Wraps tools in `AsyncMock` for call tracking |
## Basic usage
### Testing a greeting
```python theme={null}
from vision_agents.plugins import gemini
from vision_agents.testing import LLMJudge, TestSession
async def test_greeting():
llm = gemini.LLM()
judge = LLMJudge(gemini.LLM())
async with TestSession(llm=llm, instructions="Be friendly") as session:
response = await session.simple_response("Hello")
# Verify no tools were called
assert response.function_calls == []
# Judge the response intent
verdict = await judge.evaluate(
response.chat_messages[0],
intent="Friendly greeting"
)
assert verdict.success, verdict.reason
```
### Testing tool calls
```python theme={null}
async def test_weather():
llm = gemini.LLM()
judge = LLMJudge(gemini.LLM())
@llm.register_function(description="Get weather for a location")
async def get_weather(location: str) -> dict:
return {"temp": 72, "condition": "sunny"}
async with TestSession(llm=llm, instructions="You can check weather") as session:
response = await session.simple_response("Weather in Tokyo?")
# Assert the tool was called with expected arguments
response.assert_function_called("get_weather", arguments={"location": "Tokyo"})
# Judge the response
verdict = await judge.evaluate(
response.chat_messages[0],
intent="Reports weather for Tokyo"
)
assert verdict.success, verdict.reason
```
## TestResponse assertions
`TestResponse` provides built-in assertion methods:
### assert\_function\_called
Verifies a tool was called with expected arguments (partial match):
```python theme={null}
# Check function was called with specific argument
response.assert_function_called("get_weather", arguments={"location": "Tokyo"})
# Check function was called (any arguments)
response.assert_function_called("get_weather")
# Check any function was called
response.assert_function_called()
```
### assert\_function\_output
Verifies tool output:
```python theme={null}
# Check exact output
response.assert_function_output("get_weather", output={"temp": 72, "condition": "sunny"})
# Check if output was an error
response.assert_function_output("get_weather", is_error=True)
```
### Accessing events directly
```python theme={null}
# Pre-computed lists for inspection
response.function_calls # List of FunctionCallEvent
response.chat_messages # List of ChatMessageEvent
response.events # All events in order
response.output # Final assistant message text
response.duration_ms # Response time in milliseconds
```
## Mocking LLM functions
### mock\_functions
Use `TestSession.mock_functions` to wrap functions into `AsyncMock` for call tracking with standard `unittest.mock` assertions:
```python theme={null}
async def test_with_mock_functions():
llm = gemini.LLM()
@llm.register_function(description="Get weather")
async def get_weather(location: str) -> dict:
return {"temp": 72}
async def fake_weather(**_) -> dict:
return {"temp": 55, "condition": "rainy"}
async with TestSession(llm=llm, instructions="...") as session:
with session.mock_functions(
{"get_weather": fake_weather}
) as mocked:
response = await session.simple_response("Weather in Berlin?")
# unittest.mock assertions
mocked["get_weather"].assert_called_once()
mocked["get_weather"].assert_called_with(location="Berlin")
# TestResponse assertion
response.assert_function_output(
"get_weather",
output={"temp": 55, "condition": "rainy"}
)
```
## LLM-as-judge
`LLMJudge` uses a separate LLM instance to evaluate whether agent responses match target intents:
```python theme={null}
from vision_agents.testing import LLMJudge, JudgeVerdict
# Use a separate LLM instance for judging
judge = LLMJudge(gemini.LLM())
# Evaluate a response
verdict: JudgeVerdict = await judge.evaluate(
response.chat_messages[0],
intent="Provides a helpful, accurate weather report"
)
if verdict.success:
print(f"Passed: {verdict.reason}")
else:
print(f"Failed: {verdict.reason}")
```
Use a separate LLM instance for the judge to avoid polluting the agent's conversation history.
## Event types
The framework captures three event types during a conversation turn:
| Event | Description | Fields |
| ------------------------- | ------------------------- | ------------------------------------------------- |
| `ChatMessageEvent` | Assistant or user message | `role`, `content` |
| `FunctionCallEvent` | Tool invocation request | `name`, `arguments`, `tool_call_id` |
| `FunctionCallOutputEvent` | Tool execution result | `name`, `output`, `is_error`, `execution_time_ms` |
## Complete example
```python theme={null}
import os
import pytest
from vision_agents.plugins import gemini
from vision_agents.testing import LLMJudge, TestSession
MODEL = "gemini-flash-lite-latest"
INSTRUCTIONS = """You are a helpful assistant.
You can check the weather using the get_weather tool."""
def setup_llm(model: str):
llm = gemini.LLM(model)
@llm.register_function(description="Get weather for a location")
async def get_weather(location: str) -> dict:
return {"temp_f": 72, "condition": "sunny"}
return llm
@pytest.mark.integration
async def test_greeting():
"""Agent gives a friendly, short greeting."""
llm = setup_llm(MODEL)
judge = LLMJudge(gemini.LLM(MODEL))
async with TestSession(llm=llm, instructions=INSTRUCTIONS) as session:
response = await session.simple_response("Hey there!")
assert response.function_calls == []
verdict = await judge.evaluate(
response.chat_messages[0],
intent="Friendly, short greeting"
)
assert verdict.success, verdict.reason
@pytest.mark.integration
async def test_weather_tool_call():
"""Agent calls get_weather with the right location."""
llm = setup_llm(MODEL)
judge = LLMJudge(gemini.LLM(MODEL))
async with TestSession(llm=llm, instructions=INSTRUCTIONS) as session:
response = await session.simple_response("What's the weather in Berlin?")
response.assert_function_called("get_weather", arguments={"location": "Berlin"})
verdict = await judge.evaluate(
response.chat_messages[0],
intent="Reports current weather for Berlin"
)
assert verdict.success, verdict.reason
@pytest.mark.integration
async def test_weather_mocked():
"""Verify tool calls with mocked implementation."""
llm = setup_llm(MODEL)
judge = LLMJudge(gemini.LLM(MODEL))
async def fake_weather(**_) -> dict:
return {"temp_f": 55, "condition": "rainy"}
async with TestSession(llm=llm, instructions=INSTRUCTIONS) as session:
with session.mock_functions(
{"get_weather": fake_weather}
) as mocked:
response = await session.simple_response("Weather in Berlin?")
mocked["get_weather"].assert_called_once()
mocked["get_weather"].assert_called_with(location="Berlin")
response.assert_function_output(
"get_weather",
output={"temp_f": 55, "condition": "rainy"}
)
verdict = await judge.evaluate(
response.chat_messages[0],
intent="Reports rainy weather for Berlin"
)
assert verdict.success, verdict.reason
```
Run tests:
```sh theme={null}
uv run pytest tests/ -m integration
```
## API reference
### TestSession
| Parameter | Type | Description |
| -------------- | ----- | ---------------------------------- |
| `llm` | `LLM` | LLM instance with tools registered |
| `instructions` | `str` | System instructions for the agent |
**Methods:**
* `simple_response(text: str) -> TestResponse` — Send user text and capture response
* `mock_functions(mocks: dict) -> ContextManager[dict[str, AsyncMock]]` — Mock tools with call tracking
### TestResponse
| Property | Type | Description |
| ---------------- | ------------------------- | ----------------------- |
| `input` | `str` | User input text |
| `output` | `str \| None` | Final assistant message |
| `events` | `list[RunEvent]` | All captured events |
| `function_calls` | `list[FunctionCallEvent]` | Tool call events |
| `chat_messages` | `list[ChatMessageEvent]` | Message events |
| `duration_ms` | `float` | Response time |
### LLMJudge
| Parameter | Type | Description |
| --------- | ----- | ------------------------------------ |
| `llm` | `LLM` | Separate LLM instance for evaluation |
**Methods:**
* `evaluate(event: ChatMessageEvent, intent: str) -> JudgeVerdict` — Evaluate response against intent
### JudgeVerdict
| Property | Type | Description |
| --------- | ------ | -------------------------------- |
| `success` | `bool` | Whether the intent was fulfilled |
| `reason` | `str` | Explanation of the verdict |
## Next steps
Register tools for your agent
Build a basic agent with tools
# Building Video Processors
Source: https://visionagents.ai/guides/video-processors
Processors analyze, transform, or publish audio and video streams in real-time. Build custom processors or use built-in ones like YOLO pose detection. For lip-synced avatars, use the [LiveAvatar](/integrations/avatars/liveavatar) integration via the agent's `avatar` parameter.
For the base class API reference, see [Processors Class](/core/processors-core).
## How Video Processing Works
When a participant publishes video, the agent creates a `VideoForwarder` that reads frames from the track. This forwarder is shared with all video processors via the `process_video()` method. Each processor registers a frame handler with its desired FPS.
```
Participant Video Track
│
▼
VideoForwarder (shared)
│
┌────┼────┐
▼ ▼ ▼
Proc1 Proc2 Proc3 (each at independent FPS)
```
## Building a Custom Processor
A minimal video processor that logs frames:
```python theme={null}
import aiortc
import av
from typing import Optional
from vision_agents.core.processors import VideoProcessor
from vision_agents.core.utils.video_forwarder import VideoForwarder
class FrameLogger(VideoProcessor):
name = "frame_logger"
def __init__(self, fps: int = 5):
self.fps = fps
self.frame_count = 0
self._forwarder: Optional[VideoForwarder] = None
async def process_video(
self,
track: aiortc.VideoStreamTrack,
participant_id: Optional[str],
shared_forwarder: Optional[VideoForwarder] = None,
) -> None:
self._forwarder = shared_forwarder
self._forwarder.add_frame_handler(
self._log_frame,
fps=float(self.fps),
name="frame_logger",
)
async def _log_frame(self, frame: av.VideoFrame):
self.frame_count += 1
print(f"Frame {self.frame_count} ({frame.width}x{frame.height})")
async def stop_processing(self) -> None:
if self._forwarder:
await self._forwarder.remove_frame_handler(self._log_frame)
self._forwarder = None
async def close(self) -> None:
await self.stop_processing()
```
**Key patterns:**
* Set `name` as a class attribute
* Store a reference to `shared_forwarder` to remove handlers later
* Register handlers with `add_frame_handler(callback, fps, name)`
* Clean up in `stop_processing()` (called when tracks are removed) and `close()`
## Publishing Transformed Video
Use `VideoProcessorPublisher` with `QueuedVideoTrack` to publish transformed video back to the call:
```python theme={null}
import aiortc
import av
import cv2
from typing import Optional
from vision_agents.core.processors import VideoProcessorPublisher
from vision_agents.core.utils.video_track import QueuedVideoTrack
from vision_agents.core.utils.video_forwarder import VideoForwarder
class GrayscaleProcessor(VideoProcessorPublisher):
name = "grayscale"
def __init__(self, fps: int = 30):
self.fps = fps
self._forwarder: Optional[VideoForwarder] = None
self._video_track = QueuedVideoTrack()
async def process_video(
self,
track: aiortc.VideoStreamTrack,
participant_id: Optional[str],
shared_forwarder: Optional[VideoForwarder] = None,
) -> None:
# Remove existing handler if switching tracks
if self._forwarder:
await self._forwarder.remove_frame_handler(self._process_frame)
self._forwarder = shared_forwarder
self._forwarder.add_frame_handler(
self._process_frame,
fps=float(self.fps),
name="grayscale",
)
async def _process_frame(self, frame: av.VideoFrame):
img = frame.to_ndarray(format="rgb24")
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
rgb = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)
new_frame = av.VideoFrame.from_ndarray(rgb, format="rgb24")
await self._video_track.add_frame(new_frame)
def publish_video_track(self) -> aiortc.VideoStreamTrack:
return self._video_track
async def stop_processing(self) -> None:
if self._forwarder:
await self._forwarder.remove_frame_handler(self._process_frame)
self._forwarder = None
async def close(self) -> None:
await self.stop_processing()
self._video_track.stop()
```
**Key utilities:**
* `QueuedVideoTrack` — Writable video track. Call `add_frame()` to queue frames for publishing.
* `VideoForwarder` — Distributes incoming frames to handlers at independent FPS rates.
## Emitting Custom Events
Use `attach_agent()` to register custom events that other parts of your application can subscribe to:
```python theme={null}
from dataclasses import dataclass
from vision_agents.core.processors import VideoProcessorPublisher
from vision_agents.core.events import Event
@dataclass
class ObjectDetectedEvent(Event):
objects: list[str]
frame_number: int
class DetectionProcessor(VideoProcessorPublisher):
name = "detection"
def attach_agent(self, agent):
self._events = agent.events
self._events.register(ObjectDetectedEvent)
async def _process_frame(self, frame):
objects = self._detect(frame)
await self._events.emit(ObjectDetectedEvent(
objects=objects,
frame_number=self.frame_count,
))
```
## YOLO Pose Detection
The Ultralytics plugin provides `YOLOPoseProcessor` for real-time pose detection with skeleton overlays:
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import getstream, gemini, ultralytics
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Golf Coach", id="agent"),
instructions="Analyze the user's golf swing.",
llm=gemini.Realtime(fps=10),
processors=[
ultralytics.YOLOPoseProcessor(
model_path="yolo11n-pose.pt",
fps=30,
conf_threshold=0.5,
)
],
)
```
**Use cases:** Golf coaching, fitness form checking, dance instruction, physical therapy.
See [Ultralytics](/integrations/vision/ultralytics) for parameters and model options.
## LiveAvatar
The LiveAvatar plugin provides `liveavatar.Avatar()` for lip-syncing AI avatars that speak agent responses.
**Use cases:** Virtual presenters, customer service avatars, interactive tutors.
See [LiveAvatar](/integrations/avatars/liveavatar) for setup and examples.
## Next Steps
Base class API reference
Pose detection reference
Avatar integration reference
Custom events reference
# Anam
Source: https://visionagents.ai/integrations/avatars/anam
[Anam](https://anam.ai/) provides real-time interactive avatar video with automatic lip-sync. Add a video avatar to your agent that speaks with natural movements synchronized to your agent's voice output.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
[Anam](https://www.anam.ai/) provides API keys and avatar IDs through their dashboard.
## Installation
```sh theme={null}
uv add "vision-agents[anam]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import anam, gemini, deepgram, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You're a friendly AI assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
tts=deepgram.TTS(),
stt=deepgram.STT(),
avatar=anam.Avatar(),
)
```
Set `ANAM_API_KEY` and `ANAM_AVATAR_ID` in your environment, or pass them directly to `anam.Avatar(...)`.
## Parameters
| Name | Type | Default | Description |
| ----------------------- | --------------- | ------- | ------------------------------------------------------------------------- |
| `avatar_id` | `str` | `None` | Anam avatar ID (defaults to `ANAM_AVATAR_ID` env var) |
| `api_key` | `str` | `None` | API key (defaults to `ANAM_API_KEY` env var) |
| `client_options` | `ClientOptions` | `None` | Advanced Anam client configuration |
| `connect_timeout` | `float` | `None` | Seconds to wait for connection (`None` = wait indefinitely) |
| `session_ready_timeout` | `float` | `None` | Seconds to wait for session ready (`None` = wait indefinitely) |
| `width` | `int` | `720` | Output video width in pixels |
| `height` | `int` | `480` | Output video height in pixels |
| `fps` | `int` | `30` | Output video frame rate. Must be `> 0`. |
| `buffer_seconds` | `float` | `1.0` | Max video buffer depth in seconds ahead of audio playback. Must be `> 0`. |
## How It Works
1. Agent TTS audio is resampled to 24 kHz mono and streamed to Anam
2. Anam generates lip-synced avatar video and audio from the input
3. Avatar video and audio frames are streamed back to call participants via Stream Edge
4. When a user starts speaking, the avatar is automatically interrupted
**With Realtime LLMs**
Anam also works with realtime speech-to-speech models. It subscribes to both TTS audio events and realtime audio output, so you can swap in a realtime LLM without any changes to the avatar setup.
```python theme={null}
from vision_agents.plugins import anam, gemini
agent = Agent(
llm=gemini.Realtime(),
avatar=anam.Avatar(),
...
)
```
## Next Steps
Get started with voice
Add video processing
Subclass the `Avatar` base class
# LemonSlice
Source: https://visionagents.ai/integrations/avatars/lemonslice
[LemonSlice](https://lemonslice.com) provides real-time interactive AI avatars with synchronized lip-sync. Add a self-managed video avatar to your agent that speaks with natural movements and expressions.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[lemonslice]"
```
## Quick start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import cartesia, deepgram, getstream, gemini, lemonslice
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You're a friendly AI assistant.",
llm=gemini.LLM(),
tts=cartesia.TTS(),
stt=deepgram.STT(),
avatar=lemonslice.Avatar(agent_id="your-avatar-id"),
)
```
Set the following environment variables:
* `LEMONSLICE_API_KEY` - Your LemonSlice API key
* `LIVEKIT_URL` - Your LiveKit server URL (e.g., `wss://your-server.livekit.cloud`)
* `LIVEKIT_API_KEY` - Your LiveKit API key
* `LIVEKIT_API_SECRET` - Your LiveKit API secret
## Parameters
| Name | Type | Default | Description |
| -------------------- | ------- | ------- | ------------------------------------------------------------------------- |
| `agent_id` | `str` | `None` | LemonSlice agent ID (from dashboard) |
| `agent_image_url` | `str` | `None` | Custom avatar image URL (368x560px) - alternative to agent\_id |
| `agent_prompt` | `str` | `None` | Prompt to influence avatar expressions and movements |
| `idle_timeout` | `int` | `None` | Seconds before an idle session is closed |
| `api_key` | `str` | `None` | API key (defaults to `LEMONSLICE_API_KEY` env var) |
| `base_url` | `str` | `None` | Override the LemonSlice API base URL |
| `livekit_url` | `str` | `None` | LiveKit server URL (defaults to `LIVEKIT_URL` env var) |
| `livekit_api_key` | `str` | `None` | LiveKit API key (defaults to `LIVEKIT_API_KEY` env var) |
| `livekit_api_secret` | `str` | `None` | LiveKit API secret (defaults to `LIVEKIT_API_SECRET` env var) |
| `width` | `int` | `1280` | Output video width in pixels |
| `height` | `int` | `720` | Output video height in pixels |
| `fps` | `int` | `30` | Output video frame rate. Must be `> 0`. |
| `buffer_seconds` | `float` | `1.0` | Max video buffer depth in seconds ahead of audio playback. Must be `> 0`. |
## How it works
The avatar works differently depending on your LLM type:
**With standard LLMs**
1. LLM generates text → TTS converts to audio → Audio sent to LemonSlice → LemonSlice generates synchronized avatar video and audio
**With Realtime LLMs**
1. Realtime LLM generates audio → Audio sent to LemonSlice → LemonSlice generates video only (audio from LLM)
```python theme={null}
# With Gemini Realtime
agent = Agent(
llm=gemini.Realtime(),
avatar=lemonslice.Avatar(agent_id="your-avatar-id"),
)
```
## Custom avatar image
Instead of using a pre-configured agent ID, you can provide a custom avatar image:
```python theme={null}
lemonslice.Avatar(
agent_image_url="https://example.com/avatar.png", # 368x560px recommended
agent_prompt="A friendly customer service representative",
)
```
## Next steps
Get started with voice
Add video processing
Subclass the `Avatar` base class
# LiveAvatar
Source: https://visionagents.ai/integrations/avatars/liveavatar
[LiveAvatar](https://docs.liveavatar.com) (by [HeyGen](https://heygen.com)) provides real-time interactive avatars with lip-sync driven by your agent's audio. Pass `liveavatar.Avatar()` to the agent's `avatar` parameter to stream synchronized video and audio into the call.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Get a LiveAvatar API key and avatar ID from the [LiveAvatar dashboard](https://docs.liveavatar.com).
## Installation
```sh theme={null}
uv add "vision-agents[liveavatar]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import deepgram, gemini, getstream, liveavatar
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You're a friendly AI assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
tts=deepgram.TTS(),
stt=deepgram.STT(),
avatar=liveavatar.Avatar(),
)
```
Set `LIVEAVATAR_API_KEY` and `LIVEAVATAR_AVATAR_ID` in your environment, or
pass `api_key` and `avatar_id` directly to `Avatar()`.
## Parameters
| Name | Type | Default | Description |
| ---------------------- | ------- | -------- | ------------------------------------------------------------------- |
| `avatar_id` | `str` | `None` | LiveAvatar avatar UUID (defaults to `LIVEAVATAR_AVATAR_ID` env var) |
| `api_key` | `str` | `None` | API key (defaults to `LIVEAVATAR_API_KEY` env var) |
| `base_url` | `str` | `None` | Override the LiveAvatar API base URL |
| `is_sandbox` | `bool` | `True` | Sandbox sessions don't burn credits but are duration-capped |
| `max_session_duration` | `int` | `None` | Session length cap in seconds; `None` uses the API default |
| `video_quality` | `str` | `"high"` | `"low"`, `"medium"`, `"high"`, or `"very_high"` |
| `video_encoding` | `str` | `"H264"` | `"H264"` or `"VP8"` |
| `width` | `int` | `1280` | Output video width in pixels |
| `height` | `int` | `720` | Output video height in pixels |
| `fps` | `int` | `30` | Output video frame rate |
| `buffer_seconds` | `float` | `1.0` | Max video buffer depth in seconds ahead of audio playback |
## How It Works
LiveAvatar runs in [LITE mode](https://docs.liveavatar.com/docs/lite-mode/integration-paths) with the custom-agent integration path:
1. Your agent's TTS (or Realtime LLM) audio is streamed to LiveAvatar over WebSocket
2. LiveAvatar generates lip-synced avatar video and audio
3. Synchronized A/V is published to call participants via Stream Edge
**With standard LLMs**
1. LLM generates text → TTS converts to audio → Audio sent to LiveAvatar → LiveAvatar returns synchronized avatar video and audio
**With Realtime LLMs**
1. Realtime LLM generates audio → Audio sent to LiveAvatar → LiveAvatar returns synchronized avatar video and audio
```python theme={null}
# With Gemini Realtime
agent = Agent(
llm=gemini.Realtime(),
avatar=liveavatar.Avatar(is_sandbox=False),
)
```
Set `is_sandbox=False` in production. Sandbox sessions are free but
duration-capped.
## Next Steps
Get started with voice
Add video processing
Subclass the `Avatar` base class
# Create Your Own Plugin
Source: https://visionagents.ai/integrations/create-your-own-plugin
Build custom plugins to connect Vision Agents to any AI provider. Plugins wrap provider APIs with a consistent interface, enabling seamless integration with the agent framework.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Before You Build
Many providers support OpenAI-compatible APIs. Before writing a custom plugin, check if you can use existing plugins with a custom `base_url`:
```python theme={null}
from vision_agents.plugins import openai
# OpenAI-compatible LLM (Ollama, LM Studio, vLLM, etc.)
llm = openai.ChatCompletionsLLM(
model="llama-3.1-8b",
base_url="http://localhost:11434/v1",
api_key="ollama", # Some servers require a placeholder
)
# OpenAI-compatible VLM
vlm = openai.ChatCompletionsVLM(
model="llava-1.5-7b",
base_url="http://localhost:8000/v1",
fps=1,
)
# OpenAI-compatible Realtime
realtime = openai.Realtime(
model="gpt-realtime",
base_url="wss://custom-endpoint.com/v1/realtime",
)
```
**Build a custom plugin when:**
* The provider uses a proprietary API format
* You need provider-specific features not exposed through Chat Completions
* The provider requires custom authentication or connection handling
## Plugin Categories
| Category | Base Class | Abstract Methods | Example |
| ------------------ | ------------------------------------------------- | ----------------------------------------------------------------- | ------------------ |
| **STT** | `STT` | `process_audio()` | Deepgram |
| **TTS** | `TTS` | `stream_audio()`, `stop_audio()` | ElevenLabs |
| **LLM** | `LLM` | `simple_response()` (yield `LLMResponseDelta`/`LLMResponseFinal`) | OpenRouter |
| **VLM** | `VideoLLM` | `watch_video_track()`, `stop_watching_video_track()` | NVIDIA, TwelveLabs |
| **Realtime** | `Realtime` | `connect()`, `simple_audio_response()` | Gemini Live |
| **Turn Detection** | `TurnDetector` | `process_audio()` | SmartTurn |
| **Processor** | `Processor` / `VideoProcessor` / `AudioProcessor` | `close()` | Ultralytics |
The TTS and Realtime base classes also provide a built-in `interrupt()` method and `epoch` property for barge-in handling. You do not need to override these — the Agent calls `interrupt()` automatically when a user interruption is detected.
## Quickstart Template
Create your plugin in `plugins/acme/`:
```
plugins/acme/
├── pyproject.toml
├── README.md
├── vision_agents/
│ └── plugins/
│ └── acme/
│ ├── __init__.py
│ ├── stt.py
│ └── events.py # Optional custom events
└── tests/
└── test_stt.py
```
### pyproject.toml
```toml theme={null}
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "vision-agents-plugins-acme"
version = "0.1.0"
description = "Acme STT integration for Vision Agents"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"vision-agents",
"acme-sdk>=1.0",
]
[tool.hatch.build.targets.wheel]
packages = [".", "vision_agents"]
[tool.uv.sources]
vision-agents = { workspace = true }
[dependency-groups]
dev = [
"pytest>=8.0",
"pytest-asyncio>=1.0",
]
```
### **init**.py
```python theme={null}
from .stt import STT
__all__ = ["STT"]
```
### stt.py
```python theme={null}
from vision_agents.core import stt
from getstream.video.rtc.track_util import PcmData
import acme_sdk
class STT(stt.STT):
"""Acme speech-to-text integration."""
def __init__(self, api_key: str = None, model: str = "default"):
super().__init__()
self.client = acme_sdk.Client(api_key=api_key)
self.model = model
async def process_audio(self, pcm: PcmData, participant=None):
resampled = pcm.resample(16000, 1)
result = await self.client.transcribe(
audio=resampled.data,
sample_rate=resampled.sample_rate,
model=self.model,
)
self._emit_transcript_event(
text=result.text,
participant=participant,
response=result,
)
async def close(self):
await self.client.close()
```
## Base Class Interfaces
### STT
```python theme={null}
from vision_agents.core import stt
from getstream.video.rtc.track_util import PcmData
class MySTT(stt.STT):
async def process_audio(self, pcm: PcmData, participant=None):
"""Process audio and emit transcripts."""
# Resample to provider's expected format
resampled = pcm.resample(16000, 1)
result = await self.provider.transcribe(resampled.data)
# Emit transcript using helper method
self._emit_transcript_event(
text=result.text,
participant=participant,
response=result,
)
async def close(self):
"""Cleanup connections."""
await self.provider.close()
```
### TTS
```python theme={null}
from vision_agents.core import tts
from getstream.video.rtc import PcmData
class MyTTS(tts.TTS):
async def stream_audio(self, text: str, **kwargs):
"""Convert text to audio. Yield PcmData chunks."""
async for chunk in self.provider.synthesize(text):
yield PcmData.from_response(
chunk,
original_sample_rate=24000,
original_channels=1,
sample_width=2,
)
async def stop_audio(self):
"""Stop current synthesis."""
await self.provider.cancel()
async def close(self):
await self.provider.close()
```
### LLM
```python theme={null}
from typing import AsyncIterator
from vision_agents.core import llm
from vision_agents.core.edge.types import Participant
from vision_agents.core.llm.llm import LLMResponseDelta, LLMResponseFinal
class MyLLM(llm.LLM):
async def simple_response(
self,
text: str,
participant: Participant | None = None,
) -> AsyncIterator[LLMResponseDelta | LLMResponseFinal]:
"""Generate a streamed response."""
stream = await self.provider.chat_stream(text=text)
parts: list[str] = []
index = 0
async for chunk in stream:
if not chunk.text:
continue
parts.append(chunk.text)
yield LLMResponseDelta(
delta=chunk.text,
sequence_number=index,
content_index=0,
)
index += 1
yield LLMResponseFinal(
text="".join(parts),
original=stream,
)
```
### VLM (Video Language Model)
VLM plugins process video frames alongside text. The framework provides `VideoForwarder` for frame management.
```python theme={null}
from vision_agents.core.llm import VideoLLM
from vision_agents.core.edge.types import Participant
from vision_agents.core.llm.llm import LLMResponseFinal
from vision_agents.core.utils import VideoForwarder
from collections import deque
import base64
from PIL import Image
import io
class MyVLM(VideoLLM):
def __init__(self, model: str, fps: int = 1, frame_buffer_seconds: int = 10):
super().__init__()
self.model = model
self.fps = fps
self._frame_buffer = deque(maxlen=fps * frame_buffer_seconds)
self._forwarder = None
self._handler_id = None
async def watch_video_track(self, track, shared_forwarder=None):
"""Subscribe to video frames from the call."""
# Use shared forwarder if provided, otherwise create own
if shared_forwarder:
self._forwarder = shared_forwarder
else:
self._forwarder = VideoForwarder(track)
await self._forwarder.start()
# Register frame handler at desired FPS
self._handler_id = self._forwarder.add_frame_handler(
on_frame=self._on_frame,
fps=self.fps,
name="my_vlm",
)
def _on_frame(self, frame):
"""Called for each video frame."""
# Convert to PIL Image
img = frame.to_image()
self._frame_buffer.append(img)
async def stop_watching_video_track(self):
"""Unsubscribe from video frames."""
if self._forwarder and self._handler_id:
self._forwarder.remove_frame_handler(self._handler_id)
async def simple_response(self, text: str, participant: Participant | None = None):
"""Generate response using buffered frames."""
# Encode frames as base64 for API
images = []
for img in self._frame_buffer:
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
images.append(base64.b64encode(buffer.getvalue()).decode())
# Call provider with images
response = await self.provider.chat(
messages=[{"role": "user", "content": text}],
images=images,
)
yield LLMResponseFinal(
text=response.text,
original=response,
)
async def close(self):
await self.stop_watching_video_track()
if self._forwarder and not self._forwarder._shared:
await self._forwarder.stop()
```
**Key VLM concepts:**
* **VideoForwarder**: Manages frame buffering and distributes to multiple handlers at different FPS
* **Shared forwarder**: Multiple plugins can share one forwarder to avoid duplicate frame processing
* **Frame buffer**: Store recent frames for context (configurable size)
* **FPS control**: Request frames at the rate your model needs (1-30 fps typical)
### Realtime (Speech-to-Speech)
```python theme={null}
from vision_agents.core.llm import Realtime
class MyRealtime(Realtime):
async def connect(self):
"""Establish WebSocket/WebRTC connection."""
self.ws = await self.provider.connect()
self._emit_connected_event()
async def simple_audio_response(self, pcm, participant=None):
"""Process incoming audio."""
await self.ws.send_audio(pcm.data)
async def close(self):
await self.ws.close()
self._emit_disconnected_event()
```
## Audio Utilities
Vision Agents provides utilities to simplify audio handling in STT and TTS plugins.
### PcmData Resampling
Most STT providers expect **16kHz mono** audio. Use the built-in resampling:
```python theme={null}
async def process_audio(self, pcm: PcmData, participant=None):
# Resample to 16kHz mono (standard for most STT APIs)
resampled = pcm.resample(target_sample_rate=16000, target_channels=1)
# Access audio properties
audio_bytes = resampled.data
sample_rate = resampled.sample_rate # 16000
channels = resampled.channels # 1
```
### TTS Output Format
The TTS base class handles output format conversion automatically:
```python theme={null}
class MyTTS(tts.TTS):
def __init__(self):
super().__init__()
# Configure output format (called by framework based on call requirements)
# Default: 16kHz mono PCM_S16
async def stream_audio(self, text: str, **kwargs):
# Return audio in any format - base class resamples automatically
async for chunk in self.provider.synthesize(text):
yield chunk # Resampled to _desired_sample_rate before emission
```
### AudioQueue for Buffering
For plugins that need to buffer audio (e.g., accumulating before processing):
```python theme={null}
from vision_agents.core.utils import AudioQueue
queue = AudioQueue(max_duration_ms=5000) # 5 second buffer
# Add audio
queue.put_nowait(pcm_chunk)
# Get specific duration
audio = queue.get_duration(duration_ms=1000) # Get 1 second
# Get specific sample count
audio = queue.get_samples(num_samples=16000) # Get 16k samples
```
## Function Calling
To support function calling in your LLM plugin, override these methods:
```python theme={null}
class MyLLM(llm.LLM):
def _convert_tools_to_provider_format(self, tools):
"""Convert ToolSchema list to provider's format."""
return [
{
"name": t["name"],
"description": t.get("description", ""),
"parameters": t["parameters_schema"],
}
for t in tools
]
def _extract_tool_calls_from_response(self, response):
"""Extract tool calls from provider response."""
return [
{
"type": "tool_call",
"name": call.function_name,
"arguments_json": call.arguments,
"id": call.id,
}
for call in response.tool_calls
]
def _create_tool_result_message(self, tool_calls, results):
"""Format tool results for follow-up request."""
return [
{"role": "tool", "tool_call_id": tc["id"], "content": str(result)}
for tc, result in zip(tool_calls, results)
]
```
The base class handles:
* Function registration via `@llm.register_function()`
* Tool execution with `_execute_tools()` (concurrent, with timeout)
* Tool call deduplication by (name, arguments)
* Multi-round tool calling (configurable via `max_tool_rounds`)
## Event Emission
Base classes provide helper methods for common events:
| Base Class | Helper Methods |
| ------------ | ------------------------------------------------------------------------------------------------------------------------ |
| **STT** | `_emit_transcript_event()`, `_emit_partial_transcript_event()`, `_emit_turn_started_event()`, `_emit_turn_ended_event()` |
| **TTS** | `send_iter()` (streams `TTSOutputChunk` values) |
| **Realtime** | `_emit_connected_event()`, `_emit_disconnected_event()`, `_emit_audio_output_event()` |
For custom events:
```python theme={null}
from dataclasses import dataclass
from vision_agents.core.events import PluginBaseEvent
@dataclass
class AcmeCustomEvent(PluginBaseEvent):
type: str = "acme.custom"
custom_field: str = ""
# In your plugin:
self.events.send(AcmeCustomEvent(custom_field="value"))
```
## Gotchas & Best Practices
### Connection Lifecycle
**Owned vs shared clients**: Track whether your plugin created the client or received it:
```python theme={null}
def __init__(self, client=None):
self._own_client = client is None
self._client = client or aiohttp.ClientSession()
async def close(self):
if self._own_client and self._client:
await self._client.close()
```
**Connection timeouts**: Always use timeouts for connection setup:
```python theme={null}
self.connection = await asyncio.wait_for(
self.provider.connect(),
timeout=10.0
)
```
### Cleanup Order
Follow this order in `close()` to prevent deadlocks:
```python theme={null}
async def close(self):
# 1. Set closed flag first (prevents new work)
self.closed = True
# 2. Cancel background tasks
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
# 3. Close connections in try-finally
if self.connection:
try:
await self.connection.close()
finally:
self.connection = None
```
### Error Handling
**Temporary errors** (network timeouts, transient API errors): Emit and continue:
```python theme={null}
except TimeoutError as e:
self._emit_error_event(e, context="transcription timeout")
# Plugin continues operating
```
**Permanent errors** (invalid API key, unsupported model): Raise directly:
```python theme={null}
if not api_key:
raise ValueError("API key required")
```
### Threading for Blocking Operations
Some SDKs have blocking calls. Use a thread pool:
```python theme={null}
from concurrent.futures import ThreadPoolExecutor
class MySTT(stt.STT):
def __init__(self):
super().__init__()
self._executor = ThreadPoolExecutor(max_workers=1)
async def process_audio(self, pcm, participant=None):
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(
self._executor,
self.blocking_transcribe,
pcm.data
)
self._emit_transcript_event(result.text, participant, result)
async def close(self):
self._executor.shutdown(wait=False)
```
### Concurrency Control
Prevent concurrent processing when your provider doesn't support it:
```python theme={null}
def __init__(self):
self._lock = asyncio.Lock()
async def process_audio(self, pcm, participant=None):
async with self._lock:
# Only one request at a time
result = await self.provider.transcribe(pcm.data)
```
### Sample Rate Requirements
| Plugin Type | Expected Input | Standard Rate |
| ----------- | ------------------- | ---------------------------------- |
| STT | Resampled audio | **16kHz mono** |
| TTS | Output configurable | 16-48kHz |
| Realtime | Raw PCM | 24kHz or 48kHz (provider-specific) |
### Reconnection with Backoff
For WebSocket-based plugins:
```python theme={null}
async def _reconnect(self):
for attempt in range(3):
try:
await asyncio.sleep(2 ** attempt) # 1, 2, 4 seconds
await self.connect()
return
except Exception as e:
logger.warning(f"Reconnect attempt {attempt + 1} failed: {e}")
raise ConnectionError("Failed to reconnect after 3 attempts")
```
## Testing
```bash theme={null}
cd plugins/acme
uv sync
uv run pytest -v
```
## Contribution Checklist
1. Implement required abstract methods
2. Add tests with reasonable coverage
3. Pass `uv run pre-commit run --all-files`
4. Add `README.md` documenting usage and events
5. Open a PR to the [Vision Agents repo](https://github.com/GetStream/vision-agents)
## Next Steps
Learn about events
Add tool support
# Stream Video RTC
Source: https://visionagents.ai/integrations/edge-transport/getstream
[Stream](https://getstream.io/video/) is the default edge transport for Vision Agents. The `getstream` plugin connects your agent to a Stream Video call over WebRTC and exposes Stream's call platform — chat-backed conversation history, custom events, recording, transcription, broadcasting, and frontend SDKs for every major client — through the same `EdgeTransport` interface used by every other transport.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Why Stream Video RTC
* **Sub-500ms global latency.** Agents connect through Stream's edge network with PoPs worldwide — the same infrastructure that powers Stream Video for production telehealth, voice support, and live coaching apps.
* **The default in every example.** All the LLM, STT, TTS, vision, and realtime guides in these docs use `getstream.Edge()`. Swap providers freely; the edge stays the same.
* **Audio + video + screen share.** The plugin subscribes to audio, video, screen-share, and screen-share-audio tracks for every remote participant and re-publishes the agent's own audio and video.
* **Chat-backed conversation history.** `StreamConversation` mirrors the message history to a Stream Chat channel attached to the call, with markdown-aware chunking and ephemeral updates while the LLM is still generating — so your frontend can render transcripts and tool output in real time.
* **Custom events to every participant.** Push arbitrary JSON to clients via `send_custom_event(...)` (payload capped at 5 KB by the platform) — useful for surfacing tool calls, UI hints, or telemetry.
* **Built-in demo helper.** `open_demo(call)` provisions a guest user, joins them to the chat channel, mints a token, and opens [Stream's hosted demo UI](https://getstream.io/video/demos) in your browser. Handy before you wire up a real client.
* **Rich event surface.** The plugin re-exports Stream's `call.*` events — recording started/stopped, transcription ready, closed captions, HLS/RTMP broadcasting state, moderation actions, member updates, and more — so you can react to platform state from the agent's event bus.
* **First-class frontend SDKs.** Web, React, React Native, iOS, Android, Flutter, and Unity clients join the same call as your server-side agent.
* **Generous free tier.** See Stream's [pricing](https://getstream.io/video/pricing/) for details.
## Installation
```sh theme={null}
uv add "vision-agents[getstream]"
```
## Quick Start
Set `STREAM_API_KEY` and `STREAM_API_SECRET` from your [Stream dashboard](https://getstream.io/try-for-free/), then drop `getstream.Edge()` into your agent. The credentials are read by the underlying [`getstream`](https://pypi.org/project/getstream/) Python client — `Edge()` takes no required arguments.
```python theme={null}
from dotenv import load_dotenv
from vision_agents.core import Agent, AgentLauncher, User, Runner
from vision_agents.plugins import getstream, gemini
load_dotenv()
async def create_agent(**kwargs) -> Agent:
return Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You're a helpful voice assistant. Be concise.",
llm=gemini.Realtime(),
)
async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
call = await agent.create_call(call_type, call_id)
async with agent.join(call):
await agent.simple_response("Greet the user")
await agent.finish()
if __name__ == "__main__":
Runner(AgentLauncher(create_agent=create_agent, join_call=join_call)).cli()
```
Run with `uv run agent.py run`. The CLI prints a join link you can open in any browser.
## Frontend SDKs
Vision Agents runs your AI server-side. End users join the same call through [Stream's Video SDKs](https://getstream.io/video/sdk/), with customizable UI components for React, iOS (SwiftUI), Android, Flutter, React Native, and Unity.
| Platform | Docs |
| ----------------------------- | ------------------------------------------------------------------------- |
| Web (vanilla JS / TypeScript) | [Stream Video Web](https://getstream.io/video/docs/api/) |
| React | [Stream Video React](https://getstream.io/video/docs/react/) |
| React Native | [Stream Video React Native](https://getstream.io/video/docs/reactnative/) |
| iOS (Swift) | [Stream Video iOS](https://getstream.io/video/docs/ios/) |
| Android (Kotlin) | [Stream Video Android](https://getstream.io/video/docs/android/) |
| Flutter | [Stream Video Flutter](https://getstream.io/video/docs/flutter/) |
| Unity | [Stream Video Unity](https://getstream.io/video/docs/unity/) |
## Environment Variables
Credentials and base URL are read by the underlying `getstream` client at construction time.
| Variable | Default | Description |
| ------------------- | ------- | ------------------------------------------------------------------------------ |
| `STREAM_API_KEY` | — | API key from your Stream app dashboard. Required. |
| `STREAM_API_SECRET` | — | API secret used to mint server-side tokens. Required. |
| `STREAM_BASE_URL` | — | Override the Stream API base URL. Only set if instructed to by Stream support. |
## Conversation Persistence
When an agent joins a call, the plugin creates a `messaging` channel with the same ID as the call and wires it up as the agent's [`Conversation`](/guides/chat-and-memory). Every message the agent produces (and every user message it observes) is mirrored to that channel.
The mirror does three things you don't get with a plain in-memory conversation:
* **Markdown-aware chunking** — long messages are split into \~1000-character pieces, preserving code-block boundaries so frontend renderers don't break mid-fence.
* **Streaming updates** — chunks are sent as ephemeral messages while the LLM is still generating, then finalized when generation completes. Clients see partial text update in real time.
* **Bidirectional history** — anything posted to the channel from a client SDK is available to the agent for memory or RAG.
## Custom Events
Push arbitrary JSON to every participant watching the call. Clients subscribe with `call.on("custom", callback)` in any frontend SDK. The payload is capped at 5 KB by the platform.
```python theme={null}
await agent.edge.send_custom_event({
"type": "tool_result",
"tool": "search_orders",
"result": {"order_id": "1234", "status": "shipped"},
})
```
## Opening a Demo
`open_demo(call)` creates a guest user, ensures it has access to the chat channel, mints a short-lived token, and opens [Stream's hosted demo UI](https://getstream.io/video/demos) pointed at your call. Useful while iterating locally before you wire up a real frontend.
```python theme={null}
async with agent.join(call):
await agent.edge.open_demo(call)
await agent.finish()
```
The base URL can be overridden via the `EXAMPLE_BASE_URL` environment variable.
## Platform Events
The plugin registers every `call.*` event from Stream's API as well as SFU-level participant and track events on the agent's event bus. Subscribe to any of them with `agent.events.subscribe(...)`:
```python theme={null}
from vision_agents.plugins.getstream import (
CallRecordingStartedEvent,
CallTranscriptionReadyEvent,
CallSessionParticipantJoinedEvent,
)
@agent.events.subscribe
async def on_recording(event: CallRecordingStartedEvent):
print("Recording started:", event)
```
Notable categories of events that are re-exported:
* **Participants & members** — `CallSessionParticipantJoined/Left`, `CallMemberAdded/Removed/Updated`, `CallSessionStarted/Ended`.
* **Recording** — `CallRecordingStarted/Stopped/Ready/Failed`, `CallFrameRecordingStarted/Stopped/FrameReady/Failed`.
* **Transcription & captions** — `CallTranscriptionStarted/Stopped/Ready/Failed`, `CallClosedCaptionsStarted/Stopped/Failed`, `ClosedCaptionEvent`.
* **Broadcasting** — `CallHLSBroadcastingStarted/Stopped/Failed`, `CallRtmpBroadcastStarted/Stopped/Failed`.
* **Lifecycle** — `CallCreated/Updated/Deleted/Ended`, `CallRing/Accepted/Rejected/Missed/Notification/Reaction`.
* **Moderation & permissions** — `CallModerationBlur/Warning`, `BlockedUser/UnblockedUser/KickedUser`, `PermissionRequest`, `UpdatedCallPermissions`, `CallUserMuted`.
* **Telemetry** — `CallStatsReportReady`, `CallUserFeedbackSubmitted`.
See the [Events reference](/reference/events-reference) for the full schema of each event type.
## Next Steps
Pair the Stream edge with custom STT/LLM/TTS plugins.
Add real-time video understanding with VLMs and YOLO.
Use the call's Stream Chat channel for transcripts and tool surfaces.
Containerize and scale agents across Stream's edge network.
# Local transport
Source: https://visionagents.ai/integrations/edge-transport/local
The `local` plugin replaces the cloud edge with your machine's microphone, speakers, and camera. Useful for local development, desktop apps, and demos where you don't want to round-trip through a real-time transport.
No Stream account is required for the local edge — but you'll still need API
keys for whichever LLM / STT / TTS plugins you use.
## Installation
```sh theme={null}
uv add "vision-agents[local]"
```
The plugin uses [sounddevice](https://python-sounddevice.readthedocs.io/) for audio I/O and [PyAV](https://pyav.basswood-io.com/) for video. On some Linux systems you may need to install `portaudio` separately.
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import deepgram, gemini
from vision_agents.plugins.local import LocalEdge
from vision_agents.plugins.local.devices import (
select_audio_input_device,
select_audio_output_device,
select_video_device,
)
input_device = select_audio_input_device()
output_device = select_audio_output_device()
video_device = select_video_device()
agent = Agent(
edge=LocalEdge(
audio_input=input_device,
audio_output=output_device,
video_input=video_device,
),
agent_user=User(name="Local AI", id="local-agent"),
instructions="Keep responses short and conversational.",
llm=gemini.LLM("gemini-3-flash-preview"),
tts=deepgram.TTS(),
stt=deepgram.STT(),
)
```
The `select_*` helpers prompt interactively in the terminal. For headless use, instantiate `AudioInputDevice`, `AudioOutputDevice`, and `CameraDevice` directly with a known device index.
## Parameters
| Name | Type | Default | Description |
| -------------- | ------------------- | ------- | ------------------------------------------------------- |
| `audio_input` | `AudioInputDevice` | — | Microphone for capturing user audio. |
| `audio_output` | `AudioOutputDevice` | — | Speaker for playing agent audio. |
| `video_input` | `CameraDevice` | `None` | Camera for capturing user video. `None` disables video. |
| `video_width` | `int` | `640` | Output video width in pixels. |
| `video_height` | `int` | `480` | Output video height in pixels. |
| `video_fps` | `int` | `30` | Output video frame rate. |
When `video_input` is set, agent video is rendered locally in a tkinter window. Subclass the device classes (`AudioInputDevice`, `AudioOutputDevice`, `CameraDevice`) to swap in alternative backends (e.g. GStreamer).
## Next Steps
Get started with voice
Add video processing
# Tencent RTC
Source: https://visionagents.ai/integrations/edge-transport/tencent
[Tencent RTC](https://sc-rp.tencentcloud.com:8106/r/7B) (Real-Time Communication) is an edge transport plugin that replaces the default communication layer in Vision Agents. It connects your agent to Tencent's global real-time network, optimized for ultra-low latency in mainland China and across Asia.
## Why Tencent RTC
* **Low-latency in Asia** - Tencent's edge network delivers strong performance in Asia, where other transport providers may face higher latency or connectivity issues.
* **Frontend SDKs** - Tencent provides client SDKs for [Web](https://sc-rp.tencentcloud.com:8106/r/7B), iOS, and Android, so you can build end-user applications that connect to the same RTC room as your agent.
* **Drop-in replacement** - The plugin implements the same `EdgeTransport` interface. Swap `edge=tencent.Edge(...)` into your `Agent` and keep your existing LLM, STT, TTS, vision, and avatar plugins unchanged.
* **Audio and video** -- Supports both audio and video tracks for voice agents, video agents, and multimodal applications.
## Installation
```sh theme={null}
uv add vision-agents["tencent"]
```
On Linux this pulls `liteav` from PyPI. On macOS the `liteav` dependency is skipped via a platform marker, so the package installs but `tencent.Edge()` raises at runtime — use the Docker setup in **Get Started** below.
## Get Started
Talk to an agent in about five minutes. You need Docker, an `.env` at the [Vision Agents](https://github.com/GetStream/Vision-Agents) repo root with `TENCENT_SDK_APP_ID`, `TENCENT_SDK_SECRET_KEY`, `OPENAI_API_KEY`, and `ELEVEN_API_KEY`, and a working microphone in a Chromium-based browser. The example pairs Tencent TRTC with OpenAI (LLM) and ElevenLabs (STT + TTS).
Create an RTC application in the [Tencent RTC Console](https://sc-rp.tencentcloud.com:8106/r/vB) to obtain your credentials:
| Variable | Description |
| ------------------------ | ----------------------------------------- |
| `TENCENT_SDK_APP_ID` | Your RTC application's integer App ID |
| `TENCENT_SDK_SECRET_KEY` | Secret key for generating user signatures |
Open Tencent's hosted [TRTC Web SDK quick demo](https://web.sdk.qcloud.com/trtc/webrtc/v5/demo/quick-demo-js/index.html):
* Paste your `TENCENT_SDK_APP_ID` into **SDKAppID** and `TENCENT_SDK_SECRET_KEY` into **SDKSecretKey** — the page generates `UserSig` client-side.
* Leave the auto-generated **UserID** and **RoomID(String)** as is.
* Click **Enter Room** — the demo log should print `🟩 [user_***] enterRoom.`
* Click **Start Local Video** — this also publishes the mic.
Clone the Vision Agents repo, copy the **RoomID(String)** from the demo form, and start the example agent:
```sh theme={null}
git clone https://github.com/GetStream/Vision-Agents.git
cd Vision-Agents/plugins/tencent
TENCENT_TEST_ROOM_ID= docker compose run --rm tencent-agent
```
On first run this builds the image and resolves the workspace; subsequent runs are faster. When the agent joins you'll see `Tencent TRTC OnRemoteUserEnterRoom: ` matching the **UserID** shown in the demo.
The flow is `browser → Tencent → STT (ElevenLabs) → LLM (OpenAI) → TTS (ElevenLabs) → Tencent → browser`. Confirm each leg in the agent log:
* `🎤 [Transcript Complete]: …` — STT got your speech.
* `🤖 [LLM response final]: …` — LLM produced a reply.
* Reply plays back in the browser.
## Usage in your own code
All Vision Agents features work out of the box with the Tencent transport. Swap in any LLM, STT, TTS, vision, or avatar plugin, use function calling, tools, and knowledge, everything works the same way as with the default transport.
```python theme={null}
import os
from vision_agents.core import Agent, Runner, User
from vision_agents.core.agents import AgentLauncher
from vision_agents.plugins import elevenlabs, openai, tencent
async def create_agent(**kwargs) -> Agent:
agent = Agent(
edge=tencent.Edge(
sdk_app_id=int(os.environ["TENCENT_SDK_APP_ID"]),
key=os.environ["TENCENT_SDK_SECRET_KEY"],
),
agent_user=User(name="Tencent Voice Agent", id="tencent-voice-agent"),
instructions="You are a helpful voice assistant. Respond concisely.",
llm=openai.LLM(),
tts=elevenlabs.TTS(),
stt=elevenlabs.STT(),
)
return agent
async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
await agent.authenticate()
# `call_id` must match the RoomID(String) from the Tencent demo so the
# browser participant is already in the room before the agent connects.
call = await agent.create_call(call_type, call_id)
async with agent.join(call, participant_wait_timeout=None):
await agent.say("Hi! How can I help you today?")
await agent.finish()
if __name__ == "__main__":
Runner(AgentLauncher(create_agent=create_agent, join_call=join_call)).cli()
```
The underlying [`liteav`](https://pypi.org/project/liteav/) package ships only manylinux wheels — **Linux x86\_64 / aarch64 only**. On macOS and Windows, run the agent inside a Linux container (see **Get Started**). User signatures are required for room entry; either generate them with [TLSSigAPIv2](https://cloud.tencent.com/document/product/647/34399) or pass `key=` and let the plugin sign per join.
## Parameters
| Name | Type | Default | Description |
| ------------ | ----- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sdk_app_id` | `int` | `None` | RTC application ID. Falls back to the `TENCENT_SDK_APP_ID` environment variable if not provided. |
| `key` | `str` | `None` | Secret key for generating user signatures. Falls back to `TENCENT_SDK_SECRET_KEY` if not provided. |
| `user_sig` | `str` | `None` | Pre-computed user signature. If omitted, a signature is generated automatically from `key` at join time. See the [UserSig generation guide](https://trtc.io/document/35166?product=rtcengine\&menulabel=core%20sdk\&platform=web). |
| `video_fps` | `int` | `15` | Frames per second for outgoing video encoding (supported range: `5`–`60`). |
## Environment Variables
| Variable | Default | Description |
| ------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `TENCENT_SDK_APP_ID` | -- | Fallback for `sdk_app_id` when not passed to the constructor. |
| `TENCENT_SDK_SECRET_KEY` | -- | Fallback for `key` when not passed to the constructor. |
| `TENCENT_TRTC_SCENE` | `auto` | Room scene type. One of `auto`, `videocall`, `call`, or `record`. `auto` selects the first available from videocall and call. |
| `TENCENT_TEST_ROOM_ID` | -- | Used by the plugin's `docker-compose.yml` as the example runner's `--call-id` flag. |
## Frontend SDKs
Tencent provides client SDKs for joining the same RTC room from end-user applications. Your users connect with a Tencent frontend SDK while your Vision Agent runs server-side with this plugin — both in the same room.
See the [Tencent RTC documentation](https://sc-rp.tencentcloud.com:8106/r/wB) for Web, iOS, and Android client SDK guides.
## Next Steps
Run your agent locally, containerize it, and scale to production.
Build a custom plugin to connect additional services.
# Baseten
Source: https://visionagents.ai/integrations/infrastructure/baseten
[Baseten](https://www.baseten.co) is an infrastructure platform for deploying and serving AI models. It provides OpenAI-compatible endpoints for popular open-source models (DeepSeek, Qwen, Nemotron, and more) with autoscaling and optimized inference.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
Baseten uses an OpenAI-compatible API, so it works with the OpenAI plugin:
```sh theme={null}
uv add "vision-agents[openai]"
```
## Quick Start
```python theme={null}
import os
from vision_agents.core import Agent, User
from vision_agents.plugins import openai, deepgram, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=openai.ChatCompletionsLLM(
model="deepseek-ai/DeepSeek-V3.1",
base_url="https://inference.baseten.co/v1",
api_key=os.environ["BASETEN_API_KEY"],
),
stt=deepgram.STT(),
tts=deepgram.TTS(),
)
```
Set `BASETEN_API_KEY` in your environment. Create an API key at
[app.baseten.co/settings/api\_keys](https://app.baseten.co/settings/api_keys).
## Using Self-Deployed Models
If you've deployed your own model on Baseten, use the model-specific endpoint:
```python theme={null}
llm = openai.ChatCompletionsLLM(
model="your-model-name",
base_url="https://model-{MODEL_ID}.api.baseten.co/environments/production/sync/v1",
api_key=os.environ["BASETEN_API_KEY"],
)
```
## Parameters
Since Baseten uses `ChatCompletionsLLM`, it accepts the same parameters:
| Name | Type | Default | Description |
| ---------- | ----- | ------- | -------------------------------------------------- |
| `model` | `str` | — | Model slug (e.g., `"deepseek-ai/DeepSeek-V3.1"`) |
| `base_url` | `str` | — | `"https://inference.baseten.co/v1"` for Model APIs |
| `api_key` | `str` | `None` | API key (defaults to `BASETEN_API_KEY` env var) |
## Available Models
Baseten's Model APIs provide pre-deployed endpoints for popular open-source models. No deployment setup required — just call the API. See the [Baseten documentation](https://docs.baseten.co/development/model-apis/overview) for the full list of available models.
## Next Steps
Get started with voice
Add video processing
# HuggingFace Inference
Source: https://visionagents.ai/integrations/infrastructure/huggingface
[HuggingFace Inference](https://huggingface.co/docs/inference-providers/en/index) is an inference platform that provides access to thousands of models through a unified API. Routes to multiple providers (Together AI, Groq, Cerebras, Replicate, Fireworks) so you can switch backends without changing code. Supports both text **LLM** and **VLM** (vision) models.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
For local on-device inference using open-weight models, see
[HuggingFace Transformers](/integrations/llm/huggingface-transformers).
## Installation
```sh theme={null}
uv add "vision-agents[huggingface]"
```
## LLM
Text-only language model with streaming and function calling.
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import huggingface, getstream, deepgram
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=huggingface.LLM(
model="meta-llama/Meta-Llama-3-8B-Instruct",
provider="fastest"
),
stt=deepgram.STT(),
tts=deepgram.TTS(),
)
@agent.llm.register_function(description="Get weather for a location")
async def get_weather(location: str) -> dict:
return {"temperature": "72°F", "condition": "Sunny"}
```
| Name | Type | Default | Description |
| ---------- | ----- | ------- | ------------------------------------------------------------ |
| `model` | `str` | — | HuggingFace model ID |
| `provider` | `str` | `None` | Provider (`"together"`, `"groq"`, `"fastest"`, `"cheapest"`) |
| `api_key` | `str` | `None` | API key (defaults to `HF_TOKEN` env var) |
## VLM
Vision language model with automatic video frame buffering and function calling. Supports models like Qwen2-VL.
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import huggingface, getstream, deepgram
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a visual assistant.",
llm=huggingface.VLM(
model="Qwen/Qwen2-VL-7B-Instruct",
fps=1,
frame_buffer_seconds=10,
),
stt=deepgram.STT(),
tts=deepgram.TTS(),
)
```
| Name | Type | Default | Description |
| ---------------------- | ----- | ------- | --------------------------------- |
| `model` | `str` | — | HuggingFace VLM model ID |
| `fps` | `int` | `1` | Video frames per second to buffer |
| `frame_buffer_seconds` | `int` | `10` | Seconds of video to buffer |
| `provider` | `str` | `None` | Inference provider |
## Next Steps
Get started with voice
Add video processing
# TurboPuffer
Source: https://visionagents.ai/integrations/infrastructure/turbopuffer
[TurboPuffer](https://turbopuffer.com/) is a high-performance vector database with native hybrid search (vector + BM25). The plugin provides RAG with precise control over chunking, embeddings, and search strategies.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[turbopuffer]"
```
## Quick Start
```python theme={null}
from vision_agents.plugins import turbopuffer
# Initialize RAG
rag = turbopuffer.TurboPufferRAG(namespace="my-knowledge")
await rag.add_directory("./knowledge")
# Hybrid search (default)
results = await rag.search("How does the chat API work?")
```
Set `TURBO_PUFFER_KEY` and `GOOGLE_API_KEY` (for Gemini embeddings) in your
environment.
## Parameters
| Name | Type | Default | Description |
| ----------------- | ----- | ------------------------------- | ---------------------- |
| `namespace` | `str` | Required | TurboPuffer namespace |
| `embedding_model` | `str` | `"models/gemini-embedding-001"` | Embedding model |
| `chunk_size` | `int` | `10000` | Text chunk size |
| `chunk_overlap` | `int` | `200` | Overlap between chunks |
## Search Modes
```python theme={null}
# Hybrid (recommended) - combines vector and BM25
results = await rag.search(query, mode="hybrid")
# Vector only - semantic similarity
results = await rag.search(query, mode="vector")
# BM25 only - keyword matching
results = await rag.search(query, mode="bm25")
```
## How Hybrid Search Works
Hybrid search combines vector and BM25 using Reciprocal Rank Fusion (RRF):
* **Vector search** catches semantic meaning even when exact words differ
* **BM25** catches exact matches (product names, SKUs, technical terms)
* **RRF** balances both without requiring tuning
## With Function Calling
```python theme={null}
@llm.register_function(description="Search the knowledge base")
async def search_knowledge(query: str) -> str:
return await rag.search(query, top_k=5, mode="hybrid")
```
## Cache Warming
For low-latency queries, TurboPuffer supports cache warming (called automatically after `add_directory()`):
```python theme={null}
await rag.warm_cache()
```
See the [RAG Guide](/guides/rag) for more details.
## Next Steps
Get started with voice
Add video processing
# Introduction to Integrations
Source: https://visionagents.ai/integrations/introduction-to-integrations
Vision Agents ships with 35+ plugins that connect AI providers to your real-time voice and video applications. Each plugin wraps a provider's API with a consistent interface, so you can swap providers without rewriting your agent logic.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Which plugin do I need?
Pick based on what your agent needs to do:
| I want to... | Start here | What you get |
| ------------------------------------------------ | ----------------------------------- | ---------------------------------------------------------------------------------- |
| Handle calls and respond naturally by voice | [Realtime](#realtime) | End-to-end voice agent with multimodal support, unified under one plugin and model |
| Connect to my own tools, APIs, or knowledge base | [Language Models](#language-models) | Function calling, RAG, and full control over STT/TTS choices |
| Transcribe what users say in real time | [Speech-to-Text](#speech-to-text) | Streaming transcription, some with built-in turn detection |
| Give my agent a distinct, natural voice | [Text-to-Speech](#text-to-speech) | Cloud and local options, from expressive to ultra-low latency |
| See and understand what's on camera | [Vision & Video](#vision--video) | Object detection, video analysis, and style transfer |
| Put a face on my agent | [Avatars](#avatars) | Real-time lip-synced visual characters |
| Make conversations feel natural, not robotic | [Turn Detection](#turn-detection) | Smart interruption handling and silence detection |
| Run open-source models on my own infrastructure | [Infrastructure](#infrastructure) | Self-hosted inference, model routing, and vector search |
| Connect users to my agent over WebRTC | [Edge Transport](#edge-transport) | Stream's global edge network, sub-500ms latency with frontend SDKs |
| Deploy agents over Tencent's network in China | [Edge Transport](#edge-transport) | Alternative transport layer with low latency in mainland China |
| Connect phone calls (PSTN) to my agent | [Telephony](#telephony) | Inbound/outbound calls via Twilio or Telnyx + media streaming |
## Installation
### New project
Scaffold a ready-to-run agent project with the CLI. Requires [uv](https://docs.astral.sh/uv/getting-started/installation/) on your PATH:
```sh theme={null}
uvx vision-agents init my-agent
cd my-agent
cp .env.example .env # fill in API keys
uv run agent.py run
```
See the [Quickstart](/introduction/quickstart) for a full walkthrough.
### Add plugins
Plugins install as extras. Add only the ones you need to an existing project:
```sh theme={null}
uv add "vision-agents[gemini,deepgram,elevenlabs]"
```
You can also add explicit plugin packages (the style used by `init`):
```sh theme={null}
uv add vision-agents-plugins-deepgram vision-agents-plugins-elevenlabs
```
Browse the categories below for available plugins and their install commands.
## Browse by Category
### Language Models
Text generation with function calling. Requires separate STT/TTS plugins.
| Provider | Notes |
| ------------------------------------------------- | --------------------------------------------- |
| [Anthropic (Claude)](/integrations/llm/anthropic) | Messages API, streaming, function calling |
| [Gemini](/integrations/llm/gemini) | Built-in tools: search, code execution, RAG |
| [OpenAI](/integrations/llm/openai) | Responses API (GPT-5+) and ChatCompletions |
| [xAI (Grok)](/integrations/llm/xai) | Advanced reasoning, function calling |
| [OpenRouter](/integrations/llm/openrouter) | Unified API for Claude, Gemini, GPT, and more |
| [Kimi AI](/integrations/llm/kimi) | OpenAI-compatible via ChatCompletions |
| [MiniMax](/integrations/llm/minimax) | MiniMax-M3 and M-series, OpenAI-compatible |
| [Qwen](/integrations/llm/qwen) | DashScope API via ChatCompletions |
### Realtime
End-to-end speech-to-speech with built-in STT/TTS. Lowest latency, simplest setup.
| Provider | Notes |
| -------------------------------------------------- | ------------------------------------------- |
| [Gemini Realtime](/integrations/realtime/gemini) | WebSocket, optional video, built-in VAD |
| [Inworld Realtime](/integrations/realtime/inworld) | WebRTC, protocol-compatible with OpenAI |
| [OpenAI Realtime](/integrations/realtime/openai) | WebRTC, built-in STT/TTS |
| [Qwen Realtime](/integrations/realtime/qwen) | Native audio I/O, video support |
| [xAI Realtime](/integrations/realtime/xai) | WebSocket, server VAD, web + X search |
| [AWS Bedrock](/integrations/realtime/aws-bedrock) | Amazon Nova models, auto session management |
### Speech-to-Text
Real-time transcription. Some include built-in turn detection.
| Provider | Notes |
| ---------------------------------------------- | ----------------------------------------------------- |
| [Deepgram](/integrations/stt/deepgram) | Nova-3, built-in turn detection |
| [ElevenLabs](/integrations/stt/elevenlabs) | Scribe v2, \~150ms latency, built-in VAD |
| [AssemblyAI](/integrations/stt/assemblyai) | Punctuation-based turn detection |
| [Cartesia](/integrations/stt/cartesia) | Ink model, streaming PCM, turn detection |
| [Fish Audio](/integrations/stt/fish) | Auto language detection |
| [Mistral Voxtral](/integrations/stt/mistral) | WebSocket streaming, requires separate turn detection |
| [Fast-Whisper](/integrations/stt/fast-whisper) | Local, CPU/GPU accelerated |
| [Wizper](/integrations/stt/wizper) | Whisper v3, on-the-fly translation |
### Text-to-Speech
Voice synthesis for agent responses.
| Provider | Notes |
| ------------------------------------------ | ------------------------------------- |
| [ElevenLabs](/integrations/tts/elevenlabs) | Highly realistic, multilingual |
| [Cartesia](/integrations/tts/cartesia) | Low-latency Sonic model |
| [Deepgram](/integrations/tts/deepgram) | Aura-2, low-latency |
| [OpenAI](/integrations/tts/openai) | gpt-4o-mini-tts, streaming |
| [Fish Audio](/integrations/tts/fish) | Prosody control, voice cloning |
| [Inworld](/integrations/tts/inworld) | Expressive game character voices |
| [Kokoro](/integrations/tts/kokoro) | Local, runs on CPU, no API key |
| [Pocket TTS](/integrations/tts/pocket) | Local, \~200ms latency, voice cloning |
| [xAI](/integrations/tts/xai) | Five expressive voices, speech tags |
| [AWS Polly](/integrations/tts/aws-polly) | Standard and neural engines |
### Vision & Video
Video understanding, object detection, and video transformation.
| Provider | Notes |
| ---------------------------------------------------- | --------------------------------------------- |
| [Moondream](/integrations/vision/moondream) | Zero-shot detection, VQA, cloud or local |
| [NVIDIA](/integrations/vision/nvidia) | Cosmos Reason2, real-time video understanding |
| [Roboflow](/integrations/vision/roboflow) | Pre-trained and custom detection models |
| [TwelveLabs](/integrations/vision/twelvelabs) | Pegasus clip-based video understanding |
| [Ultralytics YOLO](/integrations/vision/ultralytics) | Pose estimation, object detection |
| [Decart](/integrations/vision/decart) | Real-time AI video style transfer |
### Avatars
Visual AI characters with synchronized lip-sync.
| Provider | Notes |
| ---------------------------------------------- | ------------------------------------------------- |
| [Anam](/integrations/avatars/anam) | Real-time conversational avatars |
| [LiveAvatar](/integrations/avatars/liveavatar) | Realistic AI avatars (HeyGen), automatic lip-sync |
| [LemonSlice](/integrations/avatars/lemonslice) | Real-time interactive avatars |
### Turn Detection
Controls when the agent should start and stop speaking.
| Provider | Notes |
| ----------------------------------------------------- | --------------------------------- |
| [Smart Turn](/integrations/turn-detection/smart-turn) | Silero VAD + Whisper features |
| [Vogent](/integrations/turn-detection/vogent) | Neural turn completion prediction |
[Deepgram](/integrations/stt/deepgram) and [ElevenLabs](/integrations/stt/elevenlabs) STT include built-in turn detection, so no separate plugin is needed.
### Infrastructure
Inference platforms and data services for running models on your own terms.
| Provider | Notes |
| ----------------------------------------------------------------- | --------------------------------------------------------- |
| [Baseten](/integrations/infrastructure/baseten) | OpenAI-compatible endpoints for open-source models |
| [HuggingFace Inference](/integrations/infrastructure/huggingface) | Unified API routing to Together, Groq, Cerebras, and more |
| [TurboPuffer](/integrations/infrastructure/turbopuffer) | Vector database for RAG with hybrid search |
### Edge Transport
Alternative real-time transport layers for deploying agents in specific regions.
| Provider | Notes |
| ---------------------------------------------------------- | ------------------------------------------------------------------------- |
| [Stream Video RTC](/integrations/edge-transport/getstream) | Default transport: global WebRTC, chat-backed conversation, frontend SDKs |
| [Local transport](/integrations/edge-transport/local) | Microphone, speakers, and camera as the agent edge |
| [Tencent RTC](/integrations/edge-transport/tencent) | Low-latency in China, frontend SDKs |
### Telephony
PSTN phone call integration: bridge inbound and outbound calls into a Stream call.
| Provider | Notes |
| ---------------------------------------- | ---------------------------------------------------------- |
| [Twilio](/integrations/telephony/twilio) | Media Streams, TwiML, built-in webhook helpers |
| [Telnyx](/integrations/telephony/telnyx) | Call Control, bidirectional media streaming, Stream bridge |
## Consistent Interface
Plugins of the same type share a common interface. Swap providers in one line:
```python theme={null}
# Any STT plugin works the same way
stt = deepgram.STT()
stt = elevenlabs.STT()
stt = fish.STT()
# Any TTS plugin works the same way
tts = elevenlabs.TTS()
tts = cartesia.TTS()
tts = kokoro.TTS()
# Any LLM plugin works the same way
llm = gemini.LLM("gemini-3-flash-preview")
llm = openai.LLM(model="gpt-5.4")
llm = openrouter.LLM(model="anthropic/claude-sonnet-4")
```
## Creating Custom Plugins
Don't see your provider? Build your own plugin to connect additional services. See the [Create Your Own Plugin](/integrations/create-your-own-plugin) guide.
# Anthropic
Source: https://visionagents.ai/integrations/llm/anthropic
[Anthropic](https://www.anthropic.com/) provides the Claude family of LLMs via the [Messages API](https://docs.anthropic.com/en/api/messages). Pass `anthropic.LLM()` to your agent for streaming responses and function calling.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Get an Anthropic API key from the [Anthropic Console](https://console.anthropic.com/).
## Installation
```sh theme={null}
uv add "vision-agents[anthropic]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import anthropic, cartesia, deepgram, getstream, smart_turn
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Friendly AI", id="agent"),
instructions="Be nice to the user.",
llm=anthropic.LLM("claude-sonnet-4-6"),
tts=cartesia.TTS(),
stt=deepgram.STT(),
turn_detection=smart_turn.TurnDetection(),
)
```
Set `ANTHROPIC_API_KEY` in your environment, or pass `api_key` directly to `anthropic.LLM(...)`.
## Parameters
| Name | Type | Default | Description |
| ------------------ | ---------------- | --------------------- | --------------------------------------------------------------------------------------------------------- |
| `model` | `str` | `"claude-sonnet-4-6"` | Any Claude model — see [model overview](https://docs.anthropic.com/en/docs/about-claude/models/overview). |
| `api_key` | `str` | `None` | Anthropic API key (defaults to `ANTHROPIC_API_KEY` env var). |
| `client` | `AsyncAnthropic` | `None` | Bring your own Anthropic client instance instead of letting the plugin construct one. |
| `tools_max_rounds` | `int` | `3` | Maximum multi-hop tool-calling rounds before the LLM is forced to produce a final response. |
## Function Calling
Register Python functions with `llm.register_function` and Claude will call them when the model decides they're needed:
```python theme={null}
from vision_agents.plugins import anthropic
llm = anthropic.LLM("claude-sonnet-4-6")
@llm.register_function(
name="get_weather",
description="Get the current weather for a given city",
)
async def get_weather(city: str) -> dict:
return {"city": city, "temperature": 72, "condition": "Sunny"}
```
Anthropic does not currently offer a realtime speech-to-speech model — pair `anthropic.LLM()` with a separate [STT](/integrations/stt/deepgram) and [TTS](/integrations/tts/elevenlabs) plugin for voice.
## Next Steps
Get started with voice
Add function calling and RAG
# Gemini LLM
Source: https://visionagents.ai/integrations/llm/gemini
[Google's Gemini](https://ai.google.dev/gemini-api/docs/live) provides powerful language models with built-in tools for search, code execution, RAG, and URL context. The LLM mode requires separate STT/TTS.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Gemini also provides [Realtime speech-to-speech](/integrations/realtime/gemini) with optional video over WebSocket.
## Installation
```sh theme={null}
uv add "vision-agents[gemini]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import gemini, getstream, deepgram, elevenlabs
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM(model="gemini-3.1-pro-preview"),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
)
```
## Built-in Tools
Gemini provides built-in tools you can enable:
```python theme={null}
llm = gemini.LLM(
model="gemini-3-flash-preview",
tools=[
gemini.tools.GoogleSearch(),
gemini.tools.CodeExecution(),
gemini.tools.FileSearch(store), # RAG
gemini.tools.URLContext(),
]
)
```
| Tool | Description |
| --------------- | ------------------------------ |
| `GoogleSearch` | Ground responses with web data |
| `CodeExecution` | Run Python code |
| `FileSearch` | RAG over your documents |
| `URLContext` | Read specific web pages |
## File Search (RAG)
Managed RAG with automatic chunking and retrieval:
```python theme={null}
from vision_agents.plugins import gemini
store = gemini.GeminiFilesearchRAG(name="my-knowledge-base")
await store.create()
await store.add_directory("./knowledge")
llm = gemini.LLM(
model="gemini-3-flash-preview",
tools=[gemini.tools.FileSearch(store)]
)
```
See the [RAG guide](/guides/rag) for more details.
## Function Calling
```python theme={null}
@agent.llm.register_function(description="Get weather for a location")
async def get_weather(location: str) -> dict:
return {"temperature": "22°C", "condition": "Sunny"}
```
See the [Function Calling guide](/guides/mcp-tool-calling) for details.
## Events
The Gemini plugin no longer documents provider-specific event classes for normal app code. Prefer core events and provider-agnostic response handling.
## Next Steps
Speech-to-speech with optional video
Get started with voice
# HuggingFace Transformers
Source: https://visionagents.ai/integrations/llm/huggingface-transformers
Run open-weight models locally on your own hardware using [HuggingFace Transformers](https://huggingface.co/docs/transformers). Supports text LLMs, vision-language models, and real-time object detection, all without API calls.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Some models on HuggingFace are gated and require a [HuggingFace account](https://huggingface.co/join) and access token (`HF_TOKEN`).
For cloud-based inference via HuggingFace's Inference Providers API (no GPU required), see [HuggingFace Inference](/integrations/infrastructure/huggingface).
## Installation
```sh theme={null}
# Local inference (LLM, VLM, object detection)
uv add "vision-agents-plugins-huggingface[transformers]"
# With 4-bit / 8-bit quantization support (BitsAndBytes)
uv add "vision-agents-plugins-huggingface[transformers-quantized]"
```
## Local LLM
Run text language models locally with streaming and function calling.
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import huggingface, getstream, deepgram
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=huggingface.TransformersLLM(
model="google/gemma-4-E2B-it",
),
stt=deepgram.STT(),
tts=deepgram.TTS(),
)
```
### Function Calling
```python theme={null}
llm = huggingface.TransformersLLM(model="google/gemma-3-4b-it")
@llm.register_function(description="Get current weather for a city")
async def get_weather(city: str) -> str:
return f"The weather in {city} is sunny."
```
### Quantization
Reduce memory usage with 4-bit or 8-bit quantization. Requires the `[transformers-quantized]` extra.
```python theme={null}
llm = huggingface.TransformersLLM(
model="meta-llama/Llama-3.2-3B-Instruct",
quantization="4bit",
)
```
### LLM Parameters
| Name | Type | Default | Description |
| ------------------- | ------ | -------- | ---------------------------------------------------- |
| `model` | `str` | -- | HuggingFace model ID |
| `device` | `str` | `"auto"` | `"auto"`, `"cuda"`, `"mps"`, or `"cpu"` |
| `quantization` | `str` | `"none"` | `"none"`, `"4bit"`, or `"8bit"` |
| `torch_dtype` | `str` | `"auto"` | `"auto"`, `"float16"`, `"bfloat16"`, or `"float32"` |
| `trust_remote_code` | `bool` | `False` | Allow custom model code (needed for Qwen, Phi, etc.) |
| `max_new_tokens` | `int` | `512` | Maximum tokens to generate per response |
| `max_tool_rounds` | `int` | `3` | Maximum tool-call rounds per response |
## Local VLM
Run vision-language models that can see video frames from the call. Supports function calling.
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import huggingface, getstream, deepgram
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a visual assistant. Describe what you see.",
llm=huggingface.TransformersVLM(
model="Qwen/Qwen2-VL-2B-Instruct",
fps=1,
frame_buffer_seconds=10,
),
stt=deepgram.STT(),
tts=deepgram.TTS(),
)
```
### VLM Parameters
| Name | Type | Default | Description |
| ---------------------- | ------ | -------- | --------------------------------------------------- |
| `model` | `str` | -- | HuggingFace model ID |
| `device` | `str` | `"auto"` | `"auto"`, `"cuda"`, `"mps"`, or `"cpu"` |
| `quantization` | `str` | `"none"` | `"none"`, `"4bit"`, or `"8bit"` |
| `torch_dtype` | `str` | `"auto"` | `"auto"`, `"float16"`, `"bfloat16"`, or `"float32"` |
| `trust_remote_code` | `bool` | `True` | Allow custom model code |
| `fps` | `int` | `1` | Frames per second to capture from video |
| `frame_buffer_seconds` | `int` | `10` | Seconds of video frames to buffer |
| `max_frames` | `int` | `4` | Maximum frames sent per inference (evenly sampled) |
| `max_new_tokens` | `int` | `512` | Maximum tokens to generate per response |
| `max_tool_rounds` | `int` | `3` | Maximum tool-call rounds per response |
| `do_sample` | `bool` | `True` | Use sampling for generation |
## Object Detection
Run detection models like RT-DETRv2 on live video frames. Emits `DetectionCompletedEvent` with bounding boxes for each processed frame.
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import huggingface, getstream, deepgram
processor = huggingface.TransformersDetectionProcessor(
model="PekingU/rtdetr_v2_r101vd",
conf_threshold=0.5,
fps=5,
)
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a visual assistant.",
llm=...,
stt=deepgram.STT(),
tts=deepgram.TTS(),
processors=[processor],
)
@agent.events.subscribe
async def on_detection(event: huggingface.DetectionCompletedEvent):
for obj in event.objects:
print(f"{obj['label']} ({obj['confidence']:.0%})")
```
### Detection Parameters
| Name | Type | Default | Description |
| ---------------- | ----------- | ---------------------------- | -------------------------------------------------- |
| `model` | `str` | `"PekingU/rtdetr_v2_r101vd"` | HuggingFace detection model ID |
| `conf_threshold` | `float` | `0.5` | Confidence threshold (0--1) |
| `fps` | `int` | `10` | Frame processing rate |
| `classes` | `list[str]` | `None` | Filter to specific class names (e.g. `["person"]`) |
| `device` | `str` | `"auto"` | `"auto"`, `"cuda"`, `"mps"`, or `"cpu"` |
| `annotate` | `bool` | `True` | Draw bounding boxes on output video |
## Next Steps
Cloud-based inference (no GPU needed)
Get started with voice
Add video processing
Process video frames in real-time
# Kimi AI
Source: https://visionagents.ai/integrations/llm/kimi
[Kimi AI](https://platform.moonshot.ai/) by Moonshot AI provides advanced multimodal reasoning models with up to 256K context windows. Use Kimi K2.5 and other models through the OpenAI-compatible Chat Completions API with Vision Agents.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Get your Kimi API key from the [Moonshot Platform](https://platform.moonshot.ai/).
## Installation
```sh theme={null}
uv add "vision-agents[openai]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import openai, getstream, deepgram, elevenlabs
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=openai.ChatCompletionsLLM(
model="kimi-k2.5-preview",
base_url="https://api.moonshot.ai/v1",
api_key="your_moonshot_api_key", # or set MOONSHOT_API_KEY env var
),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
)
```
Set `MOONSHOT_API_KEY` in your environment or pass `api_key` directly.
## Parameters
| Name | Type | Default | Description |
| ---------- | ----- | ------------------------------ | ------------------------------------------------ |
| `model` | `str` | — | Model identifier (see available models below) |
| `api_key` | `str` | `None` | API key (defaults to `MOONSHOT_API_KEY` env var) |
| `base_url` | `str` | `"https://api.moonshot.ai/v1"` | Kimi API endpoint |
## Available Models
| Model | Context | Description |
| ---------------------- | ------- | ------------------------------------------------- |
| `kimi-k2.5-preview` | 256K | Latest multimodal model with vision and reasoning |
| `kimi-k2-0711-preview` | 128K | 1T parameter MoE model with strong reasoning |
| `moonshot-v1-8k` | 8K | Fast responses for shorter contexts |
| `moonshot-v1-32k` | 32K | Balanced performance and context |
| `moonshot-v1-128k` | 128K | Extended context for long documents |
Moonshot recommends `temperature=0.6` for balanced accuracy, or
`temperature=1.0` for more creative responses.
## Function Calling
Kimi K2.5 supports function calling with automatic tool invocation:
```python theme={null}
@agent.llm.register_function(description="Get weather for a location")
async def get_weather(location: str) -> str:
return f"The weather in {location} is sunny and 72°F"
```
See the [Function Calling guide](/guides/mcp-tool-calling) for details.
## Next Steps
Get started with voice
Add video processing
# MiniMax
Source: https://visionagents.ai/integrations/llm/minimax
[MiniMax](https://www.minimax.io/) provides powerful large language models, including the latest [MiniMax-M3](https://platform.minimax.io/docs/api-reference/text-openai-api) agentic reasoning model and the M-series lineup. Use them with Vision Agents via the dedicated `minimax` plugin, which wraps MiniMax's OpenAI-compatible Chat Completions API.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Get your MiniMax API key from the [MiniMax Platform](https://platform.minimax.io/).
## Installation
```sh theme={null}
uv add vision-agents["minimax"]
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import minimax, getstream, deepgram, elevenlabs
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=minimax.LLM(), # defaults to MiniMax-M3
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
)
```
Set `MINIMAX_API_KEY` in your environment or pass `api_key` directly. The
plugin also honors `MINIMAX_BASE_URL` if you proxy the API.
When deploying to Asia, pair MiniMax with the
[Tencent RTC edge transport](/integrations/edge-transport/tencent) for the
lowest end-to-end latency, and point at the in-region MiniMax endpoint
`https://api.minimaxi.com/v1` using a key from
[platform.minimaxi.com](https://platform.minimaxi.com/).
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import minimax, tencent, deepgram, elevenlabs
agent = Agent(
edge=tencent.Edge(), # low-latency edge in mainland China and Asia
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=minimax.LLM(
model="MiniMax-M3",
base_url="https://api.minimaxi.com/v1",
),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
)
```
## Parameters
| Name | Type | Default | Description |
| ------------------ | ------------- | ----------------------------- | --------------------------------------------------------------------- |
| `model` | `str` | `"MiniMax-M3"` | Model identifier (see available models below) |
| `api_key` | `str` | `None` | API key (defaults to `MINIMAX_API_KEY` env var) |
| `base_url` | `str` | `"https://api.minimax.io/v1"` | MiniMax API endpoint (overridable via `MINIMAX_BASE_URL`) |
| `client` | `AsyncOpenAI` | `None` | Optional pre-configured `AsyncOpenAI` client for dependency injection |
| `max_tokens` | `int` | `None` | Upper limit for response tokens |
| `tools_max_rounds` | `int` | `3` | Max calling rounds for multi-hop tool calls (must be `>= 1`) |
## Available Models
These models are supported through the [OpenAI-compatible API](https://platform.minimax.io/docs/api-reference/text-openai-api):
| Model | Context | Description |
| ------------------------ | ------- | ----------------------------------------------------------------------------------------------- |
| `MiniMax-M3` (default) | 512K | Latest flagship for agentic reasoning, tool use, coding, and long context; supports image input |
| `MiniMax-M2.7` | 205K | Previous-generation flagship; \~60 tps output |
| `MiniMax-M2.7-highspeed` | 205K | Same as M2.7 with faster output (\~100 tps) |
For `MiniMax-M3`, MiniMax recommends `temperature=1.0` (the API rejects
`0.0`, so the plugin defaults to `1.0`) and `top_p=0.95`. M3 also supports
multimodal input (images and videos) and optional deep thinking via the
`thinking` parameter (`adaptive` by default). The `response_format` field is
not supported by MiniMax and is intentionally not exposed. See the
[MiniMax OpenAI API reference](https://platform.minimax.io/docs/api-reference/text-openai-api)
for `reasoning_split`, streaming usage, and other supported parameters.
## Function Calling
MiniMax models support function calling with automatic tool invocation:
```python theme={null}
@agent.llm.register_function(description="Get weather for a location")
async def get_weather(location: str) -> str:
return f"The weather in {location} is sunny and 72°F"
```
In multi-turn tool conversations, preserve the full assistant message (including
`tool_calls` and any reasoning content) in the conversation history so the
reasoning chain stays intact. Vision Agents handles this when using registered
functions on the agent.
See the [Function Calling guide](/guides/mcp-tool-calling) for details.
## Next Steps
Get started with voice
Add video processing
# OpenAI LLM
Source: https://visionagents.ai/integrations/llm/openai
[OpenAI](https://openai.com) provides industry-leading language models. The plugin supports the **Responses API** (for GPT-5+) and **ChatCompletionsLLM** (any OpenAI-compatible API). Requires separate STT/TTS.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
OpenAI also provides [Realtime speech-to-speech](/integrations/realtime/openai) and [text-to-speech](/integrations/tts/openai).
## Installation
```sh theme={null}
uv add "vision-agents[openai]"
```
## LLM (Responses API)
Uses the Responses API (default for GPT-5+).
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import openai, deepgram, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=openai.LLM(model="gpt-5.4"),
stt=deepgram.STT(),
tts=openai.TTS(),
)
```
| Name | Type | Default | Description |
| ----------------- | ----- | ----------- | ---------------------------------------------- |
| `model` | `str` | `"gpt-5.4"` | Model (e.g., `"gpt-5.4"`) |
| `api_key` | `str` | `None` | API key (defaults to `OPENAI_API_KEY` env var) |
| `base_url` | `str` | `None` | Custom API endpoint |
| `max_tool_rounds` | `int` | `3` | Maximum tool-call rounds per response |
## ChatCompletionsLLM
Works with any OpenAI-compatible API (Together AI, Fireworks, DeepSeek, etc.).
```python theme={null}
from vision_agents.plugins import openai
llm = openai.ChatCompletionsLLM(
model="deepseek-chat",
base_url="https://api.deepseek.com",
api_key="your_api_key"
)
```
| Name | Type | Default | Description |
| ------------------ | ----- | ------- | ---------------------------------------------- |
| `model` | `str` | -- | Model identifier |
| `api_key` | `str` | `None` | API key (defaults to `OPENAI_API_KEY` env var) |
| `base_url` | `str` | `None` | Custom API endpoint |
| `tools_max_rounds` | `int` | `3` | Maximum tool-call rounds per response |
## Function Calling
```python theme={null}
@agent.llm.register_function(description="Get weather for a location")
async def get_weather(location: str) -> dict:
return {"temperature": "72°F", "condition": "Sunny"}
```
See the [Function Calling guide](/guides/mcp-tool-calling) for details.
## Events
The OpenAI plugin no longer documents provider-specific event classes for normal app code. Prefer core events and provider-agnostic response handling.
## Next Steps
Speech-to-speech over WebRTC
Text-to-speech synthesis
# OpenRouter
Source: https://visionagents.ai/integrations/llm/openrouter
[OpenRouter](https://openrouter.ai) is a unified API that provides access to multiple LLM providers through a single OpenAI-compatible interface. Switch between Claude, Gemini, GPT, and more without changing your code.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[openrouter]"
```
## Quick Start
```python theme={null}
from vision_agents.core import User, Agent
from vision_agents.plugins import openrouter, getstream, elevenlabs, deepgram
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=openrouter.LLM(model="anthropic/claude-sonnet-4"),
tts=elevenlabs.TTS(),
stt=deepgram.STT(),
)
```
Set `OPENROUTER_API_KEY` in your environment or pass `api_key` directly.
## Parameters
| Name | Type | Default | Description |
| ------------------ | ----- | -------------------------------- | ----------------------------------------------------------------------- |
| `model` | `str` | `"openrouter/andromeda-alpha"` | Model identifier (see [available models](https://openrouter.ai/models)) |
| `api_key` | `str` | `None` | API key (defaults to `OPENROUTER_API_KEY` env var) |
| `base_url` | `str` | `"https://openrouter.ai/api/v1"` | API base URL |
| `tools_max_rounds` | `int` | `3` | Maximum tool-call rounds per response |
## Available Models
| Provider | Model ID |
| --------- | ---------------------------------------------------------------- |
| Anthropic | `anthropic/claude-sonnet-4`, `anthropic/claude-opus-4` |
| Google | `google/gemini-3-flash-preview`, `google/gemini-3.1-pro-preview` |
| OpenAI | `openai/gpt-5.4`, `openai/gpt-5.4` |
See [OpenRouter Models](https://openrouter.ai/models) for the full list.
## Next Steps
Get started with voice
Add video processing
# Qwen LLM
Source: https://visionagents.ai/integrations/llm/qwen
[Qwen](https://www.alibabacloud.com/en/solutions/generative-ai/qwen) provides powerful language models via the DashScope API. Use `ChatCompletionsLLM` from the OpenAI plugin with Qwen's OpenAI-compatible endpoint.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Qwen also provides [Realtime speech-to-speech](/integrations/realtime/qwen) with native audio I/O and built-in STT/TTS.
## Installation
```sh theme={null}
uv add "vision-agents[openai]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import openai, deepgram, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=openai.ChatCompletionsLLM(
model="qwen-plus",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
api_key="your_dashscope_api_key",
),
stt=deepgram.STT(),
tts=deepgram.TTS(),
)
```
Set `DASHSCOPE_API_KEY` in your environment.
## Next Steps
Native speech-to-speech with built-in STT/TTS
Get started with voice
# Sarvam LLM
Source: https://visionagents.ai/integrations/llm/sarvam
[Sarvam AI](https://www.sarvam.ai/) provides language models built for Indian languages. The plugin uses Sarvam's OpenAI-compatible Chat Completions endpoint and automatically strips `` reasoning blocks from streamed output.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Get your Sarvam API key from the [Sarvam dashboard](https://dashboard.sarvam.ai/).
Sarvam also provides [speech-to-text](/integrations/stt/sarvam) and [text-to-speech](/integrations/tts/sarvam). You can use all three in the same agent.
## Installation
```sh theme={null}
uv add "vision-agents[sarvam]"
```
## Quick start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import sarvam, getstream, smart_turn
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Sarvam Agent", id="agent"),
instructions="Reply in Hindi or English, whichever the user speaks.",
llm=sarvam.LLM(model="sarvam-30b"),
stt=sarvam.STT(language="hi-IN"),
tts=sarvam.TTS(speaker="shubh"),
turn_detection=smart_turn.TurnDetection(),
)
```
Set `SARVAM_API_KEY` in your environment or pass `api_key` directly.
## Parameters
| Name | Type | Default | Description |
| ---------- | ----- | ---------------------------- | ----------------------------------------------------- |
| `model` | `str` | `"sarvam-m"` | Model id (`sarvam-m`, `sarvam-30b`, or `sarvam-105b`) |
| `api_key` | `str` | `None` | API key (defaults to `SARVAM_API_KEY` env var) |
| `base_url` | `str` | `"https://api.sarvam.ai/v1"` | Sarvam API endpoint |
## Available models
| Model | Description |
| ------------- | -------------------------------------------- |
| `sarvam-m` | Default model with hybrid thinking support |
| `sarvam-30b` | 30B parameter model for balanced performance |
| `sarvam-105b` | 105B parameter model for maximum capability |
Sarvam-m supports "hybrid thinking" — it emits `` reasoning blocks before the answer. The plugin automatically strips these so they don't reach TTS.
## Function calling
```python theme={null}
@agent.llm.register_function(description="Get weather for a location")
async def get_weather(location: str) -> str:
return f"The weather in {location} is sunny and 30°C"
```
See the [Function Calling guide](/guides/mcp-tool-calling) for details.
## Next steps
Streaming speech-to-text for Indian languages
Streaming text-to-speech for Indian languages
# xAI (Grok)
Source: https://visionagents.ai/integrations/llm/xai
[xAI's Grok](https://x.ai/) provides advanced reasoning capabilities and real-time knowledge. The plugin supports conversation memory, streaming responses, and function calling (Grok 4.1+).
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
xAI also provides [Realtime speech-to-speech](/integrations/realtime/xai) and [text-to-speech](/integrations/tts/xai).
## Installation
```sh theme={null}
uv add "vision-agents[xai]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import xai, getstream, deepgram, elevenlabs
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=xai.LLM(model="grok-4-latest"),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
)
```
Set `XAI_API_KEY` in your environment or pass `api_key` directly.
## Parameters
| Name | Type | Default | Description |
| ------------------ | ----- | ----------------- | ------------------------------------------- |
| `model` | `str` | `"grok-4-latest"` | Model (`"grok-4-latest"`, `"grok-4.1"`) |
| `api_key` | `str` | `None` | API key (defaults to `XAI_API_KEY` env var) |
| `tools_max_rounds` | `int` | `3` | Maximum tool-call rounds per response |
## Function Calling
Grok 4.1+ supports function calling:
```python theme={null}
@agent.llm.register_function(description="Get weather for a location")
async def get_weather(location: str) -> str:
return f"The weather in {location} is sunny and 72°F"
```
See the [Function Calling guide](/guides/mcp-tool-calling) for details.
## Events
The xAI plugin no longer documents provider-specific event classes for normal app code. Prefer core events and provider-agnostic response handling.
## Next Steps
Speech-to-speech over WebSocket
Text-to-speech with expressive voices
# AWS Bedrock
Source: https://visionagents.ai/integrations/realtime/aws-bedrock
[AWS Bedrock](https://aws.amazon.com/bedrock/) provides realtime speech-to-speech using Amazon Nova Sonic models with automatic session management. The plugin handles Nova's 8-minute connection limit transparently.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
The AWS plugin requires **Python 3.12+**. Nova Sonic is **audio-only** — video
parameters such as `fps` have no effect. For video agents, use [Gemini
Realtime](/integrations/realtime/gemini) or a [custom
pipeline](/introduction/voice-agents#custom-pipeline-mode).
## Installation
```sh theme={null}
uv add "vision-agents[aws,getstream]"
```
The quick start uses `getstream.Edge()`, so both extras are required.
## Environment Variables
```bash theme={null}
STREAM_API_KEY=...
STREAM_API_SECRET=...
AWS_ACCESS_KEY_ID=... # or IAM role / ~/.aws profile
AWS_SECRET_ACCESS_KEY=...
```
You also need Bedrock **model access** enabled for Nova Sonic in your chosen region, and IAM permission for bidirectional streaming (`bedrock:InvokeModelWithBidirectionalStream`).
## Quick Start
```python theme={null}
from dotenv import load_dotenv
from vision_agents.core import Agent, AgentLauncher, Runner, User
from vision_agents.plugins import aws, getstream
load_dotenv()
async def create_agent(**kwargs) -> Agent:
return Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful voice assistant.",
llm=aws.Realtime(),
)
async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
call = await agent.create_call(call_type, call_id)
async with agent.join(call):
await agent.finish()
if __name__ == "__main__":
Runner(AgentLauncher(create_agent=create_agent, join_call=join_call)).cli()
```
AWS credentials are resolved via the standard AWS SDK chain (environment
variables, AWS profiles via `aws_profile`, or IAM roles). The `aws.Realtime`
constructor does not accept explicit access key parameters.
## Parameters
| Name | Type | Default | Description |
| ------------------------- | ------- | ---------------------------- | --------------------------------------------------------------------------------------------------- |
| `model` | `str` | `"amazon.nova-2-sonic-v1:0"` | Nova model ID |
| `region_name` | `str` | `"us-east-1"` | AWS region |
| `voice_id` | `str` | `"matthew"` | Voice ([available voices](https://docs.aws.amazon.com/nova/latest/userguide/available-voices.html)) |
| `reconnect_after_minutes` | `float` | `5.0` | Reconnect after this many minutes of connection age, when audio is idle |
| `aws_profile` | `str` | `None` | AWS profile name from `~/.aws/credentials` or `~/.aws/config` |
## Nova Behavior
* **`instructions` are required** before the model accepts user input.
* **Text-only prompts may not produce audio** — use audio input for reliable responses.
* **Function calling** may require at least one audio content block before text tool calls work reliably.
## Automatic Reconnection
AWS Bedrock has an 8-minute connection limit. The plugin handles this automatically:
* After **5 minutes of connection age** (configurable via `reconnect_after_minutes`) **and more than 3 seconds since last audio activity**, reconnects during a quiet moment
* After **7 minutes of connection age**, forces reconnect regardless of audio activity
Last audio activity includes incoming user speech (detected by Silero VAD) and outgoing agent audio.
## Voice Activity Detection
The plugin uses Silero VAD to track **incoming user speech** for reconnection timing. Agent audio output updates activity separately. Silero warmup is handled automatically by the Agent lifecycle.
## Function Calling
Register tools on the LLM before the agent connects:
```python theme={null}
llm = aws.Realtime()
@llm.register_function(description="Get weather for a location")
async def get_weather(location: str) -> dict:
return {"city": location, "temperature": 72, "condition": "Sunny"}
agent = Agent(..., llm=llm)
```
See the [Function Calling guide](/guides/mcp-tool-calling) for details.
## Next Steps
Get started with voice
Tools and MCP integration
# Gemini Realtime
Source: https://visionagents.ai/integrations/realtime/gemini
[Google's Gemini](https://ai.google.dev/gemini-api/docs/live) provides native multimodal speech-to-speech over WebSocket with optional video. No separate STT/TTS services required.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Gemini also provides a traditional [LLM](/integrations/llm/gemini) with built-in tools for search, code execution, and RAG.
## Installation
```sh theme={null}
uv add "vision-agents[gemini]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.Realtime(fps=3), # Video frames sent to model
)
```
## Parameters
| Name | Type | Default | Description |
| -------------------- | ------------------------ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model` | `str` | `"gemini-3.1-flash-live-preview"` | Gemini model. Pass `"gemini-3.5-live-translate-preview"` to use Live Translate. |
| `fps` | `int` | `1` | Video frames per second |
| `config` | `LiveConnectConfigDict` | `None` | Optional config dict to customize session behavior |
| `input_audio_pacing` | `AudioInputPacingConfig` | `None` | Buffers irregular upstream PCM and forwards fixed-size chunks (default 20 ms) at a stable wall-clock rate. Auto-enabled with `AudioInputPacingConfig.virtual_microphone()` when `model` is `"gemini-3.5-live-translate-preview"`; pass `None` explicitly to opt out. |
| `api_key` | `str` | `None` | API key (defaults to `GOOGLE_API_KEY` env var) |
## Voice Activity Detection
The Gemini Realtime plugin includes built-in voice activity detection (VAD) with defaults optimized for low-latency conversations. You can override these settings via the `config` parameter:
```python theme={null}
from google.genai.types import (
AutomaticActivityDetectionDict,
EndSensitivity,
RealtimeInputConfigDict,
StartSensitivity,
TurnCoverage,
)
llm = gemini.Realtime(
config={
"realtime_input_config": RealtimeInputConfigDict(
turn_coverage=TurnCoverage.TURN_INCLUDES_ONLY_ACTIVITY,
automatic_activity_detection=AutomaticActivityDetectionDict(
start_of_speech_sensitivity=StartSensitivity.START_SENSITIVITY_HIGH,
end_of_speech_sensitivity=EndSensitivity.END_SENSITIVITY_HIGH,
silence_duration_ms=250,
prefix_padding_ms=50,
),
),
},
)
```
| Name | Type | Default | Description |
| ----------------------------- | ------------------ | ------------------------ | ------------------------------------------------------------- |
| `start_of_speech_sensitivity` | `StartSensitivity` | `START_SENSITIVITY_HIGH` | How quickly the model detects the start of speech |
| `end_of_speech_sensitivity` | `EndSensitivity` | `END_SENSITIVITY_HIGH` | How quickly the model detects the end of speech |
| `silence_duration_ms` | `int` | `250` | Milliseconds of silence before the model considers a turn end |
| `prefix_padding_ms` | `int` | `50` | Milliseconds of audio to include before detected speech start |
Higher sensitivity values make the model react faster to speech starts and stops, which reduces latency but may increase false positives in noisy environments.
## VLM (Vision Language Model)
Use Gemini 3 vision models for multimodal interactions with video frames. The VLM buffers video frames, converts them to JPEG, and sends them alongside text prompts.
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import gemini, getstream, deepgram, elevenlabs
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Vision Agent", id="vision-agent"),
instructions="Describe what you see in one sentence.",
llm=gemini.VLM(model="gemini-3-flash-preview"),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
)
```
| Name | Type | Default | Description |
| ---------------------- | ----------------- | -------------------------- | ---------------------------------------------- |
| `model` | `str` | `"gemini-3-flash-preview"` | Gemini vision model |
| `fps` | `int` | `1` | Video frames per second to capture |
| `frame_buffer_seconds` | `int` | `10` | Seconds of video to buffer for model input |
| `thinking_level` | `ThinkingLevel` | `None` | Thinking level for enhanced reasoning |
| `media_resolution` | `MediaResolution` | `None` | Resolution for multimodal processing |
| `api_key` | `str` | `None` | API key (defaults to `GOOGLE_API_KEY` env var) |
## Next Steps
LLM with built-in tools and RAG
Add video processing
# Inworld Realtime
Source: https://visionagents.ai/integrations/realtime/inworld
[Inworld AI](https://inworld.ai) provides a WebRTC-based Realtime speech-to-speech API. Uses native Opus over UDP for lower latency than WebSocket alternatives.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Inworld also provides standalone [text-to-speech](/integrations/tts/inworld).
## Installation
```sh theme={null}
uv add "vision-agents[inworld]"
```
Get your API key from the [Inworld Portal](https://studio.inworld.ai/) and set `INWORLD_API_KEY` in your environment (or pass `api_key=` explicitly).
## Quick Start
Pair Inworld Realtime with a WebRTC-capable edge transport like `getstream.Edge()`.
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import getstream, inworld, smart_turn
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="My Agent", id="agent"),
llm=inworld.Realtime(
model="openai/gpt-4o-mini",
voice="Dennis",
instructions="You are a friendly voice assistant.",
),
turn_detection=smart_turn.TurnDetection(),
)
```
## Parameters
| Name | Type | Default | Description |
| ------------------ | ----------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `model` | `str` | `"openai/gpt-4o-mini"` | Provider-prefixed model ID (e.g. `"openai/gpt-4o-mini"`, `"google-ai-studio/gemini-2.5-flash"`, `"inworld/"`) |
| `voice` | `str` | `"Dennis"` | Voice for audio responses (`"Dennis"`, `"Clive"`, `"Olivia"`, or custom) |
| `api_key` | `str` | `None` | API key (defaults to `INWORLD_API_KEY` env var) |
| `instructions` | `str` | `None` | System prompt |
| `realtime_session` | `RealtimeSessionCreateRequestParam` | `None` | Advanced — pass a full session param for custom turn detection, `tool_choice`, and other fields |
## Registering Tools
Inworld's Realtime API is protocol-compatible with OpenAI's Realtime API, so registered functions follow the OpenAI function-calling schema.
```python theme={null}
realtime = inworld.Realtime()
@realtime.register_function(description="Get the current weather for a city.")
async def get_weather(city: str) -> str:
return f"It's sunny in {city}."
```
v1 is WebRTC only; a WebSocket transport may be added later. Video input is not currently supported by Inworld's Realtime API.
## Next Steps
Standalone text-to-speech
Get started with voice
# OpenAI Realtime
Source: https://visionagents.ai/integrations/realtime/openai
[OpenAI](https://openai.com) provides native speech-to-speech over WebRTC with built-in STT/TTS. No separate speech services required.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
OpenAI also provides a traditional [LLM](/integrations/llm/openai) (Responses API and ChatCompletions) and standalone [text-to-speech](/integrations/tts/openai).
## Installation
```sh theme={null}
uv add "vision-agents[openai]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import openai, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful voice assistant.",
llm=openai.Realtime(model="gpt-realtime", voice="marin", fps=1),
)
```
## Parameters
| Name | Type | Default | Description |
| ------- | ----- | ------------------ | -------------------------------------- |
| `model` | `str` | `"gpt-realtime-2"` | OpenAI realtime model |
| `voice` | `str` | `"marin"` | Voice ("marin", "alloy", "echo", etc.) |
| `fps` | `int` | `1` | Video frames per second |
## Next Steps
Responses API and ChatCompletions
Get started with voice
# Qwen Realtime
Source: https://visionagents.ai/integrations/realtime/qwen
[Qwen3 Realtime](https://www.alibabacloud.com/en/solutions/generative-ai/qwen) provides native audio I/O with built-in STT and TTS over WebSocket. No external speech services required.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Qwen models can also be used as a traditional [LLM](/integrations/llm/qwen) via their OpenAI-compatible endpoint.
## Installation
```sh theme={null}
uv add "vision-agents[qwen]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import qwen, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=qwen.Realtime(fps=1), # Enable video with fps > 0
)
```
Set `DASHSCOPE_API_KEY` in your environment.
## Parameters
| Name | Type | Default | Description |
| ------------------------- | ------ | ----------------------------- | ------------------------------------------------- |
| `model` | `str` | `"qwen3-omni-flash-realtime"` | Qwen Realtime model |
| `voice` | `str` | `"Cherry"` | Voice for audio output |
| `fps` | `int` | `1` | Video frames per second |
| `include_video` | `bool` | `False` | Include video frames |
| `vad_silence_duration_ms` | `int` | `900` | Silence before turn end |
| `api_key` | `str` | `None` | API key (defaults to `DASHSCOPE_API_KEY` env var) |
Qwen Realtime does not support text input. Start speaking once you join the
call.
## Next Steps
Traditional LLM via OpenAI-compatible API
Get started with voice
# xAI Realtime
Source: https://visionagents.ai/integrations/realtime/xai
Speech-to-speech using xAI's Grok models over WebSocket with built-in VAD.
[xAI](https://x.ai/) provides realtime speech-to-speech over WebSocket with server-side voice activity detection, built-in web search, and X search. No separate STT/TTS needed.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
xAI also provides a traditional [LLM](/integrations/llm/xai) and standalone [text-to-speech](/integrations/tts/xai).
## Installation
```sh theme={null}
uv add "vision-agents[xai]"
```
## Quick start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import xai, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful voice assistant.",
llm=xai.Realtime(),
)
```
Set `XAI_API_KEY` in your environment or pass `api_key` directly.
## Parameters
| Name | Type | Default | Description |
| -------------------------- | --------------- | ----------------------------- | ------------------------------------------------------------------ |
| `model` | `str` | `"grok-voice-think-fast-1.0"` | Grok realtime model |
| `voice` | `str` | `"ara"` | Voice (`"ara"`, `"rex"`, `"sal"`, `"eve"`, `"leo"`) |
| `api_key` | `str` | `None` | API key (defaults to `XAI_API_KEY` env var) |
| `turn_detection` | `str` or `None` | `"server_vad"` | Turn detection mode (`"server_vad"` or `None` for manual) |
| `vad_interrupt_response` | `bool` | `False` | Allow VAD to auto-cancel the assistant response on detected speech |
| `web_search` | `bool` | `True` | Enable web search tool |
| `x_search` | `bool` | `True` | Enable X (Twitter) search tool |
| `x_search_allowed_handles` | `list[str]` | `None` | Restrict X search to specific handles |
`vad_interrupt_response` defaults to `False` because speaker-to-mic echo can cause the server to cancel the agent's own response mid-sentence. Set to `True` only if your audio setup avoids echo feedback.
## Function calling
```python theme={null}
@agent.llm.register_function(description="Get weather for a location")
async def get_weather(location: str) -> str:
return f"The weather in {location} is sunny and 72°F"
```
See the [Function Calling guide](/guides/mcp-tool-calling) for details.
## Next steps
Advanced reasoning with Grok
Text-to-speech with expressive voices
# AssemblyAI
Source: https://visionagents.ai/integrations/stt/assemblyai
[AssemblyAI](https://www.assemblyai.com) provides real-time streaming speech-to-text with built-in punctuation-based turn detection and sub-300ms latency.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[assemblyai]"
```
## Quick start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import assemblyai, cartesia, gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM(),
stt=assemblyai.STT(),
tts=cartesia.TTS(),
)
```
Set `ASSEMBLYAI_API_KEY` in your environment or pass `api_key` directly.
## STT
Real-time transcription using AssemblyAI's Universal-3 Pro model with built-in turn detection.
```python theme={null}
stt = assemblyai.STT(
speech_model="u3-rt-pro",
sample_rate=16000,
)
```
### With keyterms boosting
Boost recognition accuracy for specific terms:
```python theme={null}
stt = assemblyai.STT(
keyterms_prompt=["AssemblyAI", "Vision Agents"],
)
```
### With custom turn silence thresholds
Configure turn detection timing:
```python theme={null}
stt = assemblyai.STT(
min_turn_silence=100, # ms before speculative EOT check
max_turn_silence=1200, # ms before forcing turn end
)
```
## Parameters
| Name | Type | Default | Description |
| ----------------------------- | ----------- | ------------- | ------------------------------------------------------------------------- |
| `api_key` | `str` | `None` | API key (defaults to `ASSEMBLYAI_API_KEY` env var) |
| `speech_model` | `str` | `"u3-rt-pro"` | Model identifier |
| `sample_rate` | `int` | `16000` | Audio sample rate in Hz |
| `min_turn_silence` | `int` | API default | Silence (ms) before speculative end-of-turn check |
| `max_turn_silence` | `int` | API default | Maximum silence (ms) before forcing turn end |
| `prompt` | `str` | `None` | Custom transcription prompt (cannot be combined with `keyterms_prompt`) |
| `keyterms_prompt` | `list[str]` | `None` | List of terms to boost recognition for (cannot be combined with `prompt`) |
| `max_reconnect_attempts` | `int` | `3` | Maximum reconnect attempts on transient failures |
| `reconnect_backoff_initial_s` | `float` | `0.5` | Initial backoff delay in seconds |
| `reconnect_backoff_max_s` | `float` | `4.0` | Maximum backoff delay in seconds |
## Next steps
Get started with voice
Add video processing
# Cartesia STT
Source: https://visionagents.ai/integrations/stt/cartesia
[Cartesia](https://cartesia.ai/?utm_medium=partner\&utm_source=getstream) provides low-latency speech-to-text with the Ink model. `STT` streams PCM audio to Cartesia Ink and emits transcript and turn events that Vision Agents uses for interruption and eager turn handling.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Cartesia also provides low-latency [text-to-speech](/integrations/tts/cartesia). You can use both in the same agent.
## Installation
```sh theme={null}
uv add "vision-agents[cartesia]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import cartesia, gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=cartesia.STT(),
tts=cartesia.TTS(),
)
```
Set `CARTESIA_API_KEY` in your environment or pass `api_key` directly.
## Parameters
| Name | Type | Default | Description |
| ------------------------- | ----- | --------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `api_key` | `str` | `None` | API key (defaults to `CARTESIA_API_KEY` env var) |
| `model` | `str` | `"ink-2"` | Cartesia STT model |
| `sample_rate` | `int` | `16000` | PCM sample rate (Hz) sent to Cartesia. One of `8000`, `16000`, `22050`, `24000`, `44100`, `48000` |
| `encoding` | `str` | `"pcm_s16le"` | PCM encoding sent to Cartesia |
| `cartesia_version` | `str` | `"2026-03-01"` | Cartesia API version used for the turn-detection websocket |
| `websocket_url` | `str` | `"wss://api.cartesia.ai/stt/turns/websocket"` | WebSocket endpoint (mainly useful for tests) |
| `audio_chunk_duration_ms` | `int` | `100` | Maximum duration per WebSocket audio frame (ms) |
## Next Steps
Low-latency text-to-speech
Get started with voice
# Deepgram STT
Source: https://visionagents.ai/integrations/stt/deepgram
[Deepgram](https://deepgram.com) provides fast, accurate real-time speech-to-text with built-in turn detection. Ideal for conversational agents.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Deepgram also provides low-latency [text-to-speech](/integrations/tts/deepgram). You can use both in the same agent.
## Installation
```sh theme={null}
uv add "vision-agents[deepgram]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import deepgram, gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=deepgram.STT(),
tts=deepgram.TTS(),
)
```
Set `DEEPGRAM_API_KEY` in your environment or pass `api_key` directly.
## Parameters
```python theme={null}
stt = deepgram.STT(
model="nova-3",
language="en",
eager_turn_detection=True,
)
```
| Name | Type | Default | Description |
| ---------------------- | ------ | ---------- | ------------------------------------------------ |
| `model` | `str` | `"nova-3"` | Deepgram model |
| `language` | `str` | `"en"` | Language code |
| `eager_turn_detection` | `bool` | `False` | Enable faster turn detection |
| `api_key` | `str` | `None` | API key (defaults to `DEEPGRAM_API_KEY` env var) |
## Next Steps
Low-latency text-to-speech
Get started with voice
# ElevenLabs STT
Source: https://visionagents.ai/integrations/stt/elevenlabs
[ElevenLabs](https://www.elevenlabs.io) provides real-time speech-to-text via Scribe v2 with \~150ms latency, 99 languages, and built-in VAD-based turn detection. No separate turn detection plugin is needed.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
ElevenLabs also provides highly realistic [text-to-speech](/integrations/tts/elevenlabs). You can use both in the same agent.
## Installation
```sh theme={null}
uv add "vision-agents[elevenlabs]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import elevenlabs, gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=elevenlabs.STT(),
tts=elevenlabs.TTS(),
)
```
Set `ELEVENLABS_API_KEY` in your environment or pass `api_key` directly.
## Parameters
```python theme={null}
stt = elevenlabs.STT(
model_id="scribe_v2_realtime",
language_code="en",
)
```
| Name | Type | Default | Description |
| ---------------------------- | ------- | ---------------------- | -------------------------------------------------- |
| `model_id` | `str` | `"scribe_v2_realtime"` | Scribe model |
| `language_code` | `str` | `"en"` | Language code |
| `api_key` | `str` | `None` | API key (defaults to `ELEVENLABS_API_KEY` env var) |
| `vad_silence_threshold_secs` | `float` | `0.3` | Silence duration (seconds) before VAD commits |
| `vad_threshold` | `float` | `0.4` | VAD sensitivity threshold for speech detection |
| `min_speech_duration_ms` | `int` | `100` | Minimum speech duration in milliseconds |
| `min_silence_duration_ms` | `int` | `100` | Minimum silence duration in milliseconds |
| `audio_chunk_duration_ms` | `int` | `100` | Audio chunk size sent to the server (100-1000ms) |
ElevenLabs STT includes built-in turn detection via VAD. When you use `elevenlabs.STT`, the `Agent` automatically ignores any external `TurnDetector` plugin to prevent conflicts. You do not need to add a separate turn detection plugin.
## Next Steps
Expressive text-to-speech
Get started with voice
# Fast-Whisper
Source: https://visionagents.ai/integrations/stt/fast-whisper
[Fast-Whisper](https://github.com/guillaumekln/faster-whisper) is a high-performance local STT using CTranslate2. Provides 2-4x faster inference than standard Whisper with support for CPU and GPU acceleration.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[fast-whisper]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import fast_whisper, gemini, elevenlabs, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=fast_whisper.STT(model_size="base"),
tts=elevenlabs.TTS(),
)
```
Fast-Whisper runs locally. No API key required. Models download automatically
on first use.
## Parameters
| Name | Type | Default | Description |
| -------------- | ----- | -------- | -------------------------------------------------------------------- |
| `model_size` | `str` | `"base"` | Model size (`"tiny"`, `"base"`, `"small"`, `"medium"`, `"large-v3"`) |
| `language` | `str` | `None` | Language code or `None` for auto-detect |
| `device` | `str` | `"cpu"` | Device (`"cpu"`, `"cuda"`, `"auto"`) |
| `compute_type` | `str` | `"int8"` | Precision (`"int8"`, `"float16"`, `"float32"`) |
## Model Sizes
| Model | Speed | Use Case |
| ---------- | --------- | ------------------------------- |
| `tiny` | Fastest | Real-time, resource-constrained |
| `base` | Very Fast | General purpose |
| `small` | Fast | Balanced |
| `medium` | Moderate | Higher accuracy |
| `large-v3` | Slower | Maximum accuracy |
## Optimization
```python theme={null}
# CPU (default) - use int8 for best performance
stt = fast_whisper.STT(device="cpu", compute_type="int8")
# GPU - use float16 for speed and accuracy
stt = fast_whisper.STT(device="cuda", compute_type="float16")
```
## Next Steps
Get started with voice
Add video processing
# Fish Audio STT
Source: https://visionagents.ai/integrations/stt/fish
[Fish Audio](https://fish.audio) provides speech-to-text with automatic language detection. Buffers audio per participant (minimum 1 second) before sending to the API for accurate transcription.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Fish Audio also provides high-quality [text-to-speech](/integrations/tts/fish) with prosody control and voice cloning. You can use both in the same agent.
## Installation
```sh theme={null}
uv add "vision-agents[fish]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import fish, gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=fish.STT(),
tts=fish.TTS(),
)
```
Set `FISH_API_KEY` in your environment or pass `api_key` directly.
## Parameters
```python theme={null}
stt = fish.STT(language="en") # Or None for auto-detection
```
| Name | Type | Default | Description |
| ---------- | ----- | ------- | -------------------------------------------------------------- |
| `language` | `str` | `None` | Language code (`"en"`, `"zh"`, etc.) or `None` for auto-detect |
| `api_key` | `str` | `None` | API key (defaults to `FISH_API_KEY` env var) |
## Next Steps
Text-to-speech with prosody control
Get started with voice
# Mistral Voxtral
Source: https://visionagents.ai/integrations/stt/mistral
[Mistral Voxtral](https://docs.mistral.ai/capabilities/audio_transcription#realtime-transcription) provides real-time speech-to-text via WebSocket streaming with automatic language detection and low-latency transcription.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[mistral]"
```
## Quick start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import mistral, gemini, deepgram, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=mistral.STT(),
tts=deepgram.TTS(),
)
```
Set `MISTRAL_API_KEY` in your environment or pass `api_key` directly.
## Parameters
| Name | Type | Default | Description |
| ------------- | ----- | ----------------------------------------- | ---------------------------------------------------------- |
| `api_key` | `str` | `None` | API key (defaults to `MISTRAL_API_KEY` env var) |
| `model` | `str` | `"voxtral-mini-transcribe-realtime-2602"` | Model identifier |
| `sample_rate` | `int` | `16000` | Audio sample rate in Hz (8000, 16000, 22050, 44100, 48000) |
## Turn detection
Mistral Voxtral STT does not include built-in turn detection. Pair it with an external turn detection plugin like [Smart Turn](/integrations/turn-detection/smart-turn) or [Vogent](/integrations/turn-detection/vogent).
```python theme={null}
from vision_agents.plugins import mistral, smart_turn
agent = Agent(
stt=mistral.STT(),
turn_detection=smart_turn.TurnDetection(),
# ... other config
)
```
## Next steps
Get started with voice
Add video processing
# Sarvam STT
Source: https://visionagents.ai/integrations/stt/sarvam
[Sarvam AI](https://www.sarvam.ai/) provides streaming speech-to-text built for Indian languages. The plugin uses WebSocket streaming with built-in voice activity detection for turn events.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Get your Sarvam API key from the [Sarvam dashboard](https://dashboard.sarvam.ai/).
Sarvam also provides [text-to-speech](/integrations/tts/sarvam) and an [LLM](/integrations/llm/sarvam). You can use all three in the same agent.
## Installation
```sh theme={null}
uv add "vision-agents[sarvam]"
```
## Quick start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import sarvam, getstream, smart_turn
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Sarvam Agent", id="agent"),
instructions="Reply in the same language the user speaks.",
llm=sarvam.LLM(model="sarvam-m"),
stt=sarvam.STT(language="hi-IN"),
tts=sarvam.TTS(speaker="shubh"),
turn_detection=smart_turn.TurnDetection(),
)
```
Set `SARVAM_API_KEY` in your environment or pass `api_key` directly.
## Parameters
```python theme={null}
stt = sarvam.STT(
model="saaras:v3",
language="hi-IN",
mode="transcribe",
sample_rate=16000,
high_vad_sensitivity=False,
)
```
| Name | Type | Default | Description |
| ---------------------- | ------ | ------------- | ---------------------------------------------------------------------------------- |
| `model` | `str` | `"saaras:v3"` | Streaming model (`saaras:v3`, `saarika:v2.5`, `saaras:v2.5`) |
| `language` | `str` | `None` | Language code (e.g. `hi-IN`, `en-IN`). `None` for auto-detect |
| `mode` | `str` | `None` | `transcribe`, `translate`, `verbatim`, `translit`, or `codemix` (`saaras:v3` only) |
| `sample_rate` | `int` | `16000` | Input sample rate — `8000` or `16000` Hz |
| `high_vad_sensitivity` | `bool` | `False` | Increase VAD sensitivity for noisy input |
| `vad_signals` | `bool` | `True` | Emit speech start/end events for turn detection |
| `prompt` | `str` | `None` | Optional biasing prompt sent after connect (`saaras:v2.5` and `saaras:v3` only) |
| `api_key` | `str` | `None` | API key (defaults to `SARVAM_API_KEY` env var) |
## Next steps
Streaming text-to-speech for Indian languages
Chat completions with Sarvam models
# Wizper
Source: https://visionagents.ai/integrations/stt/wizper
[Wizper](https://fal.ai/models/fal-ai/wizper) is a real-time Whisper v3 variant hosted by Fal.ai. Provides accurate STT with on-the-fly translation to 99+ languages.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[wizper]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import wizper, gemini, elevenlabs, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=wizper.STT(),
tts=elevenlabs.TTS(),
)
```
Set `FAL_KEY` in your environment for Fal.ai authentication.
## Parameters
| Name | Type | Default | Description |
| ----------------- | ----- | -------------- | ----------------------------------------------------- |
| `task` | `str` | `"transcribe"` | Task (`"transcribe"` or `"translate"`) |
| `target_language` | `str` | `None` | ISO-639-1 code for translation (e.g., `"es"`, `"fr"`) |
| `sample_rate` | `int` | `48000` | Audio sample rate in Hz |
## Translation
Translate speech to any supported language:
```python theme={null}
# Translate all speech to Spanish
stt = wizper.STT(target_language="es")
```
## Next Steps
Get started with voice
Add video processing
# Telnyx
Source: https://visionagents.ai/integrations/telephony/telnyx
Connect inbound and outbound PSTN phone calls via Telnyx Call Control and bidirectional media streaming
[Telnyx](https://telnyx.com/) is a programmable telephony provider. The `telnyx` plugin bridges PSTN phone calls into a Vision Agents + Stream call through Telnyx Call Control webhooks and bidirectional Media Streaming.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## What the plugin provides
* Call Control webhook handling pattern (via your FastAPI server)
* Bidirectional Telnyx Media Streaming over WebSocket
* `CallRegistry` for call/session/token tracking
* `attach_phone_to_call` to bridge Telnyx audio ↔ Stream WebRTC
* Audio conversion: PCMU, PCMA (8 kHz), L16 (16 kHz)
```mermaid theme={null}
sequenceDiagram
participant PSTN as PhoneNetwork
participant Telnyx as TelnyxCallControl
participant App as FastAPI_Server
participant Plugin as telnyx_plugin
participant Stream as StreamCall
PSTN->>Telnyx: inbound_or_outbound_call
Telnyx->>App: POST_/telnyx/events
App->>Telnyx: answer_or_dial_with_stream_url
Telnyx->>App: WS_/telnyx/media/{call_id}/{token}
App->>Plugin: MediaStream.accept_and_run
App->>Plugin: attach_phone_to_call
Plugin->>Stream: bridge_audio_bidirectional
```
## Prerequisites
| Requirement | Notes |
| ----------------------------------------------- | ------------------------------------------------------------------------------- |
| [Telnyx account](https://telnyx.com/) + API key | `TELNYX_API_KEY` |
| Telnyx phone number (E.164) | `TELNYX_PHONE_NUMBER` |
| **Call Control App** | Required — a forwarding-only number connection is **not** enough |
| Webhook public URL | [ngrok](https://ngrok.com/) for local dev → `https:///telnyx/events` |
| Webhook signature key | `TELNYX_PUBLIC_KEY` (Base64 Ed25519 from Mission Control Portal) |
| Stream credentials | `STREAM_API_KEY`, `STREAM_API_SECRET` |
Telnyx media streaming for programmable calls requires a **Call Control App**
with a webhook URL. A regular phone-number connection — including a
forwarding-only connection — is not enough. The Call Control App ID is also
the `connection_id` used by the outbound Dial API.
## Installation
```sh theme={null}
uv add "vision-agents[telnyx]"
```
Or install the plugin package directly:
```sh theme={null}
uv add vision-agents-plugins-telnyx
```
## Environment Variables
| Variable | Required | Description |
| -------------------------------------- | ---------------- | --------------------------------------------------------------------- |
| `TELNYX_API_KEY` | Yes | Telnyx API key for Call Control / Dial |
| `TELNYX_PUBLIC_KEY` | Yes (production) | Ed25519 public key for webhook signature verification |
| `TELNYX_PHONE_NUMBER` | Yes | Caller ID / inbound number in E.164 |
| `NGROK_URL` | Local dev | Public hostname without `https://` |
| `TELNYX_CALL_CONTROL_APP_ID` | Manual setup | Existing Call Control App ID (skip if using example `--setup-telnyx`) |
| `TELNYX_PHONE_NUMBER_ID` | Inbound manual | Telnyx phone number resource ID |
| `STREAM_API_KEY` / `STREAM_API_SECRET` | Yes | Stream edge transport |
## Telnyx account setup
**Quick local dev (`--setup-telnyx`)** — the plugin examples auto-create a temporary Call Control App, set the webhook URL, route the inbound number, and clean up on shutdown.
**Manual setup:**
1. Create a Telnyx Call Control App.
2. Set the app webhook URL to `https:///telnyx/events`.
3. Route your inbound phone number to that Call Control App.
4. For outbound calls, ensure the app has an outbound voice profile for your target country. On restricted accounts, verify destination numbers before dialing.
See the full setup guide in the [Telnyx plugin examples](https://github.com/GetStream/Vision-Agents/tree/main/plugins/telnyx/examples) on GitHub.
## Quick Start
The plugin gives you registry, media stream, and bridge primitives. Your FastAPI server wires them to Telnyx webhooks and WebSockets:
```python theme={null}
from vision_agents.plugins import telnyx
registry = telnyx.CallRegistry()
# 1. Register call from webhook handler
call = registry.create(
call_control_id,
webhook_data=webhook_data,
prepare=prepare_call, # optional: pre-warm agent + Stream call
)
# 2. Answer/dial with media stream URL
stream_url = f"wss://{NGROK_URL}/telnyx/media/{call_control_id}/{call.token}"
# 3. WebSocket handler
stream = telnyx.MediaStream(websocket)
await stream.accept()
call.telnyx_stream = stream
agent, phone_user, stream_call = await call.await_prepare()
await telnyx.attach_phone_to_call(stream_call, stream, phone_user.id)
await stream.run()
```
Webhook signature verification is shown in the plugin examples
(`example_helpers.parse_verified_telnyx_webhook`) but is **application-level**
— not exported from the plugin's public API. See the
[inbound example source](https://github.com/GetStream/Vision-Agents/blob/main/plugins/telnyx/examples/inbound_call.py)
for the Ed25519 verification pattern.
## Inbound calls
Telnyx sends webhooks to `POST /telnyx/events`. On `call.initiated` with direction `incoming`, register the call and answer with a media stream URL:
```python theme={null}
@app.post("/telnyx/events")
async def telnyx_events(request: Request):
data = await parse_verified_telnyx_webhook(request, telnyx_public_key)
event_type = data["data"]["event_type"]
payload = data["data"]["payload"]
if event_type == "call.initiated" and payload["direction"] == "incoming":
call_control_id = payload["call_control_id"]
telnyx_call = registry.create(
call_control_id,
webhook_data=data,
prepare=lambda: prepare_call(call_control_id),
)
stream_url = f"wss://{NGROK_URL}/telnyx/media/{call_control_id}/{telnyx_call.token}"
await telnyx_client.answer_call(call_control_id, stream_url=stream_url)
return {"ok": True}
@app.websocket("/telnyx/media/{call_id}/{token}")
async def media_stream(websocket: WebSocket, call_id: str, token: str):
telnyx_call = registry.validate(call_id, token)
stream = telnyx.MediaStream(websocket)
await stream.accept()
# attach to agent and run stream (see Quick Start)
```
## Outbound calls
Pre-register the call in the registry, start your server, then dial via the Telnyx Call Control API with `connection_id` set to your Call Control App ID:
```python theme={null}
call_id = str(uuid.uuid4())
telnyx_call = registry.create(call_id, prepare=lambda: prepare_call(call_id))
stream_url = f"wss://{NGROK_URL}/telnyx/media/{call_id}/{telnyx_call.token}"
await telnyx_client.dial(
to=to_number,
from_=from_number,
connection_id=call_control_app_id,
stream_url=stream_url,
)
```
The WebSocket media handler is the same as inbound.
## Key Components
| Component | Description |
| ------------------------------------- | ----------------------------------------------------------------------- |
| `TelnyxCallRegistry` / `CallRegistry` | Tracks active calls, tokens, optional async `prepare` tasks |
| `TelnyxCall` | Call session with `from_number`, `to_number`, `await_prepare()` |
| `TelnyxMediaStream` / `MediaStream` | WebSocket media handler; exposes `audio_track`, `send_audio()`, `run()` |
| `attach_phone_to_call` | Bridges Telnyx RTP audio ↔ Stream call participant |
| Audio helpers | `pcmu_to_pcm`, `pcm_to_pcmu`, `pcm_to_pcma`, `l16_to_pcm`, etc. |
## Audio formats
| Encoding | Sample rate | Direction |
| ----------- | ----------- | ------------------------------------------------ |
| PCMU / PCMA | 8 kHz | Default inbound; outbound with bidirectional RTP |
| L16 | 16 kHz | Bidirectional when configured |
To send agent audio back to the caller, start Telnyx streaming with `stream_bidirectional_mode=rtp`.
| Constant | Default | Description |
| ---------------------------- | ------- | ---------------------- |
| `TELNYX_DEFAULT_SAMPLE_RATE` | `8000` | PCMU/PCMA rate |
| `TELNYX_L16_SAMPLE_RATE` | `16000` | L16 bidirectional rate |
## Common setup errors
| Error | Fix |
| ------------------------- | ------------------------------------------------------------------------- |
| Invalid `connection_id` | Check Call Control App ID is set and active |
| Webhook URL mismatch | Update the app webhook to your current ngrok URL, or use `--setup-telnyx` |
| Inbound number not routed | Assign the Telnyx number to the Call Control App |
| Destination not verified | Verify the `--to` number in Telnyx, or use an unrestricted account |
## Next Steps
Minimal inbound and outbound phone examples
Provider overview and learning path
Alternative telephony provider
Default edge transport for agent calls
Add knowledge base to phone agents
Get started with voice
# Twilio
Source: https://visionagents.ai/integrations/telephony/twilio
Connect inbound and outbound PSTN phone calls via Twilio Media Streams and TwiML
[Twilio](https://www.twilio.com/) is a programmable telephony provider. The `twilio` plugin bridges PSTN phone calls into a Vision Agents + Stream call through Twilio voice webhooks, TwiML Media Streams, and bidirectional WebSocket audio.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## What the plugin provides
* Twilio Media Streams over WebSocket with bidirectional audio
* `TwilioCallRegistry` for call/session/token tracking
* `attach_phone_to_call` to bridge Twilio mulaw audio ↔ Stream WebRTC
* Built-in FastAPI helpers: `verify_twilio_signature`, `CallWebhookInput`, TwiML response builders
* Automatic mulaw/PCM conversion at 8 kHz
```mermaid theme={null}
sequenceDiagram
participant PSTN as PhoneNetwork
participant Twilio as TwilioVoice
participant App as FastAPI_Server
participant Plugin as twilio_plugin
participant Stream as StreamCall
PSTN->>Twilio: inbound_or_outbound_call
Twilio->>App: POST_/twilio/voice
App->>Twilio: TwiML_with_stream_url
Twilio->>App: WS_/twilio/media/{call_id}/{token}
App->>Plugin: TwilioMediaStream.accept_and_run
App->>Plugin: attach_phone_to_call
Plugin->>Stream: bridge_audio_bidirectional
```
## Prerequisites
| Requirement | Notes |
| ----------------------------------------- | ------------------------------------------------------------------------------ |
| [Twilio account](https://www.twilio.com/) | `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN` |
| Twilio phone number (E.164) | Used as caller ID and inbound number |
| Webhook public URL | [ngrok](https://ngrok.com/) for local dev → `https:///twilio/voice` |
| Stream credentials | `STREAM_API_KEY`, `STREAM_API_SECRET` |
## Installation
```sh theme={null}
uv add "vision-agents[twilio]"
```
Or install the plugin package directly:
```sh theme={null}
uv add vision-agents-plugins-twilio
```
## Environment Variables
| Variable | Required | Description |
| -------------------------------------- | --------- | ---------------------------------------------------------- |
| `TWILIO_ACCOUNT_SID` | Yes | Twilio account SID |
| `TWILIO_AUTH_TOKEN` | Yes | Auth token for REST API and webhook signature verification |
| `NGROK_URL` | Local dev | Public hostname without `https://` |
| `STREAM_API_KEY` / `STREAM_API_SECRET` | Yes | Stream edge transport |
| `GOOGLE_API_KEY` | Examples | Required by the phone agent examples (Gemini) |
## Twilio account setup
1. Buy or assign a Twilio phone number.
2. Under **Phone Numbers → Manage → Active numbers**, open your number.
3. Set **A call comes in** to **Webhook** pointing at:
```text theme={null}
https:///twilio/voice
```
4. Use **HTTP POST**.
For a full walkthrough with copy-paste commands, see the [Twilio Phone Agent example](/examples/twilio-phone-agent).
## Quick Start
The plugin gives you registry, media stream, and bridge primitives. Your FastAPI server wires them to Twilio webhooks and WebSockets:
```python theme={null}
from vision_agents.plugins import twilio
registry = twilio.TwilioCallRegistry()
# 1. Register call from webhook handler
call = registry.create(
call_id,
form_data=data.model_dump(by_alias=True),
prepare=prepare_call, # optional: pre-warm agent + Stream call
)
# 2. Return TwiML that starts a media stream
url = f"wss://{NGROK_URL}/twilio/media/{call_id}/{call.token}"
return twilio.create_media_stream_response(url)
# 3. WebSocket handler
stream = twilio.TwilioMediaStream(websocket)
await stream.accept()
agent, phone_user, stream_call = await call.await_prepare()
await twilio.attach_phone_to_call(stream_call, stream, phone_user.id)
await stream.run()
```
## Inbound calls
Twilio sends a webhook when someone calls your number. Validate the signature, register the call, and return TwiML to start the media stream:
```python theme={null}
from fastapi import Depends
@app.post("/twilio/voice")
async def voice_webhook(
_: None = Depends(twilio.verify_twilio_signature),
data: twilio.CallWebhookInput = Depends(twilio.CallWebhookInput.as_form),
):
call_id = str(uuid.uuid4())
twilio_call = registry.create(
call_id,
data.model_dump(by_alias=True),
prepare=lambda: prepare_call(call_id),
)
url = f"wss://{NGROK_URL}/twilio/media/{call_id}/{twilio_call.token}"
return twilio.create_media_stream_response(url)
@app.websocket("/twilio/media/{call_id}/{token}")
async def media_stream(websocket: WebSocket, call_id: str, token: str):
twilio_call = registry.validate(call_id, token)
stream = twilio.TwilioMediaStream(websocket)
await stream.accept()
# attach to agent and run stream (see Quick Start)
```
When running behind ngrok, add `ProxyHeadersMiddleware` so Twilio signature
validation sees the public HTTPS URL. See the
[phone agent example source](https://github.com/GetStream/Vision-Agents/blob/main/examples/03_phone_and_rag_example/inbound_phone_and_rag_example.py).
## Outbound calls
Pre-register the call in the registry, start your server, then dial via the Twilio REST API:
```python theme={null}
from twilio.rest import Client
call_id = str(uuid.uuid4())
twilio_call = registry.create(call_id, prepare=lambda: prepare_call(call_id))
url = f"wss://{NGROK_URL}/twilio/media/{call_id}/{twilio_call.token}"
client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
client.calls.create(
twiml=twilio.create_media_stream_twiml(url),
to=to_number,
from_=from_number,
)
```
The WebSocket media handler is the same as inbound.
## Key Components
| Component | Description |
| ------------------------------------------------------------ | ----------------------------------------------------------------------- |
| `TwilioCallRegistry` | Tracks active calls, tokens, optional async `prepare` tasks |
| `TwilioCall` | Call session with `from_number`, `to_number`, `await_prepare()` |
| `TwilioMediaStream` | WebSocket media handler; exposes `audio_track`, `send_audio()`, `run()` |
| `attach_phone_to_call` | Bridges Twilio mulaw audio ↔ Stream call participant |
| `verify_twilio_signature` | FastAPI dependency for webhook authentication |
| `CallWebhookInput` | Typed model for Twilio voice webhook form data |
| `create_media_stream_response` / `create_media_stream_twiml` | TwiML helpers for bidirectional streaming |
| Audio helpers | `mulaw_to_pcm`, `pcm_to_mulaw`, `TWILIO_SAMPLE_RATE` |
## Audio
Twilio Media Streams use **mulaw encoding at 8 kHz**. The plugin converts between mulaw and PCM automatically in `TwilioMediaStream` and exposes conversion helpers if you need them directly.
| Constant | Default | Description |
| -------------------- | ------- | ------------------------------- |
| `TWILIO_SAMPLE_RATE` | `8000` | Twilio media stream sample rate |
## Common setup errors
| Error | Fix |
| ------------------------ | -------------------------------------------------------------------------------------- |
| Webhook URL mismatch | Update the Twilio number webhook to your current ngrok URL |
| Invalid Twilio signature | Ensure `TWILIO_AUTH_TOKEN` is set; add `ProxyHeadersMiddleware` behind ngrok |
| No audio on call | Confirm the media WebSocket URL uses `wss://` and the server is running before dialing |
| Outbound call fails | Verify `--from` is a Twilio number on your account |
## Next Steps
Step-by-step inbound and outbound phone tutorial
Add knowledge retrieval to phone calls
Provider overview and learning path
Alternative telephony provider
Default edge transport for agent calls
Get started with voice
# AWS Polly
Source: https://visionagents.ai/integrations/tts/aws-polly
[AWS Polly](https://aws.amazon.com/polly/) provides cloud-based TTS with natural-sounding voices across multiple languages. Supports both standard and neural engines.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[aws]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import aws, gemini, deepgram, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=deepgram.STT(),
tts=aws.TTS(),
)
```
AWS credentials are resolved via the standard AWS SDK chain (environment
variables, AWS profiles, or IAM roles).
## Parameters
| Name | Type | Default | Description |
| --------------- | ----- | ---------- | ------------------------------------- |
| `voice_id` | `str` | `"Joanna"` | Voice ID |
| `engine` | `str` | `None` | Engine (`"standard"` or `"neural"`) |
| `region_name` | `str` | `None` | AWS region |
| `language_code` | `str` | `None` | Language (e.g., `"en-US"`, `"es-ES"`) |
## Neural Engine
For more natural-sounding voices:
```python theme={null}
tts = aws.TTS(engine="neural", voice_id="Joanna")
```
## SSML Support
```python theme={null}
tts = aws.TTS(text_type="ssml")
tts.send('Hello world!')
```
## Next Steps
Get started with voice
Add video processing
# Cartesia
Source: https://visionagents.ai/integrations/tts/cartesia
[Cartesia](https://cartesia.ai/?utm_medium=partner\&utm_source=getstream) provides low-latency text-to-speech with the Sonic model. Designed for real-time voice applications with natural-sounding speech synthesis.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Cartesia also provides low-latency [speech-to-text](/integrations/stt/cartesia). You can use both in the same agent.
## Installation
```sh theme={null}
uv add "vision-agents[cartesia]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import cartesia, gemini, deepgram, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=deepgram.STT(),
tts=cartesia.TTS(),
)
```
Set `CARTESIA_API_KEY` in your environment or pass `api_key` directly.
## Parameters
| Name | Type | Default | Description |
| ------------- | ----- | ---------------------------------------- | ------------------------------------------------ |
| `model_id` | `str` | `"sonic-3.5"` | Cartesia TTS model |
| `voice_id` | `str` | `"6ccbfb76-1fc6-48f7-b71d-91ac6298247b"` | Voice ID |
| `sample_rate` | `int` | `16000` | Audio sample rate in Hz |
| `api_key` | `str` | `None` | API key (defaults to `CARTESIA_API_KEY` env var) |
## Next Steps
Get started with voice
Add video processing
# Deepgram TTS
Source: https://visionagents.ai/integrations/tts/deepgram
[Deepgram](https://deepgram.com) provides low-latency text-to-speech synthesis with the Aura-2 model.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Deepgram also provides fast, accurate [speech-to-text](/integrations/stt/deepgram) with built-in turn detection. You can use both in the same agent.
## Installation
```sh theme={null}
uv add "vision-agents[deepgram]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import deepgram, gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=deepgram.STT(),
tts=deepgram.TTS(),
)
```
Set `DEEPGRAM_API_KEY` in your environment or pass `api_key` directly.
## Parameters
```python theme={null}
tts = deepgram.TTS(
model="aura-2",
voice="aura-asteria-en",
)
```
| Name | Type | Default | Description |
| --------- | ----- | ------------------- | ------------------------------------------------------------------------------ |
| `model` | `str` | `"aura-2"` | TTS model |
| `voice` | `str` | `"aura-asteria-en"` | Voice ID ([available voices](https://developers.deepgram.com/docs/tts-models)) |
| `api_key` | `str` | `None` | API key (defaults to `DEEPGRAM_API_KEY` env var) |
## Next Steps
Real-time speech-to-text
Get started with voice
# ElevenLabs TTS
Source: https://visionagents.ai/integrations/tts/elevenlabs
[ElevenLabs](https://www.elevenlabs.io) provides highly realistic and expressive text-to-speech voices. Supports multiple languages and voice styles.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
ElevenLabs also provides real-time [speech-to-text](/integrations/stt/elevenlabs) via Scribe with built-in turn detection. You can use both in the same agent.
## Installation
```sh theme={null}
uv add "vision-agents[elevenlabs]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import elevenlabs, gemini, deepgram, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
)
```
Set `ELEVENLABS_API_KEY` in your environment or pass `api_key` directly.
## Parameters
```python theme={null}
tts = elevenlabs.TTS(
voice_id="VR6AewLTigWG4xSOukaG",
model_id="eleven_multilingual_v2",
)
```
| Name | Type | Default | Description |
| ---------- | ----- | -------------------------- | -------------------------------------------------- |
| `voice_id` | `str` | `"VR6AewLTigWG4xSOukaG"` | ElevenLabs voice ID |
| `model_id` | `str` | `"eleven_multilingual_v2"` | TTS model |
| `api_key` | `str` | `None` | API key (defaults to `ELEVENLABS_API_KEY` env var) |
## Next Steps
Real-time speech-to-text via Scribe
Get started with voice
# Fish Audio TTS
Source: https://visionagents.ai/integrations/tts/fish
[Fish Audio](https://fish.audio) provides high-quality text-to-speech with fine-grained prosody control, voice cloning support, and multiple backend models. Ideal for multilingual applications.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Fish Audio also provides [speech-to-text](/integrations/stt/fish) with automatic language detection. You can use both in the same agent.
## Installation
```sh theme={null}
uv add "vision-agents[fish]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import fish, gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=fish.STT(),
tts=fish.TTS(), # Uses S2-Pro model by default
)
```
Set `FISH_API_KEY` in your environment or pass `api_key` directly.
## Basic Usage
```python theme={null}
tts = fish.TTS(reference_id="your_voice_id") # Optional voice cloning
```
## Prosody Control
The S2-Pro model (default) supports inline control tags for natural prosody:
```python theme={null}
tts = fish.TTS() # Uses s2-pro by default
# Include prosody tags in your text
text = "[whisper] This is a secret. [super happy] But this is great news!"
text = "Hello! [laugh] That's so funny."
```
## Selecting a Model
```python theme={null}
# Use the latest S2-Pro model with prosody control
tts = fish.TTS(model="s2-pro")
# Use legacy models if needed
tts = fish.TTS(model="speech-1.5")
tts = fish.TTS(model="speech-1.6")
# Use fast models for lower latency
tts = fish.TTS(model="s1")
tts = fish.TTS(model="s1-mini")
```
## Parameters
| Name | Type | Default | Description |
| -------------- | ----- | ---------- | ------------------------------------------------------------------------------ |
| `model` | `str` | `"s2-pro"` | Backend model: `"s2-pro"`, `"speech-1.5"`, `"speech-1.6"`, `"s1"`, `"s1-mini"` |
| `reference_id` | `str` | `None` | Voice ID for voice cloning |
| `api_key` | `str` | `None` | API key (defaults to `FISH_API_KEY` env var) |
| `base_url` | `str` | `None` | Custom API endpoint |
## Next Steps
Speech-to-text with auto language detection
Get started with voice
# Inworld
Source: https://visionagents.ai/integrations/tts/inworld
[Inworld AI](https://inworld.ai) provides expressive TTS designed for conversational AI and game characters. The plugin defaults to Inworld's **TTS-2** model, which adds natural-language steering, 100+ languages (15 GA, 90+ experimental), and high-quality instant voice cloning.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Inworld also offers a [Realtime](/integrations/realtime/inworld) speech-to-speech API over WebRTC.
## Installation
```sh theme={null}
uv add "vision-agents[inworld]"
```
Get your API key from the [Inworld Portal](https://studio.inworld.ai/) and set `INWORLD_API_KEY` in your environment (or pass `api_key=` explicitly).
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import inworld, gemini, deepgram, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=deepgram.STT(),
tts=inworld.TTS(), # defaults to model_id="inworld-tts-2", voice_id="Sarah"
)
```
Set `INWORLD_API_KEY` in your environment or pass `api_key` directly.
## Parameters
| Name | Type | Default | Description |
| -------------------------- | --------------- | ----------------- | -------------------------------------------------------------------------------------------- |
| `api_key` | `str` | `None` | API key (defaults to `INWORLD_API_KEY` env var) |
| `voice_id` | `str` | `"Sarah"` | Voice ID (`"Sarah"`, `"Dennis"`, `"Ashley"`, `"Olivia"`, `"Clive"`, or custom/cloned voices) |
| `model_id` | `str` | `"inworld-tts-2"` | Model (`"inworld-tts-2"`, `"inworld-tts-1.5-max"`, `"inworld-tts-1.5-mini"`) |
| `sample_rate` | `int` | `16000` | Desired PCM output sample rate in Hz |
| `temperature` | `float` | `1.1` | Randomness when sampling audio tokens (0–2) |
| `speaking_rate` | `float` | `None` | Speech speed multiplier (0.5–1.5). `None` uses the server default |
| `auto_mode` | `bool` | `True` | Let Inworld decide optimal flush behavior for streamed input |
| `apply_text_normalization` | `"ON" \| "OFF"` | `None` | Optional text normalization behavior |
| `ws_url` | `str` | Inworld endpoint | Inworld bidirectional WebSocket endpoint |
`inworld-tts-1` and `inworld-tts-1-max` are deprecated by Inworld — migrate to `inworld-tts-2` or `inworld-tts-1.5-*`.
## Steering (TTS-2)
TTS-2 takes natural-language stage directions inline with your text. Place the instruction in square brackets before the segment it should apply to:
```python theme={null}
text = (
"[whisper in a hushed style] I have to tell you something. "
"[laugh] Just kidding! [say with force] Now let's get to work."
)
async for chunk in await tts.stream_audio(text):
...
```
Steering covers articulation, intonation, volume, pitch, range, speed, and vocal style — and supports non-verbal sounds like `[laugh]`, `[breathe]`, `[clear throat]`, `[sigh]`, `[cough]`, `[yawn]`. Combining dimensions (`[whisper in a hushed style]`, `[say playfully and very fast]`) produces better results than bare single-word tags.
See Inworld's [steering docs](https://docs.inworld.ai/tts/capabilities/steering) and [prompting guide](https://docs.inworld.ai/tts/best-practices/prompting-for-tts-2) for the full reference.
Inworld TTS supports up to 2,000 characters per request. The plugin connects to Inworld's bidirectional WebSocket endpoint and streams 16-bit PCM audio at the configured `sample_rate` — no extra configuration needed.
## Next Steps
Get started with voice
Add video processing
# Kokoro
Source: https://visionagents.ai/integrations/tts/kokoro
[Kokoro](https://github.com/hexgrad/kokoro) is a local TTS engine that runs entirely on your machine. No API key or internet connection required. Ideal for offline voice synthesis, privacy-sensitive applications, or prototyping.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[kokoro]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import kokoro, gemini, deepgram, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=deepgram.STT(),
tts=kokoro.TTS(),
)
```
Kokoro runs locally. No API key or internet connection is required.
## Parameters
| Name | Type | Default | Description |
| ----------- | ------- | ------------ | ------------------------------------------------- |
| `voice` | `str` | `"af_heart"` | Voice preset |
| `lang_code` | `str` | `"a"` | Language code (`"a"` = American English) |
| `speed` | `float` | `1.0` | Playback speed (e.g., `0.9` slower, `1.2` faster) |
| `device` | `str` | `None` | Device (`"cuda"`, `"cpu"`, or auto-detect) |
## Next Steps
Get started with voice
Add video processing
# OpenAI TTS
Source: https://visionagents.ai/integrations/tts/openai
[OpenAI](https://openai.com) provides streaming text-to-speech synthesis.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
OpenAI also provides [Realtime speech-to-speech](/integrations/realtime/openai) and a traditional [LLM](/integrations/llm/openai).
## Installation
```sh theme={null}
uv add "vision-agents[openai]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import openai, deepgram, gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=deepgram.STT(),
tts=openai.TTS(),
)
```
## Parameters
```python theme={null}
tts = openai.TTS(model="gpt-4o-mini-tts", voice="alloy")
```
| Name | Type | Default | Description |
| ------- | ----- | ------------------- | ----------------------------------------------------------- |
| `model` | `str` | `"gpt-4o-mini-tts"` | TTS model |
| `voice` | `str` | `"alloy"` | Voice ("alloy", "echo", "fable", "onyx", "nova", "shimmer") |
## Next Steps
Responses API and ChatCompletions
Speech-to-speech over WebRTC
# Pocket TTS
Source: https://visionagents.ai/integrations/tts/pocket
[Pocket TTS](https://huggingface.co/kyutai/pocket-tts) is a lightweight local TTS from Kyutai that runs on CPU. Offers \~200ms latency, voice cloning, and 8 built-in voices without requiring a GPU or external API.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[pocket]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import pocket, gemini, deepgram, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=deepgram.STT(),
tts=pocket.TTS(),
)
```
Pocket TTS runs locally. No API key required.
## Parameters
| Name | Type | Default | Description |
| ------- | ----- | -------- | ---------------------------------------------- |
| `voice` | `str` | `"alba"` | Built-in voice or path to wav file for cloning |
## Built-in Voices
`alba`, `marius`, `javert`, `jean`, `fantine`, `cosette`, `eponine`, `azelma`
## Voice Cloning
```python theme={null}
# Use a local wav file
tts = pocket.TTS(voice="path/to/your/voice.wav")
# Or a HuggingFace-hosted voice
tts = pocket.TTS(voice="hf://kyutai/tts-voices/alba-mackenna/casual.wav")
```
## Next Steps
Get started with voice
Add video processing
# Sarvam TTS
Source: https://visionagents.ai/integrations/tts/sarvam
[Sarvam AI](https://www.sarvam.ai/) provides streaming text-to-speech using the Bulbul model, with configurable speaker, pace, and language support for Indian languages.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
Get your Sarvam API key from the [Sarvam dashboard](https://dashboard.sarvam.ai/).
Sarvam also provides [speech-to-text](/integrations/stt/sarvam) and an [LLM](/integrations/llm/sarvam). You can use all three in the same agent.
## Installation
```sh theme={null}
uv add "vision-agents[sarvam]"
```
## Quick start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import sarvam, getstream, smart_turn
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Sarvam Agent", id="agent"),
instructions="Reply in the same language the user speaks.",
llm=sarvam.LLM(model="sarvam-m"),
stt=sarvam.STT(language="hi-IN"),
tts=sarvam.TTS(speaker="shubh"),
turn_detection=smart_turn.TurnDetection(),
)
```
Set `SARVAM_API_KEY` in your environment or pass `api_key` directly.
## Parameters
```python theme={null}
tts = sarvam.TTS(
model="bulbul:v3",
language="hi-IN",
speaker="shubh",
pace=1.0,
)
```
| Name | Type | Default | Description |
| ---------------------- | ------- | ------------- | ----------------------------------------------------------------------- |
| `model` | `str` | `"bulbul:v3"` | TTS model (`bulbul:v2`, `bulbul:v3-beta`, or `bulbul:v3`) |
| `language` | `str` | `"hi-IN"` | Target language code (e.g. `hi-IN`, `en-IN`) |
| `speaker` | `str` | `"shubh"` | Speaker voice id — must be compatible with the chosen model (see below) |
| `sample_rate` | `int` | `24000` | Output sample rate in Hz |
| `pace` | `float` | `None` | Speech pace (bulbul:v3 supports 0.5–2.0) |
| `pitch` | `float` | `None` | Speech pitch (bulbul:v2 only) |
| `loudness` | `float` | `None` | Speech loudness (bulbul:v2 only) |
| `temperature` | `float` | `None` | Sampling temperature (bulbul:v3 and bulbul:v3-beta only) |
| `enable_preprocessing` | `bool` | `True` | Normalize mixed-language and numeric text |
| `api_key` | `str` | `None` | API key (defaults to `SARVAM_API_KEY` env var) |
### Speaker compatibility
Each model supports a specific set of speakers. Passing an incompatible speaker raises a `ValueError`.
| Model | Speakers |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `bulbul:v2` | `abhilash`, `anushka`, `arya`, `hitesh`, `karun`, `manisha`, `vidya` |
| `bulbul:v3-beta` | `aayan`, `aditya`, `advait`, `amelia`, `amit`, `ashutosh`, `dev`, `ishita`, `kabir`, `kavya`, `manan`, `neha`, `pooja`, `priya`, `rahul`, `ratan`, `ritu`, `rohan`, `roopa`, `shubh`, `shreya`, `simran`, `sophia`, `sumit`, `varun` |
| `bulbul:v3` | `aayan`, `aditya`, `advait`, `amelia`, `amit`, `ashutosh`, `dev`, `ishita`, `kabir`, `kavya`, `manan`, `neha`, `pooja`, `priya`, `rahul`, `ratan`, `ritu`, `rohan`, `roopa`, `shubh`, `shreya`, `simran`, `sophia`, `sumit`, `varun` |
## Next steps
Streaming speech-to-text for Indian languages
Chat completions with Sarvam models
# xAI TTS
Source: https://visionagents.ai/integrations/tts/xai
Text-to-speech using xAI's Grok voices with speech tag support.
[xAI](https://x.ai/) provides text-to-speech with five expressive voices, inline speech tags for delivery control, and multiple output codecs.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
xAI also provides an [LLM](/integrations/llm/xai) and [Realtime speech-to-speech](/integrations/realtime/xai). You can use all three in the same agent.
## Installation
```sh theme={null}
uv add "vision-agents[xai]"
```
## Quick start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import xai, getstream, deepgram
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=xai.LLM(model="grok-4.1"),
stt=deepgram.STT(),
tts=xai.TTS(),
)
```
Set `XAI_API_KEY` in your environment or pass `api_key` directly.
## Parameters
```python theme={null}
tts = xai.TTS(voice="eve", language="en", codec="pcm", sample_rate=24000)
```
| Name | Type | Default | Description |
| ------------- | ----- | ------- | --------------------------------------------------------------------- |
| `api_key` | `str` | `None` | API key (defaults to `XAI_API_KEY` env var) |
| `voice` | `str` | `"eve"` | Voice (`"eve"`, `"ara"`, `"leo"`, `"rex"`, `"sal"`) |
| `language` | `str` | `"en"` | BCP-47 language code (e.g. `"en"`, `"zh"`, `"pt-BR"`) or `"auto"` |
| `codec` | `str` | `"pcm"` | Output codec (`"pcm"`, `"wav"`, `"mp3"`, `"mulaw"`, `"alaw"`) |
| `sample_rate` | `int` | `24000` | Output sample rate in Hz (8000, 16000, 22050, 24000, 44100, or 48000) |
| `bit_rate` | `int` | `None` | MP3 bit rate (only used when codec is `"mp3"`) |
### Voices
| Voice | Description |
| ----- | ------------------------------------------------------------------- |
| `eve` | Energetic, upbeat — engaging and enthusiastic (default) |
| `ara` | Warm, friendly — balanced and conversational |
| `leo` | Authoritative, strong — commanding, great for instructional content |
| `rex` | Confident, clear — professional, ideal for business |
| `sal` | Smooth, balanced — versatile for a wide range of contexts |
## Speech tags
You can use inline speech tags in your text for fine-grained delivery control.
**Inline tags:** `[pause]` `[long-pause]` `[laugh]` `[chuckle]` `[giggle]` `[cry]` `[tsk]` `[tongue-click]` `[lip-smack]` `[breath]` `[inhale]` `[exhale]` `[sigh]` `[hum-tune]`
**Wrapping tags:** ``, ``, ``, ``, ``, ``, ``, ``, ``
## Next steps
Advanced reasoning with Grok
Speech-to-speech over WebSocket
# Smart Turn
Source: https://visionagents.ai/integrations/turn-detection/smart-turn
Smart Turn uses neural models (Silero VAD + Whisper features) to detect when a speaker has completed their turn. Provides natural conversation flow without relying solely on silence detection.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[smart_turn]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import smart_turn, gemini, deepgram, elevenlabs, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
turn_detection=smart_turn.TurnDetection(),
)
```
Models download automatically on first use.
## Parameters
| Name | Type | Default | Description |
| ---------------------- | ------- | ------- | ------------------------------- |
| `buffer_in_seconds` | `float` | `2.0` | Audio buffer duration |
| `confidence_threshold` | `float` | `0.5` | Turn completion threshold (0-1) |
| `sample_rate` | `int` | `16000` | Audio sample rate |
## Turn Signals
```python theme={null}
from vision_agents.core.turn_detection import TurnStarted, TurnEnded
from vision_agents.plugins import smart_turn
turn_detection = smart_turn.TurnDetection()
async for signal in turn_detection.output:
if isinstance(signal, TurnStarted):
print(f"User started speaking: {signal.participant.user_id}")
elif isinstance(signal, TurnEnded):
print(f"User finished speaking: confidence={signal.confidence}")
```
## Next Steps
Get started with voice
Add video processing
# Vogent
Source: https://visionagents.ai/integrations/turn-detection/vogent
Vogent uses neural models to predict when a speaker has completed their conversational turn. Provides intelligent turn-taking for natural conversation flow.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[vogent]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import vogent, gemini, deepgram, elevenlabs, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a helpful assistant.",
llm=gemini.LLM("gemini-3-flash-preview"),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
turn_detection=vogent.TurnDetection(),
)
```
Models download automatically on first use.
## Parameters
| Name | Type | Default | Description |
| ---------------------- | ------- | ------- | ------------------------------- |
| `buffer_in_seconds` | `float` | `2.0` | Audio buffer duration |
| `confidence_threshold` | `float` | `0.5` | Turn completion threshold (0-1) |
| `sample_rate` | `int` | `16000` | Audio sample rate |
## Turn Signals
```python theme={null}
from vision_agents.core.turn_detection import TurnStarted, TurnEnded
from vision_agents.plugins import vogent
turn_detection = vogent.TurnDetection()
async for signal in turn_detection.output:
if isinstance(signal, TurnStarted):
print(f"User started speaking: {signal.participant.user_id}")
elif isinstance(signal, TurnEnded):
print(f"User finished speaking: confidence={signal.confidence}")
```
## Next Steps
Get started with voice
Add video processing
# Decart
Source: https://visionagents.ai/integrations/vision/decart
[Decart](https://decart.ai/) provides real-time AI video transformation with style transfer and virtual try-on. Transform video streams into animated styles, apply reference-image costumes, or any custom prompt-based visual effect using models like Lucy.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[decart]"
```
## Quick start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import decart, gemini, deepgram, elevenlabs, getstream
processor = decart.RestylingProcessor(
initial_prompt="Studio Ghibli animation style",
model="lucy_2_rt",
)
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Styled AI"),
instructions="Be helpful",
llm=gemini.Realtime(),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
processors=[processor],
)
```
Set `DECART_API_KEY` in your environment or pass `api_key` directly.
## Parameters
| Name | Type | Default | Description |
| ---------------- | ---------------------- | ------------------ | --------------------------------------------------------------------------------------------------- |
| `model` | `str` | `"lucy_2_rt"` | Decart model |
| `initial_prompt` | `str` | `"Cyberpunk city"` | Style prompt for visual transformation |
| `initial_image` | `bytes \| str \| Path` | `None` | Optional reference image for first connect (bytes, file path, http(s) URL, data URI, or raw base64) |
| `enhance` | `bool` | `True` | Whether to enhance the prompt |
| `mirror` | `bool` | `True` | Mirror mode for front-facing cameras |
| `width` | `int` | `1280` | Output video width |
| `height` | `int` | `720` | Output video height |
| `api_key` | `str` | `None` | API key (defaults to `DECART_API_KEY` env var) |
## Dynamic style changes
Update the video style during a call via function calling:
```python theme={null}
@llm.register_function(description="Changes the video style")
async def change_style(prompt: str) -> str:
await processor.update_prompt(prompt)
return f"Style changed to: {prompt}"
```
## Reference images
For models like Lucy that accept a reference image, pass it at construction time and/or swap it atomically with a prompt using `update_state`:
```python theme={null}
processor = decart.RestylingProcessor(
model="lucy_2_rt",
initial_prompt="A person wearing a superhero costume",
initial_image="./costumes/superhero.png",
)
# Atomically change prompt + reference image
await processor.update_state(
prompt="A person wearing a wizard robe",
image="./costumes/wizard.png",
)
# Image-only update
await processor.update_state(image=b"")
```
`initial_image` and `update_state(image=...)` accept `bytes`, a local file path, an `http(s)` URL, a `data:` URI, or a raw base64 string.
## Next steps
Get started with voice
Add video processing
# Moondream
Source: https://visionagents.ai/integrations/vision/moondream
[Moondream](https://moondream.ai/) provides zero-shot object detection, visual question answering, and image captioning. Detect any object by describing it in natural language without training. Available as cloud-hosted API or local on-device.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[moondream]"
```
## Detection (Cloud)
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import moondream, gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a vision assistant.",
llm=gemini.Realtime(fps=10),
processors=[
moondream.CloudDetectionProcessor(
detect_objects=["person", "car", "dog"],
conf_threshold=0.3,
)
],
)
```
Set `MOONDREAM_API_KEY` in your environment or pass `api_key` directly.
| Name | Type | Default | Description |
| ---------------- | -------------------- | ---------- | ----------------------------- |
| `detect_objects` | `str` or `List[str]` | `"person"` | Objects to detect (zero-shot) |
| `conf_threshold` | `float` | `0.3` | Confidence threshold |
| `fps` | `int` | `30` | Frame processing rate |
## Detection (Local)
Runs on-device without API calls. Requires `HF_TOKEN` for model access.
```python theme={null}
processor = moondream.LocalDetectionProcessor(
detect_objects=["person", "car"],
device="cuda",
)
```
## VLM (Cloud)
Visual question answering or automatic captioning.
```python theme={null}
from vision_agents.plugins import moondream, deepgram, elevenlabs
llm = moondream.CloudVLM(mode="vqa") # or "caption"
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
llm=llm,
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
)
```
| Name | Type | Default | Description |
| ------ | ----- | ------- | ----------------------------- |
| `mode` | `str` | `"vqa"` | Mode (`"vqa"` or `"caption"`) |
## VLM (Local)
```python theme={null}
llm = moondream.LocalVLM(mode="vqa", force_cpu=False)
```
## Cloud vs Local
| | Cloud | Local |
| ------------ | ----------------------------------------------------- | ------------------------------------------ |
| **Use when** | Simple setup, no infrastructure management | Higher throughput, own GPU infrastructure |
| **Pros** | No model download, no GPU required, automatic updates | No rate limits, no API costs, full control |
| **Cons** | Requires API key, 2 RPS rate limit (can be increased) | Requires GPU for best performance |
Local models require `HF_TOKEN` for HuggingFace authentication. CUDA
recommended for best performance.
## Next Steps
Get started with voice
Add video processing
# NVIDIA
Source: https://visionagents.ai/integrations/vision/nvidia
[NVIDIA](https://www.nvidia.com/) provides powerful vision language models through their NIM platform. The plugin enables real-time video understanding using models like Cosmos Reason2 with automatic frame buffering and NVCF asset management.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[nvidia]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import nvidia, getstream, deepgram, elevenlabs
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="Analyze the video and answer questions.",
llm=nvidia.VLM(
model="nvidia/cosmos-reason2-8b",
fps=1,
frame_buffer_seconds=10,
),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
)
```
Set `NVIDIA_API_KEY` in your environment or pass `api_key` directly.
## Parameters
| Name | Type | Default | Description |
| ---------------------- | ----- | ---------------------------- | ---------------------------------------------- |
| `model` | `str` | `"nvidia/cosmos-reason2-8b"` | NVIDIA model ID |
| `fps` | `int` | `1` | Video frames per second to buffer |
| `frame_buffer_seconds` | `int` | `10` | Seconds of video to buffer |
| `frame_width` | `int` | `800` | Frame width |
| `frame_height` | `int` | `600` | Frame height |
| `api_key` | `str` | `None` | API key (defaults to `NVIDIA_API_KEY` env var) |
## Next Steps
Get started with voice
Add video processing
# Roboflow
Source: https://visionagents.ai/integrations/vision/roboflow
[Roboflow](https://roboflow.com) provides computer vision tools for object detection. The plugin offers both cloud-hosted inference (access to pre-trained models from [Roboflow Universe](https://universe.roboflow.com/)) and local RF-DETR models.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[roboflow]"
```
## Cloud Detection
Uses Roboflow's hosted API with pre-trained models.
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import roboflow, gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You are a sports analyst.",
llm=gemini.Realtime(fps=10),
processors=[
roboflow.RoboflowCloudDetectionProcessor(
model_id="football-players-detection-3zvbc/20",
classes=["player"],
conf_threshold=0.5,
)
],
)
```
Set `ROBOFLOW_API_KEY` in your environment or pass `api_key` directly.
| Name | Type | Default | Description |
| ---------------- | ----------- | ------- | ------------------------------------ |
| `model_id` | `str` | — | Roboflow Universe model ID |
| `classes` | `List[str]` | `None` | Classes to detect (or all if `None`) |
| `conf_threshold` | `float` | `0.5` | Confidence threshold |
| `fps` | `int` | `5` | Frame processing rate |
| `annotate` | `bool` | `True` | Draw bounding boxes |
## Local Detection
Runs RF-DETR models locally without API calls.
```python theme={null}
processor = roboflow.RoboflowLocalDetectionProcessor(
model_id="rfdetr-base",
classes=["person"],
conf_threshold=0.5,
)
```
| Name | Type | Default | Description |
| ---------------- | ----------- | ---------------------- | ------------------------------------------------------------------ |
| `model_id` | `str` | `"rfdetr-seg-preview"` | RF-DETR model (`"rfdetr-nano"`, `"rfdetr-base"`, `"rfdetr-large"`) |
| `classes` | `List[str]` | `None` | Classes to detect |
| `conf_threshold` | `float` | `0.5` | Confidence threshold |
## Cloud vs Local
| | Cloud | Local |
| ------------ | ------------------------------------------------ | ------------------------------------------- |
| **Use when** | Access to Roboflow Universe models | Higher throughput, avoid rate limits |
| **Pros** | Thousands of pre-trained models, no GPU required | No API costs, lower latency, works offline |
| **Cons** | Requires API key, potential rate limits | Requires local compute, RF-DETR models only |
## Next Steps
Get started with voice
Add video processing
# TwelveLabs
Source: https://visionagents.ai/integrations/vision/twelvelabs
[TwelveLabs](https://twelvelabs.io/) provides **Pegasus**, a video understanding model that analyzes short clips rather than single frames. Use it to reason about motion and events over time, such as answering "what just happened?", in real-time video calls.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[twelvelabs]"
```
You can get a free API key at [twelvelabs.io](https://twelvelabs.io/).
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import twelvelabs, getstream, deepgram, elevenlabs
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="Describe what just happened in the video.",
llm=twelvelabs.PegasusVLM(),
stt=deepgram.STT(),
tts=elevenlabs.TTS(),
)
```
Set `TWELVELABS_API_KEY` in your environment or pass `api_key` directly.
## How it works
Unlike frame-by-frame VLMs, Pegasus buffers recent frames from the watched video track, encodes them into a short MP4 clip, uploads it to the TwelveLabs Assets API, and analyzes it with your prompt. The streamed answer is spoken by your agent's TTS.
Pegasus works well for questions about recent activity: "What did they just do?", "Did anything fall?", "Describe the last few seconds."
Wait a few seconds after a participant joins before prompting, so enough video is buffered for analysis.
## Parameters
| Name | Type | Default | Description |
| -------------- | ------- | -------------- | -------------------------------------------------- |
| `api_key` | `str` | `None` | API key (defaults to `TWELVELABS_API_KEY` env var) |
| `model_name` | `str` | `"pegasus1.5"` | Pegasus model identifier |
| `fps` | `float` | `1.0` | Frame sampling rate for the buffered clip |
| `clip_seconds` | `int` | `5` | Clip length analyzed per request (minimum `4`) |
| `max_tokens` | `int` | `512` | Maximum response tokens (minimum `512`) |
## Trigger on participant join
Prompt Pegasus once a caller's camera has buffered enough video:
```python theme={null}
import asyncio
from vision_agents.plugins.getstream import CallSessionParticipantJoinedEvent
@agent.events.subscribe
async def on_participant_joined(event: CallSessionParticipantJoinedEvent):
if event.participant.user.id != "agent":
await asyncio.sleep(5)
await agent.simple_response("Describe what just happened in the video")
```
## Notes
* Pegasus requires a minimum resolution of 360×360; lower-resolution frames are scaled up on encode.
* Each request uploads a clip and runs server-side analysis, so latency is higher than single-frame VLMs. Tune `fps` and `clip_seconds` for your use case.
* Uploaded clips are deleted after analysis; asset cleanup is best-effort and does not block the response.
## Next Steps
Get started with voice
Add video processing
# Ultralytics YOLO
Source: https://visionagents.ai/integrations/vision/ultralytics
[Ultralytics YOLO](https://ultralytics.com) provides state-of-the-art computer vision for object detection, pose estimation, and segmentation. The plugin enables real-time pose detection with skeleton overlays.
Vision Agents uses [Stream Video](https://getstream.io/video/) for real-time WebRTC transport by default. [External WebRTC transports](/integrations/introduction-to-integrations#edge-transport) are supported as well. Most AI providers offer free tiers to get started.
## Installation
```sh theme={null}
uv add "vision-agents[ultralytics]"
```
## Quick Start
```python theme={null}
from vision_agents.core import Agent, User
from vision_agents.plugins import ultralytics, gemini, getstream
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Fitness Coach", id="agent"),
instructions="Analyze the user's form and provide feedback.",
llm=gemini.Realtime(fps=10),
processors=[
ultralytics.YOLOPoseProcessor(
model_path="yolo11n-pose.pt",
conf_threshold=0.5,
enable_hand_tracking=True,
)
],
)
```
YOLO models download automatically on first use.
## Parameters
| Name | Type | Default | Description |
| ------------------------- | ------- | ------------------- | ------------------------------ |
| `model_path` | `str` | `"yolo11n-pose.pt"` | YOLO pose model |
| `conf_threshold` | `float` | `0.5` | Keypoint confidence threshold |
| `device` | `str` | `"cpu"` | Device (`"cpu"` or `"cuda"`) |
| `enable_hand_tracking` | `bool` | `True` | Draw hand skeleton connections |
| `enable_wrist_highlights` | `bool` | `True` | Highlight wrist positions |
## Model Sizes
| Model | Speed | Use Case |
| ----------------- | ------- | ---------------- |
| `yolo11n-pose.pt` | Fastest | Real-time on CPU |
| `yolo11s-pose.pt` | Fast | Real-time on GPU |
| `yolo11m-pose.pt` | Medium | Quality-focused |
| `yolo11l-pose.pt` | Slower | Maximum accuracy |
## Next Steps
Get started with voice
Add video processing
# Overview
Source: https://visionagents.ai/introduction/overview
What Vision Agents is, what you can build, and where to start
Vision Agents is an open-source Python framework for real-time voice and video AI. You write an `Agent` that joins a session, connects to AI providers through swappable plugins, and responds in real time. The framework handles call lifecycle, audio/video routing, turn-taking, and deployment. You focus on instructions, provider choices and connecting to your existing knowledge bases.
## What can you build?
Teams ship **voice support bots**, **video coaches** that watch a camera, **phone agents** with knowledge bases, and **multimodal assistants** that see and hear. Common starting points:
* [Simple voice agent](/examples/simple-agent): STT, LLM, and TTS in a custom pipeline
* [Phone support with RAG](/examples/phone-and-rag): inbound calls backed by your docs
* [Golf coach](/examples/golf-coach): YOLO pose detection plus realtime voice feedback
More recipes on the [homepage](/) and in [Examples](/examples/simple-agent).
## How it fits together
A **client app** (web, iOS, Android, or local camera) connects through an **edge transport** to your **agent** process. The agent calls **AI plugins** (LLM, STT, TTS, vision), keeps **chat memory** for conversation context, and connects to **knowledge and tools** (RAG, function calling, MCP).
```mermaid theme={null}
flowchart LR
Client[Client app]
Edge[Edge transport]
Agent[Your agent]
AI[AI plugins]
Chat[Chat memory]
Data[Knowledge and tools]
Client --> Edge --> Agent
Agent --> AI
Agent --> Chat
Agent --> Data
```
**Edge transport** (pick one, swap in one line):
| Plugin | Best for |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [Stream Video RTC](/integrations/edge-transport/getstream) | Production WebRTC, [Stream client SDKs](https://getstream.io/video/sdk/), [chat-backed memory](/guides/chat-and-memory) |
| [Local transport](/integrations/edge-transport/local) | Dev on your machine with camera and mic, no Stream account |
| [Tencent RTC](/integrations/edge-transport/tencent) | Low latency in Asia, [Tencent client SDKs](/integrations/edge-transport/tencent) |
Your users join the same session as the agent via the transport's client SDK (or the Quickstart browser demo while prototyping). The agent runs server-side in Python.
**AI plugins**: realtime speech models, or separate STT + LLM + TTS, plus YOLO/VLM processors and avatars. See [Integrations](/integrations/introduction-to-integrations).
**Chat memory**: with Stream Video, transcripts and context persist to [Stream Chat](/guides/chat-and-memory) automatically. Use in-memory storage for local dev.
**Knowledge and tools**: TurboPuffer, Gemini FileSearch, `@llm.register_function`, and [MCP servers](/guides/mcp-tool-calling).
## Getting started
Follow the [Quickstart](/introduction/quickstart) to scaffold and talk to an agent in your browser (\~5 min).
Read [Voice Agents](/introduction/voice-agents) for custom pipelines and function calling, or [Video Agents](/introduction/video-agents) for VLMs and YOLO processors.
Clone a recipe from [Examples](/examples/simple-agent): phone, RAG, golf coach, and more.
Ship with [Deploy to production](/guides/deploying-overview): Docker, Kubernetes, and metrics.
# Quickstart
Source: https://visionagents.ai/introduction/quickstart
Build and run your first AI voice agent in under 5 minutes
You'll build a real-time voice agent you can talk to in your browser, using [Gemini Realtime](https://ai.google.dev/gemini-api/docs/live) on [Stream's](https://getstream.io/video/) edge network. About 18 lines of Python.
New to Vision Agents? Read the [Overview](/introduction/overview) first.
## Build your agent
Install **[uv](https://docs.astral.sh/uv/getting-started/installation/)**
first; it includes `uvx`. The `init` command runs `uv sync` to provision your
virtual environment, so `uv` must be on your PATH. Python 3.10–3.13 is
supported.
One command creates a ready-to-run agent project with dependencies installed:
```bash theme={null}
uvx vision-agents init my-agent && cd my-agent
```
This generates `agent.py`, `pyproject.toml`, `.env.example`, `tests/`, and a `Dockerfile`, then runs `uv sync`.
Pass `--no-install` to skip `uv sync` if you only want the project files: `uvx vision-agents init my-agent --no-install`
Get the keys you'll need:
* Create a free **[Stream account](https://getstream.io/try-for-free/)** for `STREAM_API_KEY` and `STREAM_API_SECRET`. See [Why Stream Video RTC](/integrations/edge-transport/getstream) for what the edge transport provides.
* Get an API key from **[Google AI Studio](https://aistudio.google.com/)** for `GOOGLE_API_KEY`.
Copy the scaffolded template and fill in your keys. Vision Agents auto-loads these for each plugin.
```bash theme={null}
cp .env.example .env
```
```bash .env theme={null}
STREAM_API_KEY=your_stream_api_key
STREAM_API_SECRET=your_stream_api_secret
GOOGLE_API_KEY=your_google_api_key
```
Open `agent.py`. The `init` command already created this file. Walk through each section to see how the agent works, then customize as needed.
```python agent.py theme={null}
from dotenv import load_dotenv
from vision_agents.core import Agent, Runner, User
from vision_agents.core.agents import AgentLauncher
from vision_agents.plugins import getstream, gemini
load_dotenv()
INSTRUCTIONS = (
"You're a helpful voice AI assistant. "
"Keep responses short and conversational."
)
async def create_agent(**kwargs) -> Agent:
return Agent(
edge=getstream.Edge(),
agent_user=User(name="My AI assistant", id="agent"),
instructions=INSTRUCTIONS,
llm=gemini.Realtime(),
)
async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
call = await agent.create_call(call_type, call_id)
async with agent.join(call):
await agent.simple_response(text="Say hi and introduce yourself.")
await agent.finish()
runner = Runner(AgentLauncher(create_agent=create_agent, join_call=join_call))
if __name__ == "__main__":
runner.cli()
```
* **`create_agent`**: builds the agent with Stream transport and Gemini Realtime.
* **`join_call`**: creates a call, joins it, and triggers the first response.
* **`runner`**: entry point for the CLI; `pyproject.toml` references it as `agent:runner`.
Start the agent. The CLI prints a join link. Open it to talk to your agent in the browser.
```bash theme={null}
uv run agent.py run
```
The agent greets you as soon as you join the call. The join link is a browser demo for testing. To embed the agent in your own app, use [Stream's Video SDKs](https://getstream.io/video/sdk/) on your target platform; your client and the agent join the same call.
**Core Reference** in the sidebar covers Agent, LLM, and processor APIs in
depth. Skip it until your first agent runs, then come back when you need API
details.
## Next steps
Custom STT/LLM/TTS pipelines, function calling, provider options
VLMs, YOLO processors, real-time video analysis
Docker, Kubernetes, and monitoring
35+ AI providers to mix and match
# Video Agents
Source: https://visionagents.ai/introduction/video-agents
Build video AI agents with realtime models, VLMs, and computer vision processors. Deploy to production with built-in metrics.
Build real-time video AI agents that process video with computer vision models, analyze frames with VLMs, or stream directly to realtime models. [Deploy to production](/guides/deployment) with [built-in metrics](/core/telemetry). Complete the [Quickstart](/introduction/quickstart) first.
## Three Approaches
| Mode | Best For | How It Works |
| ------------------- | ----------------------------- | ------------------------------------------- |
| **Realtime Models** | Lowest latency, native video | WebRTC/WebSocket direct to OpenAI or Gemini |
| **VLMs** | Video understanding, analysis | Frame buffering + chat completions API |
| **Processors** | Computer vision, detection | Custom ML pipelines alongside the LLM |
## Realtime Mode
Stream video directly to models with native vision support. The `fps` parameter controls how many frames per second are sent to the model:
```python theme={null}
from dotenv import load_dotenv
from vision_agents.core import Agent, AgentLauncher, User, Runner
from vision_agents.plugins import getstream, gemini
load_dotenv()
async def create_agent(**kwargs) -> Agent:
return Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="Describe what you see. Be concise.",
llm=gemini.Realtime(fps=3),
)
async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
call = await agent.create_call(call_type, call_id)
async with agent.join(call):
await agent.simple_response("What do you see?")
await agent.finish()
if __name__ == "__main__":
Runner(AgentLauncher(create_agent=create_agent, join_call=join_call)).cli()
```
Swap providers in one line:
```python theme={null}
llm=openai.Realtime(fps=3) # OpenAI
llm=gemini.Realtime(fps=3) # Gemini
llm=qwen.Realtime(fps=1) # Qwen 3 OMNI
```
## Vision Language Models (VLMs)
For video understanding and analysis, use VLMs that support the chat completions spec. Vision Agents automatically buffers frames and includes them with each request. Add the video-specific plugins:
```bash theme={null}
uv add "vision-agents[nvidia,deepgram,inworld]"
```
Add to your `.env`:
```bash theme={null}
NVIDIA_API_KEY=your_nvidia_api_key
DEEPGRAM_API_KEY=your_deepgram_api_key
INWORLD_API_KEY=your_inworld_api_key
```
```python theme={null}
from dotenv import load_dotenv
from vision_agents.core import Agent, AgentLauncher, User, Runner
from vision_agents.plugins import nvidia, getstream, deepgram, inworld
load_dotenv()
async def create_agent(**kwargs) -> Agent:
return Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="Analyze the video and answer questions.",
llm=nvidia.VLM(
model="nvidia/cosmos-reason2-8b",
fps=1,
frame_buffer_seconds=10,
),
stt=deepgram.STT(),
tts=inworld.TTS(),
)
async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
call = await agent.create_call(call_type, call_id)
async with agent.join(call):
await agent.simple_response("Describe what you see")
await agent.finish()
if __name__ == "__main__":
Runner(AgentLauncher(create_agent=create_agent, join_call=join_call)).cli()
```
Supported VLM providers:
| Provider | Use Case |
| ----------------------------------------------------------- | ------------------------------------------------------ |
| **[NVIDIA](/integrations/vision/nvidia)** | Cosmos 2 for advanced video reasoning |
| **[TwelveLabs](/integrations/vision/twelvelabs)** | Pegasus clip-based understanding for motion and events |
| **[HuggingFace](/integrations/infrastructure/huggingface)** | Open-source VLMs (Qwen2-VL, etc.) via inference API |
| **[OpenRouter](/integrations/llm/openrouter)** | Unified access to Claude, Gemini, and more |
## Video Processors
For computer vision tasks like object detection, pose estimation, or custom ML models, use processors. They intercept video frames, run inference, and forward results to the LLM.
```bash theme={null}
uv add "vision-agents[ultralytics]"
```
```python theme={null}
from dotenv import load_dotenv
from vision_agents.core import Agent, AgentLauncher, User, Runner
from vision_agents.plugins import getstream, gemini, ultralytics
load_dotenv()
async def create_agent(**kwargs) -> Agent:
return Agent(
edge=getstream.Edge(),
agent_user=User(name="Golf Coach", id="agent"),
instructions="Analyze the user's golf swing and provide feedback.",
llm=gemini.Realtime(fps=3),
processors=[
ultralytics.YOLOPoseProcessor(model_path="yolo26n-pose.pt")
],
)
async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
call = await agent.create_call(call_type, call_id)
async with agent.join(call):
await agent.simple_response("Say hi and offer to analyze their swing")
await agent.finish()
if __name__ == "__main__":
Runner(AgentLauncher(create_agent=create_agent, join_call=join_call)).cli()
```
Available processors:
| Processor | What It Does |
| -------------------- | ----------------------------------------------- |
| **Ultralytics YOLO** | Object detection, pose estimation, segmentation |
| **Roboflow** | Cloud or local detection with RF-DETR |
| **Custom** | Extend `VideoProcessor` for any ML model |
Processors can be chained: run detection first, then pass annotated frames to the LLM.
## Custom Pipeline with VLM
Combine VLMs with separate STT and TTS for full control:
```python theme={null}
from vision_agents.plugins import huggingface, getstream, deepgram, inworld
# Inside create_agent:
agent = Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You're a visual assistant.",
llm=huggingface.VLM(
model="Qwen/Qwen2-VL-7B-Instruct",
fps=1,
frame_buffer_seconds=10,
),
stt=deepgram.STT(),
tts=inworld.TTS(),
)
```
## What's Next
Build custom detection and analysis pipelines
Docker setup and environment configuration
## Examples
* [Golf Coach](/examples/golf-coach): Realtime pose detection + coaching
* [Security Camera](/examples/security-camera): Face recognition + package detection
* [Football Commentator](/examples/football-commentator): Object detection + live commentary
# Voice Agents
Source: https://visionagents.ai/introduction/voice-agents
Build voice agents with realtime models or custom STT/LLM/TTS pipelines. Phone integration, function calling, and production deployment.
Build voice agents with swappable providers, [phone integration](/guides/calling), [function calling](/guides/mcp-tool-calling), and [production deployment](/guides/deployment) with [built-in metrics](/core/telemetry).
If you completed the [Quickstart](/introduction/quickstart), you already have a working voice agent. This page explains the two ways to build one, and when to move beyond the default setup.
There are two common approaches. The first uses a **realtime model**, a single API that handles listening and speaking end-to-end, such as [OpenAI Realtime](/integrations/realtime/openai), [Gemini Live](/integrations/realtime/gemini), or [Qwen OMNI](/integrations/realtime/qwen). The second is a **[custom pipeline](#custom-pipeline-mode)** where you pick separate STT, LLM, and TTS providers and wire them together.
| Mode | Best for | You choose |
| ------------------- | ---------------------------- | ---------------------------------- |
| **Realtime Models** | Fastest path, lowest latency | One provider for speech in and out |
| **Custom Pipeline** | Full control over each stage | STT, LLM, and TTS independently |
## Realtime Mode
Realtime models handle the full voice loop (speech-to-text, reasoning, and text-to-speech) over a single WebRTC or WebSocket connection. No separate STT or TTS plugins needed.
The [Quickstart](/introduction/quickstart) uses `gemini.Realtime()`. To swap providers, change one line in `agent.py`:
```python theme={null}
llm=openai.Realtime() # OpenAI
llm=gemini.Realtime() # Gemini
llm=qwen.Realtime() # Qwen OMNI
```
See the integration pages for provider-specific setup: [OpenAI Realtime](/integrations/realtime/openai), [Gemini Live](/integrations/realtime/gemini), [Qwen OMNI](/integrations/realtime/qwen).
## Custom Pipeline Mode
Use a custom pipeline when you want to mix providers. For example, Deepgram for transcription, Gemini for reasoning, and Inworld for voice output. You also get control over [turn detection](/integrations/turn-detection/smart-turn) (when the agent starts and stops listening).
Or follow the steps below manually.
### 1. Add plugins
The Quickstart scaffolds with Gemini Realtime. Add STT and TTS plugins on top:
```bash theme={null}
uv add "vision-agents[deepgram,inworld]"
```
Add these keys to your `.env`:
```bash theme={null}
DEEPGRAM_API_KEY=your_deepgram_api_key
INWORLD_API_KEY=your_inworld_api_key
```
### 2. Wire up the pipeline
Replace `gemini.Realtime()` with separate `stt`, `llm`, and `tts` components in `agent.py`:
```python theme={null}
from dotenv import load_dotenv
from vision_agents.core import Agent, AgentLauncher, User, Runner
from vision_agents.plugins import getstream, gemini, deepgram, inworld
load_dotenv()
async def create_agent(**kwargs) -> Agent:
return Agent(
edge=getstream.Edge(),
agent_user=User(name="Assistant", id="agent"),
instructions="You're a helpful voice assistant.",
llm=gemini.LLM(),
stt=deepgram.STT(eager_turn_detection=True),
tts=inworld.TTS(),
)
async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
call = await agent.create_call(call_type, call_id)
async with agent.join(call):
await agent.simple_response("Greet the user")
await agent.finish()
if __name__ == "__main__":
Runner(AgentLauncher(create_agent=create_agent, join_call=join_call)).cli()
```
Audio flows: user speaks → **STT** transcribes → **LLM** generates a response → **TTS** speaks it back.
### 3. Mix and match providers
| Component | Options |
| ------------------ | ---------------------------------------------------------------- |
| **LLM** | Gemini, OpenAI, OpenRouter, Anthropic, Grok, HuggingFace |
| **STT** | Deepgram, ElevenLabs, Fast-Whisper, Fish, Wizper |
| **TTS** | Inworld, ElevenLabs, Cartesia, Deepgram, Grok, Pocket, AWS Polly |
| **Turn Detection** | Deepgram (built-in), ElevenLabs (built-in), Smart Turn, Vogent |
## Function Calling & MCP
Custom pipelines use an LLM component (`gemini.LLM()`, not `gemini.Realtime()`), which supports tool registration:
```python theme={null}
@llm.register_function(description="Get weather for a location")
async def get_weather(location: str) -> dict:
return {"temperature": "22C", "condition": "Sunny"}
```
Functions are automatically converted to the right format for each LLM provider. For MCP servers, external tools, and advanced patterns, see the [Function Calling & MCP guide](/guides/mcp-tool-calling).
## What's Next
Connect agents to inbound and outbound phone calls
Add knowledge bases with Gemini FileSearch or TurboPuffer
Docker setup and environment configuration
Console mode and HTTP server for running agents
## Examples
* [Simple Agent](/examples/simple-agent): Minimal voice agent with Deepgram STT + Inworld TTS + Gemini LLM
* [Twilio Phone Agent](/examples/twilio-phone-agent): Step-by-step inbound and outbound calls
* [Phone & RAG](/examples/phone-and-rag): Add knowledge retrieval to phone agents
# Events Reference
Source: https://visionagents.ai/reference/events-reference
Reference of every event the agent emits, with fields and import paths.
For an overview of how events are dispatched and patterns for using them, see the [Event System Guide](/guides/event-system).
For most app logic, prefer `agent.simple_response(..., interrupt=...)`, `agent.say(..., interrupt=...)`, and `agent.metrics` over wiring up low-level events.
## Base Event Structure
All events inherit from `BaseEvent`:
| Field | Type | Description |
| --------------- | --------------------- | ------------------------------------------------------ |
| `type` | `str` | Event type identifier (e.g. `"agent.user_transcript"`) |
| `event_id` | `str` | Unique UUID for this event instance |
| `timestamp` | `datetime` | When the event was created (UTC) |
| `session_id` | `str \| None` | Current session identifier |
| `participant` | `Participant \| None` | Participant the event relates to, when applicable |
| `user_metadata` | `Any \| None` | Optional caller-supplied metadata |
Plugin-emitted events extend `PluginBaseEvent`, which adds two fields:
| Field | Type | Description |
| ---------------- | ------------- | ----------------------------------------- |
| `plugin_name` | `str \| None` | Name of the plugin that emitted the event |
| `plugin_version` | `str \| None` | Version of the plugin |
***
## Agent Lifecycle Events
High-level events emitted by the agent itself as a call progresses through user and agent turns.
**Import:** `from vision_agents.core.agents.events import ...`
### UserTranscriptEvent
Emitted with the final user transcript that triggers an LLM turn. This is the event most apps should listen to for "what did the user say" — it fires in both classic STT and realtime modes.
```python theme={null}
from vision_agents.core.agents.events import UserTranscriptEvent
@agent.events.subscribe
async def on_user_transcript(event: UserTranscriptEvent):
print(f"User said: {event.text}")
```
| Field | Type | Description |
| ------------- | --------------------- | --------------------------- |
| `text` | `str` | Final transcribed user text |
| `participant` | `Participant \| None` | Who spoke |
### UserTurnStartedEvent
Emitted when the user starts speaking.
| Field | Type | Description |
| ------------- | --------------------- | -------------------- |
| `participant` | `Participant \| None` | Who started speaking |
### UserTurnEndedEvent
Emitted when the user stops speaking.
| Field | Type | Description |
| ------------- | --------------------- | -------------------- |
| `participant` | `Participant \| None` | Who stopped speaking |
### AgentTurnStartedEvent
Emitted when the agent starts speaking (first audio chunk leaving the pipeline). Carries only the `BaseEvent` fields.
### AgentTurnEndedEvent
Emitted when the agent stops speaking. Use `interrupted` to tell barge-in from natural completion.
```python theme={null}
from vision_agents.core.agents.events import AgentTurnEndedEvent
@agent.events.subscribe
async def on_agent_turn_ended(event: AgentTurnEndedEvent):
if event.interrupted:
print("User barged in")
```
| Field | Type | Description |
| ------------- | ------ | ------------------------------------------------------- |
| `interrupted` | `bool` | `True` if the turn was cut short by a user interruption |
### AgentJoinedCallEvent
Emitted after the agent has joined a call.
| Field | Type | Description |
| ------ | ------ | ------------------------ |
| `call` | `Call` | The call that was joined |
### AgentLeftCallEvent
Emitted when the agent leaves a call.
| Field | Type | Description |
| ------ | ------ | ---------------------- |
| `call` | `Call` | The call that was left |
### AgentFinishEvent
Emitted when `agent.finish()` has completed. Carries only the `BaseEvent` fields.
***
## Edge / Call Events
Events emitted by the edge transport for participant and track activity on the call.
**Import:** `from vision_agents.core.edge.events import ...`
### ParticipantJoinedEvent
Emitted when a participant (other than the agent) joins the call.
```python theme={null}
from vision_agents.core.edge.events import ParticipantJoinedEvent
@agent.events.subscribe
async def on_join(event: ParticipantJoinedEvent):
print(f"{event.participant.user_id} joined")
```
| Field | Type | Description |
| ------------- | ------------- | ------------------ |
| `participant` | `Participant` | Joined participant |
| `call` | `Call` | The call |
### ParticipantLeftEvent
Emitted when a participant (other than the agent) leaves the call.
| Field | Type | Description |
| ------------- | ------------- | --------------------- |
| `participant` | `Participant` | Participant that left |
| `call` | `Call` | The call |
### CallEndedEvent
Emitted when a call ends.
| Field | Type | Description |
| ------ | ------ | ------------------- |
| `call` | `Call` | The call that ended |
### TrackAddedEvent
Emitted when a track is added to the call.
| Field | Type | Description |
| ------------ | ------------------- | ------------------------ |
| `track_id` | `str \| None` | Track identifier |
| `track_type` | `TrackType \| None` | Track type (audio/video) |
### TrackRemovedEvent
Emitted when a track is removed from the call.
| Field | Type | Description |
| ------------ | ------------------- | ------------------------ |
| `track_id` | `str \| None` | Track identifier |
| `track_type` | `TrackType \| None` | Track type (audio/video) |
### AudioReceivedEvent
Emitted when audio is received from a participant. High-volume — silence it with `agent.events.silent(AudioReceivedEvent)` if you don't need it.
| Field | Type | Description |
| ---------- | ----------------- | ----------- |
| `pcm_data` | `PcmData \| None` | Audio data |
***
## LLM Events
Events from non-realtime LLM interactions.
**Import:** `from vision_agents.core.llm.events import ...`
### LLMResponseFinalEvent
Emitted when a final LLM response is received.
```python theme={null}
from vision_agents.core.llm.events import LLMResponseFinalEvent
@agent.events.subscribe
async def on_response(event: LLMResponseFinalEvent):
print(f"Response: {event.text}")
print(f"Model: {event.model}")
```
| Field | Type | Description |
| ------- | ------------- | ---------------------- |
| `text` | `str` | Full LLM response text |
| `model` | `str \| None` | Model identifier |
`LLMResponseCompletedEvent` and `LLMResponseChunkEvent` were removed in v0.6.2 along with the `heygen` plugin that was their only consumer. Subscribe to `LLMResponseFinalEvent` instead for completion notifications.
### LLMErrorEvent
Emitted when a non-realtime LLM error occurs.
| Field | Type | Description |
| ---------------- | ------------------- | -------------------------------------- |
| `error` | `Exception \| None` | The exception |
| `error_code` | `str \| None` | Error code |
| `context` | `str \| None` | Additional context |
| `request_id` | `str \| None` | Request identifier |
| `is_recoverable` | `bool` | Whether the error is recoverable |
| `error_message` | `str` | Property: human-readable error message |
***
## Realtime LLM Events
Events specific to realtime LLM connections (e.g. OpenAI Realtime, Gemini Live).
**Import:** `from vision_agents.core.llm.events import ...`
### RealtimeConnectedEvent
Emitted when a realtime connection is established.
| Field | Type | Description |
| ---------------- | ------------------------ | ---------------------- |
| `session_id` | `str \| None` | Session identifier |
| `session_config` | `dict[str, Any] \| None` | Session configuration |
| `capabilities` | `list[str] \| None` | Available capabilities |
### RealtimeDisconnectedEvent
Emitted when the realtime connection closes.
| Field | Type | Description |
| ------------ | ------------- | ---------------------------- |
| `session_id` | `str \| None` | Session identifier |
| `reason` | `str \| None` | Disconnection reason |
| `clean` | `bool` | Whether disconnect was clean |
For conversation content in a realtime session, subscribe to `UserTranscriptEvent` (see [Agent Lifecycle Events](#agent-lifecycle-events)) — it fires for both realtime and classic STT modes.
***
## Tool Events
Events for function calling / tool use.
**Import:** `from vision_agents.core.llm.events import ...`
### ToolStartEvent
Emitted when tool execution begins.
```python theme={null}
from vision_agents.core.llm.events import ToolStartEvent
@agent.events.subscribe
async def on_tool_start(event: ToolStartEvent):
print(f"Calling {event.tool_name}")
print(f"Args: {event.arguments}")
```
| Field | Type | Description |
| -------------- | -------------- | ----------------------------- |
| `tool_name` | `str` | Name of the tool being called |
| `arguments` | `dict \| None` | Arguments passed to the tool |
| `tool_call_id` | `str \| None` | Unique call identifier |
### ToolEndEvent
Emitted when tool execution completes.
```python theme={null}
from vision_agents.core.llm.events import ToolEndEvent
@agent.events.subscribe
async def on_tool_end(event: ToolEndEvent):
if event.success:
print(f"{event.tool_name} returned: {event.result}")
print(f"Took {event.execution_time_ms}ms")
else:
print(f"{event.tool_name} failed: {event.error}")
```
| Field | Type | Description |
| ------------------- | --------------- | --------------------------- |
| `tool_name` | `str` | Name of the tool |
| `success` | `bool` | Whether execution succeeded |
| `result` | `Any` | Return value (if success) |
| `error` | `str \| None` | Error message (if failed) |
| `tool_call_id` | `str \| None` | Unique call identifier |
| `execution_time_ms` | `float \| None` | Execution duration |
***
## STT Events
Connection-state and error events from the speech-to-text plugin. Transcripts themselves surface as `UserTranscriptEvent` (see [Agent Lifecycle Events](#agent-lifecycle-events)).
**Import:** `from vision_agents.core.stt.events import ...`
### STTConnectedEvent
Emitted when an STT connection is established. Carries only the `PluginBaseEvent` fields.
### STTDisconnectedEvent
Emitted when an STT connection is closed.
| Field | Type | Description |
| -------- | ------------- | ---------------------------- |
| `reason` | `str \| None` | Disconnection reason |
| `clean` | `bool` | Whether disconnect was clean |
### STTErrorEvent
Emitted when STT encounters an error.
| Field | Type | Description |
| --------------- | ------------------- | -------------------------------------- |
| `error` | `Exception \| None` | The exception that occurred |
| `error_code` | `str \| None` | Error code identifier |
| `context` | `str \| None` | Additional context |
| `error_message` | `str` | Property: human-readable error message |
***
## TTS Events
Events from speech synthesis.
**Import:** `from vision_agents.core.tts.events import ...`
### TTSSynthesisStartEvent
Emitted when TTS synthesis begins.
| Field | Type | Description |
| ----------------------- | --------------- | ---------------------------- |
| `text` | `str \| None` | Text being synthesized |
| `synthesis_id` | `str` | Unique ID for this synthesis |
| `model_name` | `str \| None` | TTS model name |
| `voice_id` | `str \| None` | Voice identifier |
| `estimated_duration_ms` | `float \| None` | Estimated audio duration |
### TTSSynthesisCompleteEvent
Emitted when TTS synthesis finishes.
```python theme={null}
from vision_agents.core.tts.events import TTSSynthesisCompleteEvent
@agent.events.subscribe
async def on_complete(event: TTSSynthesisCompleteEvent):
print(f"Synthesis took {event.synthesis_time_ms}ms")
print(f"Audio duration: {event.audio_duration_ms}ms")
```
| Field | Type | Description |
| ------------------- | --------------- | ---------------------------- |
| `synthesis_id` | `str \| None` | Unique ID for this synthesis |
| `text` | `str \| None` | Text that was synthesized |
| `total_audio_bytes` | `int` | Total bytes of audio |
| `synthesis_time_ms` | `float` | Processing time |
| `audio_duration_ms` | `float \| None` | Resulting audio duration |
| `chunk_count` | `int` | Number of chunks produced |
| `real_time_factor` | `float \| None` | Synthesis speed vs real-time |
### TTSConnectedEvent
Emitted when a TTS connection is established. Carries only the `PluginBaseEvent` fields.
### TTSDisconnectedEvent
Emitted when a TTS connection is closed.
| Field | Type | Description |
| -------- | ------------- | ---------------------------- |
| `reason` | `str \| None` | Disconnection reason |
| `clean` | `bool` | Whether disconnect was clean |
### TTSErrorEvent
Emitted when TTS encounters an error.
| Field | Type | Description |
| --------------- | ------------------- | -------------------------------------- |
| `error` | `Exception \| None` | The exception |
| `error_code` | `str \| None` | Error code |
| `context` | `str \| None` | Additional context |
| `error_message` | `str` | Property: human-readable error message |
***
## Video Processor Events
Vision plugins (Roboflow, Ultralytics, Huggingface, etc.) emit subclasses of `VideoProcessorDetectionEvent` whenever they finish processing a frame.
**Import:** `from vision_agents.core.events import VideoProcessorDetectionEvent`
### VideoProcessorDetectionEvent
Base class for detection events from any video processor plugin. Plugins extend it with their own fields (detected objects, bounding boxes, raw provider output).
| Field | Type | Description |
| ------------------- | --------------- | ---------------------------- |
| `model_id` | `str \| None` | Identifier of the model used |
| `inference_time_ms` | `float \| None` | Time taken for inference |
| `detection_count` | `int` | Number of objects detected |
To subscribe, import the plugin-specific subclass (for example `roboflow.DetectionCompletedEvent`). See [Create your own plugin](/integrations/create-your-own-plugin) for the conventions plugin events follow.
This event is used by `MetricsCollector` to record video processing metrics. See [Telemetry](/core/telemetry) for details.
***
## ExceptionEvent
Wraps any unhandled exception raised by a subscriber. The event system catches handler failures and re-emits them as `ExceptionEvent` so that other handlers and the agent itself keep running. Subscribe if you want a single place to log handler errors.
| Field | Type | Description |
| --------- | ----------- | -------------------------------- |
| `exc` | `Exception` | The exception that was raised |
| `handler` | `Callable` | The handler function that failed |
***
## Subscribing to Events
All events can be subscribed to using the `@agent.events.subscribe` decorator:
```python theme={null}
@agent.events.subscribe
async def my_handler(event: EventType):
# Handle event
pass
```
Subscribe to multiple event types using union types:
```python theme={null}
@agent.events.subscribe
async def my_handler(event: UserTranscriptEvent | LLMResponseFinalEvent):
print(event)
```
Event handlers must be async functions. Non-async handlers raise `RuntimeError` at subscribe time.
See the [Event System Guide](/guides/event-system) for the dispatch model (fire-and-forget, fanout, error isolation) and the patterns that go with it.