ragx-cli 0.5.0__tar.gz → 0.6.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.
Files changed (102) hide show
  1. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/CHANGELOG.md +16 -0
  2. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/PKG-INFO +24 -10
  3. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/README.md +23 -9
  4. ragx_cli-0.6.0/docs/proposal-models-command-interactive-flow.md +131 -0
  5. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/pyproject.toml +1 -1
  6. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/cli/app.py +54 -0
  7. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/cli/models_cmd.py +72 -40
  8. ragx_cli-0.6.0/src/ragx/cli/models_ui.py +117 -0
  9. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/catalog.py +52 -10
  10. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_cli.py +36 -1
  11. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_models.py +123 -0
  12. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/SKILL.md +0 -0
  13. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/api-cheatsheet.md +0 -0
  14. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/collection-management/python.md +0 -0
  15. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/collection-management/typescript.md +0 -0
  16. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/configuration.md +0 -0
  17. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/data-model.md +0 -0
  18. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/data-operations/python.md +0 -0
  19. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/data-operations/typescript.md +0 -0
  20. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/embedding.md +0 -0
  21. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/hybrid-search/python.md +0 -0
  22. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/hybrid-search/typescript.md +0 -0
  23. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/multimodal-search/python.md +0 -0
  24. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/multimodal-search/typescript.md +0 -0
  25. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/quick-start/python.md +0 -0
  26. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/quick-start/typescript.md +0 -0
  27. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/rag-system/python.md +0 -0
  28. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/rag-system/typescript.md +0 -0
  29. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/reranker.md +0 -0
  30. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/troubleshooting.md +0 -0
  31. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/vector-search/python.md +0 -0
  32. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.agents/skills/zvec/vector-search/typescript.md +0 -0
  33. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.github/workflows/publish.yml +0 -0
  34. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/.gitignore +0 -0
  35. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/AGENTS.md +0 -0
  36. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/CLAUDE.md +0 -0
  37. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/CONTRACTS-PHASE23.md +0 -0
  38. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/CONTRACTS.md +0 -0
  39. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/LICENSE +0 -0
  40. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/docs/feature-temporal-weighting.md +0 -0
  41. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/ragx-cli-plan.md +0 -0
  42. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/research/bge-m3-colbert-multi-vector-late-interaction-for-ragx-rerank-alternative-and-token-level-graph-edges.md +0 -0
  43. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/research/bge-m3-dense-embeddings-as-ragx-provider-multilingual-quality-and-threshold-calibration.md +0 -0
  44. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/research/bge-m3-dense-q8-vs-nomic-q4-benchmark-2026-07-12-worktree-eval-results.md +0 -0
  45. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/research/bge-m3-sparse-lexical-weights-hybrid-retrieval-leg-for-ragx-vs-sqlite-fts5-bm25.md +0 -0
  46. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/research/bge-m3-subchunk-edge-ablation-2026-07-13-ties-again-and-beta-zero-shortlist-mechanism.md +0 -0
  47. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/research/fine-grained-sub-chunk-edges-with-coarse-chunk-nodes-multi-granularity-graph-rag-literature-validation.md +0 -0
  48. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/research/lm-studio-model-download-api-and-curated-embedding-reranker-catalog-for-ragx-models-command.md +0 -0
  49. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/research/subchunk-edge-ablation-2026-07-12-two-round-experiment-protocol-results-and-verdict.md +0 -0
  50. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/skills/ragx-cli/SKILL.md +0 -0
  51. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/skills-lock.json +0 -0
  52. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/__init__.py +0 -0
  53. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/cli/__init__.py +0 -0
  54. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/cli/detect.py +0 -0
  55. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/cli/eval_cmd.py +0 -0
  56. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/cli/init_prompt.py +0 -0
  57. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/cli/inspect_cmd.py +0 -0
  58. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/cli/output.py +0 -0
  59. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/cli/pipeline.py +0 -0
  60. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/__init__.py +0 -0
  61. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/chunking.py +0 -0
  62. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/communities.py +0 -0
  63. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/config.py +0 -0
  64. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/discovery.py +0 -0
  65. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/errors.py +0 -0
  66. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/eval.py +0 -0
  67. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/expansion.py +0 -0
  68. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/fusion.py +0 -0
  69. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/graph.py +0 -0
  70. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/indexer.py +0 -0
  71. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/lmstudio.py +0 -0
  72. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/models.py +0 -0
  73. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/query.py +0 -0
  74. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/scoring.py +0 -0
  75. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/store.py +0 -0
  76. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/traversal.py +0 -0
  77. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/core/vectors.py +0 -0
  78. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/providers/__init__.py +0 -0
  79. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/providers/base.py +0 -0
  80. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/providers/llama_embedder.py +0 -0
  81. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/providers/llama_process.py +0 -0
  82. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/providers/llama_server.py +0 -0
  83. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/providers/openai_compat.py +0 -0
  84. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/providers/registry.py +0 -0
  85. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/src/ragx/providers/st_reranker.py +0 -0
  86. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/__init__.py +0 -0
  87. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/conftest.py +0 -0
  88. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_chunking.py +0 -0
  89. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_communities.py +0 -0
  90. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_config_rc.py +0 -0
  91. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_discovery.py +0 -0
  92. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_eval.py +0 -0
  93. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_expansion.py +0 -0
  94. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_fusion.py +0 -0
  95. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_graph.py +0 -0
  96. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_inspect.py +0 -0
  97. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_integration.py +0 -0
  98. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_llama_server.py +0 -0
  99. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_providers.py +0 -0
  100. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_store.py +0 -0
  101. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_traversal.py +0 -0
  102. {ragx_cli-0.5.0 → ragx_cli-0.6.0}/tests/test_vectors.py +0 -0
