Aller au contenu

AI Engine - Unified RAG Gateway

Version: 2.0.0
Date: 25 Mars 2026
Statut: Production


Table des Matières

  1. Vue d'Ensemble
  2. Architecture RAG Gateway
  3. Collections Qdrant Multi-Niveaux
  4. Endpoint Search (Cross-Projet)
  5. Endpoint Chat (SSE Streaming)
  6. Feedback & Score de Fiabilité
  7. Synchronisation Qdrant
  8. Configuration & Variables d'Environnement
  9. Modèles de Données AI
  10. Intégration Frontend v2

1. Vue d'Ensemble

1.1 Contexte

Le Unified RAG Gateway (Approche B) unifie le pipeline RAG existant et l'assistant IA en un service central (RAGContextGateway) qui alimente deux modes d'interaction :

  1. AI Search — Recherche sémantique cross-projet (one-shot, Dashboard)
  2. AI Chat — Conversation multi-turn project-scoped avec streaming SSE

1.2 Avant / Après

Aspect Avant (v1) Après (v2 - RAG Gateway)
RAG → AI Isolés, jamais connectés RAG alimente directement l'AI
Streaming Non (réponse synchrone bloquante) SSE token-by-token
Scope Aucun (chat global) Project-scoped ou cross-projet
Sources Aucune citation Sources citées + score de pertinence
Feedback Inexistant Rating 1-5 + corrections
Collections Qdrant 1 par transcript (isolées) Multi-niveaux (transcript + projet)
Token counting len(text) // 4 (approximation) tiktoken (comptage précis)
Contexte Documents injectés en texte brut Top-k chunks RAG + budget tokens

1.3 Architecture Globale

graph TB subgraph Frontend["Frontend v2 (React)"] Search["Dashboard Search Bar"] Chat["Chat Panel (SSE)"] FB["Feedback (pouce haut/bas)"] end subgraph BFF["Smart Transcription BFF"] subgraph AIEngine["AI Engine Router"] EP1["POST /ai-engine/search"] EP2["POST /ai-engine/chat"] EP3["POST /ai-engine/feedback"] EP4["GET /ai-engine/reliability"] end GW["RAGContextGateway"] AIS["AIService (OpenAI GPT-4o)"] end subgraph Storage["Stockage"] PG[(PostgreSQL
ai_conversations
ai_messages
ai_feedback)] QD[(Qdrant
Collections multi-niveaux)] RD[(Redis
Cache embeddings)] end Search -->|POST + Bearer| EP1 Chat -->|POST + SSE| EP2 FB -->|POST| EP3 EP1 --> GW EP2 --> GW EP2 --> AIS EP3 --> PG EP4 --> PG GW -->|Embed query| QD GW -->|Cache| RD GW -->|Collections lookup| PG AIS -->|stream=True| EP2 style BFF fill:#e0f2fe,stroke:#0284c7,stroke-width:3px style AIEngine fill:#dbeafe,stroke:#3b82f6,stroke-width:2px style GW fill:#d1fae5,stroke:#10b981,stroke-width:2px style Frontend fill:#f0f9ff,stroke:#0284c7,stroke-width:2px style Storage fill:#f3f4f6,stroke:#6b7280,stroke-width:2px

2. Architecture RAG Gateway

2.1 Service : RAGContextGateway

Le RAGContextGateway est un singleton (get_rag_context_gateway()) qui centralise tous les accès vectoriels Qdrant pour les deux modes AI.

Fichier : src/services/rag_context_gateway.py

Dépendances :

Service Rôle Chargement
QdrantService Opérations CRUD Qdrant Immédiat
EmbeddingService BAAI/bge-m3 (1024d) Lazy (propriété embedding)
CacheService Cache Redis pour embeddings Immédiat
tiktoken Comptage précis de tokens Immédiat

2.2 Méthodes Publiques

graph LR subgraph RAGContextGateway S["search()"] C["get_chat_context()"] B["build_augmented_prompt()"] SY["sync_to_project()"] R["remove_from_project()"] CT["count_tokens()"] TT["truncate_to_tokens()"] end S -->|Utilisé par| EP1["POST /search"] C -->|Utilisé par| EP2["POST /chat"] B -->|Utilisé par| EP2 SY -->|Utilisé par| HOOK1["Post-Processing"] R -->|Utilisé par| HOOK2["Delete/Move"] style RAGContextGateway fill:#d1fae5,stroke:#10b981,stroke-width:2px
Méthode Rôle Appelée par
search() Recherche multi-collection avec re-ranking POST /ai-engine/search
get_chat_context() Construction contexte RAG avec budget tokens POST /ai-engine/chat
build_augmented_prompt() Prompt système augmenté avec contexte RAG POST /ai-engine/chat
sync_to_project() Sync vecteurs transcript → collection projet Post-processing, move transcript
remove_from_project() Suppression vecteurs d'un transcript dans un projet Delete, move transcript
count_tokens() Comptage précis via tiktoken Interne
truncate_to_tokens() Troncature texte selon budget tokens Interne

