ragx-cli 0.7.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/CHANGELOG.md +33 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/CLAUDE.md +13 -6
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/PKG-INFO +38 -17
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/README.md +37 -16
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/pyproject.toml +1 -1
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/skills/ragx-cli/SKILL.md +14 -9
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/cli/app.py +8 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/cli/eval_cmd.py +2 -4
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/cli/pipeline.py +21 -10
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/config.py +7 -2
- ragx_cli-0.8.0/src/ragx/core/corpus_globs.py +29 -0
- ragx_cli-0.8.0/src/ragx/core/grouping.py +31 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/models.py +11 -2
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/query.py +42 -39
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_cli.py +7 -1
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_discovery.py +22 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_eval.py +3 -1
- ragx_cli-0.8.0/tests/test_grouping.py +55 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_integration.py +15 -11
- ragx_cli-0.8.0/tests/test_query_cmd.py +53 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/SKILL.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/api-cheatsheet.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/collection-management/python.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/collection-management/typescript.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/configuration.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/data-model.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/data-operations/python.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/data-operations/typescript.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/embedding.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/hybrid-search/python.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/hybrid-search/typescript.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/multimodal-search/python.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/multimodal-search/typescript.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/quick-start/python.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/quick-start/typescript.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/rag-system/python.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/rag-system/typescript.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/reranker.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/troubleshooting.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/vector-search/python.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.agents/skills/zvec/vector-search/typescript.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.github/workflows/publish.yml +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/.gitignore +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/AGENTS.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/CONTRACTS-PHASE23.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/CONTRACTS.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/LICENSE +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/docs/feature-temporal-weighting.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/docs/proposal-models-command-interactive-flow.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/ragx-cli-plan.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/research/bge-m3-colbert-multi-vector-late-interaction-for-ragx-rerank-alternative-and-token-level-graph-edges.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/research/bge-m3-dense-embeddings-as-ragx-provider-multilingual-quality-and-threshold-calibration.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/research/bge-m3-dense-q8-vs-nomic-q4-benchmark-2026-07-12-worktree-eval-results.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/research/bge-m3-sparse-lexical-weights-hybrid-retrieval-leg-for-ragx-vs-sqlite-fts5-bm25.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/research/bge-m3-subchunk-edge-ablation-2026-07-13-ties-again-and-beta-zero-shortlist-mechanism.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/research/fine-grained-sub-chunk-edges-with-coarse-chunk-nodes-multi-granularity-graph-rag-literature-validation.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/research/lm-studio-model-download-api-and-curated-embedding-reranker-catalog-for-ragx-models-command.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/research/subchunk-edge-ablation-2026-07-12-two-round-experiment-protocol-results-and-verdict.md +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/skills-lock.json +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/__init__.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/cli/__init__.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/cli/detect.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/cli/doctor_cmd.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/cli/init_prompt.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/cli/inspect_cmd.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/cli/models_cmd.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/cli/models_ui.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/cli/output.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/__init__.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/catalog.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/chunking.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/communities.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/discovery.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/doctor.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/errors.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/eval.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/expansion.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/fusion.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/graph.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/indexer.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/lmstudio.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/scoring.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/store.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/traversal.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/core/vectors.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/providers/__init__.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/providers/base.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/providers/llama_embedder.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/providers/llama_process.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/providers/llama_server.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/providers/lmstudio_process.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/providers/openai_compat.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/providers/registry.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/src/ragx/providers/st_reranker.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/__init__.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/conftest.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_chunking.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_communities.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_config_rc.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_doctor.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_expansion.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_fusion.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_graph.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_inspect.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_llama_server.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_lmstudio_autostart.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_models.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_providers.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_store.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_traversal.py +0 -0
- {ragx_cli-0.7.0 → ragx_cli-0.8.0}/tests/test_vectors.py +0 -0
|
@@ -4,6 +4,39 @@ All notable changes to `ragx-cli` are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.8.0] — 2026-10-20
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`query` defaults to vector search + graph traversal.** Query expansion and
|
|
12
|
+
cross-encoder rerank are now opt-in via `--expand` and `--rerank` (`QueryOptions`
|
|
13
|
+
defaults `expand=False`, `rerank=False`). A default query makes no LLM call and loads
|
|
14
|
+
no reranker. `--no-graph` is unchanged. `--no-expand` and `--no-rerank` still parse
|
|
15
|
+
as deprecated no-ops with a stderr warning. For the previous behaviour pass
|
|
16
|
+
`--expand --rerank`.
|
|
17
|
+
- **Default corpus globs index source code and text docs only.** `corpus.include` lists
|
|
18
|
+
~70 source extensions, `Dockerfile`, `Makefile`, and `*.md`/`*.mdx`/`*.rst`/`*.txt`/
|
|
19
|
+
`*.adoc`/`*.org`; `corpus.exclude` skips lockfiles, `requirements*.txt`,
|
|
20
|
+
`constraints*.txt`, licenses, minified files, source maps, and `dist/`, `build/`,
|
|
21
|
+
`target/`, `coverage/`, `vendor/`. Applies to new `ragx-cli init` configs; an existing
|
|
22
|
+
`ragx.toml` keeps its lists.
|
|
23
|
+
|
|
24
|
+
- **Query results are one entry per file (`ragx.query.v2`, breaking).** A file whose
|
|
25
|
+
chunks would appear several times in the ranked list becomes one entry with up to 3
|
|
26
|
+
sub-entries; its score is their sum, so it ranks higher. Only chunks within the visible
|
|
27
|
+
window (ranked chunks until `--top` distinct files are seen) count. JSON results are
|
|
28
|
+
`{file, score, chunks: [...]}`; chunks no longer repeat `file`. `--files-only`
|
|
29
|
+
(`ragx.files.v1`) keeps its shape and now returns the same entries. On the notes eval
|
|
30
|
+
corpus: baseline MRR .646 → .665, graph config r@5 .808 → .923 and MRR .432 → .653.
|
|
31
|
+
- **`--top` counts result entries (files), default 20** (`query.top`, was 8 chunks).
|
|
32
|
+
Existing `ragx.toml` files keep their own `query.top`. `ragx-cli eval` no longer
|
|
33
|
+
over-fetches `top × 3` chunks.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `ragx-cli init` prints a note that an AI agent can tailor `corpus.include`/`exclude`
|
|
38
|
+
to the project and add a CLAUDE.md/AGENTS.md section on using `ragx-cli query`.
|
|
39
|
+
|
|
7
40
|
## [0.7.0] — 2026-09-17
|
|
8
41
|
|
|
9
42
|
### Added
|
|
@@ -81,12 +81,13 @@ src/ragx/
|
|
|
81
81
|
cli/ # thin shells only: app.py (init/status/config + registration),
|
|
82
82
|
# pipeline.py (index/query), inspect_cmd.py, eval_cmd.py, models_cmd.py
|
|
83
83
|
# (recommend/download/configure models), doctor_cmd.py, output.py
|
|
84
|
-
tests/ #
|
|
84
|
+
tests/ # 253 tests; mocked HTTP (respx), FakeEmbedder integration tests, no live network
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
## Flows
|
|
88
88
|
|
|
89
|
-
**Index** (`ragx-cli index [--changed]`): discover files (include/exclude globs
|
|
89
|
+
**Index** (`ragx-cli index [--changed]`): discover files (include/exclude globs — defaults in
|
|
90
|
+
`core/corpus_globs.py`: source + text docs, minus lockfiles/requirements/build output — root .gitignore,
|
|
90
91
|
skip node_modules/.venv/hidden/binaries/>2MB) → xxhash diff (`--changed` = incremental, default
|
|
91
92
|
= full rebuild) → chunk (~800 tok target, chunks are exact byte slices, sub-100-char fragments
|
|
92
93
|
merge into neighbors) → embed batched → HNSW add → kNN edges per new chunk (k=8, cos ≥ 0.55)
|
|
@@ -116,8 +117,13 @@ no-op on failure) → embed all variants, HNSW top-20 each → RRF merge = seeds
|
|
|
116
117
|
(2 hops, decay 0.5, heat = max not sum, neighbor admitted only if cos(query) ≥ 0.35 floor,
|
|
117
118
|
frontier ≤ 150) → cross-encoder rerank of top-100 shortlist → final =
|
|
118
119
|
0.6·rerank + 0.25·heat + 0.15·vector (each min-max normalized; weights renormalize when a
|
|
119
|
-
stage is off).
|
|
120
|
-
|
|
120
|
+
stage is off). Since 0.8.0 expansion and rerank are OPT-IN (`--expand`/`--rerank`;
|
|
121
|
+
`QueryOptions` defaults expand=False, rerank=False; `--no-expand/--no-rerank` are hidden
|
|
122
|
+
deprecated no-ops); graph stays on, `--no-graph` skips it. Output is folded per file
|
|
123
|
+
(`core/grouping.py`, schema `ragx.query.v2`): walk ranked chunks until `top` (default 20)
|
|
124
|
+
distinct files are seen, each file keeps its ≤3 chunks inside that window, entry score = their
|
|
125
|
+
sum. The window is load-bearing — summing ALL candidates let weak graph-neighbour chunks pad
|
|
126
|
+
files (graph MRR .43 → .38; windowed .65). `--files-only` = same entries, no text; `--explain` attaches seed→parent→edge_weight→hop traces.
|
|
121
127
|
|
|
122
128
|
## State & benchmarks (as of 2026-07-10)
|
|
123
129
|
|
|
@@ -129,7 +135,8 @@ full .759/**.889**/**.613**. Key finding: graph-only HURTS precision (near-dupli
|
|
|
129
135
|
displace direct hits — parameter sweeps plateau below baseline MRR; corpus property, not a
|
|
130
136
|
tuning miss), but graph+rerank together deliver the recall win — one Dutch query's target is
|
|
131
137
|
unreachable by vector search or rerank-alone, surfaced only via a hop-1 edge then reranked
|
|
132
|
-
19→4.
|
|
138
|
+
19→4. Graph+rerank belong together for quality (`--rerank`); the 0.8.0 default (graph, no
|
|
139
|
+
rerank) is the measured "graph-only" row — chosen for speed, not quality.
|
|
133
140
|
|
|
134
141
|
UPDATE 2026-07-13: production embedding model is now `text-embedding-bge-m3` (Q8 GGUF, empty
|
|
135
142
|
doc/query prefixes) — notes-repo corpus config + index flipped in place from the worktree-built
|
|
@@ -170,7 +177,7 @@ mostly on Dutch/multilingual queries. Details: `research/bge-m3-dense-q8-vs-nomi
|
|
|
170
177
|
|
|
171
178
|
## Dev loop
|
|
172
179
|
|
|
173
|
-
`uv sync --group dev --extra rerank` · `uv run pytest -q` (
|
|
180
|
+
`uv sync --group dev --extra rerank` · `uv run pytest -q` (253 pass, ~5 s) ·
|
|
174
181
|
`uv run ruff check src tests` · file soft cap ~150 lines · expected failures raise `RagxError`
|
|
175
182
|
(CLI maps to exit 2). Live smoke: LM Studio must be running with the configured embedding model.
|
|
176
183
|
Changes that impact usage (CLI flags, config keys/precedence, output schemas, install steps)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ragx-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Similarity-graph RAG CLI: index a corpus into a chunk-level embedding graph and query it
|
|
5
5
|
Project-URL: Repository, https://github.com/alber70g/ragx-cli
|
|
6
6
|
Project-URL: Issues, https://github.com/alber70g/ragx-cli/issues
|
|
@@ -130,20 +130,30 @@ since the last index) so you can see when a re-index is due without running one.
|
|
|
130
130
|
|
|
131
131
|
### Querying
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
A default query runs vector search + graph traversal: no LLM call, no cross-encoder.
|
|
134
|
+
Expansion (`--expand`) and rerank (`--rerank`) are opt-in; `--no-graph` skips traversal,
|
|
135
|
+
which leaves pure vector search. Callers trade latency for quality per query.
|
|
136
|
+
|
|
137
|
+
Results are **one entry per file**. The pipeline ranks chunks, walks that list until it has
|
|
138
|
+
seen `--top` distinct files (default `query.top = 20`), and folds each file's chunks inside
|
|
139
|
+
that window into one entry: up to 3 sub-entries, entry score = their sum. A file that would
|
|
140
|
+
appear several times in a flat list therefore ranks higher. Chunks below the window never
|
|
141
|
+
count, so graph traversal's weak neighbour chunks cannot pad a file (measured on the notes
|
|
142
|
+
eval corpus: graph-config MRR .43 flat-list, .38 summing all chunks, .65 windowed).
|
|
135
143
|
|
|
136
144
|
```mermaid
|
|
137
145
|
flowchart TD
|
|
138
146
|
Q[query] --> X["1 · expansion (optional, one LLM call)<br/>2–4 reformulations + HyDE passage"]
|
|
139
147
|
X --> S["2 · fan-out vector search<br/>top-20 per variant"]
|
|
140
|
-
Q -. "--
|
|
148
|
+
Q -. "default (no --expand)" .-> S
|
|
141
149
|
S --> R["3 · Reciprocal Rank Fusion<br/>merged seed set"]
|
|
142
150
|
R --> H["4 · heat propagation over the graph<br/>2 hops · decay 0.5 · max-aggregation<br/>query-similarity floor · frontier cap"]
|
|
143
151
|
R -. "--no-graph" .-> K
|
|
144
|
-
H --> K["5 · cross-encoder rerank<br/>(query, chunk) pairs, capped shortlist"]
|
|
152
|
+
H --> K["5 · cross-encoder rerank (--rerank)<br/>(query, chunk) pairs, capped shortlist"]
|
|
153
|
+
H -. "default (no --rerank)" .-> F
|
|
145
154
|
K --> F["6 · combined score<br/>α·rerank + β·heat + γ·vector"]
|
|
146
|
-
F -->
|
|
155
|
+
F --> G["7 · fold per file<br/>a file's best ≤3 chunks → one entry, score = their sum"]
|
|
156
|
+
G --> O["top 20 entries (or paths via --files-only)<br/>+ --explain traversal trace"]
|
|
147
157
|
```
|
|
148
158
|
|
|
149
159
|
**Heat propagation** is what sets ragx-cli apart from plain RAG: seed chunks (from vector search)
|
|
@@ -210,8 +220,10 @@ output for that result shows the exact seed → edge → chunk path.
|
|
|
210
220
|
**Honest caveat:** graph traversal *alone* hurts precision on this corpus (MRR 0.593 → 0.522) —
|
|
211
221
|
near-duplicate neighbors (e.g. adjacent meeting notes) displace weaker direct hits. A parameter
|
|
212
222
|
sweep over decay/floor/weights plateaued below baseline MRR, so this is a property of
|
|
213
|
-
similarity-only edges, not a tuning miss. Conclusion
|
|
214
|
-
|
|
223
|
+
similarity-only edges, not a tuning miss. Conclusion: **graph and rerank belong together**.
|
|
224
|
+
Since 0.8.0 the CLI default is the fast path (vector + graph, no rerank), which is the
|
|
225
|
+
"graph-only" row above; pass `--rerank` for the measured best quality, or `--no-graph` for
|
|
226
|
+
pure vector search.
|
|
215
227
|
|
|
216
228
|
The parameter study below explains the mechanism behind this caveat — and shows that once the
|
|
217
229
|
scoring weights stop putting heat into the final score, the graph win gets much bigger.
|
|
@@ -432,13 +444,16 @@ anything. If a run dies with "connection refused", `doctor` names the stage and
|
|
|
432
444
|
|
|
433
445
|
## Agent-first conventions
|
|
434
446
|
|
|
435
|
-
- `--json` emits exactly one JSON document on stdout (versioned schemas: `ragx.query.
|
|
447
|
+
- `--json` emits exactly one JSON document on stdout (versioned schemas: `ragx.query.v2`,
|
|
436
448
|
`ragx.files.v1`, `ragx.status.v1`, `ragx.doctor.v1`, `ragx.eval.v1`, `ragx.inspect.*.v1`); logs go
|
|
437
449
|
to stderr.
|
|
438
450
|
- Exit codes: `0` results, `1` success-but-empty, `2` error.
|
|
439
|
-
-
|
|
440
|
-
|
|
441
|
-
|
|
451
|
+
- `ragx.query.v2` results are one entry per file: `{file, score, chunks: [...]}`. Each chunk
|
|
452
|
+
carries `chunk_id`, `line_start/line_end`, `byte_start/byte_end`, `score`, `breakdown`, `text`,
|
|
453
|
+
`truncated` — agents jump to the exact source location and read the full text themselves
|
|
454
|
+
(JSON chunk text is truncated). v1 (one flat entry per chunk) is gone since 0.8.0.
|
|
455
|
+
- `--files-only` returns the same entries without text (`file`, `score`, `chunk_ids`) — the
|
|
456
|
+
mode coding agents use most.
|
|
442
457
|
- `ragx-cli status` includes a `drift` object (`new`/`changed`/`deleted` file counts vs the index) —
|
|
443
458
|
agents check it to decide whether to run `ragx-cli index` before querying.
|
|
444
459
|
- `ragx-cli doctor --json` verifies the providers answer before a batch of queries (`ok: false`
|
|
@@ -458,10 +473,11 @@ This repo has a ragx-cli index (`.ragx/`). Prefer it over grep for "where is X d
|
|
|
458
473
|
questions; fall back to grep for exact identifiers.
|
|
459
474
|
|
|
460
475
|
- Find relevant files: `ragx-cli query "<natural-language question>" --json --files-only`
|
|
461
|
-
- Get chunks with exact locations: `ragx-cli query "..." --json
|
|
462
|
-
`
|
|
463
|
-
for full context.
|
|
464
|
-
-
|
|
476
|
+
- Get chunks with exact locations: `ragx-cli query "..." --json` — one result per `file`
|
|
477
|
+
(default 20, `--top N`), each with up to 3 `chunks` carrying `line_start/line_end`; the JSON
|
|
478
|
+
`text` is truncated, so read the file yourself for full context.
|
|
479
|
+
- The default query makes no LLM call and loads no cross-encoder. For higher precision add
|
|
480
|
+
`--rerank`; add `--expand` for an LLM query-expansion pass (one extra LLM call).
|
|
465
481
|
- After adding or editing files: `ragx-cli index` (incremental and cheap — hash-based).
|
|
466
482
|
`ragx-cli status --json` shows `drift` counts if you want to check staleness first.
|
|
467
483
|
- stdout is exactly one JSON document; logs are on stderr.
|
|
@@ -611,10 +627,15 @@ non-thinking model is the better expansion choice; detection is a best-effort na
|
|
|
611
627
|
heuristic, the APIs expose no capability flag), and corpus include/exclude
|
|
612
628
|
(gitignore-style globs, comma-separated). Every prompt is pre-filled with the defaults
|
|
613
629
|
below — Enter accepts them all. With piped stdin (agents), `--yes`, or
|
|
614
|
-
`--no-interactive`, it writes the defaults unchanged, exactly as before.
|
|
630
|
+
`--no-interactive`, it writes the defaults unchanged, exactly as before. After writing the
|
|
631
|
+
config, `init` prints a note: an AI agent can tailor `corpus.include`/`exclude` to the
|
|
632
|
+
project, and add a CLAUDE.md/AGENTS.md section on searching it with `ragx-cli query`
|
|
633
|
+
(see [Using ragx-cli from a coding agent](#using-ragx-cli-from-a-coding-agent-claudemd--agentsmd)).
|
|
634
|
+
Key defaults:
|
|
615
635
|
|
|
616
636
|
| section | defaults |
|
|
617
637
|
|---|---|
|
|
638
|
+
| `[corpus]` | `include` = source code (`*.py`, `*.ts`, `*.go`, `*.rs`, … ~70 extensions, plus `Dockerfile`/`Makefile`) and text docs (`*.md`, `*.mdx`, `*.rst`, `*.txt`, `*.adoc`, `*.org`); `exclude` = lockfiles (`*.lock`, `package-lock.json`, `pnpm-lock.yaml`, `go.sum`, …), `requirements*.txt`/`constraints*.txt`, `LICENSE*`/`COPYING*`, `*.min.js`/`*.min.css`/`*.map`, and `dist/`, `build/`, `target/`, `coverage/`, `vendor/`; `respect_gitignore=true`. Full lists: `src/ragx/core/corpus_globs.py`. Existing `ragx.toml` files keep their own lists |
|
|
618
639
|
| `[chunking]` | `size_tokens=800`, `overlap=0.15` |
|
|
619
640
|
| `[graph]` | `k=8`, `min_edge_sim=0.55`, `edge_source="chunk"`, `subchunk_size_tokens=128`, `near_dup_sim=0.9` |
|
|
620
641
|
| `[traversal]` | `hops=2`, `decay=0.5`, `query_floor=0.35`, `max_frontier=150` |
|
|
@@ -102,20 +102,30 @@ since the last index) so you can see when a re-index is due without running one.
|
|
|
102
102
|
|
|
103
103
|
### Querying
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
A default query runs vector search + graph traversal: no LLM call, no cross-encoder.
|
|
106
|
+
Expansion (`--expand`) and rerank (`--rerank`) are opt-in; `--no-graph` skips traversal,
|
|
107
|
+
which leaves pure vector search. Callers trade latency for quality per query.
|
|
108
|
+
|
|
109
|
+
Results are **one entry per file**. The pipeline ranks chunks, walks that list until it has
|
|
110
|
+
seen `--top` distinct files (default `query.top = 20`), and folds each file's chunks inside
|
|
111
|
+
that window into one entry: up to 3 sub-entries, entry score = their sum. A file that would
|
|
112
|
+
appear several times in a flat list therefore ranks higher. Chunks below the window never
|
|
113
|
+
count, so graph traversal's weak neighbour chunks cannot pad a file (measured on the notes
|
|
114
|
+
eval corpus: graph-config MRR .43 flat-list, .38 summing all chunks, .65 windowed).
|
|
107
115
|
|
|
108
116
|
```mermaid
|
|
109
117
|
flowchart TD
|
|
110
118
|
Q[query] --> X["1 · expansion (optional, one LLM call)<br/>2–4 reformulations + HyDE passage"]
|
|
111
119
|
X --> S["2 · fan-out vector search<br/>top-20 per variant"]
|
|
112
|
-
Q -. "--
|
|
120
|
+
Q -. "default (no --expand)" .-> S
|
|
113
121
|
S --> R["3 · Reciprocal Rank Fusion<br/>merged seed set"]
|
|
114
122
|
R --> H["4 · heat propagation over the graph<br/>2 hops · decay 0.5 · max-aggregation<br/>query-similarity floor · frontier cap"]
|
|
115
123
|
R -. "--no-graph" .-> K
|
|
116
|
-
H --> K["5 · cross-encoder rerank<br/>(query, chunk) pairs, capped shortlist"]
|
|
124
|
+
H --> K["5 · cross-encoder rerank (--rerank)<br/>(query, chunk) pairs, capped shortlist"]
|
|
125
|
+
H -. "default (no --rerank)" .-> F
|
|
117
126
|
K --> F["6 · combined score<br/>α·rerank + β·heat + γ·vector"]
|
|
118
|
-
F -->
|
|
127
|
+
F --> G["7 · fold per file<br/>a file's best ≤3 chunks → one entry, score = their sum"]
|
|
128
|
+
G --> O["top 20 entries (or paths via --files-only)<br/>+ --explain traversal trace"]
|
|
119
129
|
```
|
|
120
130
|
|
|
121
131
|
**Heat propagation** is what sets ragx-cli apart from plain RAG: seed chunks (from vector search)
|
|
@@ -182,8 +192,10 @@ output for that result shows the exact seed → edge → chunk path.
|
|
|
182
192
|
**Honest caveat:** graph traversal *alone* hurts precision on this corpus (MRR 0.593 → 0.522) —
|
|
183
193
|
near-duplicate neighbors (e.g. adjacent meeting notes) displace weaker direct hits. A parameter
|
|
184
194
|
sweep over decay/floor/weights plateaued below baseline MRR, so this is a property of
|
|
185
|
-
similarity-only edges, not a tuning miss. Conclusion
|
|
186
|
-
|
|
195
|
+
similarity-only edges, not a tuning miss. Conclusion: **graph and rerank belong together**.
|
|
196
|
+
Since 0.8.0 the CLI default is the fast path (vector + graph, no rerank), which is the
|
|
197
|
+
"graph-only" row above; pass `--rerank` for the measured best quality, or `--no-graph` for
|
|
198
|
+
pure vector search.
|
|
187
199
|
|
|
188
200
|
The parameter study below explains the mechanism behind this caveat — and shows that once the
|
|
189
201
|
scoring weights stop putting heat into the final score, the graph win gets much bigger.
|
|
@@ -404,13 +416,16 @@ anything. If a run dies with "connection refused", `doctor` names the stage and
|
|
|
404
416
|
|
|
405
417
|
## Agent-first conventions
|
|
406
418
|
|
|
407
|
-
- `--json` emits exactly one JSON document on stdout (versioned schemas: `ragx.query.
|
|
419
|
+
- `--json` emits exactly one JSON document on stdout (versioned schemas: `ragx.query.v2`,
|
|
408
420
|
`ragx.files.v1`, `ragx.status.v1`, `ragx.doctor.v1`, `ragx.eval.v1`, `ragx.inspect.*.v1`); logs go
|
|
409
421
|
to stderr.
|
|
410
422
|
- Exit codes: `0` results, `1` success-but-empty, `2` error.
|
|
411
|
-
-
|
|
412
|
-
|
|
413
|
-
|
|
423
|
+
- `ragx.query.v2` results are one entry per file: `{file, score, chunks: [...]}`. Each chunk
|
|
424
|
+
carries `chunk_id`, `line_start/line_end`, `byte_start/byte_end`, `score`, `breakdown`, `text`,
|
|
425
|
+
`truncated` — agents jump to the exact source location and read the full text themselves
|
|
426
|
+
(JSON chunk text is truncated). v1 (one flat entry per chunk) is gone since 0.8.0.
|
|
427
|
+
- `--files-only` returns the same entries without text (`file`, `score`, `chunk_ids`) — the
|
|
428
|
+
mode coding agents use most.
|
|
414
429
|
- `ragx-cli status` includes a `drift` object (`new`/`changed`/`deleted` file counts vs the index) —
|
|
415
430
|
agents check it to decide whether to run `ragx-cli index` before querying.
|
|
416
431
|
- `ragx-cli doctor --json` verifies the providers answer before a batch of queries (`ok: false`
|
|
@@ -430,10 +445,11 @@ This repo has a ragx-cli index (`.ragx/`). Prefer it over grep for "where is X d
|
|
|
430
445
|
questions; fall back to grep for exact identifiers.
|
|
431
446
|
|
|
432
447
|
- Find relevant files: `ragx-cli query "<natural-language question>" --json --files-only`
|
|
433
|
-
- Get chunks with exact locations: `ragx-cli query "..." --json
|
|
434
|
-
`
|
|
435
|
-
for full context.
|
|
436
|
-
-
|
|
448
|
+
- Get chunks with exact locations: `ragx-cli query "..." --json` — one result per `file`
|
|
449
|
+
(default 20, `--top N`), each with up to 3 `chunks` carrying `line_start/line_end`; the JSON
|
|
450
|
+
`text` is truncated, so read the file yourself for full context.
|
|
451
|
+
- The default query makes no LLM call and loads no cross-encoder. For higher precision add
|
|
452
|
+
`--rerank`; add `--expand` for an LLM query-expansion pass (one extra LLM call).
|
|
437
453
|
- After adding or editing files: `ragx-cli index` (incremental and cheap — hash-based).
|
|
438
454
|
`ragx-cli status --json` shows `drift` counts if you want to check staleness first.
|
|
439
455
|
- stdout is exactly one JSON document; logs are on stderr.
|
|
@@ -583,10 +599,15 @@ non-thinking model is the better expansion choice; detection is a best-effort na
|
|
|
583
599
|
heuristic, the APIs expose no capability flag), and corpus include/exclude
|
|
584
600
|
(gitignore-style globs, comma-separated). Every prompt is pre-filled with the defaults
|
|
585
601
|
below — Enter accepts them all. With piped stdin (agents), `--yes`, or
|
|
586
|
-
`--no-interactive`, it writes the defaults unchanged, exactly as before.
|
|
602
|
+
`--no-interactive`, it writes the defaults unchanged, exactly as before. After writing the
|
|
603
|
+
config, `init` prints a note: an AI agent can tailor `corpus.include`/`exclude` to the
|
|
604
|
+
project, and add a CLAUDE.md/AGENTS.md section on searching it with `ragx-cli query`
|
|
605
|
+
(see [Using ragx-cli from a coding agent](#using-ragx-cli-from-a-coding-agent-claudemd--agentsmd)).
|
|
606
|
+
Key defaults:
|
|
587
607
|
|
|
588
608
|
| section | defaults |
|
|
589
609
|
|---|---|
|
|
610
|
+
| `[corpus]` | `include` = source code (`*.py`, `*.ts`, `*.go`, `*.rs`, … ~70 extensions, plus `Dockerfile`/`Makefile`) and text docs (`*.md`, `*.mdx`, `*.rst`, `*.txt`, `*.adoc`, `*.org`); `exclude` = lockfiles (`*.lock`, `package-lock.json`, `pnpm-lock.yaml`, `go.sum`, …), `requirements*.txt`/`constraints*.txt`, `LICENSE*`/`COPYING*`, `*.min.js`/`*.min.css`/`*.map`, and `dist/`, `build/`, `target/`, `coverage/`, `vendor/`; `respect_gitignore=true`. Full lists: `src/ragx/core/corpus_globs.py`. Existing `ragx.toml` files keep their own lists |
|
|
590
611
|
| `[chunking]` | `size_tokens=800`, `overlap=0.15` |
|
|
591
612
|
| `[graph]` | `k=8`, `min_edge_sim=0.55`, `edge_source="chunk"`, `subchunk_size_tokens=128`, `near_dup_sim=0.9` |
|
|
592
613
|
| `[traversal]` | `hops=2`, `decay=0.5`, `query_floor=0.35`, `max_frontier=150` |
|
|
@@ -11,8 +11,9 @@ description: |
|
|
|
11
11
|
# ragx-cli — semantic search over an indexed corpus
|
|
12
12
|
|
|
13
13
|
Indexes files into a chunk-level embedding similarity graph; answers queries via vector search +
|
|
14
|
-
graph traversal
|
|
15
|
-
deterministic exit codes, byte-exact source
|
|
14
|
+
graph traversal, with optional cross-encoder rerank (`--rerank`) and LLM expansion (`--expand`).
|
|
15
|
+
Agent-first: one JSON doc on stdout, logs on stderr, deterministic exit codes, byte-exact source
|
|
16
|
+
locations.
|
|
16
17
|
|
|
17
18
|
## When
|
|
18
19
|
|
|
@@ -29,24 +30,28 @@ A corpus is ragx-indexed if its root has `ragx.toml` and `.ragx/`.
|
|
|
29
30
|
```bash
|
|
30
31
|
ragx-cli status --json # root, model, counts, and drift (new/changed/deleted vs index)
|
|
31
32
|
ragx-cli query "question" --json --files-only # which files are relevant — most-used mode
|
|
32
|
-
ragx-cli query "..." --json --top
|
|
33
|
-
ragx-cli query "..." --json --
|
|
33
|
+
ragx-cli query "..." --json --top 10 # one entry per file, best chunks + exact locations
|
|
34
|
+
ragx-cli query "..." --json --rerank # higher precision: cross-encoder rerank (slower)
|
|
34
35
|
echo "..." | ragx-cli query - --json # long/multiline query from stdin
|
|
35
36
|
ragx-cli index # incremental reindex (hash-based, cheap) — the default
|
|
36
37
|
ragx-cli index --full # full rebuild: re-chunks + re-embeds everything
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
Other query flags: `--no-graph
|
|
40
|
+
Other query flags: `--expand` (one LLM call: multi-query + HyDE), `--no-graph` (pure vector),
|
|
41
|
+
`--hops N`, `--explain`. `--no-expand`/`--no-rerank` are deprecated no-ops (both stages are off
|
|
42
|
+
by default).
|
|
40
43
|
|
|
41
44
|
## Output
|
|
42
45
|
|
|
43
|
-
- stdout is exactly one JSON doc (`ragx.query.
|
|
46
|
+
- stdout is exactly one JSON doc (`ragx.query.v2` / `ragx.files.v1` / `ragx.status.v1`); logs go
|
|
44
47
|
to stderr. Never parse stderr.
|
|
45
48
|
- Exit codes: `0` results, `1` success-but-empty (report "nothing found", don't retry as failure),
|
|
46
49
|
`2` error.
|
|
47
|
-
-
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
+
- `ragx.query.v2` results are one entry per file (default 20): `file`, `score` (sum of its
|
|
51
|
+
chunks), `chunks` (up to 3, each with `line_start/end`, `byte_start/end`, `score`,
|
|
52
|
+
`breakdown`, `text`). JSON `text` is truncated — read the file at the line range for full
|
|
53
|
+
context.
|
|
54
|
+
- Typical flow: `--files-only` to pick files → `--json` for line ranges → read files at ranges.
|
|
50
55
|
|
|
51
56
|
## Freshness
|
|
52
57
|
|
|
@@ -27,6 +27,13 @@ from ragx.core.config import (
|
|
|
27
27
|
from ragx.core.errors import RagxError
|
|
28
28
|
|
|
29
29
|
REPO_URL = "https://github.com/alber70g/ragx-cli"
|
|
30
|
+
INIT_NOTE = (
|
|
31
|
+
"note: corpus.include/exclude default to source code and text docs, minus lockfiles,\n"
|
|
32
|
+
"dependency manifests and build output. An AI agent (Claude Code, Codex, ...) can:\n"
|
|
33
|
+
f" 1. tailor corpus.include/exclude in {CONFIG_FILE} to this project's files\n"
|
|
34
|
+
" 2. add a section to CLAUDE.md or AGENTS.md telling agents to search this corpus\n"
|
|
35
|
+
' with `ragx-cli query "..."` (--json for machine output, --files-only for paths)'
|
|
36
|
+
)
|
|
30
37
|
|
|
31
38
|
app = typer.Typer(add_completion=False, no_args_is_help=True)
|
|
32
39
|
config_app = typer.Typer(add_completion=False, no_args_is_help=True)
|
|
@@ -133,6 +140,7 @@ def init(
|
|
|
133
140
|
else:
|
|
134
141
|
cfg_path = write_default_config(root)
|
|
135
142
|
typer.echo(f"created {cfg_path}")
|
|
143
|
+
typer.echo(INIT_NOTE)
|
|
136
144
|
if ask and typer.confirm(
|
|
137
145
|
"recommend + download models via LM Studio now? (rerun anytime: `ragx-cli models`)",
|
|
138
146
|
default=False,
|
|
@@ -59,10 +59,8 @@ def eval_cmd(
|
|
|
59
59
|
reranker=reranker if opts.rerank else None,
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
-
#
|
|
63
|
-
selected = [
|
|
64
|
-
(name, replace(_BUILTIN_CONFIGS[name], top=top * 3)) for name in names
|
|
65
|
-
]
|
|
62
|
+
# query results are already one entry per file, ranked
|
|
63
|
+
selected = [(name, replace(_BUILTIN_CONFIGS[name], top=top)) for name in names]
|
|
66
64
|
result = evaluate(queries, selected, query_fn, top=top)
|
|
67
65
|
|
|
68
66
|
if json_out:
|
|
@@ -64,16 +64,25 @@ def _generator(cfg: Config):
|
|
|
64
64
|
|
|
65
65
|
def query(
|
|
66
66
|
text: str = typer.Argument(..., help="query text, or '-' to read from stdin"),
|
|
67
|
-
top: int | None = typer.Option(None, "--top"),
|
|
67
|
+
top: int | None = typer.Option(None, "--top", help="result entries (files); default query.top = 20"),
|
|
68
68
|
json_out: bool = typer.Option(False, "--json"),
|
|
69
69
|
files_only: bool = typer.Option(False, "--files-only"),
|
|
70
|
-
|
|
70
|
+
expand: bool = typer.Option(False, "--expand", help="LLM query expansion (multi-query + HyDE)"),
|
|
71
71
|
no_graph: bool = typer.Option(False, "--no-graph"),
|
|
72
|
-
|
|
72
|
+
rerank: bool = typer.Option(False, "--rerank", help="cross-encoder rerank of the shortlist"),
|
|
73
|
+
no_expand: bool = typer.Option(
|
|
74
|
+
False, "--no-expand", hidden=True, help="deprecated: expansion is now off by default"
|
|
75
|
+
),
|
|
76
|
+
no_rerank: bool = typer.Option(
|
|
77
|
+
False, "--no-rerank", hidden=True, help="deprecated: rerank is now off by default"
|
|
78
|
+
),
|
|
73
79
|
hops: int | None = typer.Option(None, "--hops"),
|
|
74
80
|
explain: bool = typer.Option(False, "--explain"),
|
|
75
81
|
) -> None:
|
|
76
|
-
"""Search the index
|
|
82
|
+
"""Search the index: one entry per file with its best chunks (paths only with --files-only)."""
|
|
83
|
+
for flag, used in (("--no-expand", no_expand), ("--no-rerank", no_rerank)):
|
|
84
|
+
if used:
|
|
85
|
+
print(f"warning: {flag} is deprecated; that stage is now off by default", file=sys.stderr)
|
|
77
86
|
if text == "-":
|
|
78
87
|
text = sys.stdin.read().strip()
|
|
79
88
|
try:
|
|
@@ -83,9 +92,9 @@ def query(
|
|
|
83
92
|
opts = QueryOptions(
|
|
84
93
|
top=top if top is not None else cfg.get("query.top"),
|
|
85
94
|
files_only=files_only,
|
|
86
|
-
expand=
|
|
95
|
+
expand=expand,
|
|
87
96
|
graph=not no_graph,
|
|
88
|
-
rerank=
|
|
97
|
+
rerank=rerank,
|
|
89
98
|
hops=hops,
|
|
90
99
|
explain=explain,
|
|
91
100
|
)
|
|
@@ -105,9 +114,11 @@ def query(
|
|
|
105
114
|
for f in to_files_json(out)["files"]:
|
|
106
115
|
typer.echo(f"{f['score']:.4f} {f['file']}")
|
|
107
116
|
else:
|
|
108
|
-
for
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
for f in out.results:
|
|
118
|
+
typer.echo(f"{f.score:.4f} {f.file_path}")
|
|
119
|
+
for r in f.chunks:
|
|
120
|
+
lines = f"{r.chunk.line_start}-{r.chunk.line_end}"
|
|
121
|
+
first_line = r.chunk.text.strip().splitlines()[0][:90] if r.chunk.text.strip() else ""
|
|
122
|
+
typer.echo(f" {r.score:.4f} {lines:<9} {first_line}")
|
|
112
123
|
if not out.results:
|
|
113
124
|
raise typer.Exit(code=1)
|
|
@@ -13,6 +13,7 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
import tomli_w
|
|
15
15
|
|
|
16
|
+
from ragx.core.corpus_globs import DEFAULT_EXCLUDE, DEFAULT_INCLUDE
|
|
16
17
|
from ragx.core.errors import NotInitializedError, RagxError
|
|
17
18
|
|
|
18
19
|
RAGX_DIR = ".ragx"
|
|
@@ -26,7 +27,11 @@ def rc_path() -> Path:
|
|
|
26
27
|
return Path.home() / ".ragxrc"
|
|
27
28
|
|
|
28
29
|
DEFAULTS: dict[str, dict[str, Any]] = {
|
|
29
|
-
"corpus": {
|
|
30
|
+
"corpus": {
|
|
31
|
+
"include": DEFAULT_INCLUDE,
|
|
32
|
+
"exclude": DEFAULT_EXCLUDE,
|
|
33
|
+
"respect_gitignore": True,
|
|
34
|
+
},
|
|
30
35
|
"chunking": {"size_tokens": 800, "overlap": 0.15},
|
|
31
36
|
"graph": {
|
|
32
37
|
"k": 8,
|
|
@@ -69,7 +74,7 @@ DEFAULTS: dict[str, dict[str, Any]] = {
|
|
|
69
74
|
"gguf": "", # llama-server only: path to the reranker GGUF (LM Studio dir works)
|
|
70
75
|
"server_bin": "llama-server", # llama-server only: binary name or path
|
|
71
76
|
},
|
|
72
|
-
"query": {"top":
|
|
77
|
+
"query": {"top": 20, "max_chunk_chars": 1200}, # top = result entries (one per file)
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Default corpus globs: index source code and prose; skip lockfiles, dependency
|
|
2
|
+
manifests, licenses, minified and build output. gitignore-style patterns — a pattern
|
|
3
|
+
without a slash matches at any depth. Projects tailor these in ragx.toml."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
DOC_EXTS = ["md", "mdx", "markdown", "rst", "txt", "adoc", "org"]
|
|
8
|
+
|
|
9
|
+
SOURCE_EXTS = [
|
|
10
|
+
"py", "pyi", "js", "jsx", "mjs", "cjs", "ts", "tsx", "mts", "cts", "vue", "svelte",
|
|
11
|
+
"astro", "go", "rs", "java", "kt", "kts", "scala", "groovy", "c", "h", "cc", "cpp",
|
|
12
|
+
"cxx", "hpp", "hh", "cs", "fs", "swift", "m", "mm", "rb", "php", "pl", "pm", "lua",
|
|
13
|
+
"r", "R", "dart", "ex", "exs", "erl", "hrl", "clj", "cljs", "hs", "ml", "mli", "elm",
|
|
14
|
+
"zig", "nim", "jl", "sh", "bash", "zsh", "fish", "ps1", "sql", "graphql", "gql",
|
|
15
|
+
"proto", "tf", "hcl", "html", "css", "scss", "sass", "less",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
DEFAULT_INCLUDE = [f"*.{ext}" for ext in DOC_EXTS + SOURCE_EXTS] + ["Dockerfile", "Makefile"]
|
|
19
|
+
|
|
20
|
+
DEFAULT_EXCLUDE = [
|
|
21
|
+
# lockfiles and dependency manifests
|
|
22
|
+
"*.lock", "bun.lockb", "package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml",
|
|
23
|
+
"go.sum", "requirements*.txt", "constraints*.txt",
|
|
24
|
+
# licenses
|
|
25
|
+
"LICENSE*", "COPYING*",
|
|
26
|
+
# minified and generated output
|
|
27
|
+
"*.min.js", "*.min.css", "*.map",
|
|
28
|
+
"dist/", "build/", "target/", "coverage/", "vendor/",
|
|
29
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Fold ranked chunks into one entry per file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
from ragx.core.models import ScoredChunk, ScoredFile
|
|
8
|
+
|
|
9
|
+
PER_FILE = 3
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def group_by_file(ranked: Iterable[ScoredChunk], top: int, per_file: int = PER_FILE) -> list[ScoredFile]:
|
|
13
|
+
"""Fold the chunks a flat best-first list would show into one entry per file.
|
|
14
|
+
|
|
15
|
+
The window ends where the flat list reaches `top` distinct files: chunks below it never
|
|
16
|
+
count. Each file keeps its `per_file` best chunks inside the window and scores as their
|
|
17
|
+
sum, so a file that repeats in the list outranks one that appears once. Without the
|
|
18
|
+
window, graph traversal's many weak neighbour chunks pad multi-chunk files past strong
|
|
19
|
+
single-chunk files (measured: graph MRR .43 -> .38 unwindowed, .65 windowed).
|
|
20
|
+
|
|
21
|
+
`ranked` must be best-first; returns at most `top` files, best-first."""
|
|
22
|
+
by_file: dict[str, list[ScoredChunk]] = {}
|
|
23
|
+
for r in ranked:
|
|
24
|
+
if r.chunk.file_path not in by_file and len(by_file) == top:
|
|
25
|
+
break # the flat list would show `top` files by now; later chunks never surface
|
|
26
|
+
hits = by_file.setdefault(r.chunk.file_path, [])
|
|
27
|
+
if len(hits) < per_file:
|
|
28
|
+
hits.append(r)
|
|
29
|
+
files = [ScoredFile(path, sum(h.score for h in hits), hits) for path, hits in by_file.items()]
|
|
30
|
+
files.sort(key=lambda f: (-f.score, f.file_path))
|
|
31
|
+
return files[:top]
|
|
@@ -58,10 +58,19 @@ class ScoredChunk:
|
|
|
58
58
|
explain: dict | None = None # traversal trace: seeds, edge paths, weights
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
@dataclass
|
|
62
|
+
class ScoredFile:
|
|
63
|
+
"""One result entry: a file's best chunks folded together, scored as their sum."""
|
|
64
|
+
|
|
65
|
+
file_path: str
|
|
66
|
+
score: float
|
|
67
|
+
chunks: list[ScoredChunk] # best-first
|
|
68
|
+
|
|
69
|
+
|
|
61
70
|
@dataclass
|
|
62
71
|
class QueryOutput:
|
|
63
|
-
"""Top-level result of the query pipeline; serialized as ragx.query.
|
|
72
|
+
"""Top-level result of the query pipeline; serialized as ragx.query.v2."""
|
|
64
73
|
|
|
65
74
|
query: str
|
|
66
75
|
variants: list[str] = field(default_factory=list)
|
|
67
|
-
results: list[
|
|
76
|
+
results: list[ScoredFile] = field(default_factory=list)
|