AI Engine - Unified RAG Gateway¶
Version: 2.0.0
Date: 25 Mars 2026
Statut: Production
Table des Matières¶
- Vue d'Ensemble
- Architecture RAG Gateway
- Collections Qdrant Multi-Niveaux
- Endpoint Search (Cross-Projet)
- Endpoint Chat (SSE Streaming)
- Feedback & Score de Fiabilité
- Synchronisation Qdrant
- Configuration & Variables d'Environnement
- Modèles de Données AI
- 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 :
- AI Search — Recherche sémantique cross-projet (one-shot, Dashboard)
- 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¶
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¶
| 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 :
get_chat_context()récupère les top-k chunks depuis Qdrant- Les chunks sont ajoutés un par un jusqu'à atteindre le budget
RAG_CONTEXT_TOKEN_BUDGET - L'historique est tronqué en supprimant les messages les plus anciens
- 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 :
(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 :
- Identifie toutes les collections transcript de l'utilisateur (
_get_user_collections()) - Filtre par
project_idssi spécifié - Lance une recherche sur chaque collection en parallèle
- Agrège et re-rank les résultats avec les poids source
- Déduplique par contenu textuel
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¶
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¶
5.5 Gestion des Sessions DB¶
Le streaming SSE utilise deux sessions DB pour éviter les problèmes de connexion :
- Session principale — Utilisée pour les opérations pré-streaming (conversation, user message)
- 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 :
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¶
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¶
(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-4on'est pas disponible, utilisecl100k_base - Taille cache : ~200 KB
9. Modèles de Données AI¶
9.1 Diagramme ERD¶
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);
}
}
}
}
10.3 Dashboard Search¶
// 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