/** * POST /api/chatbot-taff * Chatbot d'aiguillage — Carte 3 "Trouver du taf" * Lit plateformes-taff.json, appelle Mistral Small, retourne recommandations. */ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { checkRateLimitJson } from '~/server/utils/rateLimitJson' 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}}` 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.' }) } // Lire le JSON statique des plateformes let plateformes: PlateformeMinimal[] = [] try { const jsonPath = resolve(process.cwd(), 'public/data/plateformes-taff.json') const raw = JSON.parse(readFileSync(jsonPath, 'utf8')) plateformes = (raw.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, })) } catch (e) { throw createError({ statusCode: 500, statusMessage: 'Données plateformes introuvables.' }) } 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 = SYSTEM_PROMPT.replace('{{PLATEFORMES_JSON}}', JSON.stringify(context, null, 0)) const mistralApiKey = config.mistralApiKey as string if (!mistralApiKey) { throw createError({ statusCode: 500, statusMessage: 'Clé API Mistral manquante.' }) } let mistralRaw: string try { const res = await $fetch<{ choices: { message: { content: string } }[] }>( 'https://api.mistral.ai/v1/chat/completions', { method: 'POST', headers: { Authorization: `Bearer ${mistralApiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'mistral-small-latest', temperature: 0.3, max_tokens: 700, response_format: { type: 'json_object' }, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: question }, ], }), } ) mistralRaw = res.choices?.[0]?.message?.content ?? '{}' } 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 ?? '', })), } } catch { return { reponse_texte: "Je n'ai pas pu analyser ta demande.", plateformes_recommandees: [] } } })