/** * POST /api/chatbot-taff * Chatbot d'aiguillage — Carte 3 "Trouver du taf" * Lit plateformes-taff.json, appelle un LLM via Bifrost (server/utils/bifrost.ts), retourne recommandations. */ // @ts-ignore — JSON import résolu par Vite/Rollup import taffData from '../../public/data/plateformes-taff.json' import { checkRateLimitJson } from '~/server/utils/rateLimitJson' import { pickBifrostTier, profondeurDuBody, bifrostMaxTokens, bifrostDebug, bifrostContent, REGLE_GUILLEMETS, type BifrostChatResponse } from '~/server/utils/bifrost' interface PlateformeMinimal { id: string nom: string type: string description_courte: string scoring: { remuneration: string | null transparence: string | null pratiques: string | null ecologie: string | null matching: string | null tag_global: string justification_tag: string } secteurs_servis: string[] cout_entree: string } const SYSTEM_PROMPT = `Tu es un conseiller expert au service des architectes indépendants français. Tu connais toutes les plateformes de mise en relation architecte↔particulier et les agrégateurs d'appels d'offres publics référencés par AEP (Architecture d'Écologie Politique). Ton rôle : aider l'architecte à choisir LA ou LES plateformes adaptées à sa situation, en t'appuyant exclusivement sur les données ci-dessous. RÈGLES : 1. Ne recommande QUE des plateformes présentes dans le contexte ci-dessous. 2. Sois direct et opinionné — c'est ça la valeur d'AEP. 3. Si une plateforme est ❌ "À éviter", signale-le clairement. 4. Réponse max 250 mots, en français, ton conseiller pair. 5. Retourne UNIQUEMENT un JSON valide, sans texte avant ou après. FORMAT : { "reponse_texte": "Ta réponse en prose, max 250 mots", "plateformes_recommandees": [ { "id": "slug-kebab", "nom": "Nom plateforme", "raison": "Pourquoi cette plateforme en 1 phrase" } ] } PLATEFORMES DISPONIBLES : {{PLATEFORMES_JSON}}` // Prompt « réflexion » (mode approfondi, v1 — prompts définitifs par onglet : B7/B9). Même format JSON. const SYSTEM_PROMPT_REFLEXION = `Tu es un conseiller pour les architectes indépendants français qui cherchent des missions. Tu connais toutes les plateformes de mise en relation architecte↔particulier et les agrégateurs d'appels d'offres publics référencés par AEP (Architecture d'Écologie Politique). POSTURE : avant de recommander une plateforme, tu aides l'architecte à clarifier ce qu'il cherche et qui il est : type de missions visées (particuliers, public, réno, neuf), volume et rythme, statut et assurances, ce qu'il accepte de céder (commission, abonnement, temps de réponse), ses valeurs. Tu reformules son profil tel que tu le comprends, tu nommes un arbitrage que sa demande laisse ouvert, tu poses 2 ou 3 questions qui l'aident à se situer, PUIS tu orientes vers LA ou LES plateformes du contexte qui correspondent, avec leurs limites. RÈGLES : 1. Ne recommande QUE des plateformes présentes dans le contexte ci-dessous. Si l'une est « À éviter », signale-le clairement. 2. ${REGLE_GUILLEMETS} 3. Sois direct et opinionné — c'est la valeur d'AEP — mais ne présume pas de ce que tu ignores de son profil : demande. 4. Réponse de 300 mots maximum, en français, en prose, ton conseiller pair, tutoiement. 5. Retourne UNIQUEMENT un JSON valide, sans texte avant ou après. FORMAT : { "reponse_texte": "Ta réponse en prose, max 300 mots : reformulation, arbitrage, questions, puis plateformes", "plateformes_recommandees": [ { "id": "slug-kebab", "nom": "Nom plateforme", "raison": "Pourquoi cette plateforme en 1 phrase" } ] } PLATEFORMES DISPONIBLES : {{PLATEFORMES_JSON}}` export default defineEventHandler(async (event) => { const config = useRuntimeConfig() const ip = getHeader(event, 'x-forwarded-for')?.split(',')[0].trim() || event.node.req.socket?.remoteAddress || '0.0.0.0' const allowed = checkRateLimitJson(ip, 'chatbot-taff', 20) if (!allowed) { throw createError({ statusCode: 429, statusMessage: 'Limite de 20 questions par jour atteinte.' }) } const body = await readBody(event) const question: string = (body?.question ?? '').trim() if (!question || question.length < 5) { throw createError({ statusCode: 400, statusMessage: 'Question trop courte.' }) } const profondeur = profondeurDuBody(body) const tier = pickBifrostTier(profondeur) // Données bundlées statiquement à la compilation (import JSON) const plateformes: PlateformeMinimal[] = ((taffData as any).plateformes ?? []).map((p: any) => ({ id: p.id, nom: p.nom, type: p.type, description_courte: p.description_courte, scoring: p.scoring, secteurs_servis: p.secteurs_servis, cout_entree: p.cout_entree, })) const context = plateformes.map(p => ({ id: p.id, nom: p.nom, type: p.type === 'b2c-mise-en-relation' ? 'B2C' : 'Appels offres publics', tag: p.scoring.tag_global, resume: p.description_courte, secteurs: p.secteurs_servis.join(', '), cout: p.cout_entree, justification: p.scoring.justification_tag, })) const systemPrompt = (profondeur === 'approfondi' ? SYSTEM_PROMPT_REFLEXION : SYSTEM_PROMPT).replace('{{PLATEFORMES_JSON}}', JSON.stringify(context, null, 0)) const bifrostUrl = config.bifrostUrl as string const bifrostVk = config.bifrostVk as string if (!bifrostVk) { throw createError({ statusCode: 500, statusMessage: 'Clé Bifrost manquante.' }) } let mistralRaw: string let debugExtra: Record = {} try { const res = await $fetch( `${bifrostUrl}/v1/chat/completions`, { method: 'POST', headers: { 'x-bf-vk': bifrostVk, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: tier.model, fallbacks: tier.fallbacks, temperature: 0.3, max_tokens: bifrostMaxTokens(700, profondeur), response_format: { type: 'json_object' }, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: question }, ], }), } ) mistralRaw = bifrostContent(res, 'chatbot-taff') debugExtra = bifrostDebug(res, 'chatbot-taff', profondeur) } catch { throw createError({ statusCode: 502, statusMessage: 'Erreur IA — réessaie dans quelques instants.' }) } try { const parsed = JSON.parse(mistralRaw) return { reponse_texte: parsed.reponse_texte ?? "Je n'ai pas pu analyser ta demande.", plateformes_recommandees: (parsed.plateformes_recommandees ?? []).map((r: any) => ({ id: r.id, nom: r.nom ?? plateformes.find(p => p.id === r.id)?.nom ?? r.id, raison: r.raison ?? '', })), ...debugExtra, } } catch { return { reponse_texte: "Je n'ai pas pu analyser ta demande.", plateformes_recommandees: [] } } })