- New corpus param defaults to 'both' (projet-centered crossing) - 3 preface modes for LightRAG query orientation - Smoke tested via SSH direct LightRAG VPS -- pondération validée V2 Phase 2.1 -- backend only, frontend toggle pending B.2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
120 lines
4.9 KiB
TypeScript
120 lines
4.9 KiB
TypeScript
import type { H3Event } from 'h3'
|
|
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
|
|
|
interface ChatbotPenseesRequest {
|
|
query: string
|
|
mode?: 'hybrid' | 'local' | 'global' | 'naive' | 'mix'
|
|
corpus?: 'pensees' | 'projets' | 'both'
|
|
filter_couche?: 'fond' | 'forme' | 'structure' | null
|
|
filter_ecole?: string | null
|
|
history?: Array<{ role: 'user' | 'assistant'; content: string }>
|
|
}
|
|
|
|
interface LightRAGQueryResponse {
|
|
response: string
|
|
}
|
|
|
|
const SYSTEM_PREFACE_PENSEES = `Tu es un agent du RAG Pensées Écologiques, infrastructure militante du collectif trans-former.fr.
|
|
Tu réponds en t'appuyant STRICTEMENT sur le corpus ingéré (auteurs FRACAS Bonpote : écosocialisme, éco-anarchisme, écoféminismes, écologies décoloniales, technocritique, pensées du vivant, décroissance...).
|
|
|
|
Règles :
|
|
- Cite les sources (auteur, livre) à chaque assertion importante.
|
|
- Si la question dépasse le corpus, dis-le clairement. Pas d'hallucination.
|
|
- Ton politique direct, pas de neutralité fade.
|
|
- Réponse en français, dense, sans délayage.
|
|
- Distingue les positions selon les écoles quand elles divergent.`
|
|
|
|
const SYSTEM_PREFACE_PROJETS = `Tu es un agent du RAG Projets de Jules Nény (architecte, collectif trans-former.fr).
|
|
Tu réponds STRICTEMENT à partir des documents projet (fichiers butte-pinson__*.md et autres projets archi de Jules).
|
|
N'utilise PAS le corpus FRACAS Pensées Écologiques pour répondre, sauf si l'usager te le demande explicitement.
|
|
|
|
Règles :
|
|
- Cite les sources (nom de projet, document) à chaque assertion importante.
|
|
- Si la question dépasse le corpus projet, dis-le clairement. Pas d'hallucination.
|
|
- Ton praticien réflexif : 1ère personne quand pertinent, narration située.
|
|
- Réponse en français, dense, sans délayage.`
|
|
|
|
const SYSTEM_PREFACE_BOTH = `Tu es un agent du RAG croisé Pensées x Projets de Jules Nény (architecte militant, collectif trans-former.fr).
|
|
CENTRE TA RÉPONSE sur les documents PROJETS (fichiers butte-pinson__*.md et autres projets archi).
|
|
Mobilise le corpus FRACAS Pensées (autres fichiers) UNIQUEMENT pour éclairer théoriquement les partis pris des projets, jamais l'inverse.
|
|
|
|
Pondération attendue : ~70% ancrage projet concret, ~30% éclairage théorique FRACAS.
|
|
|
|
Règles :
|
|
- Cite les sources (auteur ou nom de projet, document) à chaque assertion.
|
|
- Si un thème n'est pas couvert par les projets, dis-le clairement avant d'éventuellement étendre au corpus Pensées.
|
|
- Pas d'hallucination, pas d'extrapolation hors corpus.
|
|
- Ton praticien militant : direct, pas neutre, ancré dans la pratique architecturale.
|
|
- Réponse en français, dense, sans délayage.`
|
|
|
|
export default defineEventHandler(async (event: H3Event) => {
|
|
const config = useRuntimeConfig(event)
|
|
|
|
// 1. Rate limit (20 req/jour/IP, IP hashée RGPD)
|
|
const ip =
|
|
getHeader(event, 'x-forwarded-for')?.split(',')[0].trim() ||
|
|
event.node.req.socket?.remoteAddress ||
|
|
'0.0.0.0'
|
|
|
|
const allowed = checkRateLimitJson(ip, 'chatbot-pensees', 20)
|
|
if (!allowed) {
|
|
throw createError({ statusCode: 429, message: 'Limite de 20 questions par jour atteinte.' })
|
|
}
|
|
|
|
// 2. Body parse + validation
|
|
const body = await readBody<ChatbotPenseesRequest>(event)
|
|
if (!body?.query || body.query.trim().length < 3 || body.query.trim().length > 500) {
|
|
throw createError({ statusCode: 400, message: 'Query invalide (3-500 caractères).' })
|
|
}
|
|
|
|
const query = body.query.trim()
|
|
const mode = body.mode || 'hybrid'
|
|
const corpus = body.corpus || 'both'
|
|
const ragUrl = (config.ragPeUrl as string) || 'http://localhost:9621'
|
|
|
|
// Préface adaptative selon corpus demandé
|
|
const systemPreface =
|
|
corpus === 'pensees'
|
|
? SYSTEM_PREFACE_PENSEES
|
|
: corpus === 'projets'
|
|
? SYSTEM_PREFACE_PROJETS
|
|
: SYSTEM_PREFACE_BOTH
|
|
|
|
// 3. Health guard — LightRAG down = erreur claire, pas de fallback hallucinatoire
|
|
try {
|
|
await $fetch(`${ragUrl}/health`, { timeout: 5000 })
|
|
} catch {
|
|
throw createError({
|
|
statusCode: 503,
|
|
message: 'RAG indisponible pour l\'instant — réessaie dans quelques minutes.',
|
|
})
|
|
}
|
|
|
|
// 4. Call LightRAG VPS — préface système injectée dans la query
|
|
const ragQuery = `${systemPreface}\n\nQuestion : ${query}`
|
|
|
|
let ragResponse: LightRAGQueryResponse
|
|
try {
|
|
ragResponse = await $fetch<LightRAGQueryResponse>(`${ragUrl}/query`, {
|
|
method: 'POST',
|
|
body: { query: ragQuery, mode },
|
|
timeout: 90000,
|
|
})
|
|
} catch (e: any) {
|
|
const status = e?.response?.status
|
|
if (status === 429) {
|
|
throw createError({ statusCode: 429, message: 'RAG saturé — réessaie dans quelques instants.' })
|
|
}
|
|
throw createError({ statusCode: 504, message: 'RAG en cours de processing — réessaie dans quelques secondes.' })
|
|
}
|
|
|
|
// 5. Retour formaté
|
|
return {
|
|
response: ragResponse.response ?? '',
|
|
mode,
|
|
corpus,
|
|
filter: { couche: body.filter_couche ?? null, ecole: body.filter_ecole ?? null },
|
|
timestamp: new Date().toISOString(),
|
|
}
|
|
})
|