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é.
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
)
|
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(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(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(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)
|