@@ -4,6 +4,22 @@ 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.6.0] — 2026-07-15
8
+
9
+ ### Changed
10
+
11
+ - **`ragx-cli models` interactive flow redesigned** around context → choice → consequence →
12
+ commit: a machine header (RAM, platform, `llama-server`/`lms` detection) prints first;
13
+ the quality tier is picked from a numbered comparison table (model, size, languages,
14
+ context, license) and the serving engines from numbered menus with one-line tradeoffs —
15
+ recommended defaults marked `»`, invalid input re-prompts; a plan step (download sizes,
16
+ `already downloaded ✓`, config sections, re-index consequence) shows before the confirm.
17
+ Behavior changes: on <8 GB RAM machines an interactive pick of `balanced`/`best` is
18
+ confirmed and then honored instead of silently downgraded (non-interactive runs keep the
19
+ auto-downgrade); `--quality jina-nano` without `--embed-engine` now locks the embedding
20
+ engine to `llama-server` instead of erroring (an explicit `--embed-engine lm-studio`
21
+ still fails loud). Flags, `--json` output, and the non-interactive contract are unchanged.
22
+
7
23
  ## [0.5.0] — 2026-07-15
8
24
 
9
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ragx-cli
3
- Version: 0.5.0
3
+ Version: 0.6.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
@@ -53,6 +53,7 @@ ragx-cli index # chunk -> embed -> HNSW + kNN similarity graph (
53
53
  ragx-cli query "why did we switch build tools?" --json --files-only
54
54
  ragx-cli index --full # full rebuild: re-chunk + re-embed everything
55
55
  ragx-cli models # recommend + download an embedding/reranker combo via LM Studio
56
+ ragx-cli --version # version, repo, and the effective models (corpus config + ~/.ragxrc overrides)
56
57
  ```
57
58
 
58
59
  Runs against any OpenAI-compatible embedding endpoint (LM Studio, Ollama, OpenAI). Reranking
@@ -324,8 +325,19 @@ Full write-ups: `research/bge-m3-dense-q8-vs-nomic-q4-benchmark-2026-07-12-workt
324
325
 
325
326
  `ragx-cli models` recommends an embedding + reranker combo, downloads the embedding model
326
327
  **through LM Studio** (`lms get` — one download path, plays well with restricted networks),
327
- pre-fetches the reranker from Hugging Face, and writes both to `ragx.toml`. Interactive on a
328
- TTY (quality tier + engine questions, RAM auto-detected); scriptable via flags:
328
+ pre-fetches the reranker from Hugging Face, and writes both to `ragx.toml`. On a TTY it walks
329
+ three steps — context first, consequences before anything is written:
330
+
331
+ 1. **machine header** — detected RAM, platform, whether `llama-server` is on PATH and the
332
+ `lms` CLI was found (this is what drives the recommended defaults);
333
+ 2. **numbered menus** — the embedding tier table (model, size, languages, context, license)
334
+ and one menu per serving engine with a one-line tradeoff each; the recommended default is
335
+ marked `»` (Enter accepts), invalid input re-prompts;
336
+ 3. **plan** — what will be downloaded (or `already downloaded ✓`) with approximate sizes,
337
+ which config sections change, and whether a re-index is required — all shown *before*
338
+ the final confirm.
339
+
340
+ Scriptable via flags:
329
341
 
330
342
  ```bash
331
343
  ragx-cli models # interactive
@@ -335,12 +347,12 @@ ragx-cli models --quality fast --dry-run # recommend only, change
335
347
 
336
348
  The curated catalog (every entry verified working end-to-end):
337
349
 
338
- | tier | embedding model | notes |
339
- |---|---|---|
340
- | `fast` | EmbeddingGemma 300M | 308M, 100+ languages, 2048-token context |
341
- | `balanced` | BGE-M3 | ragx's benchmarked production model (~100 languages) |
342
- | `best` | Qwen3-Embedding-0.6B | 100+ languages, 32K context |
343
- | `jina-nano` | Jina embeddings v5 nano | 239M, best-in-class sub-500M; **CC-BY-NC (non-commercial)**; llama-server engine only — LM Studio downloads it but can't serve EuroBERT |
350
+ | tier | embedding model | size | langs | ctx | notes |
351
+ |---|---|---|---|---|---|
352
+ | `fast` | EmbeddingGemma 300M | 308M | 100+ | 2K | lightest; small corpora, low RAM |
353
+ | `balanced` | BGE-M3 | 568M | ~100 | 8K | ragx's benchmarked production model |
354
+ | `best` | Qwen3-Embedding-0.6B | 0.6B | 100+ | 32K | highest quality; slower indexing |
355
+ | `jina-nano` | Jina embeddings v5 nano | 239M | multi | 8K | best sub-500M; **CC-BY-NC (non-commercial)**; llama-server engine only — LM Studio downloads it but can't serve EuroBERT (picking it locks the embedding engine to llama-server automatically; an explicit `--embed-engine lm-studio` still fails loud) |
344
356
 
345
357
  **Serving engines.** Embeddings run through `--embed-engine`: **`llama-server`** (default when
346
358
  llama.cpp is installed — ragx auto-spawns `llama-server --embedding` on the downloaded GGUF and
@@ -358,7 +370,9 @@ terminates it at exit; LM Studio is only the downloader) or **`lm-studio`** (LM
358
370
 
359
371
  With both engines on `llama-server` the whole stack is ragx-managed: LM Studio downloads the
360
372
  GGUFs, ragx spawns and reaps the servers, and neither LM Studio nor huggingface.co is needed
361
- at query time. Under 8 GB RAM the embedding tier drops to `fast`; on macOS
373
+ at query time. Under 8 GB RAM the recommended default moves to `fast`: interactively, picking
374
+ a heavier tier asks for confirmation and then **honors your choice**; non-interactive runs
375
+ (`--yes`/piped) auto-downgrade to `fast` with a note, since there is nobody to ask. On macOS
362
376
  `lms get --yes` picks MLX variants where LM Studio's catalog offers them. Switching the
363
377
  embedding model invalidates the index — the command tells you to run `ragx-cli index --full`.
364
378
  `init` offers this flow as its final step; rerun `ragx-cli models` anytime to switch.
@@ -25,6 +25,7 @@ ragx-cli index # chunk -> embed -> HNSW + kNN similarity graph (
25
25
  ragx-cli query "why did we switch build tools?" --json --files-only
26
26
  ragx-cli index --full # full rebuild: re-chunk + re-embed everything
27
27
  ragx-cli models # recommend + download an embedding/reranker combo via LM Studio
28
+ ragx-cli --version # version, repo, and the effective models (corpus config + ~/.ragxrc overrides)
28
29
  ```
29
30
 
30
31
  Runs against any OpenAI-compatible embedding endpoint (LM Studio, Ollama, OpenAI). Reranking
@@ -296,8 +297,19 @@ Full write-ups: `research/bge-m3-dense-q8-vs-nomic-q4-benchmark-2026-07-12-workt
296
297
 
297
298
  `ragx-cli models` recommends an embedding + reranker combo, downloads the embedding model
298
299
  **through LM Studio** (`lms get` — one download path, plays well with restricted networks),
299
- pre-fetches the reranker from Hugging Face, and writes both to `ragx.toml`. Interactive on a
300
- TTY (quality tier + engine questions, RAM auto-detected); scriptable via flags:
300
+ pre-fetches the reranker from Hugging Face, and writes both to `ragx.toml`. On a TTY it walks
301
+ three steps — context first, consequences before anything is written:
302
+
303
+ 1. **machine header** — detected RAM, platform, whether `llama-server` is on PATH and the
304
+ `lms` CLI was found (this is what drives the recommended defaults);
305
+ 2. **numbered menus** — the embedding tier table (model, size, languages, context, license)
306
+ and one menu per serving engine with a one-line tradeoff each; the recommended default is
307
+ marked `»` (Enter accepts), invalid input re-prompts;
308
+ 3. **plan** — what will be downloaded (or `already downloaded ✓`) with approximate sizes,
309
+ which config sections change, and whether a re-index is required — all shown *before*
310
+ the final confirm.
311
+
312
+ Scriptable via flags:
301
313
 
302
314
  ```bash
303
315
  ragx-cli models # interactive
@@ -307,12 +319,12 @@ ragx-cli models --quality fast --dry-run # recommend only, change
307
319
 
