Aller au contenu
OmniRoute source

Memory System (Français)

Le moteur de mémoire d’OmniRoute prend en charge quatre sources d’embeddings (src/lib/memory/embedding/). Chacune présente différents compromis en matière de latence, coût, qualité du modèle et complexité de configuration.

Fournisseur Source Latence Coût Qualité Configuration
transformers Modèle ONNX local (Xenova/all-MiniLM-L6-v2) ~50-150ms (CPU) Gratuit Bonne npm install uniquement
static Vecteurs précalculés (mis en cache) <1ms Gratuit S/O (dépend du cache) Aucune
remote API OpenAI / Cohere / Voyage ~100-300ms $0.02-0.10/1M tokens Excellente Clé API
auto Sélectionne la meilleure source disponible à l’exécution Identique à la source choisie Gratuit Identique à la source choisie Aucune
(cache) Couche LRU en mémoire sur n’importe quelle source <1ms (succès), latence complète (échec) Gratuit Identique à la source sous-jacente Toujours active (source non sélectionnable)
Quel est votre contexte de déploiement ?
│
┌───────────┼───────────┬──────────────┐
│ │ │ │
DÉV/TEST PETITE PROD GRANDE PROD EDGE / HORS LIGNE
│ │ │ │
▼ ▼ ▼ ▼
transformers transformers remote (Qdrant) transformers
(gratuit, sans API) (qualité optimale) (sans Internet)
│ │ │ │
└────────┬──┴───────────┴──────────────┘
│
▼
TOUJOURS ajouter la couche `cache` par-dessus
(`LruCache` encapsule n’importe quel fournisseur)

Configuration de la base de données et de l’API

Section intitulée « Configuration de la base de données et de l’API »

Les options d’embedding de la mémoire sont configurées via l’API/l’interface utilisateur des paramètres, et non via des variables d’environnement. Les clés de base de données pertinentes sous Paramètres (normalizeMemorySettings dans src/lib/memory/settings.ts) sont :

  • memoryEmbeddingSource : "transformers" (local), "remote" (basé sur une API, par ex. OpenAI), "static" (stockage externe) ou "auto"
  • memoryEmbeddingProviderModel : identifiant du modèle pour les sources distantes/statiques (par ex., "text-embedding-3-small")
  • memoryTransformersEnabled : true | false
  • memoryStaticEnabled : true | false
  • memoryVectorStore : "sqlite-vec", "qdrant" ou "auto"

Utilise transformers.js en interne pour exécuter des modèles locaux :

Fenêtre de terminal
# Variables d’environnement lues dans le code (src/lib/memory/embedding/index.ts) :
MEMORY_TRANSFORMERS_MODEL=Xenova/all-MiniLM-L6-v2 # Dépôt du modèle HF
MEMORY_STATIC_MODEL=minishlab/potion-base-8M # Modèle potion statique HF
MEMORY_STATIC_CACHE_DIR=<DATA_DIR>/embeddings # Répertoire du cache

Le cache est toujours activé par défaut et configuré via des variables d’environnement :

Fenêtre de terminal
MEMORY_EMBEDDING_CACHE_MAX=1000 # Nombre maximal d’éléments mis en cache
MEMORY_EMBEDDING_CACHE_TTL_MS=300000 # TTL (5 min)

Benchmark sur un serveur x86 classique à 4 cœurs (textes d’environ 100 tokens chacun) :

Fournisseur p50 p95 p99 Coût / 1 M de représentations vectorielles
transformers (CPU) 80ms 180ms 350ms Gratuit
remote (OpenAI) 120ms 220ms 400ms ~$0.02 (ada-002) / $0.13 (3-large)
static (Qdrant) 15ms 30ms 60ms Dépend de l’hébergement de Qdrant
cache (succès) <1ms <1ms 2ms Gratuit

Le module extraction.ts (src/lib/memory/extraction.ts) utilise une correspondance par expressions régulières pour extraire des faits structurés des messages de conversation. Comprendre ces modèles vous aide à ajuster la qualité de l’extraction à votre cas d’utilisation.

Catégorie Exemple de modèle Éléments capturés
PREFERENCE_PATTERNS "Je préfère <X>", "J’aime <X>", "Je déteste <X>" Préférences de l’utilisateur
DECISION_PATTERNS "J’utiliserai <X>", "J’ai décidé de <X>", "J’ai choisi <X>" Décisions de l’utilisateur (épisodiques)
PATTERN_PATTERNS "D’habitude, je <X>", "Je <X> toujours", "Je ne <X> jamais" Modèles comportementaux persistants
// Extrait de src/lib/memory/extraction.ts
const PREFERENCE_PATTERNS = [
/\bI\s+(?:really\s+)?prefer\s+([^.,\n]+)/gi,
/\bI\s+(?:really\s+)?like\s+([^.,\n]+)/gi,
/\bI\s+(?:hate|dislike|avoid)\s+([^.,\n]+)/gi,
];
const DECISION_PATTERNS = [
/\bI'?(?:ll|will)\s+use\s+([^.,\n]+)/gi,
/\bI\s+(?:have\s+)?decided\s+(?:to\s+)?([^.,\n]+)/gi,
];
const PATTERN_PATTERNS = [/\bI\s+usually\s+([^.,\n]+)/gi, /\bI\s+always\s+([^.,\n]+)/gi];

Lorsqu’un utilisateur dit :

« Je préfère TypeScript. J’utiliserai Postgres pour ce projet. Je fais toujours un commit avant de pousser. Je n’aime pas Python. » L’extraction produit 4 souvenirs :

Clé Catégorie Type Contenu
preference:typescript preference factual « TypeScript »
decision:postgres_for_this_project decision episodic « Postgres pour ce projet »
pattern:commit_before_pushing pattern factual « faire un commit avant de pousser »
preference:python preference factual « Python »

Pour éviter une extraction incontrôlée, les limites suivantes s’appliquent :

| Longueur minimale du contenu | 3 caractères | | Longueur maximale du contenu | 500 caractères |

L’extraction s’exécute automatiquement chaque fois que la mémoire est activée ; il n’existe pas d’option distincte permettant d’activer ou de désactiver uniquement l’extraction. Pour la désactiver, désactivez entièrement la mémoire (enabled: false via PUT /api/settings/memory). Envisagez de le faire dans les cas suivants :

  • Vous traitez un volume élevé de messages et le coût de l’extraction n’est pas négligeable
  • Vos conversations sont principalement temporaires (discussion, débogage) et n’ont aucune valeur à long terme
  • Vous capturez déjà le contexte au moyen de plugins personnalisés

L’algorithme Reciprocal Rank Fusion (RRF) combine les résultats de FTS5 (mots-clés) et vectoriels (sémantiques). Le paramètre k contrôle le poids accordé aux résultats moins bien classés.

Pour chaque souvenir candidat, le score RRF est :

RRF(d) = Σ 1 / (k + rank_i(d))

Où :

  • k est la constante (60 par défaut)
  • rank_i(d) est le rang du document d dans le iᵉ système de recherche (FTS, vectoriel)
  • La somme porte sur tous les systèmes de recherche
Valeur de k Effet Idéal pour
k=0 Fusion pure des rangs (sans lissage) Référence théorique
k=10-30 Accorde beaucoup de poids aux premiers résultats ; les rangs faibles contribuent à peine Lorsque les 3 premiers résultats sont généralement corrects
k=60 (par défaut) Équilibré — les 10 premiers résultats contribuent tous de manière significative Recherche généraliste
k=100+ Plus uniforme — même les résultats de rang faible peuvent dominer s’ils apparaissent dans plusieurs systèmes Lorsque le rappel est plus important que la précision
Fenêtre de terminal
# Valeur par défaut
MEMORY_RRF_K=60
# Précision élevée (petite mémoire, peu de documents)
MEMORY_RRF_K=20
# Rappel maximal (grande mémoire, requêtes variées)
MEMORY_RRF_K=120

Exemple avec k=20 :

  • Rang FTS 1 → contribution 1/21 = 0.048
  • Rang FTS 10 → contribution 1/30 = 0.033
  • Rang vectoriel 1 → contribution 0.048
  • Maximum combiné : 0.096

Exemple avec k=60 :

  • Rang FTS 1 → contribution 1/61 = 0.016
  • Rang FTS 10 → contribution 1/70 = 0.014
  • Rang vectoriel 1 → contribution 0.016
  • Maximum combiné : 0.033

Avec une valeur de k plus élevée, la différence relative entre le premier résultat et celui de rang 10 est plus faible ; l’algorithme s’appuie donc davantage sur le consensus entre les systèmes de recherche que sur le niveau de confiance associé au premier rang.

Symptôme Essai recommandé
Le premier résultat gagne toujours, mais il est incorrect Réduire k (p. ex., 20) — le niveau de confiance du premier rang compte davantage
La bonne réponse figure dans les 5 premiers résultats, mais pas en première position Augmenter k (p. ex., 100) — un score plus uniforme favorise le consensus
Le rappel est élevé, mais la précision est faible Réduire k — affiner le classement
Le rappel est faible (des documents pertinents manquent) Augmenter k — donner une chance aux documents moins bien classés

La fusion réciproque des rangs utilise des poids égaux pour le rang vectoriel sémantique et le rang de recherche en texte intégral :

RRF(d) = 1/(k + rank_vector) + 1/(k + rank_fts)

Il n’existe aucune variable d’environnement permettant d’ajuster les poids individuels (MEMORY_RRF_VECTOR_WEIGHT/MEMORY_RRF_FTS_WEIGHT n’existent pas).


Le module summarization.ts (src/lib/memory/summarization.ts) compresse les souvenirs plus anciens afin de limiter la taille de l’ensemble actif tout en préservant les capacités de rappel.

Déclencheur Seuil (par défaut)
Déclenchement manuel via l’API s/o

Deux points d’entrée sont exportés depuis summarization.ts :

  • summarizeMemories(apiKeyId, sessionId?, maxTokens = 4000) — condense les souvenirs d’une session en un seul texte de synthèse respectant un budget de jetons.
  • summarizeMemoriesOlderThan(apiKeyId, days, dryRun) — la compaction basée sur l’âge utilisée par l’API : elle sélectionne chaque souvenir antérieur à days, crée à partir de ceux-ci un unique souvenir de synthèse condensé et, lorsque dryRun vaut false, supprime les originaux. Transmettez dryRun: true pour prévisualiser l’ensemble des candidats et le nombre total de jetons sans rien modifier.

Il n’existe aucune étape de regroupement par étiquette/clé ni aucune notation « essentiel ou synthétisable » par souvenir — la sélection repose uniquement sur la limite d’âge, et le texte de synthèse est constitué d’une ligne condensée préfixée par le type pour chaque candidat.

La synthèse est manuelle / facultative — le paramètre autoSummarize vaut false par défaut, de sorte que rien n’est compacté automatiquement. Déclenchez-la via l’API :

Fenêtre de terminal
curl -X POST http://localhost:20128/api/memory/summarize \
-H "Authorization: Bearer $OMNIROUTE_KEY"

Pour la laisser désactivée, conservez simplement la valeur par défaut de autoSummarize (false).

Conseils pour améliorer la qualité de la synthèse

Section intitulée « Conseils pour améliorer la qualité de la synthèse »
  • Commencez par une prévisualisation avec dryRun — summarizeMemoriesOlderThan(..., true) renvoie la liste des candidats et le nombre total de jetons afin que vous puissiez vérifier ce qui serait fusionné avant de supprimer les originaux.
  • Exécutez la synthèse pendant les heures de faible trafic si vous disposez d’un corpus de souvenirs volumineux — l’appel au LLM est l’étape la plus lente
Fenêtre de terminal
# Style cron : effectuer la synthèse chaque jour à 3 h
0 3 * * * curl -X POST http://localhost:20128/api/memory/summarize \
-H "Authorization: Bearer $OMNIROUTE_KEY"

Source de référence : src/lib/memory/backend.ts, src/lib/memory/genericBackend.ts, src/lib/memory/manager.ts Tests : src/lib/memory/__tests__/generic-backend.test.ts

Le modèle de fournisseur MemoryBackend introduit une couche d’abstraction de backend extensible au-dessus du moteur de mémoire existant. Au lieu d’être lié à une seule implémentation de stockage, le système de mémoire prend désormais en charge plusieurs backends (SQLite, Obsidian, Notion, backends HTTP personnalisés) avec un routage configurable entre le backend principal et les backends de secours.

┌──────────────────────────────────────────────────────────┐
│ Routes API │
│ (src/app/api/memory/route.ts) │
└──────────────────────┬───────────────────────────────────┘
│
┌──────────────────────▼───────────────────────────────────┐
│ MemoryManager │
│ Orchestrateur singleton (manager.ts) │
│ │
│ Principal ─► Backend A (p. ex. SQLite) │
│ Secours ───► Backend B (p. ex. Obsidian) │
│ Backend C (p. ex. Notion via GenericBackend)│
└──────────────────────┬───────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌──────────────────┐
│ Backend │ │ Backend │ │ Backend mémoire │
│ SQLite │ │ Obsidian │ │ générique (HTTP) │
└────────────┘ └────────────┘ └──────────────────┘

Chaque backend doit implémenter l’interface MemoryBackend :

interface MemoryBackend {
readonly id: string;
readonly displayName: string;
// CRUD
create(input: CreateMemoryInput): Promise<Memory>;
get(id: string): Promise<Memory | null>;
update(id: string, updates: Partial<...>): Promise&lt;boolean&gt;;
delete(id: string): Promise&lt;boolean&gt;;
list(filter: MemoryFilter): Promise<{ data: Memory[]; total: number; byType: Record<string, number> }>;
// Recherche
search(config: SearchConfig): Promise<Memory[]>;
// État
health(): Promise<HealthCheckResult>;
// Cycle de vie (facultatif)
initialize?(): Promise&lt;void&gt;;
shutdown?(): Promise&lt;void&gt;;
}

Orchestrateur singleton qui :

  • Enregistre les backends via register(backend) — appelé au démarrage depuis index.ts
  • Configure le backend principal et les backends de secours via configure(primary, fallbacks)
  • Achemine les opérations CRUD et les recherches vers le backend principal, avec une chaîne de secours en cas d’échec
  • Vérifie l’état de tous les backends périodiquement

Comportement de secours :

Opération Principal Backends de secours
create ✅ Principal uniquement ❌
get ✅ Essayer d’abord le principal ✅ Secours si la valeur est nulle
update ✅ Principal uniquement ✅ Synchronisation sans attente de résultat
delete ✅ Principal uniquement ✅ Synchronisation sans attente de résultat
list ✅ Principal uniquement ❌
search ✅ Principal d’abord ✅ Secours en cas d’erreur

Un connecteur HTTP générique qui adapte n’importe quelle API REST en MemoryBackend. Utile pour :

  • Notion — connexion via l’API Notion
  • Obsidian — connexion via l’API REST locale d’Obsidian
  • Backends personnalisés — tout service exposant une API RESTful de mémoire

Configuration :

interface GenericBackendConfig {
baseUrl: string; // URL de base de l’API du backend
apiKey?: string; // Jeton Bearer pour l’authentification
headers?: Record<string, string>; // En-têtes HTTP personnalisés
timeout?: number; // Délai d’expiration de la requête (par défaut : 30000ms)
backendType?: string; // Pour la journalisation
// Remplacements des points de terminaison (les valeurs par défaut suivent les conventions REST)
endpoints?: {
search?: string; // par défaut : "/memories/search"
create?: string; // par défaut : "/memories"
list?: string; // par défaut : "/memories"
get?: string; // par défaut : "/memories/{id}"
update?: string; // par défaut : "/memories/{id}"
delete?: string; // par défaut : "/memories/{id}"
health?: string; // par défaut : "/health"
};
// Correspondances des noms de paramètres de requête
queryParams?: {
query?/apiKeyId?/limit?/offset?/strategy?/maxTokens?/type?/sessionId?/orderBy?/orderDir?/options?
};
// Correspondances des noms de paramètres de chemin
pathParams?: {
id?/memoryId?
};
}

Les backends connus sont préconfigurés dans KNOWN_BACKENDS :

createKnownBackend("obsidian"); // → GenericMemoryBackend pointant vers localhost:27123
createKnownBackend("notion"); // → GenericMemoryBackend pointant vers api.notion.com/v1

Le backend principal par défaut. Encapsule le stockage de mémoire SQLite existant à l’aide de src/lib/memory/store.ts. Il est automatiquement enregistré au démarrage.

import { sqliteBackend } from "./sqliteBackend";
memoryManager.register(sqliteBackend);

Encapsule l’intégration Obsidian existante (src/lib/memory/obsidianBackend.ts). Se connecte à un coffre Obsidian via l’API REST locale d’Obsidian.

Les paramètres des backends de mémoire sont stockés dans la table des paramètres de l’application et gérés via src/lib/memory/settings.ts :

Paramètre Clé d’environnement/configuration Valeur par défaut Description
Backend principal memoryPrimaryBackend "sqlite" ID du backend principal
Backends de repli memoryFallbackBackends [] ID ordonnés des backends de repli
Configurations des backends memoryBackendConfigs {} Remplacements de configuration par backend

Les paramètres sont normalisés via normalizeMemorySettings() et mis en cache dans getMemorySettings().

Amorçage de l’application
→ Importations de index.ts (effet secondaire) : enregistre SQLiteBackend
→ initMemoryBackends() appelé depuis le cycle de vie de l’application :
1. Charger les paramètres (getMemorySettings)
2. Configurer le backend principal et les backends de repli
3. Initialiser tous les backends (vérification de l’état)
4. Prêt à traiter les requêtes
  1. Implémenter l’interface MemoryBackend dans src/lib/memory/&lt;name&gt;Backend.ts
  2. Exporter depuis src/lib/memory/index.ts
  3. Enregistrer avec memoryManager.register(yourBackend) au démarrage
  4. Configurer via les paramètres : définir memoryPrimaryBackend sur l’ID de votre backend
  5. Tester en prenant src/lib/memory/__tests__/generic-backend.test.ts comme référence
import { createGenericMemoryBackend } from "./genericBackend";
const brainBackend = createGenericMemoryBackend("brain", "BK-Brain", {
baseUrl: process.env.BRAIN_API_URL || "http://localhost:9099",
apiKey: process.env.BRAIN_API_KEY,
endpoints: {
search: "/api/memory/search",
create: "/api/memory",
health: "/api/health",
},
});
memoryManager.register(brainBackend);
Fenêtre de terminal
npx vitest run src/lib/memory/__tests__/generic-backend.test.ts --reporter=verbose

Résultat attendu : 35 tests, tous réussis, couvrant :

  • Constructeur (2)
  • Vérification de l’état (4) — réussite, échec 500, erreur réseau, latence
  • Initialisation (2) — réussite, échec
  • Création (2) — point de terminaison par défaut, point de terminaison personnalisé
  • Récupération (4) — réussite, 404 → null, exception pour un code autre que 404, paramètres de chemin personnalisés
  • Mise à jour (2) — réussite, 404 → false
  • Suppression (2) — réussite, 404 → false
  • Liste (2) — paramètres de requête, noms de paramètres personnalisés
  • Recherche (3) — paramètres de requête, point de terminaison personnalisé, sérialisation des options
  • En-têtes d’authentification (2) — jeton Bearer, en-têtes personnalisés
  • Fabrique (1)
Fenêtre de terminal
npm run typecheck:core

Résultat attendu : 0 erreur.


Code source d’OmniRoute (a58000c7685f)

HagiCode

HagiCode est un espace de développement agentique qui associe workflows structurés, exécution multi-agent et vues Hero Dungeon.

Transformez vos idées en logiciels utiles grâce à un workflow agentique plus intelligent, rapide et agréable.

Interface principale de HagiCode en thème clair
  • SmartDes workflows structurés transforment une intention en parcours exécutable, de l’idée à la livraison.
  • EfficientLes workflows multi-agents font avancer recherche, réalisation et revue en parallèle.
  • FunHero Dungeon rend les longues sessions de code plus visuelles et collaboratives.
Visiter HagiCode