""" backend/tools/jina_reader.py — P24-F3: Jina Reader fallback per SPA e siti JS. Usa r.jina.ai per estrarre contenuto leggibile da qualsiasi URL, incluse Single Page App (React/Vue/Next.js) che il fetch normale non legge. Funziona senza API key (free, rate-limited). Con JINA_API_KEY: limiti più alti e accesso a funzioni extra. Endpoint: GET https://r.jina.ai/{url} """ from __future__ import annotations import logging import os from typing import Any from urllib.parse import quote import httpx _logger = logging.getLogger("agente_ai.tools.jina_reader") _JINA_API_KEY: str = os.getenv("JINA_API_KEY", "") _TIMEOUT = 25.0 # Jina renderizza JS — serve più tempo _MAX_CONTENT = 12000 # max caratteri restituiti def _jina_headers() -> dict[str, str]: headers: dict[str, str] = { "Accept": "application/json", "X-Return-Format": "markdown", "X-Timeout": "20", } if _JINA_API_KEY: headers["Authorization"] = f"Bearer {_JINA_API_KEY}" return headers async def jina_fetch( url: str, query: str = "", target_selector: str = "", remove_selector: str = "", max_length: int = _MAX_CONTENT, ) -> dict[str, Any]: """ Legge qualsiasi URL tramite Jina Reader e restituisce contenuto markdown pulito. Parametri: url — URL da leggere (obbligatorio) query — se fornito, Jina filtra il contenuto per rilevanza (grounded reading) target_selector — CSS selector per isolare un elemento specifico remove_selector — CSS selector per rimuovere elementi indesiderati (nav, footer, ads) max_length — max caratteri restituiti (default 12000) Ideale per: • SPA (React/Vue/Angular/Next.js) • Pagine con paywall leggero • Articoli con molto boilerplate da rimuovere • Documentazione tecnica renderizzata JS """ if not url or not url.startswith(("http://", "https://")): return {"ok": False, "error": "url obbligatorio e deve iniziare con http:// o https://"} max_length = max(500, min(max_length, 20000)) # Costruisci URL Jina: https://r.jina.ai/{url} jina_url = f"https://r.jina.ai/{url}" hdrs = _jina_headers() if query: hdrs["X-With-Generated-Alt"] = "true" # Jina grounded reading: passa query nel header hdrs["X-Target-Selector"] = target_selector or "" if target_selector: hdrs["X-Target-Selector"] = target_selector if remove_selector: hdrs["X-Remove-Selector"] = remove_selector # Rimuovi header vuoti hdrs = {k: v for k, v in hdrs.items() if v} try: async with httpx.AsyncClient(timeout=_TIMEOUT, follow_redirects=True) as c: resp = await c.get(jina_url, headers=hdrs) if resp.status_code == 429: return { "ok": False, "error": "Jina rate limit raggiunto. Aggiungi JINA_API_KEY per limiti più alti (jina.ai).", } if resp.status_code != 200: return {"ok": False, "error": f"Jina HTTP {resp.status_code}: {resp.text[:200]}"} # Risposta JSON da Jina try: data = resp.json() jina_data = data.get("data", data) content = jina_data.get("content", "") or jina_data.get("text", "") or "" title = jina_data.get("title", "") jina_url_out = jina_data.get("url", url) description = jina_data.get("description", "") except Exception: # noqa: BLE001 # Fallback: risposta testo plain content = resp.text title = "" jina_url_out = url description = "" if not content: return {"ok": False, "error": "Jina ha restituito contenuto vuoto per questo URL"} # Tronca se troppo lungo truncated = len(content) > max_length content = content[:max_length] # Filtra per query se fornita (simple keyword boost — Jina lo fa server-side meglio) if query and query.lower() not in content.lower(): _logger.debug("[jina] query '%s' non trovata nel contenuto", query) return { "ok": True, "url": jina_url_out, "title": title, "description": description, "content": content, "char_count": len(content), "truncated": truncated, "via": "jina_reader", "has_api_key": bool(_JINA_API_KEY), } except httpx.TimeoutException: return { "ok": False, "error": f"Jina timeout ({_TIMEOUT:.0f}s) — la pagina è troppo pesante o non risponde", } except Exception as exc: # noqa: BLE001 _logger.warning("[jina_reader] %s: %s", url, exc) return {"ok": False, "error": str(exc)[:300]} # ── Tool registry descriptor ─────────────────────────────────────────────────── TOOL_DESCRIPTOR = { "name": "jina_fetch", "description": ( "Legge qualsiasi URL tramite Jina Reader restituendo testo markdown pulito. " "Funziona anche con SPA (React/Vue/Next.js/Angular) e siti a rendering JavaScript " "che il fetch normale non riesce a leggere. " "Parametri: url (obbligatorio), query (filtra per rilevanza), " "target_selector (CSS selector da isolare), remove_selector (CSS da rimuovere). " "Non richiede API key ma con JINA_API_KEY i limiti sono più alti." ), "parameters": { "type": "object", "properties": { "url": {"type": "string", "description": "URL da leggere (http/https)"}, "query": {"type": "string", "description": "Query per filtrare il contenuto per rilevanza"}, "target_selector": {"type": "string", "description": "CSS selector per isolare un elemento"}, "remove_selector": {"type": "string", "description": "CSS selector per rimuovere elementi (ads, nav, footer)"}, "max_length": {"type": "integer", "description": "Max caratteri restituiti (default 12000)"}, }, "required": ["url"], }, "fn": jina_fetch, }