308
320
  The curated catalog (every entry verified working end-to-end):
309
321
 
310
- | tier | embedding model | notes |
311
- |---|---|---|
312
- | `fast` | EmbeddingGemma 300M | 308M, 100+ languages, 2048-token context |
313
- | `balanced` | BGE-M3 | ragx's benchmarked production model (~100 languages) |
314
- | `best` | Qwen3-Embedding-0.6B | 100+ languages, 32K context |
315
- | `jina-nano` | Jina embeddings v5 nano | 239M, best-in-class sub-500M; **CC-BY-NC (non-commercial)**; llama-server engine only — LM Studio downloads it but can't serve EuroBERT |
322
+ | tier | embedding model | size | langs | ctx | notes |
323
+ |---|---|---|---|---|---|
324
+ | `fast` | EmbeddingGemma 300M | 308M | 100+ | 2K | lightest; small corpora, low RAM |
325
+ | `balanced` | BGE-M3 | 568M | ~100 | 8K | ragx's benchmarked production model |
326
+ | `best` | Qwen3-Embedding-0.6B | 0.6B | 100+ | 32K | highest quality; slower indexing |
327
+ | `jina-nano` | Jina embeddings v5 nano | 239M | multi | 8K | best sub-500M; **CC-BY-NC (non-commercial)**; llama-server engine only — LM Studio downloads it but can't serve EuroBERT (picking it locks the embedding engine to llama-server automatically; an explicit `--embed-engine lm-studio` still fails loud) |
316
328
 