2.3 Stratégie de Budget Tokens

Le gateway gère un budget strict pour éviter les dépassements de contexte OpenAI :

Fenêtre totale GPT-4o : ~128K tokens
├── System prompt fixe : ~200 tokens
├── Contexte RAG : 8 000 tokens (RAG_CONTEXT_TOKEN_BUDGET)
├── Historique conversation : 4 000 tokens (RAG_HISTORY_TOKEN_BUDGET)
├── Message utilisateur : ~500 tokens
└── Réponse AI : ~4 000 tokens (réservé)

Mécanisme :

  1. get_chat_context() récupère les top-k chunks depuis Qdrant
  2. Les chunks sont ajoutés un par un jusqu'à atteindre le budget RAG_CONTEXT_TOKEN_BUDGET
  3. L'historique est tronqué en supprimant les messages les plus anciens
  4. Le comptage utilise tiktoken.encoding_for_model("gpt-4o") (fallback: cl100k_base)

2.4 Re-Ranking Cross-Source

Les résultats Qdrant sont repondérés selon le type de source :

# Poids par défaut
WEIGHTS = {
    "transcript": 1.0,        # Priorité maximale
    "contextual_file": 0.9,   # Légèrement inférieur
    "meeting_summary": 0.8,   # Synthèses
}

# Score final = score_qdrant * poids_source
weighted_score = raw_score * weights[source_type]

Les résultats sont dédupliqués par contenu textuel pour éviter les doublons cross-collection.


3. Collections Qdrant Multi-Niveaux

3.1 Architecture 2 Niveaux

L'architecture Qdrant est organisée en 2 niveaux de collections :

