Remplace readFileSync (chemin instable Nitro) par $fetch sur le serveur lui-même qui sert déjà plateformes-taff.json en statique. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
144 lines
4.8 KiB
TypeScript
144 lines
4.8 KiB
TypeScript
/**
|
|
* POST /api/chatbot-taff
|
|
* Chatbot d'aiguillage — Carte 3 "Trouver du taf"
|
|
* Lit plateformes-taff.json, appelle Mistral Small, retourne recommandations.
|
|
*/
|
|
|
|
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 (servi en public par Nitro)
|
|
let plateformes: PlateformeMinimal[] = []
|
|
try {
|
|
const origin = getRequestURL(event).origin
|
|
const json = await $fetch<{ plateformes: PlateformeMinimal[] }>('/data/plateformes-taff.json', {
|
|
baseURL: origin,
|
|
})
|
|
plateformes = (json.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: [] }
|
|
}
|
|
})
|