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:

  1. Define the tool – Create an async callable that performs the external HTTP request and returns a string the LLM can parse.
  2. Expose to the agent – Pass the tool to the tools= argument when constructing the Agent, AgentSession, or crew.
  3. 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_tool for LiveKit agents or pass plain callables to tools= for Pydantic‑AI and CrewAI.
  • Environment Management: Load API keys via dotenv.load_dotenv(). The repository provides .env.example files for each project to document required variables like OLOSTEP_API_KEY and NEBIUS_API_KEY.
  • Error Handling: Wrap external HTTP calls in try/except blocks 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_tool decorator integrates Olostep search into real‑time Gemini voice sessions.
  • starter_ai_agents/pydantic_starter/main.py demonstrates zero‑boilerplate DuckDuckGo integration using duckduckgo_search_tool().
  • simple_ai_agents/newsletter_agent/main.py shows how to mutate search_params on 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →