🇬🇧 English version
Voir aussi : Système de mémoire · Référence YAML · Inventaire des outils · Retour à l'index
RaggableTree
Graphe sémantique multi-langage du code source : parsing Tree-sitter, enrichissement embeddings, stockage vectoriel, et exposition aux agents via 15 tools. Remplace la lecture de fichiers brute par un index structuré à cinq niveaux stratifiés (L0 monorepo → L1 package → L2 module → L3 symbole → L4 statement) plus la couche d'arêtes entre eux.
Vue d'ensemble
Un agent qui n'a que file_read et directory_read doit lire des fichiers entiers, deviner leurs relations et surcharger son contexte LLM. Le RaggableTree précalcule ces éléments :
- Structure : chaque fichier est décomposé en classes, méthodes, fonctions, avec des Fully-Qualified Names stables (
pkg::Module::Class::method). - Relations : imports, appels, héritage et implémentations sont résolus à partir de l'AST et stockés comme arêtes du graphe.
- Sémantique : signatures, docstrings, extraits de code et, optionnellement, résumés LLM sont composés en
EmbeddingTextpuis vectorisés. - Évolution : un moteur de réindexation incrémentale met à jour l'index après chaque commit ou événement watcher.
Les tools exposés (codebase_map, symbol_detail, flow_trace, impact_analysis, etc.) donnent à l'agent des requêtes structurelles au lieu de grep sur du plain-text.
Quick start
using Orkeon.Analysis.Abstractions.DTOs.Tools;
using Orkeon.Analysis.Adapters;
using Orkeon.Analysis.Core;
using Orkeon.Domain.FileSystem;
// fileSystem : IFileSystemService (VFS) avec le projet monté sur /src
var builder = new RaggableTreeBuilder(new TypeScriptAdapter(), fileSystem);
var result = await builder.BuildAsync(
"/src",
new IndexCodebaseRequest { RootPath = "/src" },
CancellationToken.None);
Console.WriteLine($"{result.Tree.Nodes.Count} nodes, {result.Tree.Edges.Count} edges, indexId={result.IndexId}");
Pour un usage en DI :
services.AddRaggableTree(new RaggableTreeOptions
{
Languages = ["typescript", "python"],
Embedding = new EmbeddingOptions { Provider = EmbeddingProviderKind.OpenAI, ApiKey = apiKey },
});
Configuration
La seule surface de configuration que les hôtes livrés lient est la section
appsettings RaggableTree (lue clé par clé par le runner host — il n'existe pas de
clé raggableTree: dans le YAML de crew) :
{
"RaggableTree": {
"Enabled": true, // false désactive entièrement la feature (opt-out du runner)
"Exclude": ["node_modules", "dist", ".git", "bin", "obj"],
"RootAlias": "", // raccourcit chaque FQN quand renseigné
"EnrichWithLlm": false, // true génère SemanticSummary via le summarizer
"IncludeStatements": false, // true indexe L4 (par statement)
"IndexMode": "frozen", // assigné mais consommé par rien — voir la note historique plus bas
"Embedding": {
"Provider": "LocalSmartComponents", // le défaut — ou None | OpenAI | Ollama (Onnx est un bras no-op réservé)
"Model": "", // vide → le défaut du provider
"ApiKey": null,
"BaseUrl": null,
"Dimensions": null,
"MaxTextChars": null
}
}
}
Les langages ne se configurent jamais : ils sont auto-détectés depuis la codebase
et scellés par appel index_codebase. Tout le reste de RaggableTreeOptions
(Summarizer, ValidateCitations — défaut true, qui gate les validateurs de
citations) est accessible en C# via AddRaggableTree(options) ; les groupes d'options
VectorStore/Cache existent sur le record mais ne sont consommés par rien — le store
est InMemoryRaggableStore, adapté sur un IVectorStoreProvider ambiant quand il y en
a un d'enregistré.
Tous les champs sont des record immuables dans Orkeon.Analysis.DependencyInjection.RaggableTreeOptions.
Pipeline en six phases
- Discovery (
IFileSystemDiscoverer) — parcours récursif, détection de packages via markers (package.json,*.csproj,pyproject.toml,go.mod,Cargo.toml), exclusion via patterns et.gitignore. - Parse + Extract (
UniversalSemanticMapper+ILanguageAdapter) — Tree-sitter parse → application des queries de l'adaptateur → nœuds L2 (module) et L3 (symbole) avecFqn,Signature,SourceSnippet,Sha256. - Dependency resolution (
DependencyGraphBuilder+IReferenceResolver) — résolution des imports, appels, héritage, implémentations en arêtes (EdgeKind.Imports,Calls,Extends,Implements). Les FQN non résolus deviennent desUnresolvedRef. - Fingerprinting (
IFrameworkFingerprinter) — application des règles par décorateur/annotation (NestJS, Angular, ASP.NET, Flask, FastAPI) : pose de tags commehttp-endpoint,guard,servicesur les nœuds concernés. - Enrichissement — composition du
EmbeddingText(IEmbeddingTextComposer), optionnellement résumé LLM (INodeSummarizer), puis embedding batch (IEmbeddingProvider). - Persistance —
IVectorStoreProvider.IndexAsyncpour les embeddings ; l'arbre lui-même reste dans le store en mémoire (le sérialiseur JSONRaggableTreeCacheexiste mais n'est câblé par aucun hôte livré).
La réindexation incrémentale (IncrementalReindexEngine) repart de la phase 2 pour les fichiers changés uniquement, réutilise les nœuds des fichiers inchangés, et ne lance les phases 4-6 que sur les newNodes.
Catalogue de tools
| Tool | Usage | Requête type |
|---|---|---|
index_codebase |
Construit l'index initial | { "root_path": "/src" } |
incremental_reindex |
Met à jour l'index après modif de fichiers | { "changed_files": ["src/a.ts"] } |
index_status |
Liste les racines virtuelles indexées (date, comptes de nœuds/arêtes) | {} |
is_path_indexed |
Vérifie si un chemin est couvert par une racine indexée | { "virtual_path": "/src/app/main.ts" } |
codebase_map |
Vue d'ensemble : packages, compte de fichiers/symboles | { "level": "L2_Module" } |
package_summary |
Résumé d'un package (modules, dépendances, symboles clés) | { "fqn": "app::core" } |
symbol_detail |
Détail d'un symbole (signature, doc, callers, callees) | { "fqn": "app::UserService::create" } |
symbol_source |
Extrait du code source (signature, body, span complet) | { "fqn": "...", "mode": "SignatureAndBody" } |
codebase_search |
Recherche sémantique par embedding | { "query": "session expiration", "top_k": 10 } |
dependency_graph |
Graphe de dépendances à une portée (L1/L2/L3), rendu Mermaid/DOT | { "scope": "L2_Module", "edge_kinds": ["Imports", "Calls"] } |
sub_graph |
Expansion BFS autour de seeds | { "seeds": ["pkg::X"], "depth": 3 } |
flow_trace |
Chemins d'appel entre deux FQN | { "from": "A", "to": "B", "max_paths": 5 } |
impact_analysis |
Impacts transitifs d'un changement | { "target": "...", "direction": "Backward" } |
complexity_report |
Top-N par métrique (Cyclomatic, LoC, Callers...) | { "metric": "Cyclomatic", "top_n": 20 } |
statement_query |
Requêtes structurelles L4 (if, try, return...) | { "parent_fqns": ["..."], "kinds": ["TryCatch"] } |
Les 15 tools sont enregistrés via AddRaggableTreeTools (Orkeon.Tools.Analysis.DependencyInjection.RaggableToolsExtensions).
Stratégies d'agents
Le framework expose ICodebaseContextProvider qui produit un résumé compact du codebase (packages, top complexité, top couplage, patterns détectés) adapté au system prompt d'un agent. Trois formats : markdown (défaut, ~300 tokens), compact (~150 tokens), json (~400 tokens). Aucun composant du framework ne l'appelle automatiquement — il est enregistré par AddRaggableTree et l'hôte le résout et injecte le résumé là où il le souhaite.
Recherche hybride
codebase_search (et IRaggableStore.SemanticSearchAsync) est hybride depuis le
chantier fraîcheur : un classement cosinus par embeddings et un classement lexical BM25
sont fusionnés par Reciprocal Rank Fusion (SemanticQuery.Mode : Hybrid par défaut /
Vector / Lexical). Le versant lexical utilise un tokenizer conscient du code
(CodeTokenizer) : les identifiants sont découpés aux frontières camelCase / snake_case /
chiffres et indexés à la fois en sous-tokens et en entier
(getUserById → get user by id getuserbyid), de sorte que les requêtes par identifiant
exact gardent leur signal fort tandis que les requêtes conceptuelles gagnent en rappel.
Chaque SearchHit porte MatchOrigin (hybrid/vector/bm25) ; les scores hybrides
sont des agrégats de rangs RRF (pas des similarités — ne jamais les comparer entre
origines). Sans embedder câblé, Hybrid dégrade vers Lexical au lieu de renvoyer vide ;
un Vector explicite conserve le contrat historique.
Fraîcheur (coordination édition ↔ recherche)
L'index reste fidèle à un workspace en cours d'édition grâce à une conception mark-dirty + réindexation paresseuse :
- Hook d'écriture —
FileWriteToolprend unIIndexInvalidationoptionnel (enregistré parAddRaggableTree, implémenté par le store) : chaque écriture réussie marque son chemin comme sale. O(1), synchrone, no-op hors des racines indexées. - Passe paresseuse — les tools de lecture (
codebase_search,symbol_source,flow_trace,codebase_map) appellentIndexFreshnessService.EnsureFreshAsyncavant de répondre : l'ensemble sale ∪ les changements du working tree git (attrape les éditions viashell_command) est réindexé incrémentalement — groupé, single-flight, avec debounce (une sonde git propre fait foi pendant 2 s). Les réponses decodebase_searchrapportentrefreshed_files;index_statusrapportedirty_count/dirty_paths, de sorte que le « périmé » est observable. - Sûreté du store —
InMemoryRaggableStoredétient unReaderWriterLockSlim: les recherches énumèrent en sécurité PENDANT une réindexation incrémentale (auparavant une recherche concurrente pouvait lever sur les dictionnaires mutés). Le rafraîchissement publieRaggableTreeUpdatedsur l'IRaggableTreeEventBus. - Barrière d'échec — un rafraîchissement en échec sert l'index courant (périmé) et conserve la dette de saleté pour la tentative suivante : un résultat périmé vaut mieux qu'une recherche morte.
Note historique : une version antérieure de ce document décrivait trois modes de
synchronisation pilotés par le watcher (frozen/live/breakOnChange via
RaggableTreeIndexMode). Ces modes n'ont jamais été consommés par aucun code — l'enum
existait, rien ne la lisait. La conception de fraîcheur ci-dessus remplace cette fiction ;
ICodebaseWatcher reste disponible pour les hôtes qui veulent une invalidation en push
par-dessus la passe paresseuse.
Extensibilité
Ajouter un langage
Implémenter ILanguageAdapter (Orkeon.Analysis.Abstractions.Interfaces) : fournir LanguageName, FileExtensions, les sept queries Tree-sitter (DeclarationQuery, ImportQuery, CallQuery, InheritanceQuery, DocCommentQuery, DecoratorQuery, StatementQuery), le mapping MapNodeKind et les extracteurs ExtractSignature / ResolveImportPath. Enregistrer l'adaptateur via DI (services.AddSingleton<ILanguageAdapter, MyAdapter>()) ; AddRaggableTree le découvre automatiquement.
Ajouter un fingerprinter
Créer un IReadOnlyList<FingerprintRule> listant les décorateurs/annotations à matcher avec leurs tags puis construire un FrameworkFingerprinter(framework, rules). Voir Orkeon.Analysis.Fingerprinters.NestJsRules comme référence (15 règles).
Ajouter un embedding provider
Implémenter IEmbeddingProvider.EmbedBatchAsync(texts, ct) → IReadOnlyList<ReadOnlyMemory<float>>. Trois implémentations de référence : OpenAIEmbeddingProvider, OllamaEmbeddingProvider, et LocalEmbeddingProvider (cf. section ci-dessous). La méthode d'interface par défaut EmbedAsync(nodes, model, ct) (sur IEmbeddingProvider, Orkeon.Analysis.Abstractions.Interfaces) remplit node.Embedding à partir du EmbeddingText composé.
Embedding providers
Cinq membres existent sur EmbeddingProviderKind :
| Provider | Réseau | Clé API | Dims | Notes |
|---|---|---|---|---|
None |
n/a | n/a | n/a | Pipeline sans embedding (phases 5-6 désactivées) |
Onnx |
non | non | — | Réservé — actuellement un bras d'enregistrement no-op |
OpenAI |
requis | requis | 1536 (text-embedding-3-small) |
Qualité maximale, MTEB ~62.3 |
Ollama |
local (HTTP) | non | dépend du modèle | Daemon Ollama / llama.cpp local requis |
LocalSmartComponents |
non | non | 384 (BGE-micro-v2) | In-process, ONNX CPU, démarrage à froid ~200 ms |
Provider local LocalSmartComponents
Implémenté par Orkeon.Tools.Embeddings.Local (package opt-in, package upstream SmartComponents.LocalEmbeddings v0.1.0-preview10148, modèle BGE-micro-v2). Adapté aux contextes CLI dev, indexation CI offline, démos sans clé d'API.
Trade-off qualité / coût (cf. spec §10) :
- 384 dimensions vs 1536 (OpenAI) — recherche sémantique moins fine sur du Q&A factuel hors-domaine.
- Score MTEB ~58.5 vs ~62.3 — acceptable pour l'indexation de code (les signatures, FQN et docstrings sont des signaux structurés moins ambigus que du texte long).
- Coût d'API → 0 (modèle embarqué dans le NuGet, ~22 MB).
- Aucune dépendance réseau, aucune clé à gérer.
- Démarrage à froid ~150-300 ms (chargement ONNX en mémoire) ; ~5-15 ms / texte sur CPU x86_64 moderne.
- Empreinte RAM ~200 MB une fois le modèle chargé.
Configuration minimale (appsettings.json, cf. spec §7.2)
{
"RaggableTree": {
"Embedding": {
"Provider": "LocalSmartComponents"
// Dimensions, BaseUrl, ApiKey, Model : tous optionnels et ignorés.
// Defaults : 384 dims (BGE-micro-v2), CPU, in-process.
}
}
}
Côté code, l'enregistrement DI est :
services.AddOrkeonLocalEmbeddings(); // package Orkeon.Tools.Embeddings.Local
services.AddRaggableTree(new RaggableTreeOptions
{
Embedding = new EmbeddingOptions
{
Provider = EmbeddingProviderKind.LocalSmartComponents,
},
});
Configuration étendue avec ModelPath VFS (cf. spec §7.3)
Pour pointer vers un autre modèle ONNX que le BGE-micro-v2 embarqué, déclarer un mount VFS lecture seule et passer un chemin virtuel dans Local.ModelPath. Les chemins physiques (C:/..., /var/...) sont rejetés par IFileSystemService au démarrage — voir vfs-compliance.md.
{
"Orkeon": {
"FileSystem": {
"Mounts": [
"C:\\data\\embedding-models:/models:ro" // <physique>:<virtuel>:<droits>
]
}
},
"RaggableTree": {
"Embedding": {
"Provider": "LocalSmartComponents",
"Dimensions": 384,
"MaxTextChars": 2000,
"Local": {
"ModelPath": "/models/my-custom.onnx", // chemin virtuel — résolu par IFileSystemService
"MaxConcurrency": 8
}
}
}
}
Les licences du modèle et du package upstream sont tracées dans THIRD-PARTY-NOTICES.md. Un exemple console minimal vit dans examples/local-embeddings/.
Ajouter un vector store
Implémenter IVectorStoreProvider (IndexAsync, SearchAsync, DeleteAsync). Voir les six providers mémoire existants (Redis, SQLite, ChromaDB, Pinecone, LanceDB, InMemory) pour les patterns de mapping VectorDocument/VectorMetadata.
Limitations V1
- Pas de support des macros C++ / templates pleinement résolus, ni des méta-programmations Ruby/Elixir.
- Le résolveur d'imports est heuristique pour les langages dynamiques (Python, TypeScript) : les re-exports et les monkey patches peuvent produire des
UnresolvedRef. - L'embedding et le summarizer nécessitent un provider externe (clés API ou endpoint Ollama local) pour être activés ; le pipeline fonctionne sans (phases 5-6 désactivées).
- Le watcher se base sur
System.IO.FileSystemWatcher— sous Linux/WSL, les événements de rename peuvent arriver décomposés (observés comme Deleted puis Created). - Le cache JSON n'est pas versionné : une mise à jour d'adapter peut invalider les caches existants, qui sont alors reconstruits à la demande.
Voir aussi : ADR RaggableTree · Inventaire des outils · Retour à l'index