You can add live web search and page extraction capabilities to a Cartesia Line voice agent using loopback tools backed by Tavily. This allows the agent to answer questions about current events, fresh facts, and specific URLs during a live call.
Prerequisites
- Python 3.10+
CARTESIA_API_KEYTAVILY_API_KEY- An LLM provider key (e.g.,
OPENAI_API_KEY) - Cartesia CLI installed for local testing
Installation
pip install cartesia-line tavily-python
Implementation Example
To implement this, define a class containing loopback_tool methods that wrap the AsyncTavilyClient. Use search_depth="fast" to maintain low latency suitable for voice interactions.
from typing import Annotated, Optional
from tavily import AsyncTavilyClient
from line.llm_agent import LlmAgent, LlmConfig, ToolEnv, end_call, loopback_tool
from line.voice_agent_app import AgentEnv, CallRequest, VoiceAgentApp
class TavilyTools:
def __init__(self, api_key: str):
# Reusing the client ensures the httpx connection pool is reused across tool calls
self._client = AsyncTavilyClient(api_key=api_key, client_source="cartesia-line-agent")
@loopback_tool
async def web_search(
self,
ctx: ToolEnv,
query: Annotated[str, "The search query. Be specific."],
time_range: Annotated[Optional[str], "Optional recency filter: 'day', 'week', 'month', or 'year'."] = None,
) -> str:
"""Search the web for current information."""
# 'fast' depth is recommended for voice latency
kwargs: dict = {"query": query, "search_depth": "fast", "max_results": 5}
if time_range is not None:
kwargs["time_range"] = time_range
response = await self._client.search(**kwargs)
results = response.get("results", [])
if not results:
return "No relevant information found."
parts = [f"Search results for: '{query}'\n"]
for i, r in enumerate(results, start=1):
parts.append(f"\n--- Source {i}: {r['title']} (score {r.get('score', 0):.2f}) ---\n")
if r.get("content"):
parts.append(f"{r['content']}\n")
parts.append(f"URL: {r['url']}\n")
return "".join(parts)
@loopback_tool
async def web_extract(
self,
ctx: ToolEnv,
url: Annotated[str, "The URL to extract content from."],
) -> str:
"""Extract the full content of a webpage given its URL."""
response = await self._client.extract(urls=[url])
results = response.get("results", [])
if not results:
return "No content could be extracted."
raw = results[0].get("raw_content", "")
# Truncate to keep LLM context tight
EXTRACT_MAX_CHARS = 3000
if len(raw) > EXTRACT_MAX_CHARS:
raw = raw[:EXTRACT_MAX_CHARS] + "\n\n[Content truncated]"
return f"Extracted content from {url}:\n\n{raw}"
Running the Agent
- Start the application:
python main.py - In a separate terminal, connect via CLI:
cartesia chat 8000