AgentBench / tools /web_search.py
Adityax-07's picture
feat: add core LangGraph multi-agent pipeline
be745f3
Raw
History Blame Contribute Delete
5.33 kB
#TavilyClient → performs the actual web search.
#tool → converts this function into a LangChain tool usable by agents.
#os → securely read API key from environment (never hardcode keys).
#typing → helps readability and maintainability (production standard).
from tavily import TavilyClient
from langchain_core.tools import tool
from dotenv import load_dotenv
import os
from typing import List, Dict
# WHY load_dotenv() here?
# os.getenv() only reads variables already in the OS environment.
# load_dotenv() reads the .env file and injects them into os.environ
# so the getenv() call below can find them.
# This must happen BEFORE any os.getenv() call.
load_dotenv()
#Why this section exists?
#Fail fast principle -> If API key missing → crash immediately at startup instead of failing randomly later.
# Security best practice -> Keys must come from environment variables, not source code.
#Debuggability ->Clear error instead of mysterious tool failure.
TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
if not TAVILY_API_KEY:
raise ValueError("TAVILY_API_KEY not found in environment variables")
#Why global client?
#Creating the client once:
# avoids recreating object on every tool call
# improves speed and reduces latency
# prevents rate-limit overhead
# important because agents may call tools many times
# This pattern is called a singleton client.
tavily_client = TavilyClient(api_key=TAVILY_API_KEY)
#Why a separate helper function?
#Separation of concerns:
# searching logic ≠ formatting logic
# easier to test independently
# easier to upgrade later (ex: switch to JSON output)
#This is a production design pattern.
def _format_results(results: List[Dict]) -> str:
"""Clean and compress Tavily results for LLM consumption."""
# We create a new list instead of modifying raw results to avoid side effects
# and maintain immutability, which is a good practice in production code.
cleaned = []
#Why this block?
#Key reliability + hallucination control decisions:
# .get() prevents crashes if API response changes.
#Default fallbacks avoid KeyError.
#Content truncation (VERY important)
#LLMs:
#have token limits
#get confused by long noisy text
#Short summaries → better answers + lower cost.
for r in results:
title = r.get("title", "No title")
content = r.get("content", "")[:500] # truncate long content
url = r.get("url", "")
#Why structured formatting?
#LLMs reason better with predictable structure:
# Title
# Summary
# Source
#This reduces hallucinations dramatically.
cleaned.append(
f"Title: {title}\nSummary: {content}\nSource: {url}"
)
#Why join results?
#Agents perform better when:
#each result separated clearly
#easier for LLM to cite sources
return "\n\n".join(cleaned)
# Tool definition :
@tool
def web_search(query: str) -> str:
#Why such a detailed docstring?
#This is not for humans — it's for the LLM agent.
#Agents use this text to decide:
#Should I call the tool?
#What is it good for?
#Better docstring = smarter agent behavior.
"""
Search the web for recent and factual information.
Use this tool when:
- The question needs up-to-date info
- The question involves current events
- The LLM lacks knowledge
Input:
query (str): short search query (max 200 chars)
Output:
Formatted search results with title, summary and source URL.
"""
# Why Guardrails?
#Agents sometimes send:
#empty strings
#garbage prompts
#broken inputs
#Never trust tool input.
#This prevents crashes.
if not query or len(query.strip()) == 0:
return "Error: Empty search query."
#Why limit query length?
#Security + cost control:
#prevents prompt injection attacks
#prevents extremely long inputs
#keeps search focused
#reduces API cost
query = query.strip()[:200] # prevent prompt injection / long inputs
#Why try/except?
#External APIs can fail:
#network errors
#rate limits
#service outages
#Production tools must never crash the agent.
#Why these parameters?
#max_results=5 → balance between context and noise.
#search_depth="advanced" → better quality results.
try:
response = tavily_client.search(
query=query,
max_results=5,
search_depth="advanced"
)
#Again defensive coding — never assume API shape.
results = response.get("results", [])
#Agents need explicit feedback instead of empty output.
if not results:
return "No relevant results found."
#Separation of concerns again:
#search → format → return
return _format_results(results)
#Why return error instead of raising?
#Agents cannot handle Python exceptions.
#They can handle text.
#So we convert crashes → readable tool output.
except Exception as e:
return f"Search tool error: {str(e)}"