112 lines
4.3 KiB
Python
112 lines
4.3 KiB
Python
"""Search routes — /api/search/config GET, /api/search POST."""
|
|
|
|
import logging
|
|
from typing import Dict, Any
|
|
|
|
from fastapi import APIRouter, Request
|
|
|
|
import time
|
|
|
|
from services.search import get_search_config, comprehensive_web_search, PROVIDER_INFO
|
|
from services.search.core import _call_provider
|
|
from services.search.providers import _get_provider_key, _get_search_instance
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def _request_values(request: Request) -> Dict[str, Any]:
|
|
"""Accept JSON, form data, or query params for search endpoints.
|
|
|
|
The browser UI posts FormData, while the agent's generic app_api tool
|
|
posts JSON. FastAPI Form(...) rejects JSON with a 422 before our handler
|
|
runs, which made the model think SearXNG was broken.
|
|
"""
|
|
values: Dict[str, Any] = dict(request.query_params)
|
|
content_type = (request.headers.get("content-type") or "").lower()
|
|
try:
|
|
if "application/json" in content_type:
|
|
body = await request.json()
|
|
if isinstance(body, dict):
|
|
values.update(body)
|
|
else:
|
|
form = await request.form()
|
|
values.update(dict(form))
|
|
except Exception:
|
|
pass
|
|
return values
|
|
|
|
|
|
def setup_search_routes(config) -> APIRouter:
|
|
router = APIRouter(tags=["search"])
|
|
|
|
@router.get("/api/search/config")
|
|
async def get_search_settings() -> Dict[str, Any]:
|
|
return get_search_config()
|
|
|
|
@router.post("/api/search")
|
|
async def do_web_search(request: Request) -> Dict[str, Any]:
|
|
"""Standalone web search — returns context string + source list.
|
|
|
|
Used by Compare mode to pre-search once and share results across panes.
|
|
"""
|
|
values = await _request_values(request)
|
|
query = str(values.get("query") or values.get("q") or "").strip()
|
|
if not query:
|
|
return {"context": "", "sources": [], "error": "query is required"}
|
|
time_filter = values.get("time_filter") or values.get("freshness")
|
|
if time_filter is not None:
|
|
time_filter = str(time_filter).strip() or None
|
|
try:
|
|
context, sources = comprehensive_web_search(
|
|
query, return_sources=True, time_filter=time_filter,
|
|
)
|
|
return {"context": context, "sources": sources}
|
|
except Exception as e:
|
|
logger.error(f"Standalone web search failed: {e}")
|
|
return {"context": "", "sources": [], "error": str(e)}
|
|
|
|
@router.get("/api/search/providers")
|
|
async def list_search_providers():
|
|
"""Return available search providers with config status."""
|
|
providers = []
|
|
for pid, (label, needs_key, needs_url) in PROVIDER_INFO.items():
|
|
if pid == "disabled":
|
|
continue
|
|
available = True
|
|
if needs_key and not _get_provider_key(pid):
|
|
available = False
|
|
if needs_url and pid == "searxng" and not _get_search_instance():
|
|
available = False
|
|
providers.append({
|
|
"id": pid,
|
|
"label": label,
|
|
"available": available,
|
|
})
|
|
return providers
|
|
|
|
@router.post("/api/search/query")
|
|
async def search_with_provider(request: Request) -> Dict[str, Any]:
|
|
"""Search using a specific provider. Used by compare search mode."""
|
|
values = await _request_values(request)
|
|
query = str(values.get("query") or values.get("q") or "").strip()
|
|
provider = str(values.get("provider") or "").strip()
|
|
try:
|
|
count = int(values.get("count") or values.get("limit") or 10)
|
|
except Exception:
|
|
count = 10
|
|
if not query:
|
|
return {"results": [], "provider": provider, "error": "query is required"}
|
|
if provider not in PROVIDER_INFO or provider == "disabled":
|
|
return {"results": [], "provider": provider, "error": "Unknown provider"}
|
|
t0 = time.time()
|
|
try:
|
|
results = _call_provider(provider, query, min(count, 20))
|
|
elapsed = round(time.time() - t0, 2)
|
|
return {"results": results, "provider": provider, "time": elapsed}
|
|
except Exception as e:
|
|
elapsed = round(time.time() - t0, 2)
|
|
logger.error(f"Search provider {provider} failed: {e}")
|
|
return {"results": [], "provider": provider, "time": elapsed, "error": str(e)}
|
|
|
|
return router
|