Skip to content

web_search

Web search tool — Tavily API with DuckDuckGo fallback.

20 septembre 2026 : « Qui est le président actuel du Canada ? » cherchait avec les réglages par défaut de ddgs — région us-en, aucune fraîcheur, pas de vertical actualités, cinq extraits sans date, moteur tiré au hasard (backend="auto"). La page Wikipédia de Trudeau sortait en tête, et le 9b ne pouvait pas « dater l'information » : aucune date ne lui arrivait. Les résultats portent désormais un numéro, un domaine et une date quand elle existe ; la région suit la config, la fraîcheur et le vertical actualités se demandent par paramètre, et le moteur qui a répondu est nommé.

Classes

WebSearchTool

WebSearchTool(
    api_key: str | None = None,
    max_results: int = 5,
    region: str | None = None,
)

Bases: BaseTool

Search the web via Tavily API.

Source code in src/diapason/tools/web_search.py
def __init__(
    self,
    api_key: str | None = None,
    max_results: int = 5,
    region: str | None = None,
):
    self._api_key = api_key or os.environ.get("TAVILY_API_KEY")
    self._max_results = max_results
    self._region = (
        region or os.environ.get("DIAPASON_SEARCH_REGION") or REGION_PAR_DEFAUT
    )

Functions:

url_canonique

url_canonique(url: str) -> str

La même page sous deux habits (schéma, utm, ordre des paramètres, fragment, barre finale) compte une fois.

Source code in src/diapason/tools/web_search.py
def url_canonique(url: str) -> str:
    """La même page sous deux habits (schéma, utm, ordre des paramètres,
    fragment, barre finale) compte une fois."""
    try:
        parts = urlsplit(url.strip())
    except ValueError:
        return url.strip()
    query = urlencode(
        sorted((k, v) for k, v in parse_qsl(parts.query) if not _UTM.match(k))
    )
    chemin = parts.path.rstrip("/") or "/"
    hote = parts.netloc.lower()
    # www.nhl.com et nhl.com sont la même page (revue du 21/09 : lue deux fois).
    if hote.startswith("www."):
        hote = hote[4:]
    return urlunsplit(("https", hote, chemin, query, ""))

date_locale

date_locale(brute: str) -> str

AAAA-MM-JJ dans le fuseau du poste ; ddgs rend de l'UTC.

Revue du 20/09 : « 1 hour ago » lu à 23:50 à Ottawa donnait le lendemain. Revue du 21/09 : la published_date de Tavily est en RFC 2822 (« Tue, 11 Mar 2025 17:00:00 GMT ») et devenait « Tue, 11 Ma » — une forme que l'analyseur de web_read ne lit pas vaut « pas de date ».

Source code in src/diapason/tools/web_search.py
def date_locale(brute: str) -> str:
    """AAAA-MM-JJ dans le fuseau du poste ; ddgs rend de l'UTC.

    Revue du 20/09 : « 1 hour ago » lu à 23:50 à Ottawa donnait le lendemain.
    Revue du 21/09 : la ``published_date`` de Tavily est en RFC 2822 (« Tue,
    11 Mar 2025 17:00:00 GMT ») et devenait « Tue, 11 Ma » — une forme que
    l'analyseur de web_read ne lit pas vaut « pas de date ».
    """
    texte = str(brute or "").strip()
    if not texte:
        return ""
    try:
        from datetime import datetime

        instant = datetime.fromisoformat(texte.replace("Z", "+00:00"))
    except ValueError:
        age, _reste = date_en_tete(texte + " - ")
        if age:
            return age
        from diapason.tools.web_read import date_iso

        return date_iso(texte)
    if instant.tzinfo is not None:
        instant = instant.astimezone()
    return instant.date().isoformat()

date_en_tete

date_en_tete(extrait: str) -> tuple[str, str]

(date AAAA-MM-JJ ou "", extrait sans son en-tête de date).

Un âge relatif (« 2 days ago ») se compte depuis aujourd'hui, au jour près ; un mois vaut trente jours et un an trois cent soixante-cinq : c'est l'âge que brave affiche, pas une date de publication, et il sert à mesurer la fraîcheur, pas à la citer au jour près.

Source code in src/diapason/tools/web_search.py
def date_en_tete(extrait: str) -> tuple[str, str]:
    """(date AAAA-MM-JJ ou "", extrait sans son en-tête de date).

    Un âge relatif (« 2 days ago ») se compte depuis aujourd'hui, au jour
    près ; un mois vaut trente jours et un an trois cent soixante-cinq : c'est
    l'âge que brave affiche, pas une date de publication, et il sert à
    mesurer la fraîcheur, pas à la citer au jour près.
    """
    texte = str(extrait or "")
    m = _AGE_RELATIF.match(texte)
    if m:
        from datetime import date, timedelta

        nombre, unite = (
            (m.group(1), m.group(2)) if m.group(1) else (m.group(3), m.group(4))
        )
        jours = int(nombre) * _JOURS_PAR_UNITE[_singulier(unite)]
        return (date.today() - timedelta(days=jours)).isoformat(), texte[m.end() :]
    m = _DATE_EN_TETE.match(texte)
    if m:
        from diapason.tools.web_read import date_iso

        iso = date_iso(m.group(1))
        if iso:
            return iso, texte[m.end() :]
    return "", texte

formater

formater(resultats: list[dict]) -> str

Numéroté [N] pour que le modèle cite ; date et domaine s'ils existent.

Source code in src/diapason/tools/web_search.py
def formater(resultats: list[dict]) -> str:
    """Numéroté [N] pour que le modèle cite ; date et domaine s'ils existent."""
    blocs = []
    for i, r in enumerate(resultats, 1):
        entete = f"[{i}] {r['title']}{r['source']}"
        if r.get("date"):
            entete += f" · {r['date']}"
        blocs.append(f"{entete}\nSource: {r['url']}\nExtrait: {r['snippet']}")
    return "\n\n".join(blocs)