graph TB subgraph "Niveau 1 — TRANSCRIPT (Pipeline RAG existant, inchangé)" T1["user_{user_id}_transcript_{id_1}"] T2["user_{user_id}_transcript_{id_2}"] T3["user_{user_id}_transcript_{id_3}"] end subgraph "Niveau 2 — PROJET (Nouveau, RAG Gateway)" P1["Projet A collection
(chunks de T1 + T2)"] P2["Projet B collection
(chunks de T3)"] end T1 -.sync.-> P1 T2 -.sync.-> P1 T3 -.sync.-> P2 style T1 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px style T2 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px style T3 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px style P1 fill:#ddd6fe,stroke:#7c3aed,stroke-width:2px style P2 fill:#ddd6fe,stroke:#7c3aed,stroke-width:2px
Niveau Pattern Utilisé par Alimentation
Transcript user_{id}_transcript_{id} Pipeline post-processing (speaker ID) Indexation RAG existante
Projet Collection projet Qdrant POST /ai-engine/search, POST /ai-engine/chat sync_to_project() automatique

3.2 Stratégie Search Cross-Projet

Pour une recherche cross-projet (POST /ai-engine/search), le gateway :

  1. Identifie toutes les collections transcript de l'utilisateur (_get_user_collections())
  2. Filtre par project_ids si spécifié
  3. Lance une recherche sur chaque collection en parallèle
  4. Agrège et re-rank les résultats avec les poids source
  5. Déduplique par contenu textuel
sequenceDiagram participant User participant Gateway as RAGContextGateway participant PG as PostgreSQL participant QD as Qdrant User->>Gateway: search("budget validation") Gateway->>PG: Get user's transcript collections PG-->>Gateway: [collection_1, collection_2, ...] loop Pour chaque collection Gateway->>QD: search(query_vector, collection_name) QD-->>Gateway: [{text, score, metadata}, ...] end Gateway->>Gateway: Re-rank + deduplicate Gateway-->>User: Résultats triés par weighted_score

3.3 Payload Qdrant

Chaque point dans les collections contient ce payload :

{
  "text": "Le directeur technique a validé le budget...",
  "source_type": "transcript",
  "source_id": "uuid-transcript",
  "project_id": "uuid-projet",
  "transcript_id": "uuid-transcript",
  "chunk_index": 3,
  "speaker": "Sophie Martin",
  "all_participants": ["Sophie Martin", "Jean Dupont"],
  "keywords": ["budget", "validation", "technique"],
  "created_at": "2025-03-15T14:30:00Z"
}

4. Endpoint Search (Cross-Projet)

4.1 Spécification API

Attribut Valeur
Méthode POST
Path /api/ai-engine/search
Auth Bearer Token (Keycloak JWT)
Request AISearchRequest
Response AISearchResponse

4.2 Request Body

{
  "query": "Quelles décisions ont été prises cette semaine ?",
  "project_ids": ["uuid-projet-1", "uuid-projet-2"],
  "source_types": ["transcript", "contextual_file"],
  "limit": 10
}
Champ Type Requis Description
query string Oui Texte de recherche sémantique
project_ids list[string] Non Filtrer par projets (null = tous)
source_types list[string] Non Filtrer par type de source
limit int Non Nombre max de résultats (défaut: 10)

4.3 Response

{
  "query": "Quelles décisions ont été prises cette semaine ?",
  "results": [
    {
      "source_type": "transcript",
      "source_id": "uuid-transcript-1",
      "project_id": "uuid-projet-1",
      "text": "Le comité a décidé de lancer la phase 2 du projet...",
      "score": 0.87,
      "metadata": {
        "speaker": "Sophie Martin",
        "transcript_id": "uuid-transcript-1"
      }
    }
  ],
  "total_results": 5
}

4.4 Flow

graph TB A["POST /ai-engine/search"] --> B["Auth: get_current_active_user"] B --> C["RAGContextGateway.search()"] C --> D["Embed query (BGE-M3)"] D --> E["Search collections Qdrant"] E --> F["Re-rank + deduplicate"] F --> G["Return AISearchResponse"] style A fill:#e0f2fe,stroke:#0284c7,stroke-width:2px style C fill:#d1fae5,stroke:#10b981,stroke-width:2px style E fill:#ddd6fe,stroke:#7c3aed,stroke-width:2px style G fill:#d1fae5,stroke:#10b981,stroke-width:2px

5. Endpoint Chat (SSE Streaming)

5.1 Spécification API

Attribut Valeur
Méthode POST
Path /api/ai-engine/chat
Auth Bearer Token (Keycloak JWT)
Request AIChatRequest
Response text/event-stream (SSE)

5.2 Request Body

{
  "message": "Résume les points clés de ma dernière réunion",
  "conversation_id": "uuid-conversation",
  "project_id": "uuid-projet",
  "attachments": [
    {
      "type": "transcript",
      "name": "Comité qualité",
      "source_id": "uuid-transcript-1"
    }
  ]
}
Champ Type Requis Description
message string Oui Message utilisateur
conversation_id string Non ID conversation existante (null = nouvelle)
project_id string Non Scope projet pour le RAG
attachments list[object] Non Pièces jointes (transcripts, documents)

5.3 Événements SSE

Le stream émet 3 types d'événements :

Événement Données Description
token {"type": "token", "content": "..."} Chaque token généré par GPT-4o
done {"type": "done", "conversation_id": "...", "message_id": "...", "sources": [...], "confidence_score": 0.85} Fin du streaming + métadonnées
error {"type": "error", "content": "..."} Erreur durant le streaming

5.4 Flow Complet

sequenceDiagram participant User participant Frontend participant BFF as AI Engine Router participant GW as RAGContextGateway participant QD as Qdrant participant GPT as OpenAI GPT-4o participant DB as PostgreSQL User->>Frontend: Envoie message Frontend->>BFF: POST /ai-engine/chat (SSE) Note over BFF: 1. Get/Create Conversation BFF->>DB: Query AIConversation DB-->>BFF: Conversation (ou create) Note over BFF: 2. Save User Message BFF->>DB: INSERT AIMessage (role=user) Note over BFF: 3. Build RAG Context BFF->>GW: get_chat_context() GW->>QD: Search collections projet QD-->>GW: Top-k chunks GW->>GW: Budget tokens + truncate history GW-->>BFF: {rag_context, sources, history} Note over BFF: 4. Build Augmented Prompt BFF->>GW: build_augmented_prompt() GW-->>BFF: System prompt + RAG context Note over BFF: 5. Stream OpenAI Response BFF->>GPT: chat.completions.create(stream=True) loop Pour chaque token GPT-->>BFF: chunk.delta.content BFF-->>Frontend: SSE: {"type":"token","content":"..."} end Note over BFF: 6. Save AI Response + Sources BFF->>DB: INSERT AIMessage (role=assistant) BFF->>DB: INSERT AIMessageSource[] (citations) BFF-->>Frontend: SSE: {"type":"done","sources":[...]} Frontend-->>User: Affichage complet + sources

5.5 Gestion des Sessions DB

Le streaming SSE utilise deux sessions DB pour éviter les problèmes de connexion :

  1. Session principale — Utilisée pour les opérations pré-streaming (conversation, user message)
  2. Session SessionLocal() — Créée dans le générateur SSE pour la sauvegarde post-streaming

Cette séparation est nécessaire car la session principale est fermée par FastAPI avant la fin du générateur async.


6. Feedback & Score de Fiabilité

6.1 Endpoint Feedback

Attribut Valeur
Méthode POST
Path /api/ai-engine/feedback
Auth Bearer Token (Keycloak JWT)

Request :

{
  "message_id": "uuid-message",
  "rating": 4,
  "correction_text": "Le nom correct est Jean Dupont, pas Jean Martin"
}
Champ Type Requis Description
message_id string Oui ID du message AI noté
rating int Oui Note de 1 (mauvais) à 5 (excellent)
correction_text string Non Texte de correction libre

Validations :

  • Le message doit exister
  • La conversation associée doit appartenir à l'utilisateur
  • Le rating doit être entre 1 et 5

6.2 Endpoint Reliability Score

Attribut Valeur
Méthode GET
Path /api/ai-engine/reliability
Auth Bearer Token (Keycloak JWT)

Response :

{
  "score": 0.87,
  "total_feedbacks": 21,
  "window_size": 100
}

Calcul :

Score = moyenne(ratings des N derniers feedbacks) / 5.0

Exemple : ratings = [5, 4, 5, 3, 5, 4, 4, 5]
         moyenne = 4.375
         score = 4.375 / 5.0 = 0.875 → affiché 87%

Le window_size est configurable via FEEDBACK_WINDOW_SIZE (défaut: 100).
Si aucun feedback n'existe, le score est 1.0 (confiance par défaut).

6.3 Flow Feedback

graph LR A["Réponse AI affichée"] --> B["Utilisateur clique pouce haut/bas"] B --> C["POST /ai-engine/feedback"] C --> D["Validation: message + conversation + user"] D --> E["INSERT ai_feedback"] E --> F["Score recalculé au prochain GET /reliability"] style A fill:#e0f2fe,stroke:#0284c7,stroke-width:2px style C fill:#d1fae5,stroke:#10b981,stroke-width:2px style E fill:#fef3c7,stroke:#f59e0b,stroke-width:2px

7. Synchronisation Qdrant

7.1 Points d'Accroche (Hooks)

Le RAGContextGateway est appelé automatiquement par 3 hooks dans le code existant :

Événement Fichier Action
Transcript finalisé post_processing_orchestrator.py sync_to_project() — copie les vecteurs dans la collection projet
Transcript supprimé main.py remove_from_project() + delete_collection()
Transcript déplacé projects.py remove_from_project() ancien + sync_to_project() nouveau
Projet supprimé projects.py Nettoyage de toutes les collections associées

7.2 Diagramme de Sync

graph TB subgraph "Déclencheurs" A["Transcript complété
(post_processing_orchestrator)"] B["Transcript supprimé
(main.py)"] C["Transcript déplacé
(projects.py)"] D["Projet supprimé
(projects.py)"] end subgraph "Actions Qdrant" S["sync_to_project()
Copie vecteurs → collection projet"] R["remove_from_project()
Supprime vecteurs du projet"] DEL["delete_collection()
Supprime collection transcript"] end A --> S B --> R B --> DEL C --> R C --> S D --> R style S fill:#d1fae5,stroke:#10b981,stroke-width:2px style R fill:#fee2e2,stroke:#ef4444,stroke-width:2px style DEL fill:#fee2e2,stroke:#ef4444,stroke-width:2px

7.3 Idempotence

Les opérations sync_to_project() sont idempotentes :
- Les points utilisent des point_id déterministes basés sur le contenu
- Un upsert d'un point existant le met à jour sans erreur
- En cas d'échec, la synchronisation peut être relancée sans effet de bord


8. Configuration & Variables d'Environnement

8.1 Variables RAG Gateway

Variable Défaut Description
RAG_SEARCH_LIMIT 10 Nombre max de résultats pour /ai-engine/search
RAG_CHAT_CONTEXT_LIMIT 5 Nombre max de chunks RAG injectés dans le chat
RAG_CONTEXT_TOKEN_BUDGET 8000 Budget tokens pour le contexte RAG
RAG_HISTORY_TOKEN_BUDGET 4000 Budget tokens pour l'historique de conversation
RAG_TRANSCRIPT_WEIGHT 1.0 Poids re-ranking des transcripts
RAG_FILE_WEIGHT 0.9 Poids re-ranking des fichiers contextuels
RAG_SUMMARY_WEIGHT 0.8 Poids re-ranking des meeting summaries
FEEDBACK_WINDOW_SIZE 100 Nombre de feedbacks pour le score de fiabilité

8.2 Variables Pré-requises (Existantes)

Variable Rôle Requis
WHISPER_API_KEY Clé API OpenAI (GPT-4o) Oui (critique)
QDRANT_URL URL du serveur Qdrant Oui
QDRANT_API_KEY Clé API Qdrant Oui
EMBEDDING_MODEL Modèle d'embeddings (BAAI/bge-m3) Oui
EMBEDDING_DEVICE Device (cpu/cuda) Oui
DATABASE_URL URL PostgreSQL Oui

8.3 tiktoken

Le module tiktoken télécharge ses fichiers d'encodage depuis le CDN OpenAI au premier appel.

  • Cache : ~/.cache/tiktoken/ (Linux) ou %AppData%\Local\cache\tiktoken\ (Windows)
  • Fallback : Si le modèle gpt-4o n'est pas disponible, utilise cl100k_base
  • Taille cache : ~200 KB

9. Modèles de Données AI

9.1 Diagramme ERD

erDiagram ai_conversations ||--o{ ai_messages : "contient" ai_messages ||--o{ ai_attachments : "a" ai_messages ||--o{ ai_message_sources : "cite" ai_messages ||--o{ ai_feedback : "recoit" users ||--o{ ai_conversations : "possede" projects ||--o{ ai_conversations : "scope" ai_conversations { string id PK string user_id FK string project_id FK string title datetime created_at datetime updated_at } ai_messages { string id PK string conversation_id FK string role text content datetime created_at } ai_attachments { string id PK string message_id FK string type string name string source_id json attachment_metadata } ai_message_sources { string id PK string message_id FK string source_type string source_id text chunk_text float relevance_score string created_at } ai_feedback { string id PK string message_id FK string user_id int rating text correction_text string created_at } ai_search_history { string id PK string user_id text query int results_count text top_sources float confidence_score string created_at }

9.2 Tables Nouvelles (Migration v2.0)

Table Description FK
ai_message_sources Citations RAG utilisées par l'AI ai_messages.id (CASCADE)
ai_feedback Feedback utilisateur (rating 1-5) ai_messages.id (CASCADE)
ai_search_history Historique recherches Dashboard Aucune

9.3 Modification Existante

Table Modification Description
ai_conversations + project_id (FK → projects.id, nullable, ON DELETE SET NULL) Scope la conversation à un projet

10. Intégration Frontend v2

10.1 Endpoints Résumé

Endpoint Méthode Usage Frontend Auth
/api/ai-engine/search POST Dashboard Search Bar Bearer JWT
/api/ai-engine/chat POST Chat Panel (SSE) Bearer JWT
/api/ai-engine/feedback POST Boutons pouce haut/bas Bearer JWT
/api/ai-engine/reliability GET Badge fiabilité (sidebar) Bearer JWT

10.2 Connexion SSE (Chat)

// Frontend — Connexion SSE au chat
const response = await fetch('/api/ai-engine/chat', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
  },
  body: JSON.stringify({
    message: userMessage,
    conversation_id: conversationId,
    project_id: selectedProjectId,
  }),
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  const chunk = decoder.decode(value);
  const lines = chunk.split('\n\n');

  for (const line of lines) {
    if (line.startsWith('data: ')) {
      const event = JSON.parse(line.slice(6));

      if (event.type === 'token') {
        appendToken(event.content);
      } else if (event.type === 'done') {
        setConversationId(event.conversation_id);
        setSources(event.sources);
        setConfidence(event.confidence_score);
      } else if (event.type === 'error') {
        showError(event.content);
      }
    }
  }
}
// Frontend — Recherche sémantique
const results = await api.post('/ai-engine/search', {
  query: searchQuery,
  project_ids: selectedProjects,
  limit: 10,
});

// results.data.results contient les résultats + sources
// results.data.total_results contient le nombre total

10.4 Feedback

// Frontend — Envoi feedback
await api.post('/ai-engine/feedback', {
  message_id: aiMessageId,
  rating: isThumbUp ? 5 : 1,
  correction_text: correctionText || undefined,
});