Terminal / tools /jina_reader.py
Pulka
sync: Gap A/B/C — P24 recovery chains + TOOL_SUBSTITUTE_MAP + selfLearning threshold (commit 0fa9aa40)
331397a verified
Raw
History Blame Contribute Delete
6.37 kB
"""
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,
}