Coverage for src / mcp_server_langgraph / tools / search_tools.py: 90%
72 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-03 00:43 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-03 00:43 +0000
1"""
2Search tools for querying information
4Provides knowledge base and web search capabilities for the agent.
5"""
7from typing import Annotated
9import httpx
10from langchain_core.tools import tool
11from pydantic import Field
12from qdrant_client import QdrantClient
14from mcp_server_langgraph.core.config import settings
15from mcp_server_langgraph.observability.telemetry import logger, metrics
18@tool
19def search_knowledge_base(
20 query: Annotated[str, Field(description="Search query to find relevant information")],
21 limit: Annotated[int, Field(ge=1, le=20, description="Maximum number of results (1-20)")] = 5,
22) -> str:
23 """
24 Search internal knowledge base for relevant information.
26 Use this to find:
27 - Documentation and guides
28 - Previous conversations and context
29 - System configuration
30 - Frequently asked questions
32 Returns top matching results with relevance scores.
33 """
34 try:
35 logger.info("Knowledge base search invoked", extra={"query": query, "limit": limit})
36 metrics.tool_calls.add(1, {"tool": "search_knowledge_base"})
38 # Check if Qdrant is configured
39 if not hasattr(settings, "qdrant_url") or not settings.qdrant_url:
40 return """Knowledge base search not configured.
42To enable:
431. Deploy Qdrant vector database
442. Set QDRANT_URL and QDRANT_PORT in .env
453. Set ENABLE_DYNAMIC_CONTEXT_LOADING=true
464. Index your knowledge base documents
48See: docs/advanced/dynamic-context.md"""
50 # Query Qdrant for semantic search
51 try:
52 client = QdrantClient( # noqa: F841
53 url=settings.qdrant_url,
54 port=getattr(settings, "qdrant_port", 6333),
55 )
57 # Use configured collection or default
58 collection_name = getattr(settings, "qdrant_collection_name", "mcp_context")
60 # Perform semantic search (requires embeddings)
61 # Note: This assumes documents are already indexed
62 # For full implementation, add embedding generation here
64 results_text = f"""Knowledge base search: "{query}"
66Connected to Qdrant at {settings.qdrant_url}:{getattr(settings, "qdrant_port", 6333)}
67Collection: {collection_name}
69Note: Semantic search requires embeddings and indexed documents.
70Configure EMBEDDING_PROVIDER and index your knowledge base.
72For setup: See docs/advanced/dynamic-context.md"""
74 logger.info("Knowledge base search completed", extra={"query": query})
75 return results_text
77 except Exception as e:
78 logger.warning(f"Qdrant query failed: {e}")
79 return f"""Knowledge base search error: {e}
81Verify Qdrant is running and accessible at {settings.qdrant_url}"""
83 except Exception as e:
84 error_msg = f"Error searching knowledge base: {e}"
85 logger.error(error_msg, exc_info=True)
86 return f"Error: {e}"
89@tool
90async def web_search(
91 query: Annotated[str, Field(description="Search query for web search")],
92 num_results: Annotated[int, Field(ge=1, le=10, description="Number of results to return (1-10)")] = 5,
93) -> str:
94 """
95 Search the web for current information.
97 Use this to find:
98 - Current events and news
99 - External documentation
100 - Public information not in knowledge base
101 - Real-time data
103 Returns titles, snippets, and URLs of top results.
105 IMPORTANT: This tool requires web search API integration.
106 Currently returns placeholder results.
107 """
108 try:
109 logger.info("Web search invoked", extra={"query": query, "num_results": num_results})
110 metrics.tool_calls.add(1, {"tool": "web_search"})
112 # Check for configured web search API key
113 serper_api_key = getattr(settings, "serper_api_key", None)
114 tavily_api_key = getattr(settings, "tavily_api_key", None)
115 brave_api_key = getattr(settings, "brave_api_key", None) # noqa: F841
117 # Try Tavily API (recommended for AI applications)
118 if tavily_api_key:
119 try:
120 async with httpx.AsyncClient() as client:
121 response = await client.post(
122 "https://api.tavily.com/search",
123 json={"api_key": tavily_api_key, "query": query, "max_results": num_results},
124 timeout=30.0,
125 )
126 response.raise_for_status()
127 data = await response.json()
129 results = [f'Web search: "{query}"\n']
130 for i, result in enumerate(data.get("results", [])[:num_results], 1):
131 results.append(f"\n{i}. {result.get('title', 'No title')}")
132 results.append(f" {result.get('content', 'No snippet')[:200]}...")
133 results.append(f" URL: {result.get('url', 'N/A')}")
135 logger.info("Tavily web search completed", extra={"results": len(data.get("results", []))})
136 return "\n".join(results) if results else "No results found"
138 except Exception as e:
139 logger.error(f"Tavily search failed: {e}", exc_info=True)
140 # Fall through to other providers or placeholder
142 # Try Serper API
143 elif serper_api_key:
144 try:
145 async with httpx.AsyncClient() as client:
146 response = await client.post(
147 "https://google.serper.dev/search",
148 json={"q": query, "num": num_results},
149 headers={"X-API-KEY": serper_api_key, "Content-Type": "application/json"},
150 timeout=30.0,
151 )
152 response.raise_for_status()
153 data = await response.json()
155 results = [f'Web search: "{query}"\n']
156 for i, result in enumerate(data.get("organic", [])[:num_results], 1):
157 results.append(f"\n{i}. {result.get('title', 'No title')}")
158 results.append(f" {result.get('snippet', 'No snippet')}")
159 results.append(f" URL: {result.get('link', 'N/A')}")
161 logger.info("Serper web search completed", extra={"results": len(data.get("organic", []))})
162 return "\n".join(results) if results else "No results found"
164 except Exception as e:
165 logger.error(f"Serper search failed: {e}", exc_info=True)
166 # Fall through to placeholder
168 # No API key configured
169 config_message = f"""Web search for: "{query}"
171❌ Web search API not configured.
173To enable web search, add one of these API keys to .env:
175**Option 1: Tavily (Recommended for AI)**
176- Get key: https://tavily.com/
177- Add to .env: TAVILY_API_KEY=your-key
178- Best for: AI-optimized search results
180**Option 2: Serper (Google Search)**
181- Get key: https://serper.dev/
182- Add to .env: SERPER_API_KEY=your-key
183- Best for: Google search results
185**Option 3: Brave Search**
186- Get key: https://brave.com/search/api/
187- Add to .env: BRAVE_API_KEY=your-key
188- Best for: Privacy-focused search
190After configuration, this tool will return real web search results."""
192 logger.info("Web search completed (no API key)", extra={"query": query})
193 return config_message
195 except Exception as e:
196 error_msg = f"Error performing web search: {e}"
197 logger.error(error_msg, exc_info=True)
198 return f"Error: {e}"