Cartesia Voice Agents SDK
Project description
Cartesia Line SDK
Build intelligent, low-latency voice agents with Line.
Line brings voice to your text agents with Cartesia's state-of-the-art speech models. We handle audio orchestration, deployment, and observability so you can focus on your agent's reasoning.
Features
- Real-time interruption support — Handles audio interruptions and turn-taking out-of-the-box
- Tool calling — Connect to databases, APIs, and external services
- Multi-agent handoffs — Route conversations between specialized agents
- Web search — Built-in tool for real-time information lookup
- 100+ LLM providers — Works with any LLM via LiteLLM
- Instant deployment — Build, deploy, and start talking in minutes
Quick Start
1. Clone and run an example:
git clone https://github.com/cartesia-ai/line.git
cd line/examples/basic_chat
GEMINI_API_KEY=your-key uv run python main.py
2. Or create from scratch:
mkdir my-agent && cd my-agent
uv init && uv add cartesia-line
Create main.py:
import os
from line.llm_agent import LlmAgent, LlmConfig, end_call
from line.voice_agent_app import VoiceAgentApp
async def get_agent(env, call_request):
return LlmAgent(
model="gemini/gemini-2.5-flash-preview-09-2025",
api_key=os.getenv("GEMINI_API_KEY"),
tools=[end_call],
config=LlmConfig(
system_prompt="You are a helpful voice assistant.",
introduction="Hello! How can I help you today?",
),
)
app = VoiceAgentApp(get_agent=get_agent)
if __name__ == "__main__":
app.run()
Run it:
GEMINI_API_KEY=your-key uv run python main.py
3. (Optional) Install the CLI to test locally:
curl -fsSL https://cartesia.sh | sh
Then chat with your agent:
PORT=8000 uv run python main.py
cartesia chat 8000
See the CLI documentation for deployment and management commands.
Customize Your Agent's Prompt
System Prompt & Introduction
Configure your agent's personality and behavior via LlmConfig:
config = LlmConfig(
system_prompt="You are a customer service agent for Acme Corp. Be friendly and concise.",
introduction="Hi! Thanks for calling Acme. How can I help?",
)
system_prompt— Defines the agent's personality, rules, and contextintroduction— First message spoken when the call starts (set to""to wait for user)
Dynamic Prompts from API
Use LlmConfig.from_call_request() to configure prompts dynamically from your API:
async def get_agent(env: AgentEnv, call_request: CallRequest):
# Prompts come from call_request.agent.system_prompt and call_request.agent.introduction
# Falls back to your defaults if not provided
return LlmAgent(
model="gemini/gemini-2.5-flash-preview-09-2025",
tools=[end_call],
config=LlmConfig.from_call_request(
call_request,
fallback_system_prompt="You are a helpful assistant.",
fallback_introduction="Hello! How can I help?",
),
)
Add Tools to Your Agent
Built-in Tools
Ready-to-use tools for common actions:
from line.llm_agent import LlmAgent, LlmConfig, end_call, send_dtmf, transfer_call, web_search
agent = LlmAgent(
model="gemini/gemini-2.5-flash-preview-09-2025",
tools=[end_call, send_dtmf, transfer_call, web_search],
config=LlmConfig(...),
)
| Tool | What it does |
|---|---|
end_call |
Ends the call |
send_dtmf |
Presses phone buttons (0-9, *, #) |
transfer_call |
Transfers to a phone number (E.164 format) |
web_search |
Searches the web (native LLM search or DuckDuckGo fallback) |
Loopback Tools — Fetch Data & Call APIs
Results go back to the LLM for a natural language response:
from typing import Annotated
from line.llm_agent import loopback_tool
@loopback_tool
async def get_order_status(ctx, order_id: Annotated[str, "The order ID"]) -> str:
"""Look up order status."""
order = await db.get_order(order_id)
return f"Order {order_id}: {order.status}"
agent = LlmAgent(tools=[get_order_status, end_call], ...)
User: "What's the status of order 12345?" Agent: Calls tool → LLM responds: "Your order was delivered on January 5th!"
Passthrough Tools — Deterministic Actions
Output goes directly to the user, bypassing the LLM:
from typing import Annotated
from line.events import AgentSendText, AgentTransferCall
from line.llm_agent import passthrough_tool
@passthrough_tool
async def transfer_to_support(ctx, reason: Annotated[str, "Why they need support"]):
"""Transfer to support team."""
yield AgentSendText(text="Transferring you to support now.")
yield AgentTransferCall(target_phone_number="+18005551234")
agent = LlmAgent(tools=[transfer_to_support, end_call], ...)
Handoff Tools — Multi-Agent Workflows
Transfer control to a specialized agent:
from line.llm_agent import LlmAgent, LlmConfig, agent_as_handoff, end_call
spanish_agent = LlmAgent(
model="anthropic/claude-sonnet-4-5",
tools=[end_call],
config=LlmConfig(
system_prompt="You speak only in Spanish.",
introduction="¡Hola! ¿Cómo puedo ayudarte?",
),
)
main_agent = LlmAgent(
model="gemini/gemini-2.5-flash-preview-09-2025",
tools=[
end_call,
agent_as_handoff(
spanish_agent,
handoff_message="Transferring you to our Spanish-speaking agent...",
name="transfer_to_spanish",
description="Transfer when user wants to speak Spanish.",
),
],
config=LlmConfig(system_prompt="Transfer to Spanish if requested."),
)
Tool Types Summary
| Type | How to create | Result goes to | Use for |
|---|---|---|---|
| Loopback | @loopback_tool |
Back to LLM | API calls, data lookup |
| Passthrough | @passthrough_tool |
Directly to user | Deterministic actions |
| Handoff | agent_as_handoff() or @handoff_tool |
Another agent | Multi-agent workflows |
Long-Running Tools
By default, tool calls are terminated when the agent is interrupted (though any reasoning and tool call response values already produced are preserved for use in the next generation).
For tools that take a long time to complete, set is_background=True. The tool will continue running in the background until completion regardless of interruptions, then loop back to the LLM:
from typing import Annotated
from line.llm_agent import loopback_tool
@loopback_tool(is_background=True)
async def search_database(ctx, query: Annotated[str, "Search query"]) -> str:
"""Search that may take a while."""
results = await slow_database_search(query)
return results
Context Management
Control what the LLM sees in its conversation history using add_history_entry and set_history_processor.
Inject Context with add_history_entry
Insert text into the LLM's conversation history. By default, entries appear as system messages. This is useful for injecting context for controlling exactly what the LLM sees from tool calls, or integrating information from external APIs.
from line.llm_agent import LlmAgent, LlmConfig, loopback_tool
agent = LlmAgent(
model="gemini/gemini-2.5-flash-preview-09-2025",
api_key=os.getenv("GEMINI_API_KEY"),
config=LlmConfig(system_prompt="You are a helpful assistant."),
)
# Inject context before the conversation starts
agent.add_history_entry("The customer's name is Alice and she has a premium account.")
# Or inject context from within a tool call
@loopback_tool
async def lookup_customer(ctx, customer_id: str) -> str:
"""Look up customer details."""
customer = await db.get_customer(customer_id)
# Inject rich context that persists across turns
agent.add_history_entry(f"Customer profile: {customer.summary}")
return f"Found customer {customer.name}"
Each entry defaults to a system message (role="system"). Pass role="user" to inject as a user message instead.
Transform History with set_history_processor
Register a function that transforms the full conversation history before it's passed to the LLM. This gives you control over filtering, reordering, or injecting events
from line import HistoryEvent, CustomHistoryEntry, UserTextSent
# Filter history to only keep user messages and custom entries
def keep_relevant(history: list[HistoryEvent]) -> list[HistoryEvent]:
return [e for e in history if isinstance(e, (UserTextSent, CustomHistoryEntry))]
agent.set_history_processor(keep_relevant)
# Append a reminder to every LLM call
def add_reminder(history: list[HistoryEvent]) -> list[HistoryEvent]:
return list(history) + [CustomHistoryEntry(content="Remember: be concise and friendly.")]
agent.set_history_processor(add_reminder)
# Async transforms work too — useful for fetching external context
async def inject_live_context(history: list[HistoryEvent]) -> list[HistoryEvent]:
context = await fetch_latest_context()
return [CustomHistoryEntry(content=context)] + list(history)
agent.set_history_processor(inject_live_context)
The transform receives the full history (input events + local events) as a list of HistoryEvent items and must return a list of HistoryEvent items.
Customize Your Agent's Implementation
Wrap with Custom Logic
Implement the Agent protocol to add guardrails, logging, or preprocessing:
from line.agent import TurnEnv
from line.events import InputEvent, OutputEvent, UserTurnEnded, AgentSendText
from line.llm_agent import LlmAgent, LlmConfig, end_call
class GuardedAgent:
def __init__(self, inner_agent):
self.inner = inner_agent
self.blocked_words = ["competitor", "confidential"]
async def process(self, env: TurnEnv, event: InputEvent):
# Pre-process: check user input for blocked words
if isinstance(event, UserTurnEnded):
user_text = " ".join(
item.content for item in event.content if hasattr(item, "content")
)
if any(word in user_text.lower() for word in self.blocked_words):
yield AgentSendText(text="I can't discuss that topic.")
return
# Delegate to inner agent
async for output in self.inner.process(env, event):
yield output
async def get_agent(env, call_request):
inner = LlmAgent(
model="gemini/gemini-2.5-flash-preview-09-2025",
tools=[end_call],
config=LlmConfig(system_prompt="You are a helpful assistant."),
)
return GuardedAgent(inner)
LLM Provider Support
Line leverages LiteLLM to support 100+ LLM providers. Pass any LiteLLM-compatible model string to LlmAgent:
| Provider | Model format |
|---|---|
| OpenAI | gpt-5-nano, gpt-5.2 |
| Anthropic | anthropic/claude-haiku-4-5-20251001, anthropic/claude-sonnet-4-5 |
gemini/gemini-2.5-flash-preview-09-2025 |
Agent Examples
| Example | Description |
|---|---|
| Basic Chat | Simple conversational agent |
| Form Filler | Collect structured data |
| Phone Transfer | IVR navigation & transfers |
| Multi-Agent | Hand off between agents |
| Echo Tool | Custom handoff tool |
Integrations
| Integration | Description |
|---|---|
| Exa Web Research | Real-time web search |
| Browserbase | Fill web forms via voice |
Documentation
- SDK Overview — Architecture and installation
- Tools Guide — Tool types in depth
- Agents Guide — LlmAgent, custom agents, conversation loop
- Events Reference — Input/output events
Getting Help
Acknowledgments
Line leverages the fantastic work by the maintainers of LiteLLM. Their open-source library provides the unified LLM interface that makes it possible to support 100+ providers out of the box.
LiteLLM is licensed under the MIT License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cartesia_line-0.2.3a1.tar.gz.
File metadata
- Download URL: cartesia_line-0.2.3a1.tar.gz
- Upload date:
- Size: 69.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0bac5b2cd08c37ce0fe603962a4fd563bd46dd2a4bfdf964715d59753f71a422
|
|
| MD5 |
9cecfa201d76c734ee226ffa607113b9
|
|
| BLAKE2b-256 |
1ec6a13c3a25eff558bc65d35c58a296fd5137f828abe9b8fffa1ffac3366c47
|
Provenance
The following attestation bundles were made for cartesia_line-0.2.3a1.tar.gz:
Publisher:
publish-to-pypi.yaml on cartesia-ai/line
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cartesia_line-0.2.3a1.tar.gz -
Subject digest:
0bac5b2cd08c37ce0fe603962a4fd563bd46dd2a4bfdf964715d59753f71a422 - Sigstore transparency entry: 942298108
- Sigstore integration time:
-
Permalink:
cartesia-ai/line@eda9bfabc7823032a66e236abc0aef607ff2efca -
Branch / Tag:
refs/heads/main - Owner: https://github.com/cartesia-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yaml@eda9bfabc7823032a66e236abc0aef607ff2efca -
Trigger Event:
workflow_run
-
Statement type:
File details
Details for the file cartesia_line-0.2.3a1-py3-none-any.whl.
File metadata
- Download URL: cartesia_line-0.2.3a1-py3-none-any.whl
- Upload date:
- Size: 53.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7213d9a2ae348aed26080225abf3db6769dbfe7355fed320969445178d95de8
|
|
| MD5 |
9527c5e2eb813f6c183257a733dab3e7
|
|
| BLAKE2b-256 |
790e2f77275fbad4f72003985a8a31c747f1b7967b9561792039afb04be79bfe
|
Provenance
The following attestation bundles were made for cartesia_line-0.2.3a1-py3-none-any.whl:
Publisher:
publish-to-pypi.yaml on cartesia-ai/line
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cartesia_line-0.2.3a1-py3-none-any.whl -
Subject digest:
f7213d9a2ae348aed26080225abf3db6769dbfe7355fed320969445178d95de8 - Sigstore transparency entry: 942298113
- Sigstore integration time:
-
Permalink:
cartesia-ai/line@eda9bfabc7823032a66e236abc0aef607ff2efca -
Branch / Tag:
refs/heads/main - Owner: https://github.com/cartesia-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yaml@eda9bfabc7823032a66e236abc0aef607ff2efca -
Trigger Event:
workflow_run
-
Statement type: