458 lines
14 KiB
Markdown
458 lines
14 KiB
Markdown
# Retrieval Inventory
|
|
|
|
## Scope and method
|
|
|
|
This document describes the retrieval and indexing pipeline as implemented in code today. The inventory is based primarily on:
|
|
|
|
- `app/modules/rag/services/rag_service.py`
|
|
- `app/modules/rag/persistence/*.py`
|
|
- `app/modules/rag/indexing/code/**/*.py`
|
|
- `app/modules/rag/indexing/docs/**/*.py`
|
|
- `app/modules/rag_session/module.py`
|
|
- `app/modules/agent/engine/graphs/project_qa_step_graphs.py`
|
|
- `app/modules/agent/engine/orchestrator/*.py`
|
|
|
|
`ASSUMPTION:` the intended layer semantics are the ones implied by code and tests, not by future architecture plans. This matters because only `C0` through `C3` are materially implemented today; `C4+` exist only as enum constants.
|
|
|
|
## Current retrieval pipeline
|
|
|
|
1. Retrieval entrypoint is `POST /internal/rag/retrieve` in `app/modules/rag_session/module.py`.
|
|
2. The endpoint calls `RagService.retrieve(rag_session_id, query)`.
|
|
3. `RagQueryRouter` chooses `docs` or `code` mode from the raw query text.
|
|
4. `RagService` computes a single embedding for the full query via `GigaChatEmbedder`.
|
|
5. `RagQueryRepository.retrieve(...)` runs one SQL query against `rag_chunks` in PostgreSQL with `pgvector`.
|
|
6. Ranking order is:
|
|
- lexical rank
|
|
- test-file penalty
|
|
- layer rank
|
|
- vector distance `embedding <=> query_embedding`
|
|
7. Response items are normalized to `{source, content, layer, title, metadata, score}`.
|
|
8. If embeddings fail, retrieval falls back to latest chunks from the same layers.
|
|
9. If code retrieval returns nothing, service falls back to docs layers.
|
|
|
|
## Storage and indices
|
|
|
|
- Primary store: PostgreSQL from `DATABASE_URL`, configured in `app/modules/shared/db.py`.
|
|
- Vector extension: `CREATE EXTENSION IF NOT EXISTS vector` in `app/modules/rag/persistence/schema_repository.py`.
|
|
- Primary table: `rag_chunks`.
|
|
- Cache tables:
|
|
- `rag_blob_cache`
|
|
- `rag_chunk_cache`
|
|
- `rag_session_chunk_map`
|
|
- SQL indexes currently created:
|
|
- `(rag_session_id)`
|
|
- `(rag_session_id, layer)`
|
|
- `(rag_session_id, layer, path)`
|
|
- `(qname)`
|
|
- `(symbol_id)`
|
|
- `(module_id)`
|
|
- `(doc_kind)`
|
|
- `(entrypoint_type, framework)`
|
|
|
|
`ASSUMPTION:` there is no explicit ANN index for the vector column in schema code. The code creates general SQL indexes, but no `ivfflat`/`hnsw` index is defined here.
|
|
|
|
## Layer: C0_SOURCE_CHUNKS
|
|
|
|
### Implementation
|
|
|
|
- Produced by `CodeIndexingPipeline.index_file(...)` in `app/modules/rag/indexing/code/pipeline.py`.
|
|
- Chunking logic: `CodeTextChunker.chunk(...)` in `app/modules/rag/indexing/code/code_text/chunker.py`.
|
|
- Document builder: `CodeTextDocumentBuilder.build(...)` in `app/modules/rag/indexing/code/code_text/document_builder.py`.
|
|
- Persisted via `RagDocumentRepository.insert_documents(...)` into `rag_chunks`.
|
|
|
|
### Input contract
|
|
|
|
This is an indexing layer, not a direct public retriever. The observed upstream indexing input is a file dict with at least:
|
|
|
|
- required:
|
|
- `path: str`
|
|
- `content: str`
|
|
- optional:
|
|
- `commit_sha: str | None`
|
|
- `content_hash: str`
|
|
- metadata fields copied through by `RagService._document_metadata(...)`
|
|
|
|
For retrieval, the layer is queried only indirectly through:
|
|
|
|
- `rag_session_id: str`
|
|
- `query: str`
|
|
- inferred mode/layers from `RagQueryRouter`
|
|
- fixed `limit=8`
|
|
|
|
### Output contract
|
|
|
|
Stored document shape:
|
|
|
|
- top-level:
|
|
- `layer = "C0_SOURCE_CHUNKS"`
|
|
- `lang = "python"`
|
|
- `source.repo_id`
|
|
- `source.commit_sha`
|
|
- `source.path`
|
|
- `title`
|
|
- `text`
|
|
- `span.start_line`
|
|
- `span.end_line`
|
|
- `embedding`
|
|
- metadata:
|
|
- `chunk_index`
|
|
- `chunk_type`: `symbol_block` or `window`
|
|
- `module_or_unit`
|
|
- `artifact_type = "CODE"`
|
|
- plus file-level metadata injected by `RagService`
|
|
|
|
Returned retrieval item shape:
|
|
|
|
- `source`
|
|
- `content`
|
|
- `layer`
|
|
- `title`
|
|
- `metadata`
|
|
- `score`
|
|
|
|
No `line_start` / `line_end` are returned to the caller directly; they remain in DB columns `span_start` / `span_end` and are only used in logs.
|
|
|
|
### Defaults & limits
|
|
|
|
- AST chunking prefers one chunk per top-level class/function/async function.
|
|
- Fallback window chunking:
|
|
- `size = 80` lines
|
|
- `overlap = 15` lines
|
|
- Global retrieval limit from `RagService.retrieve(...)`: `8`
|
|
- Embedding batch size from env:
|
|
- `RAG_EMBED_BATCH_SIZE`
|
|
- default `16`
|
|
|
|
### Known issues
|
|
|
|
- Nested methods/functions are not emitted as C0 chunks unless represented inside a selected top-level block.
|
|
- Returned API payload omits line spans even though storage has them.
|
|
- No direct filter by path, namespace, symbol, or `top_k` is exposed through the current endpoint.
|
|
|
|
## Layer: C1_SYMBOL_CATALOG
|
|
|
|
### Implementation
|
|
|
|
- Symbol extraction: `SymbolExtractor.extract(...)` in `app/modules/rag/indexing/code/symbols/extractor.py`.
|
|
- AST parsing: `PythonAstParser.parse_module(...)`.
|
|
- Document builder: `SymbolDocumentBuilder.build(...)`.
|
|
- Retrieval reads rows from `rag_chunks`; there is no dedicated symbol table.
|
|
|
|
### Input contract
|
|
|
|
Indexing input is the same per-file payload as C0.
|
|
|
|
Observed symbol extraction source:
|
|
|
|
- Python AST only
|
|
- supported symbol kinds:
|
|
- `class`
|
|
- `function`
|
|
- `method`
|
|
- `const` for top-level imports/import aliases
|
|
|
|
Retrieval input is still the generic text query endpoint. Query terms are enriched by `extract_query_terms(...)`:
|
|
|
|
- extracts identifier-like tokens from query text
|
|
- normalizes camelCase/PascalCase to snake_case
|
|
- adds special intent terms for management/control-related queries
|
|
- max observed query terms: `6`
|
|
|
|
### Output contract
|
|
|
|
Stored document shape:
|
|
|
|
- top-level:
|
|
- `layer = "C1_SYMBOL_CATALOG"`
|
|
- `title = qname`
|
|
- `text = "<kind> <qname>\n<signature>\n<docstring?>"`
|
|
- `span.start_line`
|
|
- `span.end_line`
|
|
- metadata:
|
|
- `symbol_id`
|
|
- `qname`
|
|
- `kind`
|
|
- `signature`
|
|
- `decorators_or_annotations`
|
|
- `docstring_or_javadoc`
|
|
- `parent_symbol_id`
|
|
- `package_or_module`
|
|
- `is_entry_candidate`
|
|
- `lang_payload`
|
|
- `artifact_type = "CODE"`
|
|
|
|
Observed `lang_payload` variants:
|
|
|
|
- class:
|
|
- `bases`
|
|
- function/method:
|
|
- `async`
|
|
- import alias:
|
|
- `imported_from`
|
|
- `import_alias`
|
|
|
|
### Defaults & limits
|
|
|
|
- Only Python source files are indexed into C-layers.
|
|
- Import and import-from declarations are materialized as `const` symbols only at module top level.
|
|
- Retrieval ranking gives C1 priority rank `1`, after C3 and before C2/C0.
|
|
|
|
### Known issues
|
|
|
|
- No explicit visibility/public-private model.
|
|
- `parent_symbol_id` currently stores the parent qname string from the stack, not the parent symbol hash. This is an observed implementation detail.
|
|
- Cross-file symbol resolution is not implemented; `dst_symbol_id` in edges resolves only against symbols extracted from the same file.
|
|
|
|
## Layer: C2_DEPENDENCY_GRAPH
|
|
|
|
### Implementation
|
|
|
|
- Edge extraction: `EdgeExtractor.extract(...)` in `app/modules/rag/indexing/code/edges/extractor.py`.
|
|
- Document builder: `EdgeDocumentBuilder.build(...)`.
|
|
- Built during `CodeIndexingPipeline.index_file(...)`.
|
|
|
|
### Input contract
|
|
|
|
Indexing input is the same per-file source payload as C0/C1.
|
|
|
|
Graph construction method:
|
|
|
|
- static analysis only
|
|
- Python AST walk only
|
|
- no runtime tracing
|
|
- no tree-sitter
|
|
|
|
Observed edge types:
|
|
|
|
- `calls`
|
|
- `imports`
|
|
- `inherits`
|
|
|
|
### Output contract
|
|
|
|
Stored document shape:
|
|
|
|
- top-level:
|
|
- `layer = "C2_DEPENDENCY_GRAPH"`
|
|
- `title = "<src_qname>:<edge_type>"`
|
|
- `text = "<src_qname> <edge_type> <dst>"`
|
|
- `span.start_line`
|
|
- `span.end_line`
|
|
- `links` contains one evidence link of type `EDGE`
|
|
- metadata:
|
|
- `edge_id`
|
|
- `edge_type`
|
|
- `src_symbol_id`
|
|
- `src_qname`
|
|
- `dst_symbol_id`
|
|
- `dst_ref`
|
|
- `resolution`: `resolved` or `partial`
|
|
- `lang_payload`
|
|
- `artifact_type = "CODE"`
|
|
|
|
Observed `lang_payload` usage:
|
|
|
|
- for calls: may include `callsite_kind = "function_call"`
|
|
|
|
### Defaults & limits
|
|
|
|
- Edge extraction is per-file only.
|
|
- `imports` edges are emitted only while visiting a class/function scope; top-level imports do not become C2 edges.
|
|
- Layer rank in retrieval SQL: `2`
|
|
|
|
### Known issues
|
|
|
|
- There is no traversal API, graph repository, or query language over C2. Retrieval only treats edges as text/vector rows in `rag_chunks`.
|
|
- Destination resolution is local to the file-level qname map.
|
|
- Top-level module import relationships are incompletely represented because `visit_Import` / `visit_ImportFrom` skip when there is no current scope.
|
|
|
|
## Layer: C3_ENTRYPOINTS
|
|
|
|
### Implementation
|
|
|
|
- Detection registry: `EntrypointDetectorRegistry.detect_all(...)`.
|
|
- Detectors:
|
|
- `FastApiEntrypointDetector`
|
|
- `FlaskEntrypointDetector`
|
|
- `TyperClickEntrypointDetector`
|
|
- Document builder: `EntrypointDocumentBuilder.build(...)`.
|
|
|
|
### Input contract
|
|
|
|
Indexing input is the same per-file source payload as other C-layers.
|
|
|
|
Detected entrypoint families today:
|
|
|
|
- HTTP:
|
|
- FastAPI decorators such as `.get`, `.post`, `.put`, `.patch`, `.delete`, `.route`
|
|
- Flask `.route`
|
|
- CLI:
|
|
- Typer/Click `.command`
|
|
- Typer/Click `.callback`
|
|
|
|
Not detected:
|
|
|
|
- Django routes
|
|
- Celery tasks
|
|
- RQ jobs
|
|
- cron jobs / scheduler entries
|
|
|
|
### Output contract
|
|
|
|
Stored document shape:
|
|
|
|
- top-level:
|
|
- `layer = "C3_ENTRYPOINTS"`
|
|
- `title = route_or_command`
|
|
- `text = "<framework> <entry_type> <route_or_command>"`
|
|
- `span.start_line`
|
|
- `span.end_line`
|
|
- `links` contains one evidence link of type `CODE_SPAN`
|
|
- metadata:
|
|
- `entry_id`
|
|
- `entry_type`: observed `http` or `cli`
|
|
- `framework`: observed `fastapi`, `flask`, `typer`, `click`
|
|
- `route_or_command`
|
|
- `handler_symbol_id`
|
|
- `lang_payload`
|
|
- `artifact_type = "CODE"`
|
|
|
|
FastAPI-specific observed payload:
|
|
|
|
- `lang_payload.methods = [HTTP_METHOD]` for `.get/.post/...`
|
|
|
|
### Defaults & limits
|
|
|
|
- Retrieval layer rank: `0` highest among code layers.
|
|
- Entrypoint mapping is handler-symbol centric:
|
|
- decorator match -> symbol -> `handler_symbol_id`
|
|
- physical location comes from symbol span
|
|
|
|
### Known issues
|
|
|
|
- Route parsing is string-based from decorator text, not semantic AST argument parsing.
|
|
- No dedicated entrypoint tags beyond `entry_type`, `framework`, and raw decorator-derived payload.
|
|
- Background jobs and non-decorator entrypoints are not indexed.
|
|
|
|
## Dependency graph / trace current state
|
|
|
|
### Exists or stub?
|
|
|
|
- C2 exists and is populated.
|
|
- It is not a stub.
|
|
- It is also not a full-project dependency graph service; it is a set of per-edge documents stored in `rag_chunks`.
|
|
|
|
### How the graph is built
|
|
|
|
- static Python AST analysis
|
|
- no runtime instrumentation
|
|
- no import graph resolver across modules
|
|
- no tree-sitter
|
|
|
|
### Edge types in data
|
|
|
|
- `calls`
|
|
- `imports`
|
|
- `inherits`
|
|
|
|
### Traversal API
|
|
|
|
- No traversal API was found in `app/modules/rag/*` or `app/modules/agent/*`.
|
|
- No method accepts graph traversal parameters such as depth, start node, edge filters, or BFS/DFS strategy.
|
|
- Current access path is only retrieval over indexed edge documents.
|
|
|
|
## Entrypoints current state
|
|
|
|
### Implemented extraction
|
|
|
|
- HTTP routes:
|
|
- FastAPI
|
|
- Flask
|
|
- CLI:
|
|
- Typer
|
|
- Click
|
|
|
|
### Mapping model
|
|
|
|
- `entrypoint -> handler_symbol_id -> symbol span/path`
|
|
- The entrypoint record itself stores:
|
|
- framework
|
|
- entry type
|
|
- raw route/command string
|
|
- handler symbol id
|
|
|
|
### Tags/types
|
|
|
|
- `entry_type` is the main normalized tag.
|
|
- Observed values: `http`, `cli`.
|
|
- `framework` is the second discriminator.
|
|
- There are no richer endpoint taxonomies such as `job`, `worker`, `webhook`, `scheduler`.
|
|
|
|
## Defaults and operational limits
|
|
|
|
- Query mode default: `docs`
|
|
- Code mode is enabled by keyword heuristics in `RagQueryRouter`
|
|
- Retrieval hard limit: `8`
|
|
- Fallback limit: `8`
|
|
- Query term extraction limit: `6`
|
|
- Ranked source bundle for project QA:
|
|
- top `12` RAG items
|
|
- top `10` file candidates
|
|
- No exposed `namespace`, `path_prefixes`, `top_k`, `max_chars`, `max_chunks`, `max_depth` in the public/internal retrieval endpoint
|
|
|
|
`ASSUMPTION:` the absence of these controls in endpoint and service signatures means they are not part of the current supported contract, even though `RagQueryRepository.retrieve(...)` has an internal `path_prefixes` parameter.
|
|
|
|
## Known cross-cutting issues
|
|
|
|
- Retrieval contract is effectively text-only at API level; structured retrieval exists only as internal SQL parameters.
|
|
- Response payload drops explicit line spans even though spans are stored.
|
|
- Vector retrieval is coupled to a single provider-specific embedder.
|
|
- Docs mode is the default, so code retrieval depends on heuristic query phrasing unless the project/qa graph prepends `по коду`.
|
|
- There is no separate retrieval contract per layer exposed over API; all layer selection is implicit.
|
|
|
|
## Where to plug ExplainPack pipeline
|
|
|
|
### Option 1: replace or extend `project_qa/context_analysis`
|
|
|
|
- Code location:
|
|
- `app/modules/agent/engine/graphs/project_qa_step_graphs.py`
|
|
- Why:
|
|
- retrieval is already complete at this step
|
|
- input bundle already contains ranked `rag_items` and `file_candidates`
|
|
- output is already a structured `analysis_brief`
|
|
- Risk:
|
|
- low
|
|
- minimal invasion if ExplainPack consumes `source_bundle` and emits the same `analysis_brief` shape
|
|
|
|
### Option 2: insert a new orchestrator step between `context_retrieval` and `context_analysis`
|
|
|
|
- Code location:
|
|
- `app/modules/agent/engine/orchestrator/template_registry.py`
|
|
- `app/modules/agent/engine/orchestrator/step_registry.py`
|
|
- Why:
|
|
- preserves current retrieval behavior
|
|
- makes ExplainPack an explicit pipeline stage with its own artifact
|
|
- cleanest for observability and future A/B migration
|
|
- Risk:
|
|
- low to medium
|
|
- requires one new artifact contract and one extra orchestration step, but no change to retrieval storage
|
|
|
|
### Option 3: introduce ExplainPack inside `ExplainActions.extract_logic`
|
|
|
|
- Code location:
|
|
- `app/modules/agent/engine/orchestrator/actions/explain_actions.py`
|
|
- Why:
|
|
- useful if ExplainPack is meant only for explain-style scenarios
|
|
- keeps general project QA untouched
|
|
- Risk:
|
|
- medium
|
|
- narrower integration point; may create duplicate reasoning logic separate from project QA analysis path
|
|
|
|
## Bottom line
|
|
|
|
- C0-C3 are implemented and persisted in one physical store: `rag_chunks`.
|
|
- Retrieval is a hybrid SQL ranking over lexical heuristics plus pgvector distance.
|
|
- C2 exists, but only as retrievable edge documents, not as a traversable graph subsystem.
|
|
- C3 covers FastAPI/Flask/Typer/Click only.
|
|
- The least invasive ExplainPack integration point is after retrieval and before answer composition, preferably as a new explicit orchestrator artifact or as a replacement for `context_analysis`.
|