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,
}