import logging import os from langchain_core.tools import StructuredTool from pydantic import BaseModel, Field from typing import Optional, List from serpapi import GoogleSearch logger = logging.getLogger(__name__) class SearchInput(BaseModel): query: str = Field(description="Search query") async def search_func(query: str) -> List[str]: """ Perform a web search using SerpAPI and return relevant snippets. Args: query (str): The search query to execute. Returns: List[str]: A list of search result snippets. """ try: logger.info(f"Executing SerpAPI search for query: {query}") params = { "q": query, "api_key": os.getenv("SERPAPI_API_KEY"), "num": 10 } search = GoogleSearch(params) results = search.get_dict().get("organic_results", []) return [result.get("snippet", "") for result in results if "snippet" in result] except Exception as e: logger.error(f"SerpAPI search failed for query '{query}': {e}") return [] search_tool = StructuredTool.from_function( func=search_func, name="search_tool", args_schema=SearchInput, coroutine=search_func ) class MultiHopSearchInput(BaseModel): query: str = Field(description="Multi-hop search query") steps: int = Field(description="Number of search steps", ge=1, le=3) llm_client: Optional[object] = Field(description="LLM client", default=None) llm_type: Optional[str] = Field(description="LLM type", default="together") llm_model: Optional[str] = Field(description="LLM model", default="meta-llama/Llama-3.3-70B-Instruct-Turbo-Free") async def multi_hop_search_func(query: str, steps: int, llm_client: Optional[object] = None, llm_type: Optional[str] = "together", llm_model: Optional[str] = "meta-llama/Llama-3.3-70B-Instruct-Turbo-Free") -> List[str]: """ Perform a multi-hop web search using SerpAPI with iterative query refinement. Args: query (str): The initial multi-hop search query. steps (int): Number of search steps to perform (1 to 3). llm_client (Optional[object]): LLM client for query refinement. llm_type (Optional[str]): Type of LLM (e.g., 'together'). llm_model (Optional[str]): LLM model name. Returns: List[str]: A list of search result snippets from all steps. """ try: logger.info(f"Executing multi-hop search for query: {query}, steps: {steps}") results = [] current_query = query for step in range(steps): logger.info(f"Multi-hop step {step + 1}: {current_query}") step_results = await search_func(current_query) results.extend(step_results) if step < steps - 1 and llm_client: prompt = f"Given the query '{current_query}' and results: {step_results[:3]}, generate a follow-up search query to refine or expand the search." messages = [ {"role": "system", "content": "Generate a single search query as a string."}, {"role": "user", "content": prompt} ] if llm_type == "together": response = llm_client.chat.completions.create( model=llm_model, messages=messages, max_tokens=50, temperature=0.7 ) current_query = response.choices[0].message.content.strip() else: logger.warning("LLM not configured for multi-hop refinement") break return results[:5] if results else ["No results found"] except Exception as e: logger.error(f"Multi-hop search failed for query '{query}': {e}") return [f"Error: {str(e)}"] multi_hop_search_tool = StructuredTool.from_function( func=multi_hop_search_func, name="multi_hop_search_tool", args_schema=MultiHopSearchInput, coroutine=multi_hop_search_func )