// Outils de recherche Web avancée pour le chat : Exa (recherche sémantique) et // Firecrawl (lecture d'une page en Markdown). Réservés aux modes qui autorisent // les connaissances hors matériel officiel. Les clés restent côté serveur. import type { ToolDef } from "../rag/tools.ts"; export function webToolsAvailable(): boolean { return !!process.env.EXA_API_KEY || !!process.env.FIRECRAWL_API_KEY; } export function webTools(): ToolDef[] { const tools: ToolDef[] = []; if (process.env.EXA_API_KEY) { tools.push({ type: "function", function: { name: "recherche_web", description: "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.", parameters: { type: "object", properties: { requete: { type: "string", minLength: 3, maxLength: 300, description: "Requête en français ou en anglais" }, nombre: { type: "integer", minimum: 1, maximum: 8, description: "Nombre de résultats (défaut 5)" }, }, required: ["requete"], }, }, }); } if (process.env.FIRECRAWL_API_KEY) { tools.push({ type: "function", function: { name: "lire_page_web", description: "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.", parameters: { type: "object", properties: { url: { type: "string", minLength: 10, maxLength: 500, description: "URL complète (https://…)" }, }, required: ["url"], }, }, }); } return tools; } export async function executeWebTool(name: string, rawArgs: string): Promise { let args: Record = {}; try { args = rawArgs ? JSON.parse(rawArgs) : {}; } catch { return "Erreur : arguments JSON invalides."; } if (name === "recherche_web") { const query = String(args.requete ?? "").trim(); if (query.length < 3) return "Erreur : requête trop courte."; const numResults = Math.min(8, Math.max(1, Number(args.nombre) || 5)); try { const res = await fetch("https://api.exa.ai/search", { method: "POST", headers: { "x-api-key": process.env.EXA_API_KEY!, "Content-Type": "application/json" }, body: JSON.stringify({ query, numResults, type: "auto", contents: { text: { maxCharacters: 1200 } }, }), signal: AbortSignal.timeout(20_000), }); if (!res.ok) return `Erreur Exa ${res.status} : ${(await res.text()).slice(0, 200)}`; const json = (await res.json()) as { results?: { title?: string; url: string; publishedDate?: string; text?: string }[] }; const results = json.results ?? []; if (!results.length) return `Aucun résultat Web pour « ${query} ».`; return results .map((r, i) => `${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)}` ) .join("\n\n"); } catch (e) { return "Erreur de recherche Web : " + (e instanceof Error ? e.message : String(e)); } } if (name === "lire_page_web") { const url = String(args.url ?? "").trim(); if (!/^https?:\/\/[^\s]+$/i.test(url)) return "Erreur : URL invalide."; try { const res = await fetch("https://api.firecrawl.dev/v1/scrape", { method: "POST", headers: { Authorization: `Bearer ${process.env.FIRECRAWL_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url, formats: ["markdown"], onlyMainContent: true }), signal: AbortSignal.timeout(35_000), }); if (!res.ok) return `Erreur Firecrawl ${res.status} : ${(await res.text()).slice(0, 200)}`; const json = (await res.json()) as { data?: { markdown?: string; metadata?: { title?: string } } }; const md = json.data?.markdown ?? ""; if (!md) return "Page vide ou illisible."; return `Page : ${json.data?.metadata?.title ?? url}\nURL : ${url}\n\n${md.slice(0, 14_000)}${md.length > 14_000 ? "\n\n[… contenu tronqué]" : ""}`; } catch (e) { return "Erreur de lecture Web : " + (e instanceof Error ? e.message : String(e)); } } return `Erreur : outil Web inconnu « ${name} ».`; } export const WEB_TOOL_NAMES = new Set(["recherche_web", "lire_page_web"]);