Files
vocero-client/s2s-client/s2s_client/api/routes/search.py
T

93 lines
3.5 KiB
Python

from __future__ import annotations
import json
import httpx
from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse
from s2s_client.api.responses import validated_json_response
from s2s_client.core import config as settings
from s2s_client.core import logging as app_logging
from s2s_client.models import requests as models_requests
from s2s_client.models.responses import SearchResponse
router = APIRouter()
@router.post("/api/search", response_model=SearchResponse)
async def search(req: models_requests.SearchRequest) -> JSONResponse:
"""Proxy a Google search via Serper.dev. The key stays on the server unless
the user brought their own (then theirs is used for this request only)."""
query = (req.query or "").strip()
if not query:
raise HTTPException(status_code=400, detail="La consulta está vacía.")
key = (req.key or "").strip() or settings.SERPER_KEY
has_user_key = bool((req.key or "").strip())
if not key:
# No server key and the user didn't supply one — search is unavailable.
raise HTTPException(status_code=503, detail="La búsqueda no está configurada.")
headers = {"X-API-KEY": key, "Content-Type": "application/json"}
payload = {"q": query, "num": settings.MAX_RESULTS}
app_logging.logger.debug(
"[EXT][SERPER] search POST %s query=%s userKey=%s num=%d",
settings.SERPER_URL,
json.dumps(query, ensure_ascii=True)[:200],
has_user_key,
settings.MAX_RESULTS,
)
try:
async with httpx.AsyncClient(timeout=12.0) as http:
resp = await http.post(settings.SERPER_URL, headers=headers, json=payload)
except httpx.RequestError as exc:
app_logging.logger.warning("Serper unreachable: %r", exc)
raise HTTPException(status_code=502, detail="No se puede conectar con el proveedor de búsqueda.") # noqa: B904
app_logging.logger.debug(
"[EXT][SERPER] search RESP status=%s len=%dB",
resp.status_code,
len(resp.content or b""),
)
if resp.status_code != 200:
# Serper's error body carries the real reason (e.g. "Not enough
# credits") and contains no key, so it's safe to log and relay.
body = resp.text[:300]
app_logging.logger.warning("Serper error %s: %s", resp.status_code, body)
msg = None
try:
msg = resp.json().get("message")
except Exception:
pass
detail = f"Error del proveedor de búsqueda ({resp.status_code})"
if msg:
detail += f": {msg}"
raise HTTPException(status_code=502, detail=detail)
data = resp.json()
results = []
for item in (data.get("organic") or [])[: settings.MAX_RESULTS]:
results.append(
{
"title": item.get("title", ""),
"snippet": item.get("snippet", ""),
"url": item.get("link", ""),
}
)
# A direct answer when Google has one — saves the model a hop.
box = data.get("answerBox") or {}
answer = box.get("answer") or box.get("snippet") or None
if not answer:
kg = data.get("knowledgeGraph") or {}
answer = kg.get("description") or None
app_logging.logger.debug(
"[EXT][SERPER] search PARSED hasAnswer=%s resultCount=%d titles=%s",
bool(answer),
len(results),
[r.get("title", "")[:40] for r in results],
)
return validated_json_response({"query": query, "answer": answer, "results": results}, SearchResponse)