SPB Git forge

spb/immbot-ai

Public
1commits 1branches 0releases
1.5 MBsize
maindefault branch
21 days agolast push
TypeScript 98.3% CSS 0.9% Shell 0.7%
4.7 KB · 113 lines typescript
Raw Blame History
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