Compare commits
2 Commits
snapshot-w
...
feat/bifro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d06c09a476 | ||
|
|
4291fe7529 |
188
PILOTE.md
188
PILOTE.md
@@ -1,188 +0,0 @@
|
|||||||
---
|
|
||||||
type: pilote
|
|
||||||
projet: AEP — nav-carte (aep.trans-former.fr)
|
|
||||||
casquette: Stack technique
|
|
||||||
pilote: ATIS Dev
|
|
||||||
created: 2026-06-23
|
|
||||||
updated: 2026-06-28
|
|
||||||
---
|
|
||||||
|
|
||||||
# PILOTE — AEP / nav-carte
|
|
||||||
|
|
||||||
> Doc de pilotage vivant du site **aep.trans-former.fr**. Source de vérité opérationnelle : état déployé, chantiers, décisions, dette. Le journal de dev détaillé reste dans `JOURNAL-V2.md`. Ce doc est l'index de pilotage, pas le journal.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Ce que c'est
|
|
||||||
|
|
||||||
- **Site** : `https://aep.trans-former.fr` — cartographie politique AEP (Architecture En Plus / écosystème militant).
|
|
||||||
- **Stack** : Nuxt 3 (Nitro SSR) + Vue + Leaflet + D3. Backend RAG = LightRAG (Docker).
|
|
||||||
- **Repo** : `1 PROJETS/TECH .../nav-carte/` — remote Gitea `git.trans-former.fr/jules/nav-carte.git`.
|
|
||||||
- **Pilote** : ATIS Dev (ne code pas, dispatche des agents Sonnet).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Architecture déployée (VÉRIFIÉE 2026-06-23) ⚠ lire avant toute session
|
|
||||||
|
|
||||||
Source fréquente de confusion — état réel constaté sur le VPS :
|
|
||||||
|
|
||||||
```
|
|
||||||
aep.trans-former.fr
|
|
||||||
│ Caddy : reverse_proxy localhost:3333
|
|
||||||
▼
|
|
||||||
aep.service ← LE service réel (systemd, active/running, Nitro Node)
|
|
||||||
├── App dir : /opt/aep
|
|
||||||
├── ExecStart : node /opt/aep/.output/server/index.mjs
|
|
||||||
├── EnvironmentFile: /opt/aep/.env (contient RAG_PE_URL, MISTRAL_API_KEY, etc.)
|
|
||||||
└── Port : 3333
|
|
||||||
|
|
||||||
LightRAG PE : localhost:9621 (Docker, health 200 ✓) ← backend du chatbot RAG
|
|
||||||
|
|
||||||
PIÈGES / fausses pistes :
|
|
||||||
✗ nav-carte.service → inactive/dead (ANCIEN nom, abandonné — ne pas chercher de ce côté)
|
|
||||||
✗ nav-worker.service → FAILED (worker enrichissement IA, mort — à statuer)
|
|
||||||
✗ /opt/nav-carte → n'existe pas (le code n'est PAS sur le VPS en git)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Mécanisme de déploiement = MANUEL, scp/tar (PAS de git pull sur le VPS) :**
|
|
||||||
|
|
||||||
```
|
|
||||||
1. npm run build (local → génère .output/)
|
|
||||||
2. scp/rsync .output/ → vps-hetzner:/opt/aep/.output/
|
|
||||||
3. ssh vps-hetzner "systemctl restart aep.service"
|
|
||||||
4. vérifier : curl -I https://aep.trans-former.fr/<route>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Disque VPS : 91 % (3,5 G libres) — à surveiller, pas bloquant.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. État git du repo (2026-06-23)
|
|
||||||
|
|
||||||
- `main` == `gitea/main` == `cd8fe9e2` (0 ahead / 0 behind, parfaitement sync).
|
|
||||||
- **Le commit déployé sur le VPS est PÉRIMÉ** (antérieur à `cd8fe9e2`) → `/outils`, `/media`, routes chatbot = 404 live alors que le code existe sur `main`.
|
|
||||||
- **Piège repos imbriqués** : `nav-carte/` est un sous-repo dans le vault. Le vault parent n'est PLUS un repo git (chirurgie 2026-05-07). Travailler git uniquement au niveau `nav-carte/`.
|
|
||||||
|
|
||||||
### Travail local non committé au 2026-06-23
|
|
||||||
|
|
||||||
| Fichier | État | Destination |
|
|
||||||
|---------|------|-------------|
|
|
||||||
| `components/ChatbotPensees.vue` | untracked | **P0/P1 — à committer + déployer** (chatbot RAG) |
|
|
||||||
| `server/api/chatbot-pensees.post.ts` | untracked | **P0/P1 — à committer + déployer** (route chatbot, appelle LightRAG :9621) |
|
|
||||||
| `nuxt.config.ts` | modifié (+1) | **P0/P1** — ajoute `runtimeConfig.ragPeUrl` (requis par la route chatbot) |
|
|
||||||
| `components/MediaTabVisuel.vue` | modifié (CSS) | **P0/P1** — layout toolbar/carte/chatbot |
|
|
||||||
| `pages/a-propos.vue` | modifié (+64) | **P0/P1** — contenu À propos |
|
|
||||||
| `pages/contribuer-job.vue` | untracked (stub) | **P2** — laisser untracked (feature Proposer) |
|
|
||||||
| `pages/contribuer-reseau.vue` | untracked (stub) | **P2** — laisser untracked |
|
|
||||||
| `.firecrawl/` | untracked | junk — gitignore |
|
|
||||||
| `pages/index-v1-backup.vue.tmp.*` | untracked | junk — supprimer |
|
|
||||||
|
|
||||||
### Branches dispersées (dette — voir §6)
|
|
||||||
|
|
||||||
```
|
|
||||||
main ← prod
|
|
||||||
feat/outils-v1 / -build / -media ← travail Outils (en partie déjà sur main)
|
|
||||||
feat/aep-v1.1-nav-carte ← contient a6fff9a9 "PA3 bouton Proposer contextuel"
|
|
||||||
feat/aep-pratiques-regeneratives ← contient d10586c4 "page /proposer-pratique" + form
|
|
||||||
feat/aep-rag-pensees-ecologiques ← travail RAG/chatbot
|
|
||||||
feat/aep-taff-v1 / feat/aep-v2-restore / feat/codev-mvp / wip-stash-v2-onglet2-20260506
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Chantiers (état au 2026-06-23)
|
|
||||||
|
|
||||||
| Prio | Chantier | Diagnostic | Statut | Nature |
|
|
||||||
|------|----------|-----------|--------|--------|
|
|
||||||
| **P0** | Onglet Outils + page /media + redirect /rag manquants live | Build VPS périmé (code committé, pas déployé) | ✅ **Fait 2026-06-23** (commit `f3ab28bd` déployé, routes 200, /rag→301) | Mécanique : build + deploy |
|
|
||||||
| **P1** | Chatbot RAG cassé | Route + composant non committés ; LightRAG PE :9621 = UP | ✅ **Fait 2026-06-23** (route déployée, testé public OK, réponses sourcées) | Mixte : commit + deploy + test |
|
|
||||||
| **P2** | Interface Proposer 5 onglets (Écosystème/Réseau/Jobs/Outils/Références RAG) | Feature dispersée sur branches feature jamais mergées ; `main` n'a qu'1 form (`contribuer.vue`) + 2 stubs untracked | ✅ **Fait 2026-06-23** (commit `c860dabd`, déployé, testé 5/5 types 201) | 4 types → NocoDB `orgas` + `submission_type` ; Réfs → `ressources_references` ; `/contribuer` → 301 → `/proposer` |
|
|
||||||
| **P3** | Google Search Console / indexation | Non diagnostiqué | Reporté (Jules) | Voir `pilotage-indexation-seo.md` (dossier TECH) |
|
|
||||||
| **dette** | Organisation docs/branches/services | Multi-build folders, 10+ Caddyfile.bak, services morts | À cadrer | Voir §6 |
|
|
||||||
|
|
||||||
### Mapping bugs Jules → vraie cause
|
|
||||||
|
|
||||||
```
|
|
||||||
"mauvais commit, RAG/Outils pas up" → FAUX : commit OK. Build VPS périmé. → P0
|
|
||||||
"bouton Proposer 1 carte / 4" → feature sur branches non mergées. → P2
|
|
||||||
"chatbot slug rag/media KO" → redirect /rag→/media en code (pas déployé) → P0/P1
|
|
||||||
+ composant chatbot non committé
|
|
||||||
+ backend LightRAG PE :9621 = UP ✓
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Décisions techniques runtime
|
|
||||||
|
|
||||||
| # | Décision | Pourquoi |
|
|
||||||
|---|----------|----------|
|
|
||||||
| τ.1 | P0+P1 (deploy + chatbot) cette session ; P2 (Proposer) session dédiée | P2 = archéologie multi-branches, dilue le fix de deploy |
|
|
||||||
| τ.2 | Stubs `contribuer-*` laissés untracked au deploy P0 | Ne pas exposer de formulaires inachevés en prod |
|
|
||||||
| τ.3 | Commit P0 = chatbot route + composant + nuxt.config + MediaTabVisuel + a-propos uniquement | Périmètre minimal du « remettre up » ; pas de WIP Proposer |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Dette d'organisation (constatée 2026-06-23)
|
|
||||||
|
|
||||||
Le système tech est éclaté. Symptômes :
|
|
||||||
- Dossier projet `TECH - infra VPS, website pro, RAG/` mélange **6+ projets** (nav-carte/AEP, Website Pro/trans-former.fr, RAG Militant, Simulateur autonomie V2, codev, vps-kit…).
|
|
||||||
- Multiples dossiers `*-build` (cascades méga-dispatch passées : `aep-communaute-build`, `codev-build`, `astro-site-cerveau`).
|
|
||||||
- 10+ `Caddyfile.bak.*` sur le VPS, services systemd morts (`nav-carte.service`, `nav-worker.service`).
|
|
||||||
- Branches feature jamais mergées ni nettoyées.
|
|
||||||
- Plusieurs `.md` de pilotage épars à la racine TECH (`next-steps`, `stack-opencode-v2`, `pilotage-indexation-seo`…).
|
|
||||||
|
|
||||||
→ **Proposition de rangement : voir le plan dans le message de session du 2026-06-23 / à formaliser en note dédiée.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Journal & refs
|
|
||||||
|
|
||||||
- Journal dev détaillé : `JOURNAL-V2.md` (même dossier).
|
|
||||||
- Mémoire pilote : `2 CASQUETTES/Stack technique/ATIS-Dev.md`.
|
|
||||||
- Prompt P2 : `0 INBOX/PROMPTS/aep-P2-proposer-4-onglets.md`.
|
|
||||||
- SEO/indexation : `1 PROJETS/TECH .../pilotage-indexation-seo.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Log de pilotage
|
|
||||||
|
|
||||||
### 2026-06-23 — Session diagnostic (ATIS Dev / Opus)
|
|
||||||
**Accompli :**
|
|
||||||
- Diagnostic complet : renversé l'hypothèse « mauvais commit » → build VPS périmé + travail local non committé.
|
|
||||||
- Cartographié l'archi de service réelle (`aep.service`, pas `nav-carte.service`).
|
|
||||||
- Confirmé LightRAG PE :9621 UP, deploy = scp manuel vers `/opt/aep/.output`.
|
|
||||||
- Créé ce doc pilote + prompt P2.
|
|
||||||
|
|
||||||
**Accompli (suite) :**
|
|
||||||
- ✅ P0+P1 déployés : commit `f3ab28bd` build+push (agent Sonnet), deploy prod (tar+scp → `/opt/aep/.output`, restart `aep.service`). Vérifié : `/outils`, `/media`, `/contribuer`, `/trouver-du-taf` = 200 ; `/rag` → 301 ; chatbot RAG public OK (réponses sourcées via LightRAG :9621).
|
|
||||||
- Prompts P2 + hygiène TECH écrits avec préflight git anti-superposition + clause supervision pilote.
|
|
||||||
|
|
||||||
**Prochaines étapes :**
|
|
||||||
- [x] P2 : Proposer 5 onglets — fait 2026-06-23 (commit c860dabd, deploye, teste 5/5 types 201).
|
|
||||||
- [x] Hygiene TECH : execute 2026-06-23 (agent Sonnet). Voir recap dans `_infra/TECH.md`.
|
|
||||||
- [ ] **Batch 3 (prochain chantier)** : bouton selecteur 3 cartes + popup 5 axes Jobs + GraphView Carte 1. Prompt : `REPRISE-aep-carto-fix-batch3.md`. Session dediee.
|
|
||||||
- [ ] AEP nav-carte — verifier chatbots EN PROD : `/` = chatbot Entraide / `/agences` = chatbot Reseaux AEP distincts.
|
|
||||||
- [ ] P3 : Google Search Console (reporte).
|
|
||||||
|
|
||||||
### 2026-06-23 — Session hygiène TECH (AXE 1+2+3) + fix chatbot (Agent Sonnet)
|
|
||||||
**Accompli :**
|
|
||||||
- **AXE 1 VPS** : docker prune (156M), décommission listmonk (conteneur + image + 2 volumes + 2 routes Caddy), décommission heyform (volume 528M + compose), décommission uptime (volume 88M + route status), `/opt/migration` supprimé (252M, aucune réf active), nav-worker + nav-carte services nettoyés, Caddyfile.bak réduit de 17→4 fichiers. Disque 91%→87% (+~1.3 GB).
|
|
||||||
- **AXE 2 Docs** : `_archive-build/` créé (3 dossiers `*-build` déplacés), `_infra/TECH.md` créé (inventaire VPS complet), `INDEX.md` créé, 3 squelettes `PILOTE.md` (Website Pro, RAG Militant, Simulateur autonomie V2). 4 `.md` épars routés.
|
|
||||||
- **AXE 3 Git** : 4 branches à 0 commit supprimées (`feat/outils-v1`, `-build`, `-media`, `feat/codev-mvp`). `feat/outils-v1-build` worktree retiré.
|
|
||||||
- **Fix chatbot régresse** : `auteurs-pensees.json` (171 auteurs) restauré en prod (scp), versionné sur `main` (commit `04b02351`), push gitea. Worktree `nav-carte-media` retiré, les 4 fichiers untrackés sauvés sur `feat/outils-v1-media` push gitea (branche préservée).
|
|
||||||
|
|
||||||
### 2026-06-23 — Session P2 : Proposer 5 onglets (ATIS Dev / Sonnet)
|
|
||||||
**Accompli :**
|
|
||||||
- Interface Proposer avec 5 tabs (Écosystème/Réseau/Jobs/Outils/Références RAG)
|
|
||||||
- 4 types (ecosysteme/reseau/job/outil) → NocoDB `orgas` table avec `submission_type`
|
|
||||||
- Références → table dédiée NocoDB `ressources_references`
|
|
||||||
- `/contribuer` → 301 redirect vers `/proposer`
|
|
||||||
- 6 corrections pilote appliquées
|
|
||||||
- Build + deploy VPS (scp .output/ + systemctl restart aep)
|
|
||||||
- Tests : 1 par type (4×201), 1 référence (201), test records supprimés (404)
|
|
||||||
- Routes vérifiées : /proposer (200), /contribuer (301), /api/submit (201), /api/submit/reference (201)
|
|
||||||
- ✅ Tout est bon, GO pilote obtenu, P2 terminé
|
|
||||||
|
|
||||||
**Prochaine étape :**
|
|
||||||
- [ ] Push `main` → `gitea/main` (P2 + fixes chatbot non encore poussés)
|
|
||||||
- [ ] P3 : Google Search Console (reporté)
|
|
||||||
@@ -16,6 +16,8 @@ export default defineNuxtConfig({
|
|||||||
commentTableId: process.env.COMMENT_TABLE_ID || process.env.AVIS_TABLE_ID,
|
commentTableId: process.env.COMMENT_TABLE_ID || process.env.AVIS_TABLE_ID,
|
||||||
statsTableId: process.env.STATS_TABLE_ID || 'mbbq7n47ixy19mc',
|
statsTableId: process.env.STATS_TABLE_ID || 'mbbq7n47ixy19mc',
|
||||||
mistralApiKey: process.env.MISTRAL_API_KEY,
|
mistralApiKey: process.env.MISTRAL_API_KEY,
|
||||||
|
bifrostUrl: process.env.BIFROST_URL || 'http://127.0.0.1:8080',
|
||||||
|
bifrostVk: process.env.BIFROST_VK,
|
||||||
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
|
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
|
||||||
resendApiKey: process.env.RESEND_API_KEY,
|
resendApiKey: process.env.RESEND_API_KEY,
|
||||||
emailJules: process.env.EMAIL_JULES || 'jules@trans-former.fr',
|
emailJules: process.env.EMAIL_JULES || 'jules@trans-former.fr',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
// @ts-ignore — JSON import résolu par Rollup
|
// @ts-ignore — JSON import résolu par Rollup
|
||||||
import reseauxData from '../../public/data/reseaux-bifurcation.json'
|
import reseauxData from '../../public/data/reseaux-bifurcation.json'
|
||||||
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
||||||
|
import { pickBifrostTier, type BifrostChatResponse } from '~/server/utils/bifrost'
|
||||||
|
|
||||||
interface Structure {
|
interface Structure {
|
||||||
id: string
|
id: string
|
||||||
@@ -61,6 +62,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
const question: string = (body?.question ?? '').trim()
|
const question: string = (body?.question ?? '').trim()
|
||||||
if (!question || question.length < 5) throw createError({ statusCode: 400, message: 'Question trop courte.' })
|
if (!question || question.length < 5) throw createError({ statusCode: 400, message: 'Question trop courte.' })
|
||||||
|
const tier = pickBifrostTier(body?.mode)
|
||||||
|
|
||||||
const structures: Structure[] = ((reseauxData as any).structures ?? [])
|
const structures: Structure[] = ((reseauxData as any).structures ?? [])
|
||||||
const keywords = extractKeywords(question)
|
const keywords = extractKeywords(question)
|
||||||
@@ -82,18 +84,20 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const systemPrompt = SYSTEM_PROMPT.replace('{{STRUCTURES_JSON}}', JSON.stringify(context, null, 0))
|
const systemPrompt = SYSTEM_PROMPT.replace('{{STRUCTURES_JSON}}', JSON.stringify(context, null, 0))
|
||||||
|
|
||||||
const mistralApiKey = config.mistralApiKey as string
|
const bifrostUrl = config.bifrostUrl as string
|
||||||
if (!mistralApiKey) throw createError({ statusCode: 500, message: 'Clé API Mistral manquante.' })
|
const bifrostVk = config.bifrostVk as string
|
||||||
|
if (!bifrostVk) throw createError({ statusCode: 500, message: 'Clé Bifrost manquante.' })
|
||||||
|
|
||||||
let mistralRaw: string
|
let mistralRaw: string
|
||||||
try {
|
try {
|
||||||
const res = await $fetch<{ choices: { message: { content: string } }[] }>(
|
const res = await $fetch<BifrostChatResponse>(
|
||||||
'https://api.mistral.ai/v1/chat/completions',
|
`${bifrostUrl}/v1/chat/completions`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: `Bearer ${mistralApiKey}`, 'Content-Type': 'application/json' },
|
headers: { 'x-bf-vk': bifrostVk, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'mistral-small-latest',
|
model: tier.model,
|
||||||
|
fallbacks: tier.fallbacks,
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
max_tokens: 700,
|
max_tokens: 700,
|
||||||
response_format: { type: 'json_object' },
|
response_format: { type: 'json_object' },
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// @ts-ignore — JSON import résolu par Vite/Rollup
|
// @ts-ignore — JSON import résolu par Vite/Rollup
|
||||||
import taffData from '../../public/data/plateformes-taff.json'
|
import taffData from '../../public/data/plateformes-taff.json'
|
||||||
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
||||||
|
import { pickBifrostTier, type BifrostChatResponse } from '~/server/utils/bifrost'
|
||||||
|
|
||||||
interface PlateformeMinimal {
|
interface PlateformeMinimal {
|
||||||
id: string
|
id: string
|
||||||
@@ -66,6 +67,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (!question || question.length < 5) {
|
if (!question || question.length < 5) {
|
||||||
throw createError({ statusCode: 400, statusMessage: 'Question trop courte.' })
|
throw createError({ statusCode: 400, statusMessage: 'Question trop courte.' })
|
||||||
}
|
}
|
||||||
|
const tier = pickBifrostTier(body?.mode)
|
||||||
|
|
||||||
// Données bundlées statiquement à la compilation (import JSON)
|
// Données bundlées statiquement à la compilation (import JSON)
|
||||||
const plateformes: PlateformeMinimal[] = ((taffData as any).plateformes ?? []).map((p: any) => ({
|
const plateformes: PlateformeMinimal[] = ((taffData as any).plateformes ?? []).map((p: any) => ({
|
||||||
@@ -91,20 +93,22 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const systemPrompt = SYSTEM_PROMPT.replace('{{PLATEFORMES_JSON}}', JSON.stringify(context, null, 0))
|
const systemPrompt = SYSTEM_PROMPT.replace('{{PLATEFORMES_JSON}}', JSON.stringify(context, null, 0))
|
||||||
|
|
||||||
const mistralApiKey = config.mistralApiKey as string
|
const bifrostUrl = config.bifrostUrl as string
|
||||||
if (!mistralApiKey) {
|
const bifrostVk = config.bifrostVk as string
|
||||||
throw createError({ statusCode: 500, statusMessage: 'Clé API Mistral manquante.' })
|
if (!bifrostVk) {
|
||||||
|
throw createError({ statusCode: 500, statusMessage: 'Clé Bifrost manquante.' })
|
||||||
}
|
}
|
||||||
|
|
||||||
let mistralRaw: string
|
let mistralRaw: string
|
||||||
try {
|
try {
|
||||||
const res = await $fetch<{ choices: { message: { content: string } }[] }>(
|
const res = await $fetch<BifrostChatResponse>(
|
||||||
'https://api.mistral.ai/v1/chat/completions',
|
`${bifrostUrl}/v1/chat/completions`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: `Bearer ${mistralApiKey}`, 'Content-Type': 'application/json' },
|
headers: { 'x-bf-vk': bifrostVk, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'mistral-small-latest',
|
model: tier.model,
|
||||||
|
fallbacks: tier.fallbacks,
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
max_tokens: 700,
|
max_tokens: 700,
|
||||||
response_format: { type: 'json_object' },
|
response_format: { type: 'json_object' },
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
|
||||||
import { checkBudget, calcCoutMistralSmall } from '~/server/utils/circuitBreaker'
|
import { checkBudget, calcCoutMistralSmall } from '~/server/utils/circuitBreaker'
|
||||||
|
import { pickBifrostTier, type BifrostChatResponse } from '~/server/utils/bifrost'
|
||||||
|
|
||||||
// ── Types ──────────────────────────────────────────────────────────────────────
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -131,18 +132,19 @@ async function logUsage(params: {
|
|||||||
nocodbUrl: string
|
nocodbUrl: string
|
||||||
nocodbToken: string
|
nocodbToken: string
|
||||||
statsTableId: string
|
statsTableId: string
|
||||||
|
model: string
|
||||||
tokensIn: number
|
tokensIn: number
|
||||||
tokensOut: number
|
tokensOut: number
|
||||||
coutEur: number
|
coutEur: number
|
||||||
}) {
|
}) {
|
||||||
const { nocodbUrl, nocodbToken, statsTableId, tokensIn, tokensOut, coutEur } = params
|
const { nocodbUrl, nocodbToken, statsTableId, model, tokensIn, tokensOut, coutEur } = params
|
||||||
const logUrl = `${nocodbUrl}/api/v2/tables/${statsTableId}/records`
|
const logUrl = `${nocodbUrl}/api/v2/tables/${statsTableId}/records`
|
||||||
try {
|
try {
|
||||||
await $fetch(logUrl, {
|
await $fetch(logUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'xc-token': nocodbToken, 'Content-Type': 'application/json' },
|
headers: { 'xc-token': nocodbToken, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'mistral-small-latest',
|
model,
|
||||||
endpoint: 'chatbot',
|
endpoint: 'chatbot',
|
||||||
tokens_in: tokensIn,
|
tokens_in: tokensIn,
|
||||||
tokens_out: tokensOut,
|
tokens_out: tokensOut,
|
||||||
@@ -180,6 +182,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
const question: string = (body?.question ?? '').trim()
|
const question: string = (body?.question ?? '').trim()
|
||||||
const filters: { fonction?: string; echelle?: string } = body?.filters ?? {}
|
const filters: { fonction?: string; echelle?: string } = body?.filters ?? {}
|
||||||
|
const tier = pickBifrostTier(body?.mode)
|
||||||
|
|
||||||
if (!question || question.length < 3) {
|
if (!question || question.length < 3) {
|
||||||
throw createError({
|
throw createError({
|
||||||
@@ -247,32 +250,32 @@ export default defineEventHandler(async (event) => {
|
|||||||
JSON.stringify(fichesContext, null, 0),
|
JSON.stringify(fichesContext, null, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 6. Appel Mistral Small
|
// 6. Appel Bifrost (gateway LLM — remplace l'appel Mistral direct)
|
||||||
const mistralApiKey = config.mistralApiKey as string
|
const bifrostUrl = config.bifrostUrl as string
|
||||||
|
const bifrostVk = config.bifrostVk as string
|
||||||
|
|
||||||
if (!mistralApiKey) {
|
if (!bifrostVk) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
statusMessage: 'Clé API Mistral manquante.',
|
statusMessage: 'Clé Bifrost manquante.',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let mistralRaw: string
|
let mistralRaw: string
|
||||||
let tokensIn = 0
|
let tokensIn = 0
|
||||||
let tokensOut = 0
|
let tokensOut = 0
|
||||||
|
let realModel = tier.model
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mistralRes = await $fetch<{
|
const bifrostRes = await $fetch<BifrostChatResponse>(`${bifrostUrl}/v1/chat/completions`, {
|
||||||
choices: { message: { content: string } }[]
|
|
||||||
usage?: { prompt_tokens: number; completion_tokens: number }
|
|
||||||
}>('https://api.mistral.ai/v1/chat/completions', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${mistralApiKey}`,
|
'x-bf-vk': bifrostVk,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'mistral-small-latest',
|
model: tier.model,
|
||||||
|
fallbacks: tier.fallbacks,
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
max_tokens: 600,
|
max_tokens: 600,
|
||||||
response_format: { type: 'json_object' },
|
response_format: { type: 'json_object' },
|
||||||
@@ -283,11 +286,16 @@ export default defineEventHandler(async (event) => {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
mistralRaw = mistralRes.choices?.[0]?.message?.content ?? '{}'
|
mistralRaw = bifrostRes.choices?.[0]?.message?.content ?? '{}'
|
||||||
tokensIn = mistralRes.usage?.prompt_tokens ?? 0
|
tokensIn = bifrostRes.usage?.prompt_tokens ?? 0
|
||||||
tokensOut = mistralRes.usage?.completion_tokens ?? 0
|
tokensOut = bifrostRes.usage?.completion_tokens ?? 0
|
||||||
|
realModel =
|
||||||
|
bifrostRes.extra_fields?.resolved_model_used ??
|
||||||
|
(bifrostRes.extra_fields?.provider
|
||||||
|
? `${bifrostRes.extra_fields.provider}/${tier.model.split('/').slice(1).join('/')}`
|
||||||
|
: tier.model)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[chatbot] Erreur Mistral Small:', e?.message ?? e)
|
console.error('[chatbot] Erreur Bifrost:', e?.message ?? e)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 502,
|
statusCode: 502,
|
||||||
statusMessage: 'Erreur appel IA — réessaie dans quelques instants.',
|
statusMessage: 'Erreur appel IA — réessaie dans quelques instants.',
|
||||||
@@ -317,11 +325,16 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 8. Log usage (non bloquant)
|
// 8. Log usage (non bloquant)
|
||||||
const coutEur = calcCoutMistralSmall(tokensIn, tokensOut)
|
// Coût réel calculable seulement si le provider ayant répondu est Mistral —
|
||||||
|
// les autres tiers Bifrost (Groq, Cerebras, Gemini free tier, Cohere) sont free-tier,
|
||||||
|
// donc cout_eur=0 pour ne pas fausser le circuit breaker avec un tarif Mistral inapplicable.
|
||||||
|
const isMistral = realModel.toLowerCase().startsWith('mistral')
|
||||||
|
const coutEur = isMistral ? calcCoutMistralSmall(tokensIn, tokensOut) : 0
|
||||||
logUsage({
|
logUsage({
|
||||||
nocodbUrl: config.nocodbUrl as string,
|
nocodbUrl: config.nocodbUrl as string,
|
||||||
nocodbToken: config.nocodbToken as string,
|
nocodbToken: config.nocodbToken as string,
|
||||||
statsTableId,
|
statsTableId,
|
||||||
|
model: realModel,
|
||||||
tokensIn,
|
tokensIn,
|
||||||
tokensOut,
|
tokensOut,
|
||||||
coutEur,
|
coutEur,
|
||||||
|
|||||||
47
server/utils/bifrost.ts
Normal file
47
server/utils/bifrost.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Bifrost — gateway LLM (remplace les appels directs Mistral)
|
||||||
|
* Endpoint OpenAI-compatible : POST {bifrostUrl}/v1/chat/completions
|
||||||
|
* Auth : header x-bf-vk
|
||||||
|
*
|
||||||
|
* 2 tiers validés (Mission M3, build Bifrost) :
|
||||||
|
* RAPIDE — défaut, pas de toggle UI mode rapide/approfondi sur le site actuellement
|
||||||
|
* APPROFONDI — activable via body.mode === 'approfondi' (prêt pour un futur toggle front)
|
||||||
|
*
|
||||||
|
* ⚠ openrouter-oai exclu (bug Bifrost confirmé — 404 HTML sur modèles avec slash)
|
||||||
|
* ⚠ gemini-oai exige le préfixe "models/" (sinon 403 silencieux)
|
||||||
|
* ⚠ cerebras/gemma-4-31b RETIRÉ du tier RAPIDE (M4, 15/07) : en JSON mode avec un contexte
|
||||||
|
* réel (prompt + ~20 fiches), le modèle part en boucle de répétition dégénérée ("1 1 1...")
|
||||||
|
* dans ~75% des cas où il sert de fallback, produit un JSON invalide, et Bifrost ne bascule
|
||||||
|
* PAS plus loin dans la chaîne (HTTP 200 côté provider = pas une erreur pour Bifrost). Bug
|
||||||
|
* constaté en prod sur chatbot-reseaux (1 échec/3), reproduit 3/4 sur appel direct Bifrost.
|
||||||
|
* Cerebras reste configuré dans Bifrost (dispo pour tier CODE ou reconfiguration future),
|
||||||
|
* juste plus dans cette chaîne tant que ce n'est pas fiabilisé.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const BIFROST_TIER_RAPIDE = {
|
||||||
|
model: 'groq/llama-3.1-8b-instant',
|
||||||
|
fallbacks: [
|
||||||
|
'gemini-oai/models/gemini-2.5-flash-lite',
|
||||||
|
'cohere/command-r-08-2024',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BIFROST_TIER_APPROFONDI = {
|
||||||
|
model: 'groq/llama-3.3-70b-versatile',
|
||||||
|
fallbacks: [
|
||||||
|
'gemini-oai/models/gemini-2.5-flash',
|
||||||
|
'mistral/mistral-large-latest',
|
||||||
|
'cohere/command-r-plus-08-2024',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sélectionne le tier selon le param optionnel body.mode. */
|
||||||
|
export function pickBifrostTier(mode?: string) {
|
||||||
|
return mode === 'approfondi' ? BIFROST_TIER_APPROFONDI : BIFROST_TIER_RAPIDE
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BifrostChatResponse {
|
||||||
|
choices: { message: { content: string } }[]
|
||||||
|
usage?: { prompt_tokens: number; completion_tokens: number }
|
||||||
|
extra_fields?: { provider?: string; resolved_model_used?: string }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user