TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1// Outils de recherche Web avancée pour le chat : Exa (recherche sémantique) et2// Firecrawl (lecture d'une page en Markdown). Réservés aux modes qui autorisent3// les connaissances hors matériel officiel. Les clés restent côté serveur.45import type { ToolDef } from "../rag/tools.ts";67export function webToolsAvailable(): boolean {8 return !!process.env.EXA_API_KEY || !!process.env.FIRECRAWL_API_KEY;9}1011export function webTools(): ToolDef[] {12 const tools: ToolDef[] = [];13 if (process.env.EXA_API_KEY) {14 tools.push({15 type: "function",16 function: {17 name: "recherche_web",18 description:19 "Recherche Web avancée (Exa) : retourne les pages les plus pertinentes avec titre, URL et extrait. À utiliser pour l'actualité du marché immobilier québécois, les taux, les sources réglementaires (OEAQ, LFM, SCHL) ou toute information hors du matériel de cours. Cite toujours l'URL de ce que tu utilises.",20 parameters: {21 type: "object",22 properties: {23 requete: { type: "string", minLength: 3, maxLength: 300, description: "Requête en français ou en anglais" },24 nombre: { type: "integer", minimum: 1, maximum: 8, description: "Nombre de résultats (défaut 5)" },25 },26 required: ["requete"],27 },28 },29 });30 }31 if (process.env.FIRECRAWL_API_KEY) {32 tools.push({33 type: "function",34 function: {35 name: "lire_page_web",36 description:37 "Lit une page Web et retourne son contenu en Markdown propre (Firecrawl). À utiliser après recherche_web pour approfondir une source précise. Cite l'URL.",38 parameters: {39 type: "object",40 properties: {41 url: { type: "string", minLength: 10, maxLength: 500, description: "URL complète (https://…)" },42 },43 required: ["url"],44 },45 },46 });47 }48 return tools;49}5051export async function executeWebTool(name: string, rawArgs: string): Promise<string> {52 let args: Record<string, unknown> = {};53 try {54 args = rawArgs ? JSON.parse(rawArgs) : {};55 } catch {56 return "Erreur : arguments JSON invalides.";57 }5859 if (name === "recherche_web") {60 const query = String(args.requete ?? "").trim();61 if (query.length < 3) return "Erreur : requête trop courte.";62 const numResults = Math.min(8, Math.max(1, Number(args.nombre) || 5));63 try {64 const res = await fetch("https://api.exa.ai/search", {65 method: "POST",66 headers: { "x-api-key": process.env.EXA_API_KEY!, "Content-Type": "application/json" },67 body: JSON.stringify({68 query,69 numResults,70 type: "auto",71 contents: { text: { maxCharacters: 1200 } },72 }),73 signal: AbortSignal.timeout(20_000),74 });75 if (!res.ok) return `Erreur Exa ${res.status} : ${(await res.text()).slice(0, 200)}`;76 const json = (await res.json()) as { results?: { title?: string; url: string; publishedDate?: string; text?: string }[] };77 const results = json.results ?? [];78 if (!results.length) return `Aucun résultat Web pour « ${query} ».`;79 return results80 .map((r, i) =>81 `${i + 1}. ${r.title ?? "(sans titre)"}\nURL : ${r.url}${r.publishedDate ? `\nDate : ${r.publishedDate.slice(0, 10)}` : ""}\nExtrait : ${(r.text ?? "").replace(/\s+/g, " ").slice(0, 1000)}`82 )83 .join("\n\n");84 } catch (e) {85 return "Erreur de recherche Web : " + (e instanceof Error ? e.message : String(e));86 }87 }8889 if (name === "lire_page_web") {90 const url = String(args.url ?? "").trim();91 if (!/^https?:\/\/[^\s]+$/i.test(url)) return "Erreur : URL invalide.";92 try {93 const res = await fetch("https://api.firecrawl.dev/v1/scrape", {94 method: "POST",95 headers: { Authorization: `Bearer ${process.env.FIRECRAWL_API_KEY}`, "Content-Type": "application/json" },96 body: JSON.stringify({ url, formats: ["markdown"], onlyMainContent: true }),97 signal: AbortSignal.timeout(35_000),98 });99 if (!res.ok) return `Erreur Firecrawl ${res.status} : ${(await res.text()).slice(0, 200)}`;100 const json = (await res.json()) as { data?: { markdown?: string; metadata?: { title?: string } } };101 const md = json.data?.markdown ?? "";102 if (!md) return "Page vide ou illisible.";103 return `Page : ${json.data?.metadata?.title ?? url}\nURL : ${url}\n\n${md.slice(0, 14_000)}${md.length > 14_000 ? "\n\n[… contenu tronqué]" : ""}`;104 } catch (e) {105 return "Erreur de lecture Web : " + (e instanceof Error ? e.message : String(e));106 }107 }108109 return `Erreur : outil Web inconnu « ${name} ».`;110}111112export const WEB_TOOL_NAMES = new Set(["recherche_web", "lire_page_web"]);113