5. Les Filtres (Conscience & Mémoire)
En termes Open WebUI, un Filter est un middleware Python qui intercepte les messages
avant (hook inlet) ou après (hook outlet)
qu'ils atteignent le Pipe. ECHO en déploie deux, aux rôles complémentaires :
new_context_filter.py prépare le contexte en amont, et
conversation_memory_filter.py consolide la mémoire en aval.
💡 Pourquoi deux filtres ?
La séparation des responsabilités est délibérée. Le filtre de contexte opère avant la génération : il analyse les fichiers, construit le Draft et injecte l'AEC. Le filtre mémoire opère après la génération : il distille la réponse, la vectorise et l'archive dans Qdrant. Cette architecture garantit que chaque opération coûteuse (vectorisation, distillation) n'est jamais sur le chemin critique de la génération.
Filtre 1 — new_context_filter.py (v7.18)
Ce filtre est la porte d'entrée cognitive d'ECHO. Il opère en deux phases : en Inlet (avant le Pipe) pour préparer le Draft, et en Outlet (après génération) pour masquer les secrets.
Phase Inlet — Pipeline d'aiguillage multimodal
Pour chaque fichier joint par l'utilisateur, le filtre détermine la stratégie de traitement
en fonction du type MIME (via get_gemini_mime) et de la taille
(seuil MAX_DIRECT_TEXT_SIZE = 256 Ko) :
MIME + Taille} AN -->|"Image/Audio/PDF/Vidéo
≤ 256 Ko"| B64["📎 Encapsulation Base64
(inlineData)"] AN -->|"Texte/Code
≤ 256 Ko"| TXT["📝 Injection Texte Brut"] AN -->|"Tout fichier
> 256 Ko"| EXTR["🔍 Extraction Transmodale
(API ou Lecture Locale)"] EXTR -->|"Texte unifié"| RAG["🧱 RAG Éphémère & Map-Reduce
(Qdrant echo_ephemeral)"] AN -->|"Déjà dans le Vault
(file_id détecté)"| VAULT["📂 Référence Vault"] OWUI -->|"Images inline
(drag/paste)"| INLINE["🖼️ Extraction Ordonnée
(texte + images entrelacés)"] B64 & TXT & RAG & VAULT & INLINE --> DRAFT["📦 Draft Sémantique
(_echo_user_parts_draft)"] DRAFT -->|"Métadonnées
+ AEC YAML"| PIPE["⚙️ Pipe Engine"] classDef neutral fill:#1e293b,stroke:#475569,color:#f8fafc classDef rag fill:#064e3b,stroke:#10b981,color:#f8fafc classDef core fill:#172554,stroke:#4338ca,color:#f8fafc classDef inline fill:#4a1d96,stroke:#8b5cf6,color:#f8fafc class U,OWUI,F,AN neutral class EXTR,RAG,VAULT rag class DRAFT,PIPE core class INLINE inline
Extraction ordonnée des parts multimodales (v7.9)
Lorsqu'un utilisateur colle ou glisse des images directement dans le champ de saisie
d'Open WebUI (sans utiliser le file picker), OWUI construit un content
de type liste multipart au lieu d'une simple chaîne :
[{"type": "text", "text": "Voici l'image 1 :"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}},
{"type": "text", "text": "Et l'image 2 :"},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}},
{"type": "text", "text": "Compare-les."}]
Le filtre itère sur cette liste en préservant l'ordre d'origine :
chaque part text et chaque part image_url est extraite
séquentiellement dans ordered_user_parts, puis injectée dans le Draft.
Cette extraction ordonnée garantit que l'entrelacement texte/image voulu par
l'utilisateur est fidèlement reproduit dans le contexte envoyé à Gemini.
⚠ Distinction fichiers inline vs. file picker
Les images inline (collées/glissées) arrivent dans
messages[-1]["content"] (liste). Les fichiers du file picker
arrivent dans body["files"] et sont traités par
_process_file_task(). Les deux chemins sont fusionnés dans le Draft
final : parts ordonnées en premier, résultats fichiers ensuite.
RAG Éphémère Transmodal et Map-Reduce
Lorsqu'un fichier dépasse MAX_DIRECT_TEXT_SIZE (256 Ko), le filtre déclenche
un processus de traitement approfondi en arrière-plan : le RAG Éphémère.
La première phase est l'Extraction Transmodale : les fichiers textuels sont lus localement,
tandis que les fichiers multimédias complexes (images, vidéos) sont transcrits textuellement.
Le texte brut unifié est immédiatement vectorisé par bge-m3 (1024d) et stocké dans
la collection echo_ephemeral. Ensuite, un processus de Map-Reduce
est exécuté (via l'API Gemini) pour générer un résumé synthétique.
Un Fast-path (court-circuit) est appliqué si le texte est suffisamment court (ECHO_MR_SUMMARY_MAX_WORDS),
évitant une distillation inutile.
Le modèle principal reçoit uniquement le résumé et le source_id (identifiant de fichier natif),
lui permettant d'interroger la donnée brute avec précision via search_session_context(source_id, query)
sans saturer sa fenêtre de contexte. Le concept obsolète de "slug" a été définitivement supprimé.
Le bloc AEC (environnement_contexte)
À la fin de la phase Inlet, le filtre génère et injecte un bloc YAML dans les métadonnées
du message. Ce bloc, délimité par <environnement_contexte>, constitue la
proprioception du modèle pour le tour courant.
environnement_contexte (v7.18)<environnement_contexte>
version_framework_echo: "5.173.6"
modèle_actuel: "##MODEL_ID##" # Résolu par le Pipe (ex: MODEL_FLASH)
modèle_origine: "##MODEL_ORIGIN##" # Modèle ayant initié le tour
nom_utilisateur: "anonyme"
date_et_heure: "2026-05-31 14:00"
localisation: "Paris, France"
timezone: "Europe/Paris"
registre_fichiers:
rapport.pdf:
id: "U_abc123_C_xyz789_T_1748246400"
mime: "application/pdf"
statut: "rag_ephemeral"
source_id: "U_abc123_C_xyz789_T_1748246400"
registre_plan:
- id: "plan_abc"
goal: "Refactorer le module d'auth"
status: "in_progress"
modele: "MODEL_PRO"
registre_codex:
- id: "mon_script.py"
lang: "python"
lines: 142
last_commit: "feat: add error handler"
- id: "config.yaml"
lang: "yaml"
lines: 38
last_commit: "chore: update timeout"
</environnement_contexte>
<smart_context filename="rapport.pdf" mime_type="application/pdf" mode="rag_ephemeral" source_id="U_abc123_C_xyz789_T_1748246400">
[Résumé ≤ 300 mots généré par Map-Reduce local]
> ⚙️ DIRECTIVE SYSTÈME : Les détails exhaustifs du fichier sont stockés dans le RAG éphémère.
> Pour analyser le contenu brut ou répondre à l'utilisateur, vous DEVEZ utiliser l'outil `search_session_context` avec le paramètre source_id="U_abc123_C_xyz789_T_1748246400" et une `query` précise.
</smart_context>
Règle d'Or — Registre des Fichiers
Le modèle doit consulter le registre_fichiers avant toute
manipulation d'une ressource. Le champ statut indique où se trouve le fichier :
inline (dans le contexte actif), rag_ephemeral (indexé dans
Qdrant — utiliser search_session_context), ou vault (dans
l'Espace Personnel — utiliser les outils Vault).
Phase Outlet — Masquage chirurgical des secrets
Après la génération, le filtre balaie le texte de la réponse via une expression régulière
correspondant au format des clés API Google (AIza[0-9A-Za-z_-]{35}).
Toute clé détectée est remplacée par la chaîne
[CLÉ API GOOGLE MASQUÉE PAR SÉCURITÉ]. Ce mécanisme garantit qu'aucun
secret ne « fuit » vers l'interface visuelle quelle que soit la réponse du modèle.
Filtre 2 — conversation_memory_filter.py (v4.0)
Ce filtre ne traite que la phase Outlet (post-génération). Il orchestre la consolidation de la mémoire organique à long terme dans Qdrant via une fenêtre glissante déterministe à stride fixe.
Fenêtre glissante déterministe
Le déclenchement de la distillation repose sur deux paramètres configurables (Valves) :
| Valve | Défaut | Min | Max | Rôle |
|---|---|---|---|---|
WINDOW_SIZE | 5 | 2 | 10 | Nombre de nouveaux messages avant déclenchement |
WINDOW_OVERLAP | 2 | 0 | 5 | Messages de la fenêtre précédente réinjectés pour continuité contextuelle |
Tous les WINDOW_SIZE nouveaux messages, le filtre extrait une fenêtre de
WINDOW_SIZE + WINDOW_OVERLAP messages (défaut : 7) et lance la distillation
en arrière-plan via asyncio.create_task. Le recouvrement de 2 messages
assure la continuité sémantique entre les fenêtres successives, sans perte d'information
aux jointures.
Nettoyage des messages
Avant distillation, la méthode _clean_messages() produit un texte épuré
ne conservant que role, content et les noms de fichiers joints.
Les UUIDs, timestamps, metadata OWUI et identifiants de branches sont éliminés.
Ce nettoyage réduit le budget tokens d'environ 8× par rapport à la sérialisation
brute str(messages), garantissant que chaque token transmis à l'API de distillation
porte de l'information sémantique utile.
Pipeline de distillation et vectorisation
-
Distillation asynchrone :
call_distillationroute versgemini-2.5-flash(API). Génère un JSON structuré :summary(100–1000 mots),memory_importance(1–5),memory_idtechnique court, et 3–5tagsqualifiants. -
Vectorisation locale : le résumé est envoyé à l'Embedding Worker
(
BAAI/bge-m3, 1024d,http://echo-embedding:7997). Aucune donnée ne quitte l'infrastructure. -
Collision sémantique : recherche vectorielle dans
echo_memoryfiltrée paruser_id. Si la similarité cosinus dépasseSIMILARITY_THRESHOLD(0.85), fusion LLM des résumés. Si elle dépasseEXACT_MATCH_THRESHOLD(0.95), simple mise à jour de date sans coût LLM. -
Fusion préservative : lors d'une collision, le score
memory_importanceconservé est le maximum de l'ancien et du nouveau (pas de dégradation d'un souvenir critique). -
Stockage Qdrant : le vecteur est upserted dans la collection
echo_memory, avec les métadonnéesuser_id,chat_id,timestamp,memory_importance(1–5),memory_id,tags.
⚙️ Système de pondération mémorielle
Lors du rappel (search_memory), le score de reranking est calculé ainsi :
score_pondéré = cos_score × MEMORY_IMPORTANCE_WEIGHTS[niveau].
| Niveau | Label | Poids | Durée TTL |
|---|---|---|---|
| 1 | Trivial | 0,55 | 30 jours |
| 2 | Mineur | 0,75 | 60 jours |
| 3 | Utile | 1,00 | 180 jours |
| 4 | Majeur | 1,30 | 365 jours |
| 5 | Axiome | 1,70 | 540 jours |
Un Axiome (niveau 5) à similarité cosinus 0,60 obtient un score de 1,02, surclassant un Trivial (niveau 1) à similarité 0,85 (score 0,47). Les informations cruciales remontent toujours dans les résultats.
✅ Déduplication d'accès (Dédoublonnage Vault)
Avant d'envoyer un fichier au Pipe, le filtre vérifie si le fichier provient déjà du Vault
local de l'utilisateur (champ vault_path dans processed_files).
Si c'est le cas, seule la référence est transmise — pas le contenu binaire.
Cette optimisation évite des ré-uploads inutiles pour des fichiers déjà indexés.