| | """ |
| | LangChain-compatible tools for the LangGraph multi-agent system |
| | |
| | This module provides LangChain tools that work properly with LangGraph agents, |
| | replacing the LlamaIndex tools with native LangChain implementations. |
| | """ |
| |
|
| | import os |
| | import wikipedia |
| | import arxiv |
| | from typing import List, Optional, Type |
| | from langchain_core.tools import BaseTool, tool |
| | from pydantic import BaseModel, Field |
| | from huggingface_hub import list_models |
| | from observability import tool_span |
| |
|
| | |
| | try: |
| | from langchain_tavily import TavilySearch |
| | TAVILY_AVAILABLE = True |
| | except ImportError as e: |
| | print(f"Warning: langchain_tavily not available: {e}") |
| | TAVILY_AVAILABLE = False |
| | TavilySearch = None |
| |
|
| |
|
| | |
| | class WikipediaSearchInput(BaseModel): |
| | """Input for Wikipedia search tool.""" |
| | query: str = Field(description="The search query for Wikipedia") |
| |
|
| |
|
| | class ArxivSearchInput(BaseModel): |
| | """Input for ArXiv search tool.""" |
| | query: str = Field(description="The search query for ArXiv papers") |
| |
|
| |
|
| | class HubStatsInput(BaseModel): |
| | """Input for Hugging Face Hub stats tool.""" |
| | author: str = Field(description="The author/organization name on Hugging Face Hub") |
| |
|
| |
|
| | class TavilySearchInput(BaseModel): |
| | """Input for Tavily search tool.""" |
| | query: str = Field(description="The search query for web search") |
| |
|
| |
|
| | |
| |
|
| | @tool("wikipedia_search", args_schema=WikipediaSearchInput) |
| | def wikipedia_search_tool(query: str) -> str: |
| | """Search Wikipedia for information about a topic.""" |
| | try: |
| | with tool_span("wikipedia_search", metadata={"query": query}): |
| | |
| | try: |
| | |
| | search_results = wikipedia.search(query, results=3) |
| | if not search_results: |
| | return f"No Wikipedia results found for '{query}'" |
| | |
| | |
| | for page_title in search_results: |
| | try: |
| | page = wikipedia.page(page_title) |
| | |
| | content = page.summary |
| | if len(content) > 1000: |
| | content = content[:1000] + "..." |
| | |
| | return f"Wikipedia: {page.title}\n\nURL: {page.url}\n\nSummary:\n{content}" |
| | |
| | except wikipedia.exceptions.DisambiguationError as e: |
| | |
| | try: |
| | page = wikipedia.page(e.options[0]) |
| | content = page.summary |
| | if len(content) > 1000: |
| | content = content[:1000] + "..." |
| | return f"Wikipedia: {page.title}\n\nURL: {page.url}\n\nSummary:\n{content}" |
| | except: |
| | continue |
| | except: |
| | continue |
| | |
| | return f"Could not retrieve Wikipedia content for '{query}'" |
| | |
| | except Exception as e: |
| | return f"Wikipedia search error: {str(e)}" |
| | |
| | except Exception as e: |
| | return f"Wikipedia search failed: {str(e)}" |
| |
|
| |
|
| | @tool("arxiv_search", args_schema=ArxivSearchInput) |
| | def arxiv_search_tool(query: str) -> str: |
| | """Search ArXiv for academic papers.""" |
| | try: |
| | with tool_span("arxiv_search", metadata={"query": query}): |
| | |
| | search = arxiv.Search( |
| | query=query, |
| | max_results=3, |
| | sort_by=arxiv.SortCriterion.Relevance |
| | ) |
| | |
| | results = [] |
| | for paper in search.results(): |
| | result = f"""Title: {paper.title} |
| | Authors: {', '.join([author.name for author in paper.authors])} |
| | Published: {paper.published.strftime('%Y-%m-%d')} |
| | URL: {paper.entry_id} |
| | Summary: {paper.summary[:500]}...""" |
| | results.append(result) |
| | |
| | if results: |
| | return f"ArXiv Search Results for '{query}':\n\n" + "\n\n---\n\n".join(results) |
| | else: |
| | return f"No ArXiv papers found for '{query}'" |
| | |
| | except Exception as e: |
| | return f"ArXiv search failed: {str(e)}" |
| |
|
| |
|
| | @tool("huggingface_hub_stats", args_schema=HubStatsInput) |
| | def huggingface_hub_stats_tool(author: str) -> str: |
| | """Get statistics for a Hugging Face Hub author.""" |
| | try: |
| | with tool_span("huggingface_hub_stats", metadata={"author": author}): |
| | models = list(list_models(author=author, sort="downloads", direction=-1, limit=5)) |
| | if models: |
| | results = [] |
| | for i, model in enumerate(models, 1): |
| | results.append(f"{i}. {model.id} - {model.downloads:,} downloads") |
| | |
| | top_model = models[0] |
| | summary = f"Top 5 models by {author}:\n" + "\n".join(results) |
| | summary += f"\n\nMost popular: {top_model.id} with {top_model.downloads:,} downloads" |
| | return summary |
| | else: |
| | return f"No models found for author '{author}'" |
| | |
| | except Exception as e: |
| | return f"Hub stats error: {str(e)}" |
| |
|
| |
|
| | @tool("tavily_search_results_json", args_schema=TavilySearchInput) |
| | def tavily_search_fallback_tool(query: str) -> str: |
| | """Fallback web search tool when Tavily is not available.""" |
| | try: |
| | with tool_span("tavily_search_fallback", metadata={"query": query}): |
| | |
| | import requests |
| | |
| | |
| | |
| | search_url = f"https://duckduckgo.com/lite/?q={query}" |
| | headers = { |
| | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' |
| | } |
| | |
| | try: |
| | response = requests.get(search_url, headers=headers, timeout=10) |
| | if response.status_code == 200: |
| | return f"Web search completed for '{query}'. Found general web results (fallback mode - Tavily not available)." |
| | else: |
| | return f"Web search failed for '{query}' (status: {response.status_code})" |
| | except Exception as e: |
| | return f"Web search error for '{query}': {str(e)}" |
| | |
| | except Exception as e: |
| | return f"Web search failed: {str(e)}" |
| |
|
| |
|
| | def get_tavily_search_tool() -> BaseTool: |
| | """Get the Tavily search tool from LangChain community, with fallback.""" |
| | if TAVILY_AVAILABLE and TavilySearch: |
| | try: |
| | return TavilySearch( |
| | api_key=os.getenv("TAVILY_API_KEY"), |
| | max_results=6, |
| | include_answer=True, |
| | include_raw_content=True, |
| | description="Search the web for current information and facts" |
| | ) |
| | except Exception as e: |
| | print(f"Warning: Failed to create TavilySearch tool: {e}") |
| | return tavily_search_fallback_tool |
| | else: |
| | print("Warning: Using fallback search tool (Tavily not available)") |
| | return tavily_search_fallback_tool |
| |
|
| |
|
| | def get_calculator_tools() -> List[BaseTool]: |
| | """Get calculator tools as LangChain tools.""" |
| | |
| | @tool("multiply") |
| | def multiply(a: float, b: float) -> float: |
| | """Multiply two numbers.""" |
| | return a * b |
| | |
| | @tool("add") |
| | def add(a: float, b: float) -> float: |
| | """Add two numbers.""" |
| | return a + b |
| | |
| | @tool("subtract") |
| | def subtract(a: float, b: float) -> float: |
| | """Subtract two numbers.""" |
| | return a - b |
| | |
| | @tool("divide") |
| | def divide(a: float, b: float) -> float: |
| | """Divide two numbers.""" |
| | if b == 0: |
| | raise ValueError("Cannot divide by zero") |
| | return a / b |
| | |
| | @tool("modulus") |
| | def modulus(a: int, b: int) -> int: |
| | """Get the modulus of two integers.""" |
| | if b == 0: |
| | raise ValueError("Cannot modulo by zero") |
| | return a % b |
| | |
| | return [multiply, add, subtract, divide, modulus] |
| |
|
| |
|
| | def get_research_tools() -> List[BaseTool]: |
| | """Get all research tools for the research agent.""" |
| | tools = [ |
| | get_tavily_search_tool(), |
| | wikipedia_search_tool, |
| | arxiv_search_tool, |
| | ] |
| | return tools |
| |
|
| |
|
| | def get_code_tools() -> List[BaseTool]: |
| | """Get all code/computation tools for the code agent.""" |
| | tools = get_calculator_tools() |
| | tools.append(huggingface_hub_stats_tool) |
| | return tools |
| |
|
| |
|
| | def get_all_tools() -> List[BaseTool]: |
| | """Get all available tools.""" |
| | return get_research_tools() + get_code_tools() |