Implementing Web Search Tools for AI Agents: LiveKit, Pydantic‑AI, and Firecrawl Examples
Implementing web search tools for AI agents requires defining an async function decorated with @function_tool, exposing it via the tools= parameter during agent construction, and providing system instructions that tell the LLM when to invoke the search.
The awesome‑ai‑apps repository demonstrates production‑ready patterns for equipping AI agents with real‑time web search across voice, text, and automation workflows. By following a consistent three‑step architecture—define, expose, and invoke—you can integrate search capabilities using Olostep, DuckDuckGo, or Firecrawl regardless of your underlying framework.
Core Architecture Pattern
Every implementation in the repository follows the same structural blueprint:
- Define the tool – Create an
asynccallable that performs the external HTTP request and returns a string the LLM can parse. - Expose to the agent – Pass the tool to the
tools=argument when constructing theAgent,AgentSession, or crew. - Prompt for invocation – Write system instructions that explicitly tell the model when to call the tool and how to format citations.
This pattern ensures type safety, proper error handling, and clean separation between the LLM logic and external API calls.
LiveKit Voice Agent with Olostep
In voice_agents/livekit_web_search_agent/main.py, the repository shows how to give a real‑time voice assistant access to fresh web data using the Olostep API and LiveKit’s realtime model.
Tool Definition
The web_search function is decorated with @function_tool and performs an async call to Olostep’s answers endpoint:
from livekit.agents import function_tool
import os
from olostep import Olostep
@function_tool
async def web_search(query: str) -> str:
"""Search the web using Olostep and return a formatted answer with sources."""
client = Olostep(api_key=os.getenv("OLOSTEP_API_KEY"))
answer = client.answers.create(task=query)
return f"Answer: {answer.text}\nSources: {answer.sources}"
Session Integration
The tool is injected into the AgentSession via the tools= parameter, allowing the Gemini‑3.1‑flash‑live‑preview model to call it during a voice conversation:
from livekit.agents import AgentSession
from livekit.plugins import google
session = AgentSession(
llm=google.realtime.RealtimeModel(model="gemini-3.1-flash-live-preview"),
tools=[web_search],
)
Prompt Engineering
The system instructions explicitly guide the model to use the tool for factual queries:
INSTRUCTIONS = """
You are a helpful voice assistant. When a question needs fresh or factual information,
call the web_search tool. After receiving results, cite the sources in your response.
"""
This setup enables the agent to answer time‑sensitive questions (e.g., “What is Bitcoin’s current price?”) without leaving the voice channel.
Pydantic‑AI Starter with DuckDuckGo
The file starter_ai_agents/pydantic_starter/main.py demonstrates a minimal implementation using Pydantic‑AI’s built‑in DuckDuckGo integration.
Import and Registration
Rather than writing a custom wrapper, you import the pre‑built tool and pass an instance to the Agent constructor:
from pydantic_ai import Agent
from pydantic_ai.common_tools.duckduckgo import duckduckgo_search_tool
weather_agent = Agent(
model=model,
tools=[duckduckgo_search_tool()],
system_prompt="You are a weather assistant. Use DuckDuckGo to find the current weather forecast.",
)
Execution Flow
When you run weather_agent.run_sync("What is the weather in Paris today?"), Pydantic‑AI automatically:
- Serializes the tool call
- Executes the DuckDuckGo search
- Injects the results back into the LLM context
- Returns the final natural‑language answer
This approach requires zero boilerplate for the search logic itself, making it ideal for rapid prototyping.
Newsletter Generator with Firecrawl
For multi‑step research workflows, simple_ai_agents/newsletter_agent/main.py shows how to dynamically configure Firecrawl search parameters at runtime.
Dynamic Tool Configuration
The agent’s first tool is a firecrawl_search instance stored in newsletter_agent.tools[0]. Before invoking the agent, the code updates search_params to control result breadth:
# Inside NewsletterGenerator class
self.agent.tools[0].search_params = {
"limit": 8,
"time_range": "qdr:w" # Last week
}
Workflow Integration
The system prompt names the specific tool the LLM should use:
system_prompt="""You are NewsletterResearch‑X. Use firecrawl_search to find recent articles
about the topic, extract key points, and compose a markdown newsletter."""
By embedding the search tool as the first element of the tools list, the agent retrieves curated URLs, extracts content, and generates a formatted newsletter in a single execution loop.
Best Practices for Production
Based on the patterns in awesome‑ai‑apps, follow these guidelines when implementing web search tools for AI agents:
- Tool Contract: Define functions as
async def tool_name(arg: str) -> str. Return plain strings or JSON that the LLM can parse easily. - Decoration: Use
@function_toolfor LiveKit agents or pass plain callables totools=for Pydantic‑AI and CrewAI. - Environment Management: Load API keys via
dotenv.load_dotenv(). The repository provides.env.examplefiles for each project to document required variables likeOLOSTEP_API_KEYandNEBIUS_API_KEY. - Error Handling: Wrap external HTTP calls in
try/exceptblocks and return user‑friendly error messages. This allows the LLM to fall back gracefully when search services are unavailable. - Citation Requirements: Always instruct the model to cite sources when using web search tools to prevent hallucinations and provide verifiability.
Complete Implementation Examples
Stand‑Alone Web Search Tool
This minimal example shows the raw pattern using httpx and DuckDuckGo:
from livekit.agents import function_tool
import httpx
@function_tool
async def web_search(query: str) -> str:
"""Simple DuckDuckGo search wrapper."""
resp = await httpx.get(
"https://api.duckduckgo.com/",
params={"q": query, "format": "json", "no_html": "1"},
timeout=10,
)
data = resp.json()
answer = data.get("Abstract") or "No concise answer found."
source = data.get("AbstractURL", "unknown")
return f"{answer}\n\nSource: {source}"
Pydantic‑AI with Nebius Provider
Connect DuckDuckGo search to a Llama model hosted on Nebius:
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.common_tools.duckduckgo import duckduckgo_search_tool
import os
model = OpenAIModel(
model_name="meta-llama/Meta-Llama-3.1-70B-Instruct",
provider=OpenAIProvider(
base_url="https://api.tokenfactory.nebius.com/v1",
api_key=os.getenv("NEBIUS_API_KEY")
),
)
agent = Agent(
model=model,
tools=[duckduckgo_search_tool()],
system_prompt="You are a research assistant. Use DuckDuckGo for current data.",
)
print(agent.run_sync("Latest developments in quantum computing"))
Dynamic Firecrawl Newsletter
Instantiate the newsletter generator with custom search depth:
from simple_ai_agents.newsletter_agent.main import NewsletterGenerator
newsletter = NewsletterGenerator(
topic="AI regulation",
search_limit=10,
time_range="qdr:m", # Last month
)
print(newsletter.generate())
Summary
- Implementing web search tools for AI agents follows a three‑step architecture: define an async tool function, expose it via
tools=, and prompt the LLM to invoke it. - In
voice_agents/livekit_web_search_agent/main.py, the@function_tooldecorator integrates Olostep search into real‑time Gemini voice sessions. starter_ai_agents/pydantic_starter/main.pydemonstrates zero‑boilerplate DuckDuckGo integration usingduckduckgo_search_tool().simple_ai_agents/newsletter_agent/main.pyshows how to mutatesearch_paramson a Firecrawl tool at runtime for dynamic research workflows.- Always use environment variables for API keys and wrap external calls in error handling to maintain agent reliability.
Frequently Asked Questions
What is the @function_tool decorator used for in AI agents?
The @function_tool decorator is used in LiveKit agents to mark an async Python function as a callable tool that the LLM can invoke during a conversation. It automatically generates the JSON schema required for the model to understand parameter types and descriptions, bridging the gap between natural language requests and your Python code.
How do I choose between DuckDuckGo, Olostep, and Firecrawl for web search?
Choose DuckDuckGo for free, quick instant answers without API keys, suitable for prototypes or low‑volume agents. Use Olostep when you need high‑quality, hosted answer extraction with source citations for voice or chat agents. Select Firecrawl for research workflows requiring raw page content extraction, specific time‑range filtering, or when building multi‑step pipelines like newsletter generation.
Can web search tools work with real‑time voice agents?
Yes. As shown in voice_agents/livekit_web_search_agent/main.py, you can pass web search tools to an AgentSession using the google.realtime.RealtimeModel (Gemini). The model can invoke the tool mid‑conversation to fetch fresh data and return the results audibly while maintaining the voice connection latency requirements.
What is the correct way to handle API keys for search tools in AI agents?
You should load API keys from environment variables using os.getenv() inside your tool functions or agent configuration, never hardcode them. The awesome‑ai‑apps repository standardizes on python-dotenv to load .env files, with .env.example templates provided in each project directory to document required variables like OLOSTEP_API_KEY or NEBIUS_API_KEY.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →