Spaces:
Running
Running
File size: 6,372 Bytes
28a08e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | """
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,
}
|