feat(chatbot): mode profondeur côté serveur (rapide / approfondi) sur les 4 routes

Contrat unique `profondeur` ('rapide' | 'approfondi', défaut rapide) dans
utils/chatProfondeur.ts (libellés partagés). Les 3 routes Bifrost lisent
body.profondeur ?? body.mode, choisissent le tier (20b / 120b + replis), un
prompt système « réflexion » (Entraide, Réseaux, Taff) au même format JSON, et un
budget max_tokens adapté. Pensées lit body.profondeur (jamais body.mode, réservé à
LightRAG) : posture réflexion ajoutée au user_prompt + max_total_tokens 40000.
Modèle servi journalisé, et champ debug exposé uniquement en dev.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vcVGz8i4WpZm2dixKUiSD
This commit is contained in:
Jules Neny
2026-10-09 03:45:27 +02:00
co-authored by Claude Opus 5.5
parent 75d8440601
commit 80f69565aa
6 changed files with 198 additions and 19 deletions
+31 -4
View File
@@ -6,7 +6,7 @@
// @ts-ignore — JSON import résolu par Rollup
import reseauxData from '../../public/data/reseaux-bifurcation.json'
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
import { pickBifrostTier, bifrostContent, REASONING_MARGIN_TOKENS, type BifrostChatResponse } from '~/server/utils/bifrost'
import { pickBifrostTier, profondeurDuBody, bifrostMaxTokens, bifrostDebug, bifrostContent, REGLE_GUILLEMETS, type BifrostChatResponse } from '~/server/utils/bifrost'
interface Structure {
id: string
@@ -41,6 +41,29 @@ FORMAT :
STRUCTURES DISPONIBLES :
{{STRUCTURES_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 archi, au service des architectes engagés dans la transition écologique. Tu connais le réseau AEP (Architecture d'Écologie Politique) — une cartographie des collectifs, agences, initiatives et réseaux qui portent une vision alternative de l'architecture en France et en Europe.
POSTURE : avant de recommander des structures, tu questionnes le projet de l'usager. Tu reformules ce que tu comprends de son projet, tu nommes ce qui reste flou ou en tension (échelle, maîtrise d'ouvrage, filière de matériaux, rapport au territoire, posture professionnelle, moyens), tu poses 2 ou 3 questions précises qui l'aident à se situer, PUIS tu orientes vers les structures du contexte qui correspondent à ce qu'il a dit, en expliquant ce que chacune lui apporterait ou lui ferait choisir.
RÈGLES :
1. Ne cite QUE des structures présentes dans le contexte ci-dessous.
2. ${REGLE_GUILLEMETS}
3. Sois direct et engagé — c'est l'esprit AEP — mais n'affirme pas ce que tu ne sais pas de son projet : demande.
4. Réponse de 300 mots maximum, en français, en prose, 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, tension, questions, puis structures",
"fiches_recommandees": [
{ "id": "slug-id", "nom": "Nom structure", "explication": "Pourquoi en 1 phrase" }
]
}
STRUCTURES DISPONIBLES :
{{STRUCTURES_JSON}}`
function scoreStructure(s: Structure, keywords: string[]): number {
if (keywords.length === 0) return 1
const haystack = [s.nom, s.famille_principale, s.description_courte, s.description_longue, (s.hashtags ?? []).join(' '), s.ville, s.pays]
@@ -62,7 +85,8 @@ export default defineEventHandler(async (event) => {
const body = await readBody(event)
const question: string = (body?.question ?? '').trim()
if (!question || question.length < 5) throw createError({ statusCode: 400, message: 'Question trop courte.' })
const tier = pickBifrostTier(body?.mode)
const profondeur = profondeurDuBody(body)
const tier = pickBifrostTier(profondeur)
const structures: Structure[] = ((reseauxData as any).structures ?? [])
const keywords = extractKeywords(question)
@@ -82,13 +106,14 @@ export default defineEventHandler(async (event) => {
description: (s.description_courte ?? s.description_longue ?? '').slice(0, 200),
}))
const systemPrompt = SYSTEM_PROMPT.replace('{{STRUCTURES_JSON}}', JSON.stringify(context, null, 0))
const systemPrompt = (profondeur === 'approfondi' ? SYSTEM_PROMPT_REFLEXION : SYSTEM_PROMPT).replace('{{STRUCTURES_JSON}}', JSON.stringify(context, null, 0))
const bifrostUrl = config.bifrostUrl as string
const bifrostVk = config.bifrostVk as string
if (!bifrostVk) throw createError({ statusCode: 500, message: 'Clé Bifrost manquante.' })
let mistralRaw: string
let debugExtra: Record<string, unknown> = {}
try {
const res = await $fetch<BifrostChatResponse>(
`${bifrostUrl}/v1/chat/completions`,
@@ -99,7 +124,7 @@ export default defineEventHandler(async (event) => {
model: tier.model,
fallbacks: tier.fallbacks,
temperature: 0.3,
max_tokens: 700 + REASONING_MARGIN_TOKENS,
max_tokens: bifrostMaxTokens(700, profondeur),
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: systemPrompt },
@@ -109,6 +134,7 @@ export default defineEventHandler(async (event) => {
}
)
mistralRaw = bifrostContent(res, 'chatbot-reseaux')
debugExtra = bifrostDebug(res, 'chatbot-reseaux', profondeur)
} catch {
throw createError({ statusCode: 502, message: 'Erreur IA — réessaie dans quelques instants.' })
}
@@ -122,6 +148,7 @@ export default defineEventHandler(async (event) => {
nom: r.nom ?? structures.find(s => s.id === r.id)?.nom ?? r.id,
explication: r.explication ?? '',
})),
...debugExtra,
}
} catch {
return { reponse_texte: "Je n'ai pas pu analyser ta demande.", fiches_recommandees: [] }