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) :

Aiguillage Multimodal — new_context_filter.py [HOOK INLET]
flowchart LR U([Utilisateur]) -->|Upload + Texte| OWUI[Open WebUI] OWUI -->|hook.inlet| F["🌒 new_context_filter.py"] F --> AN{Analyse
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 :

Format multipart OWUI — content liste
[{"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.

Structure du bloc 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) :

ValveDéfautMinMaxRôle
WINDOW_SIZE5210Nombre de nouveaux messages avant déclenchement
WINDOW_OVERLAP205Messages 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

  1. Distillation asynchrone : call_distillation route vers gemini-2.5-flash (API). Génère un JSON structuré : summary (100–1000 mots), memory_importance (1–5), memory_id technique court, et 3–5 tags qualifiants.
  2. 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.
  3. Collision sémantique : recherche vectorielle dans echo_memory filtrée par user_id. Si la similarité cosinus dépasse SIMILARITY_THRESHOLD (0.85), fusion LLM des résumés. Si elle dépasse EXACT_MATCH_THRESHOLD (0.95), simple mise à jour de date sans coût LLM.
  4. Fusion préservative : lors d'une collision, le score memory_importance conservé est le maximum de l'ancien et du nouveau (pas de dégradation d'un souvenir critique).
  5. Stockage Qdrant : le vecteur est upserted dans la collection echo_memory, avec les métadonnées user_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].

NiveauLabelPoidsDurée TTL
1Trivial0,5530 jours
2Mineur0,7560 jours
3Utile1,00180 jours
4Majeur1,30365 jours
5Axiome1,70540 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.

Les données préparées par le Filtre sont transmises au Pipe, qui opère la reconstruction bit-perfect de l'historique. Découvrir le Pipe (Cortex) ➔