feat(rag-pe): PRG-5 + PRG-6 frontend pensees ecologiques

- server/api/chatbot-pensees.post.ts : endpoint LightRAG VPS (hybrid mode, preface militante, rate limit 20/jour, health guard)
- nuxt.config.ts : ragPeUrl runtimeConfig (NUXT_RAG_PE_URL)
- public/data/auteurs-pensees.json : 18 auteurs FRACAS, 8 ecoles, theses, livres RAG
- components/CartePensees.vue : D3 force-directed (8 ecoles fixes + auteurs gravitants)
- components/FicheAuteur.vue : modal auteur (bio + theses + livres RAG + bouton RAG)
- components/ChatbotPensees.vue : overlay chatbot bottom-right (sources expansibles)
- pages/pensees-ecologiques.vue : page dedicee /pensees-ecologiques (toggle Familiale/Graphe)
- pages/agences.vue : 4e onglet "Pensees" (desktop + mobile) -> /pensees-ecologiques

Branche : feat/aep-rag-pensees-ecologiques
Checkpoint Jules requis avant merge main.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jules Neny
2026-05-11 15:07:42 +02:00
parent f5732bf336
commit 668ae5caff
8 changed files with 890 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
import type { H3Event } from 'h3'
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
interface ChatbotPenseesRequest {
query: string
mode?: 'hybrid' | 'local' | 'global' | 'naive' | 'mix'
filter_couche?: 'fond' | 'forme' | 'structure' | null
filter_ecole?: string | null
history?: Array<{ role: 'user' | 'assistant'; content: string }>
}
interface LightRAGQueryResponse {
response: string
}
const SYSTEM_PREFACE = `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.`
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 ragUrl = (config.ragPeUrl as string) || 'http://localhost:9621'
// 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 = `${SYSTEM_PREFACE}\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,
filter: { couche: body.filter_couche ?? null, ecole: body.filter_ecole ?? null },
timestamp: new Date().toISOString(),
}
})