Spaces:
Runtime error
Runtime error
| #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 : | |
| 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)}" |