317
329
  **Serving engines.** Embeddings run through `--embed-engine`: **`llama-server`** (default when
318
330
  llama.cpp is installed — ragx auto-spawns `llama-server --embedding` on the downloaded GGUF and
@@ -330,7 +342,9 @@ terminates it at exit; LM Studio is only the downloader) or **`lm-studio`** (LM
330
342
 
331
343
  With both engines on `llama-server` the whole stack is ragx-managed: LM Studio downloads the
332
344
  GGUFs, ragx spawns and reaps the servers, and neither LM Studio nor huggingface.co is needed
333
- at query time. Under 8 GB RAM the embedding tier drops to `fast`; on macOS
345
+ at query time. Under 8 GB RAM the recommended default moves to `fast`: interactively, picking
346
+ a heavier tier asks for confirmation and then **honors your choice**; non-interactive runs
347
+ (`--yes`/piped) auto-downgrade to `fast` with a note, since there is nobody to ask. On macOS
334
348
  `lms get --yes` picks MLX variants where LM Studio's catalog offers them. Switching the
335
349
  embedding model invalidates the index — the command tells you to run `ragx-cli index --full`.
336
350
  `init` offers this flow as its final step; rerun `ragx-cli models` anytime to switch.
@@ -0,0 +1,131 @@
1
+ # Proposal: `ragx-cli models` interactive flow redesign
2
+
3
+ **Status:** implemented 2026-07-15 (`cli/models_ui.py` + `cli/models_cmd.py`); kept as the
4
+ design rationale. The open decision below was resolved as proposed: warn-and-honor
5
+ interactively, auto-downgrade non-interactively.
6
+
7
+ ## Problem
8
+
9
+ The current interactive flow (`src/ragx/cli/models_cmd.py:62-104`) asks all three
10
+ questions **before** showing any of the information needed to answer them:
11
+
12
+ 1. **Blind prompts.** `quality (fast/balanced/best/jina-nano) [balanced]:` offers four
13
+ opaque words. The catalog knows each tier's model name, parameter count, languages,
14
+ context length, and license — none of it is shown at prompt time. Machine specs print
15
+ *after* the answers.
16
+ 2. **Engine prompts don't state tradeoffs.** "llama-server vs sentence-transformers"
17
+ means nothing unless you already know one avoids huggingface.co entirely and the
18
+ other needs the `[rerank]` extra. That context lives only in `--help` flag text.
19
+ 3. **Silent downgrade.** Pick `best` on a <8 GB machine and `recommend()`
20
+ (`catalog.py:133-135`) substitutes `fast`, telling you only afterwards. No way to
21
+ override or reconsider.
22
+ 4. **The jina-nano trap.** Choosing `jina-nano` with the default `lm-studio` embed
23
+ engine hard-errors at the end ("rerun with `--embed-engine llama-server`") — even
24
+ though the catalog already encodes the constraint (`requires_llama_server`) and
25
+ could drive the default.
26
+ 5. **Fragile input.** A typo in the free-text tier name raises `RagxError`, exit 2,
27
+ start over.
28
+ 6. **Consequences surface too late.** Download sizes are never shown; "your index is
29
+ now invalid, run `index --full`" prints *after* `ragx.toml` is written.
30
+
31
+ ## Proposed flow
32
+
33
+ Principle: **context → choice → consequence → commit.** Detect everything first, show
34
+ it, ask with numbered menus, and present a plan before touching anything.
35
+
36
+ ```
37
+ $ ragx-cli models
38
+
39
+ machine: 32 GB RAM · macOS · llama-server: on PATH · LM Studio (lms): found
40
+
41
+ ── Step 1/3 · embedding model ─────────────────────────────────────────
42
+ Determines retrieval quality. Changing it later invalidates the
43
+ index (full re-index required).
44
+
45
+ # tier model size langs ctx notes
46
+ 1 fast EmbeddingGemma 300M 308M 100+ 2K lightest; small corpora, low RAM
47
+ » 2 balanced BGE-M3 568M ~100 8K ragx's benchmarked production model
48
+ 3 best Qwen3-Embedding-0.6B 0.6B 100+ 32K highest quality; slower indexing
49
+ 4 jina-nano Jina v5 nano 239M multi 8K best <500M quality · CC-BY-NC
50
+ (non-commercial) · llama-server only
51
+ select 1-4 [2]:
52
+
53
+ ── Step 2/3 · engines ─────────────────────────────────────────────────
54
+ embedding serving:
55
+ » 1 llama-server ragx auto-spawns llama.cpp; LM Studio only
56
+ downloads (detected on PATH)
57
+ 2 lm-studio LM Studio app must be running to serve
58
+ select 1-2 [1]:
59
+
60
+ reranker:
61
+ » 1 llama-server GGUF via LM Studio — no huggingface.co needed
62
+ 2 sentence-transformers downloads from Hugging Face; needs the
63
+ ragx-cli[rerank] extra
64
+ select 1-2 [1]:
65
+
66
+ ── Step 3/3 · plan ────────────────────────────────────────────────────
67
+ embedding BGE-M3 Q8 GGUF (~600 MB) already downloaded ✓
68
+ reranker bge-reranker-v2-m3 Q8 (~600 MB) will download via LM Studio
69
+ config ragx.toml → embeddings.*, rerank.* (7 keys)
70
+ re-index REQUIRED — embedding model changes; run `ragx-cli index --full` after
71
+
72
+ proceed? [Y/n]:
73
+ ```
74
+
75
+ ### How each problem maps to a fix
76
+
77
+ - **Specs shown first**, including whether `llama-server` was found on PATH — the
78
+ engine defaults stop being invisible magic.
79
+ - **Numbered menus with a re-prompt loop** instead of one-shot free text. `»` marks
80
+ the recommended default; Enter accepts it. Invalid input re-prompts instead of
81
+ exiting.
82
+ - **RAM guard becomes visible, not substituting.** On a <8 GB machine the `»` marker
83
+ moves to `fast` and heavy rows get a `⚠ needs ≥8 GB` annotation. If the user still
84
+ picks `best`, honor it after a one-line confirm — a balanced choice means respecting
85
+ an informed override. Non-interactive `--quality best` keeps today's auto-downgrade
86
+ + note (an agent can't be asked); this interactive/non-interactive asymmetry should
87
+ be stated in the README.
88
+ - **jina-nano stops being a trap:** picking it prints
89
+ `→ embedding engine locked to llama-server (LM Studio cannot serve this model)` and
90
+ skips that question instead of erroring at the end.
91
+ - **The plan step** shows download sizes, marks what is already downloaded (the
92
+ `_ensure_downloaded` check moves before the confirm), lists which config keys
93
+ change, and states the re-index consequence *before* the write.
94
+
95
+ ## Implementation sketch
96
+
97
+ Small and contained — no behavior change for flags / `--json` / non-interactive:
98
+
99
+ 1. **`core/catalog.py`** — add structured fields to the choices (`params`, `context`,
100
+ `languages`, `download_mb`) instead of parsing prose out of `notes`.
101
+ Verify: existing tests pass; table renders from fields.
102
+ 2. **`cli/models_cmd.py`** — a `_pick_numbered(title, rows, default)` helper
103
+ (validation loop, `»` marker) replacing `typer.prompt`/`_pick_engine` in
104
+ interactive mode; reorder so specs + `lms`/`llama-server` detection and the
105
+ already-downloaded check happen before any question.
106
+ Verify: interactive smoke run against a scratch corpus.
107
+ 3. **`recommend()`** — split recommendation from enforcement: return annotations
108
+ (RAM warning, license note, engine lock) rather than silently swapping tiers in
109
+ interactive mode.
110
+ Verify: unit tests for low-RAM interactive vs non-interactive paths.
111
+ 4. **README** — update the `models` section in the same change (CLAUDE.md rule:
112
+ usage-visible changes ship with their docs).
113
+
114
+ Estimated diff: ~150 lines, centered on `models_cmd.py`.
115
+
116
+ ### Rendering choice
117
+
118
+ Plain f-string column alignment is enough. `rich` is already available (transitive
119
+ dependency of typer ≥0.12) if fancier tables are ever wanted, but no new dependency
120
+ and no speculative widgets now.
121
+
122
+ ### Deliberate non-goal
123
+
124
+ No full TUI (arrow-key selection à la questionary). Numbered menus keep the code
125
+ testable with piped stdin and keep the agent-first stdin/stdout contract intact.
126
+
127
+ ## Open decision
128
+
129
+ Interactive RAM-override behavior: warn-and-honor (proposed) vs keep the current
130
+ auto-downgrade everywhere. Proposal argues warn-and-honor interactively, downgrade
131
+ non-interactively.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ragx-cli"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "Similarity-graph RAG CLI: index a corpus into a chunk-level embedding graph and query it"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import sys
7
+ from importlib import metadata
7
8
  from pathlib import Path
8
9
 
9
10
  import typer
@@ -16,6 +17,8 @@ from ragx.core.config import (
16
17
  CONFIG_FILE,
17
18
  config_path,
18
19
  db_path,
20
+ find_root,
21
+ load_rc,
19
22
  rc_path,
20
23
  require_root,
21
24
  write_default_config,
@@ -23,6 +26,8 @@ from ragx.core.config import (
23
26
  )
24
27
  from ragx.core.errors import RagxError
25
28
 
29
+ REPO_URL = "https://github.com/alber70g/ragx-cli"
30
+
26
31
  app = typer.Typer(add_completion=False, no_args_is_help=True)
27
32
  config_app = typer.Typer(add_completion=False, no_args_is_help=True)
28
33
  app.add_typer(config_app, name="config")
@@ -35,6 +40,55 @@ eval_cmd.register(app)
35
40
  models_cmd.register(app)
36
41
 
37
42
 
43
+ def _print_version() -> None:
44
+ try:
45
+ version = metadata.version("ragx-cli")
46
+ except metadata.PackageNotFoundError:
47
+ version = "unknown"
48
+ typer.echo(f"ragx-cli {version}")
49
+ typer.echo(REPO_URL)
50
+ root = find_root()
51
+ if root is not None:
52
+ try:
53
+ cfg = Config.load(root, confirm=migrate_confirm())
54
+ typer.echo(f"config: {config_path(root)}")
55
+ for section in ("embeddings", "expansion", "rerank"):
56
+ line = f" {section}: {cfg.get(f'{section}.provider')}/{cfg.get(f'{section}.model')}"
57
+ if cfg.data[section].get("enabled") is False:
58
+ line += " (disabled)"
59
+ typer.echo(line)
60
+ except RagxError as exc:
61
+ typer.echo(f"config: unreadable ({exc})")
62
+ rc = rc_path()
63
+ if rc.exists():
64
+ typer.echo(f"global: {rc}")
65
+ try:
66
+ for section, values in load_rc().items():
67
+ for key, value in values.items():
68
+ typer.echo(f" {section}.{key} = {value}")
69
+ except RagxError as exc:
70
+ typer.echo(f" unreadable ({exc})")
71
+
72
+
73
+ def _version_option(value: bool) -> None:
74
+ if value:
75
+ _print_version()
76
+ raise typer.Exit()
77
+
78
+
79
+ @app.callback()
80
+ def _main(
81
+ version: bool = typer.Option(
82
+ False,
83
+ "--version",
84
+ callback=_version_option,
85
+ is_eager=True,
86
+ help="show version, repo, and configured models (local + ~/.ragxrc)",
87
+ ),
88
+ ) -> None:
89
+ """Similarity-graph RAG for your files."""
90
+
91
+
38
92
  def _require_root() -> Path:
39
93
  try:
40
94
  return require_root()
@@ -4,11 +4,13 @@ onto the main app by app.py; `init` routes here as an optional final step."""
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import shutil
7
8
  import sys
8
9
  from pathlib import Path
9
10
 
10
11
  import typer
11
12
 
13
+ from ragx.cli import models_ui as ui
12
14
  from ragx.cli.output import emit_json, fail, migrate_confirm
13
15
  from ragx.core import lmstudio
14
16
  from ragx.core.catalog import QUALITY_TIERS, detect_specs, recommend
@@ -61,27 +63,40 @@ def run_flow(
61
63
  """The actual flow, callable from `init`. Raises RagxError on failure."""
62
64
  cfg = Config.load(root, confirm=migrate_confirm())
63
65
  interactive = sys.stdin.isatty() and not yes
66
+ specs = detect_specs()
67
+ llama_found = shutil.which("llama-server") is not None
68
+ lms = lmstudio.find_lms() if (interactive or not dry_run) else None
69
+ ui.header(specs, llama_found, lms_found=None if (dry_run and not interactive) else lms is not None)
70
+
64
71
  if quality is None:
65
72
  if not interactive:
66
73
  raise RagxError(f"--quality required when not interactive ({'/'.join(QUALITY_TIERS)})")
67
- quality = typer.prompt(f" quality ({'/'.join(QUALITY_TIERS)})", default="balanced")
68
- engine = _pick_engine(
69
- "rerank engine", rerank_engine, ("llama-server", "sentence-transformers"),
70
- interactive, fallback="sentence-transformers",
71
- )
72
- emb_engine = _pick_engine(
73
- "embedding engine", embed_engine, ("llama-server", "lm-studio"),
74
- interactive, fallback="lm-studio",
75
- )
74
+ quality = ui.pick_tier(specs)
75
+ embedding, reranker, notes = recommend(quality, specs, enforce_ram=not interactive)
76
76
 
77
- specs = detect_specs()
78
- embedding, reranker, notes = recommend(quality, specs)
79
- if embedding.requires_llama_server and emb_engine != "llama-server":
80
- raise RagxError(
81
- f"{embedding.label} can only be served by the llama-server engine "
82
- "(LM Studio downloads it but cannot serve EuroBERT models) — "
83
- "rerun with --embed-engine llama-server"
77
+ if embedding.requires_llama_server and embed_engine != "llama-server":
78
+ if embed_engine is not None:
79
+ raise RagxError(
80
+ f"{embedding.label} can only be served by the llama-server engine "
81
+ "(LM Studio downloads it but cannot serve EuroBERT models) — "
82
+ "rerun with --embed-engine llama-server"
83
+ )
84
+ emb_engine = "llama-server"
85
+ lock_note = f"embedding engine locked to llama-server — LM Studio cannot serve {embedding.label}"
86
+ if interactive:
87
+ ui.echo(f"\n → {lock_note}")
88
+ else:
89
+ notes.append(lock_note)
90
+ else:
91
+ emb_engine = _resolve_engine(
92
+ "embedding engine", embed_engine, ui.EMBED_ENGINES,
93
+ interactive, llama_found, fallback="lm-studio",
84
94
  )
95
+ engine = _resolve_engine(
96
+ "rerank engine", rerank_engine, ui.RERANK_ENGINES,
97
+ interactive, llama_found, fallback="sentence-transformers",
98
+ )
99
+
85
100
  engine_desc = (
86
101
  "llama.cpp llama-server, GGUF downloaded via LM Studio — no huggingface.co needed"
87
102
  if engine == "llama-server"
@@ -90,8 +105,8 @@ def run_flow(
90
105
  emb_desc = (
91
106
  "llama-server auto-spawned by ragx" if emb_engine == "llama-server" else "served by LM Studio"
92
107
  )
93
- _echo(f"machine: {specs.ram_gb:.0f} GB RAM, {'macOS' if specs.macos else sys.platform}")
94
- _echo(f"embedding: {embedding.label} ({embedding.notes}) — {emb_desc}")
108
+ spec_line = f"{embedding.params}, {embedding.languages} languages, {embedding.context} context"
109
+ _echo(f"embedding: {embedding.label} ({spec_line}) — {emb_desc}")
95
110
  _echo(f"reranker: {reranker.label} ({reranker.notes}) — via {engine_desc}")
96
111
  for note in notes:
97
112
  _echo(f"note: {note}")
@@ -100,23 +115,39 @@ def run_flow(
100
115
  _finish(json_out, embedding, reranker, notes, engine=engine, emb_engine=emb_engine,
101
116
  downloaded=False, config_updated=False, reindex_required=False)
102
117
  return
103
- if interactive and not typer.confirm("download and update ragx.toml?", default=True):
104
- raise typer.Exit(code=0)
105
118
 
106
- lms = lmstudio.find_lms()
107
119
  if lms is None:
108
120
  raise RagxError(INSTALL_HINT)
109
121
 
122
+ emb_frag = embedding.gguf_fragment if emb_engine == "llama-server" else embedding.repo_fragment
123
+ emb_pre = lmstudio.find_installed(lms, emb_frag)
124
+ rr_pre = lmstudio.find_installed(lms, reranker.gguf_fragment) if engine == "llama-server" else None
125
+
126
+ if interactive:
127
+ if emb_pre is not None and emb_pre.model_key == cfg.get("embeddings.model"):
128
+ reindex_line = "not needed — embedding model unchanged"
129
+ else:
130
+ reindex_line = "REQUIRED — embedding model changes; run `ragx-cli index --full` after"
131
+ ui.show_plan(embedding=embedding, reranker=reranker, emb_engine=emb_engine,
132
+ rerank_engine=engine, emb_installed=emb_pre is not None,
133
+ rr_installed=rr_pre is not None, reindex_line=reindex_line)
134
+ if not typer.confirm(" proceed — download and update ragx.toml?", default=True, err=True):
135
+ raise typer.Exit(code=0)
136
+
110
137
  embed_updates: list[tuple[str, str]]
111
138
  if emb_engine == "llama-server":
112
- installed, downloaded = _ensure_downloaded(lms, embedding.gguf_ref, embedding.gguf_fragment)
139
+ installed, downloaded = _ensure_downloaded(
140
+ lms, embedding.gguf_ref, embedding.gguf_fragment, pre=emb_pre
141
+ )
113
142
  embed_updates = [
114
143
  ("embeddings.provider", "llama-server"),
115
144
  ("embeddings.gguf", str(lmstudio.models_root() / installed.path)),
116
145
  ("embeddings.base_url", "http://127.0.0.1:9813/v1"),
117
146
  ]
118
147
  else:
119
- installed, downloaded = _ensure_downloaded(lms, embedding.ref, embedding.repo_fragment)
148
+ installed, downloaded = _ensure_downloaded(
149
+ lms, embedding.ref, embedding.repo_fragment, pre=emb_pre
150
+ )
120
151
  embed_updates = [
121
152
  ("embeddings.provider", "openai"),
122
153
  ("embeddings.gguf", ""),
@@ -125,7 +156,9 @@ def run_flow(
125
156
 
126
157
  rerank_updates: list[tuple[str, str]]
127
158
  if engine == "llama-server":
128
- gguf_model, gguf_downloaded = _ensure_downloaded(lms, reranker.gguf_ref, reranker.gguf_fragment)
159
+ gguf_model, gguf_downloaded = _ensure_downloaded(
160
+ lms, reranker.gguf_ref, reranker.gguf_fragment, pre=rr_pre
161
+ )
129
162
  downloaded = downloaded or gguf_downloaded
130
163
  gguf_path = str(lmstudio.models_root() / gguf_model.path)
131
164
  rerank_updates = [("rerank.provider", "llama-server"), ("rerank.gguf", gguf_path),
@@ -156,29 +189,28 @@ def run_flow(
156
189
  model_key=installed.model_key)
157
190
 
158
191
 
159
- def _pick_engine(
160
- label: str, value: str | None, engines: tuple[str, str], interactive: bool, *, fallback: str
192
+ def _resolve_engine(
193
+ label: str, value: str | None, options: tuple[tuple[str, str], ...],
194
+ interactive: bool, llama_found: bool, *, fallback: str,
161
195
  ) -> str:
162
- """Explicit flag wins; otherwise default to llama-server when the binary is present
163
- (fully ragx-managed, no huggingface.co), falling back to `fallback`."""
196
+ """Explicit flag wins; interactive shows a numbered menu; otherwise default to
197
+ llama-server when the binary is present (fully ragx-managed, no huggingface.co),
198
+ falling back to `fallback`."""
199
+ names = tuple(name for name, _ in options)
164
200
  if value is not None:
165
- if value not in engines:
166
- raise RagxError(f"{label} must be one of {'/'.join(engines)}")
201
+ if value not in names:
202
+ raise RagxError(f"{label} must be one of {'/'.join(names)}")
167
203
  return value
168
- import shutil
169
-
170
- default = "llama-server" if shutil.which("llama-server") else fallback
204
+ default = "llama-server" if llama_found else fallback
171
205
  if interactive:
172
- picked = typer.prompt(f" {label} ({'/'.join(engines)})", default=default)
173
- if picked not in engines:
174
- raise RagxError(f"{label} must be one of {'/'.join(engines)}")
175
- return picked
206
+ return ui.pick_engine(label, options, default, llama_found)
176
207
  return default
177
208
 
178
209
 
179
- def _ensure_downloaded(lms: str, ref: str, fragment: str):
180
- """Download `ref` via LM Studio unless a model matching `fragment` is already there."""
181
- installed = lmstudio.find_installed(lms, fragment)
210
+ def _ensure_downloaded(lms: str, ref: str, fragment: str, pre: lmstudio.InstalledModel | None = None):
211
+ """Download `ref` via LM Studio unless a model matching `fragment` is already there
212
+ (`pre` = an earlier find_installed result to reuse)."""
213
+ installed = pre if pre is not None else lmstudio.find_installed(lms, fragment)
182
214
  if installed is not None:
183
215
  _echo(f"already downloaded: {installed.model_key}")
184
216
  return installed, False