Spaces:
Configuration error
Configuration error
Pulka
sync: Gap A/B/C — P24 recovery chains + TOOL_SUBSTITUTE_MAP + selfLearning threshold (commit 0fa9aa40)
331397a verified | """ | |
| 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, | |
| } | |