NextQuest.ai / src /search.py
ajeet9843
Deploy to Hugging Face Spaces
f10fe83
Raw
History Blame Contribute Delete
7.33 kB
import os
import asyncio
import logging
from typing import List, Optional, Any
from dataclasses import dataclass
from abc import ABC, abstractmethod
import httpx
logger = logging.getLogger(__name__)
DEFAULT_SEARCH_TIMEOUT = 30.0
DEFAULT_MAX_RESULTS = 5
try:
from ddgs import DDGS
DDGS_AVAILABLE = True
except ImportError:
try:
from duckduckgo_search import DDGS
DDGS_AVAILABLE = True
except ImportError:
DDGS_AVAILABLE = False
logger.warning("DuckDuckGo search not available (ddgs package not installed)")
@dataclass
class SearchResult:
title: str
url: str
content: str
score: float
published_date: Optional[str] = None
class SearchProvider(ABC):
@abstractmethod
async def search(self, query: str, max_results: int = DEFAULT_MAX_RESULTS) -> List[SearchResult]:
pass
class TavilySearch(SearchProvider):
def __init__(self, api_key: Optional[str] = None):
self._api_key = api_key
@property
def api_key(self):
return self._api_key or os.getenv("TAVILY_API_KEY")
async def search(self, query: str, max_results: int = DEFAULT_MAX_RESULTS) -> List[SearchResult]:
if not self.api_key:
logger.error("[Tavily] API key not configured")
return []
try:
async with httpx.AsyncClient(timeout=DEFAULT_SEARCH_TIMEOUT) as client:
response = await client.post(
"https://api.tavily.com/search",
json={
"query": query,
"max_results": max_results,
"include_answer": True,
"include_raw_content": False,
},
headers={"Content-Type": "application/json"},
)
response.raise_for_status()
data = response.json()
results = []
for item in data.get("results", []):
results.append(
SearchResult(
title=item.get("title", ""),
url=item.get("url", ""),
content=item.get("content", ""),
score=item.get("score", 0.0),
)
)
logger.info(f"[Tavily] Found {len(results)} results for query: {query[:50]}")
return results
except httpx.HTTPStatusError as e:
logger.error(f"[Tavily] HTTP error {e.response.status_code}: {e}")
return []
except httpx.RequestError as e:
logger.error(f"[Tavily] Request error: {e}")
return []
except Exception as e:
logger.error(f"[Tavily] Unexpected error: {e}", exc_info=True)
return []
class DuckDuckGoSearch(SearchProvider):
def __init__(self):
pass
async def search(self, query: str, max_results: int = DEFAULT_MAX_RESULTS) -> List[SearchResult]:
if not DDGS_AVAILABLE:
logger.warning("[DuckDuckGo] DDGS not available")
return []
def _sync_search():
results = []
try:
with DDGS() as ddgs:
for i, r in enumerate(ddgs.text(query, max_results=max_results)):
results.append(
{
"title": r.get("title", ""),
"url": r.get("href", ""),
"content": r.get("body", ""),
"score": 1.0 - (i * 0.1),
}
)
except Exception as e:
logger.error(f"[DuckDuckGo] Search error: {e}")
return []
return results
try:
raw_results = await asyncio.get_running_loop().run_in_executor(None, _sync_search)
results = [
SearchResult(
title=r.get("title", ""),
url=r.get("url", ""),
content=r.get("content", ""),
score=r.get("score", 0.0),
)
for r in raw_results
]
logger.info(f"[DuckDuckGo] Found {len(results)} results for query: {query[:50]}")
return results
except Exception as e:
logger.error(f"[DuckDuckGo] Unexpected error: {e}", exc_info=True)
return []
class SerperSearch(SearchProvider):
def __init__(self, api_key: Optional[str] = None):
self._api_key = api_key
@property
def api_key(self):
return self._api_key or os.getenv("SERPER_API_KEY")
async def search(self, query: str, max_results: int = DEFAULT_MAX_RESULTS) -> List[SearchResult]:
if not self.api_key:
logger.error("[Serper] API key not configured")
return []
try:
async with httpx.AsyncClient(timeout=DEFAULT_SEARCH_TIMEOUT) as client:
response = await client.post(
"https://google.serper.dev/search",
json={"q": query, "num": max_results},
headers={
"X-API-KEY": self.api_key,
"Content-Type": "application/json",
},
)
response.raise_for_status()
data = response.json()
results = []
for item in data.get("organic", []):
results.append(
SearchResult(
title=item.get("title", ""),
url=item.get("link", ""),
content=item.get("snippet", ""),
score=item.get("position", 0) / 10,
)
)
logger.info(f"[Serper] Found {len(results)} results for query: {query[:50]}")
return results
except httpx.HTTPStatusError as e:
logger.error(f"[Serper] HTTP error {e.response.status_code}: {e}")
return []
except httpx.RequestError as e:
logger.error(f"[Serper] Request error: {e}")
return []
except Exception as e:
logger.error(f"[Serper] Unexpected error: {e}", exc_info=True)
return []
class SearchManager:
def __init__(self):
self.providers = {
"tavily": TavilySearch(),
"duckduckgo": DuckDuckGoSearch(),
"serper": SerperSearch(),
}
async def search(
self, query: str, provider: str = "duckduckgo", max_results: int = DEFAULT_MAX_RESULTS
) -> List[SearchResult]:
if provider in self.providers:
results = await self.providers[provider].search(query, max_results)
if results:
return results
logger.warning(f"[SearchManager] Provider {provider} returned no results, trying fallback")
if provider != "duckduckgo":
fallback_results = await self.providers["duckduckgo"].search(query, max_results)
if fallback_results:
return fallback_results
logger.error("[SearchManager] All search providers returned empty results")
return []
search_manager = SearchManager()