ragx-cli 0.5.1__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.
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/CHANGELOG.md +16 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/PKG-INFO +23 -10
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/README.md +22 -9
- ragx_cli-0.6.0/docs/proposal-models-command-interactive-flow.md +131 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/pyproject.toml +1 -1
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/cli/models_cmd.py +72 -40
- ragx_cli-0.6.0/src/ragx/cli/models_ui.py +117 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/catalog.py +52 -10
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_models.py +123 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/SKILL.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/api-cheatsheet.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/collection-management/python.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/collection-management/typescript.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/configuration.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/data-model.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/data-operations/python.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/data-operations/typescript.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/embedding.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/hybrid-search/python.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/hybrid-search/typescript.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/multimodal-search/python.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/multimodal-search/typescript.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/quick-start/python.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/quick-start/typescript.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/rag-system/python.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/rag-system/typescript.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/reranker.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/troubleshooting.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/vector-search/python.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.agents/skills/zvec/vector-search/typescript.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.github/workflows/publish.yml +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/.gitignore +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/AGENTS.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/CLAUDE.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/CONTRACTS-PHASE23.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/CONTRACTS.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/LICENSE +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/docs/feature-temporal-weighting.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/ragx-cli-plan.md +0 -0
- {ragx_cli-0.5.1 → 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
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/research/bge-m3-dense-embeddings-as-ragx-provider-multilingual-quality-and-threshold-calibration.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/research/bge-m3-dense-q8-vs-nomic-q4-benchmark-2026-07-12-worktree-eval-results.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/research/bge-m3-sparse-lexical-weights-hybrid-retrieval-leg-for-ragx-vs-sqlite-fts5-bm25.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/research/bge-m3-subchunk-edge-ablation-2026-07-13-ties-again-and-beta-zero-shortlist-mechanism.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/research/fine-grained-sub-chunk-edges-with-coarse-chunk-nodes-multi-granularity-graph-rag-literature-validation.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/research/lm-studio-model-download-api-and-curated-embedding-reranker-catalog-for-ragx-models-command.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/research/subchunk-edge-ablation-2026-07-12-two-round-experiment-protocol-results-and-verdict.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/skills/ragx-cli/SKILL.md +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/skills-lock.json +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/__init__.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/cli/__init__.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/cli/app.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/cli/detect.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/cli/eval_cmd.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/cli/init_prompt.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/cli/inspect_cmd.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/cli/output.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/cli/pipeline.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/__init__.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/chunking.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/communities.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/config.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/discovery.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/errors.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/eval.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/expansion.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/fusion.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/graph.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/indexer.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/lmstudio.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/models.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/query.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/scoring.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/store.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/traversal.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/core/vectors.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/providers/__init__.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/providers/base.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/providers/llama_embedder.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/providers/llama_process.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/providers/llama_server.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/providers/openai_compat.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/providers/registry.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/src/ragx/providers/st_reranker.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/__init__.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/conftest.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_chunking.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_cli.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_communities.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_config_rc.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_discovery.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_eval.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_expansion.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_fusion.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_graph.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_inspect.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_integration.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_llama_server.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_providers.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_store.py +0 -0
- {ragx_cli-0.5.1 → ragx_cli-0.6.0}/tests/test_traversal.py +0 -0
- {ragx_cli-0.5.1 → 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.
|
|
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
|
|
@@ -325,8 +325,19 @@ Full write-ups: `research/bge-m3-dense-q8-vs-nomic-q4-benchmark-2026-07-12-workt
|
|
|
325
325
|
|
|
326
326
|
`ragx-cli models` recommends an embedding + reranker combo, downloads the embedding model
|
|
327
327
|
**through LM Studio** (`lms get` — one download path, plays well with restricted networks),
|
|
328
|
-
pre-fetches the reranker from Hugging Face, and writes both to `ragx.toml`.
|
|
329
|
-
|
|
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:
|
|
330
341
|
|
|
331
342
|
```bash
|
|
332
343
|
ragx-cli models # interactive
|
|
@@ -336,12 +347,12 @@ ragx-cli models --quality fast --dry-run # recommend only, change
|
|
|
336
347
|
|
|
337
348
|
The curated catalog (every entry verified working end-to-end):
|
|
338
349
|
|
|
339
|
-
| tier | embedding model | notes |
|
|
340
|
-
|
|
341
|
-
| `fast` | EmbeddingGemma 300M | 308M
|
|
342
|
-
| `balanced` | BGE-M3 | ragx's benchmarked production model
|
|
343
|
-
| `best` | Qwen3-Embedding-0.6B | 100+
|
|
344
|
-
| `jina-nano` | Jina embeddings v5 nano | 239M
|
|
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) |
|
|
345
356
|
|
|
346
357
|
**Serving engines.** Embeddings run through `--embed-engine`: **`llama-server`** (default when
|
|
347
358
|
llama.cpp is installed — ragx auto-spawns `llama-server --embedding` on the downloaded GGUF and
|
|
@@ -359,7 +370,9 @@ terminates it at exit; LM Studio is only the downloader) or **`lm-studio`** (LM
|
|
|
359
370
|
|
|
360
371
|
With both engines on `llama-server` the whole stack is ragx-managed: LM Studio downloads the
|
|
361
372
|
GGUFs, ragx spawns and reaps the servers, and neither LM Studio nor huggingface.co is needed
|
|
362
|
-
at query time. Under 8 GB RAM the
|
|
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
|
|
363
376
|
`lms get --yes` picks MLX variants where LM Studio's catalog offers them. Switching the
|
|
364
377
|
embedding model invalidates the index — the command tells you to run `ragx-cli index --full`.
|
|
365
378
|
`init` offers this flow as its final step; rerun `ragx-cli models` anytime to switch.
|
|
@@ -297,8 +297,19 @@ Full write-ups: `research/bge-m3-dense-q8-vs-nomic-q4-benchmark-2026-07-12-workt
|
|
|
297
297
|
|
|
298
298
|
`ragx-cli models` recommends an embedding + reranker combo, downloads the embedding model
|
|
299
299
|
**through LM Studio** (`lms get` — one download path, plays well with restricted networks),
|
|
300
|
-
pre-fetches the reranker from Hugging Face, and writes both to `ragx.toml`.
|
|
301
|
-
|
|
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:
|
|
302
313
|
|
|
303
314
|
```bash
|
|
304
315
|
ragx-cli models # interactive
|
|
@@ -308,12 +319,12 @@ ragx-cli models --quality fast --dry-run # recommend only, change
|
|
|
308
319
|
|
|
309
320
|
The curated catalog (every entry verified working end-to-end):
|
|
310
321
|
|
|
311
|
-
| tier | embedding model | notes |
|
|
312
|
-
|
|
313
|
-
| `fast` | EmbeddingGemma 300M | 308M
|
|
314
|
-
| `balanced` | BGE-M3 | ragx's benchmarked production model
|
|
315
|
-
| `best` | Qwen3-Embedding-0.6B | 100+
|
|
316
|
-
| `jina-nano` | Jina embeddings v5 nano | 239M
|
|
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) |
|
|
317
328
|
|
|
318
329
|
**Serving engines.** Embeddings run through `--embed-engine`: **`llama-server`** (default when
|
|
319
330
|
llama.cpp is installed — ragx auto-spawns `llama-server --embedding` on the downloaded GGUF and
|
|
@@ -331,7 +342,9 @@ terminates it at exit; LM Studio is only the downloader) or **`lm-studio`** (LM
|
|
|
331
342
|
|
|
332
343
|
With both engines on `llama-server` the whole stack is ragx-managed: LM Studio downloads the
|
|
333
344
|
GGUFs, ragx spawns and reaps the servers, and neither LM Studio nor huggingface.co is needed
|
|
334
|
-
at query time. Under 8 GB RAM the
|
|
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
|
|
335
348
|
`lms get --yes` picks MLX variants where LM Studio's catalog offers them. Switching the
|
|
336
349
|
embedding model invalidates the index — the command tells you to run `ragx-cli index --full`.
|
|
337
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.
|
|
@@ -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 =
|
|
68
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
94
|
-
_echo(f"embedding: {embedding.label} ({
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
160
|
-
label: str, value: str | None,
|
|
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;
|
|
163
|
-
(fully ragx-managed, no huggingface.co),
|
|
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
|
|
166
|
-
raise RagxError(f"{label} must be one of {'/'.join(
|
|
201
|
+
if value not in names:
|
|
202
|
+
raise RagxError(f"{label} must be one of {'/'.join(names)}")
|
|
167
203
|
return value
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Interactive presentation for `ragx-cli models`: machine header, tier table,
|
|
2
|
+
numbered pickers, and the pre-confirm plan. Rendering + prompting only — no
|
|
3
|
+
downloads, no config writes. Everything goes to stderr (stdout stays pure for
|
|
4
|
+
--json)."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from ragx.core.catalog import EMBEDDINGS, EmbeddingChoice, RerankerChoice, Specs, ram_limited
|
|
13
|
+
|
|
14
|
+
EMBED_ENGINES = (
|
|
15
|
+
("llama-server", "ragx auto-spawns llama.cpp; LM Studio only downloads"),
|
|
16
|
+
("lm-studio", "LM Studio app must be running to serve embeddings"),
|
|
17
|
+
)
|
|
18
|
+
RERANK_ENGINES = (
|
|
19
|
+
("llama-server", "GGUF via LM Studio — no huggingface.co access needed"),
|
|
20
|
+
("sentence-transformers", "downloads from Hugging Face; needs the ragx-cli[rerank] extra"),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def echo(msg: str = "") -> None:
|
|
25
|
+
typer.echo(msg, err=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def header(specs: Specs, llama_found: bool, lms_found: bool | None) -> None:
|
|
29
|
+
"""One line of detected facts; lms_found=None means it wasn't looked up (dry-run)."""
|
|
30
|
+
ram = f"{specs.ram_gb:.0f} GB RAM" if specs.ram_gb else "RAM unknown"
|
|
31
|
+
parts = [
|
|
32
|
+
ram,
|
|
33
|
+
"macOS" if specs.macos else sys.platform,
|
|
34
|
+
f"llama-server: {'on PATH' if llama_found else 'not found'}",
|
|
35
|
+
]
|
|
36
|
+
if lms_found is not None:
|
|
37
|
+
parts.append(f"LM Studio (lms): {'found' if lms_found else 'NOT FOUND'}")
|
|
38
|
+
echo("machine: " + " · ".join(parts))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _rule(title: str) -> None:
|
|
42
|
+
echo()
|
|
43
|
+
echo(f"── {title} " + "─" * max(4, 58 - len(title)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def pick_numbered(rows: list[str], default: int) -> int:
|
|
47
|
+
"""Numbered menu (`»` marks the default); loops until a valid 1-based pick."""
|
|
48
|
+
for i, row in enumerate(rows, 1):
|
|
49
|
+
echo(f" {'»' if i == default else ' '} {i} {row}")
|
|
50
|
+
while True:
|
|
51
|
+
raw = typer.prompt(f" select 1-{len(rows)}", default=str(default), err=True).strip()
|
|
52
|
+
if raw.isdigit() and 1 <= int(raw) <= len(rows):
|
|
53
|
+
return int(raw)
|
|
54
|
+
echo(f" enter a number between 1 and {len(rows)}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def pick_tier(specs: Specs) -> str:
|
|
58
|
+
limited = ram_limited(specs)
|
|
59
|
+
tiers = list(EMBEDDINGS)
|
|
60
|
+
_rule("embedding model")
|
|
61
|
+
echo(" determines retrieval quality — changing it later requires a full re-index")
|
|
62
|
+
echo()
|
|
63
|
+
echo(f" # {'tier':<10} {'model':<24} {'size':<5} {'langs':<6} {'ctx':<4} notes")
|
|
64
|
+
rows = []
|
|
65
|
+
for name in tiers:
|
|
66
|
+
e = EMBEDDINGS[name]
|
|
67
|
+
note = e.notes + (" ⚠ needs ≥8 GB RAM" if limited and name in ("balanced", "best") else "")
|
|
68
|
+
rows.append(f"{e.tier:<10} {e.label:<24} {e.params:<5} {e.languages:<6} {e.context:<4} {note}")
|
|
69
|
+
default = tiers.index("fast" if limited else "balanced") + 1
|
|
70
|
+
while True:
|
|
71
|
+
tier = tiers[pick_numbered(rows, default) - 1]
|
|
72
|
+
if limited and tier in ("balanced", "best") and not typer.confirm(
|
|
73
|
+
f" only {specs.ram_gb:.0f} GB RAM detected — {tier} may be slow here; use it anyway?",
|
|
74
|
+
default=False,
|
|
75
|
+
err=True,
|
|
76
|
+
):
|
|
77
|
+
continue
|
|
78
|
+
return tier
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def pick_engine(label: str, options: tuple[tuple[str, str], ...], default: str, llama_found: bool) -> str:
|
|
82
|
+
_rule(label)
|
|
83
|
+
names = [name for name, _ in options]
|
|
84
|
+
rows = [
|
|
85
|
+
f"{name:<22} {desc}" + (" (detected on PATH)" if name == "llama-server" and llama_found else "")
|
|
86
|
+
for name, desc in options
|
|
87
|
+
]
|
|
88
|
+
return names[pick_numbered(rows, names.index(default) + 1) - 1]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def show_plan(
|
|
92
|
+
*,
|
|
93
|
+
embedding: EmbeddingChoice,
|
|
94
|
+
reranker: RerankerChoice,
|
|
95
|
+
emb_engine: str,
|
|
96
|
+
rerank_engine: str,
|
|
97
|
+
emb_installed: bool,
|
|
98
|
+
rr_installed: bool,
|
|
99
|
+
reindex_line: str,
|
|
100
|
+
) -> None:
|
|
101
|
+
_rule("plan")
|
|
102
|
+
emb_status = (
|
|
103
|
+
"already downloaded ✓" if emb_installed
|
|
104
|
+
else f"will download via LM Studio (~{embedding.download_mb} MB)"
|
|
105
|
+
)
|
|
106
|
+
if rerank_engine == "llama-server":
|
|
107
|
+
rr_status = (
|
|
108
|
+
"already downloaded ✓" if rr_installed
|
|
109
|
+
else f"will download via LM Studio (~{reranker.download_mb} MB)"
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
rr_status = f"downloads from Hugging Face (~{reranker.download_mb} MB)"
|
|
113
|
+
echo(f" embedding {embedding.label:<26} via {emb_engine:<22} {emb_status}")
|
|
114
|
+
echo(f" reranker {reranker.label:<26} via {rerank_engine:<22} {rr_status}")
|
|
115
|
+
echo(" config ragx.toml → embeddings.*, rerank.*")
|
|
116
|
+
echo(f" re-index {reindex_line}")
|
|
117
|
+
echo()
|
|
@@ -18,6 +18,8 @@ from ragx.core.errors import RagxError
|
|
|
18
18
|
|
|
19
19
|
QUALITY_TIERS = ("fast", "balanced", "best", "jina-nano")
|
|
20
20
|
|
|
21
|
+
LOW_RAM_GB = 8.0
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
@dataclass(frozen=True)
|
|
23
25
|
class EmbeddingChoice:
|
|
@@ -27,7 +29,11 @@ class EmbeddingChoice:
|
|
|
27
29
|
repo_fragment: str # substring that identifies it in `lms ls --json` output
|
|
28
30
|
doc_prefix: str
|
|
29
31
|
query_prefix: str
|
|
30
|
-
|
|
32
|
+
params: str # parameter count, for display ("308M")
|
|
33
|
+
languages: str # language coverage, for display ("100+")
|
|
34
|
+
context: str # context window, for display ("2K")
|
|
35
|
+
download_mb: int # approximate download size shown in the plan step
|
|
36
|
+
notes: str # short qualitative note for the interactive picker table
|
|
31
37
|
# llama-server engine: explicit GGUF repo (the catalog-id ref may resolve to MLX,
|
|
32
38
|
# which llama-server can't load) + the fragment locating the downloaded .gguf file
|
|
33
39
|
gguf_ref: str
|
|
@@ -42,6 +48,7 @@ class RerankerChoice:
|
|
|
42
48
|
label: str
|
|
43
49
|
model: str # HF repo id, loadable by sentence-transformers CrossEncoder
|
|
44
50
|
languages: str
|
|
51
|
+
download_mb: int # approximate download size shown in the plan step
|
|
45
52
|
notes: str
|
|
46
53
|
# llama-server engine: Q8_0 GGUF downloadable via LM Studio (validated against the
|
|
47
54
|
# safetensors originals — scores within ~0.5 logit, identical ordering)
|
|
@@ -57,7 +64,11 @@ EMBEDDINGS: dict[str, EmbeddingChoice] = {
|
|
|
57
64
|
repo_fragment="embeddinggemma",
|
|
58
65
|
doc_prefix="title: none | text: ",
|
|
59
66
|
query_prefix="task: search result | query: ",
|
|
60
|
-
|
|
67
|
+
params="308M",
|
|
68
|
+
languages="100+",
|
|
69
|
+
context="2K",
|
|
70
|
+
download_mb=300,
|
|
71
|
+
notes="lightest; small corpora, low RAM",
|
|
61
72
|
gguf_ref="https://huggingface.co/lmstudio-community/embeddinggemma-300m-qat-GGUF",
|
|
62
73
|
gguf_fragment="embeddinggemma-300m-qat",
|
|
63
74
|
),
|
|
@@ -68,7 +79,11 @@ EMBEDDINGS: dict[str, EmbeddingChoice] = {
|
|
|
68
79
|
repo_fragment="bge-m3",
|
|
69
80
|
doc_prefix="",
|
|
70
81
|
query_prefix="",
|
|
71
|
-
|
|
82
|
+
params="568M",
|
|
83
|
+
languages="~100",
|
|
84
|
+
context="8K",
|
|
85
|
+
download_mb=600,
|
|
86
|
+
notes="ragx's benchmarked production model",
|
|
72
87
|
gguf_ref="https://huggingface.co/gaianet/bge-m3-GGUF",
|
|
73
88
|
gguf_fragment="bge-m3",
|
|
74
89
|
),
|
|
@@ -82,7 +97,11 @@ EMBEDDINGS: dict[str, EmbeddingChoice] = {
|
|
|
82
97
|
"Instruct: Given a web search query, retrieve relevant passages that answer "
|
|
83
98
|
"the query\nQuery: "
|
|
84
99
|
),
|
|
85
|
-
|
|
100
|
+
params="0.6B",
|
|
101
|
+
languages="100+",
|
|
102
|
+
context="32K",
|
|
103
|
+
download_mb=640,
|
|
104
|
+
notes="highest quality; slower indexing",
|
|
86
105
|
gguf_ref="https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF",
|
|
87
106
|
gguf_fragment="qwen3-embedding-0.6b",
|
|
88
107
|
),
|
|
@@ -93,7 +112,11 @@ EMBEDDINGS: dict[str, EmbeddingChoice] = {
|
|
|
93
112
|
repo_fragment="v5-nano-retrieval-q8_0",
|
|
94
113
|
doc_prefix="Document: ",
|
|
95
114
|
query_prefix="Query: ",
|
|
96
|
-
|
|
115
|
+
params="239M",
|
|
116
|
+
languages="multi",
|
|
117
|
+
context="8K",
|
|
118
|
+
download_mb=250,
|
|
119
|
+
notes="best sub-500M quality; CC-BY-NC (non-commercial); llama-server only",
|
|
97
120
|
gguf_ref="https://huggingface.co/jinaai/jina-embeddings-v5-text-nano-retrieval-GGUF@Q8_0",
|
|
98
121
|
gguf_fragment="v5-nano-retrieval-q8_0",
|
|
99
122
|
requires_llama_server=True,
|
|
@@ -104,6 +127,7 @@ RERANKER = RerankerChoice(
|
|
|
104
127
|
label="BGE reranker v2-m3",
|
|
105
128
|
model="BAAI/bge-reranker-v2-m3",
|
|
106
129
|
languages="multilingual",
|
|
130
|
+
download_mb=600,
|
|
107
131
|
notes="568M, multilingual; keeps the measured Dutch-query recall win",
|
|
108
132
|
gguf_ref="https://huggingface.co/gpustack/bge-reranker-v2-m3-GGUF@Q8_0",
|
|
109
133
|
gguf_fragment="bge-reranker-v2-m3-q8_0",
|
|
@@ -124,15 +148,33 @@ def detect_specs() -> Specs:
|
|
|
124
148
|
return Specs(ram_gb=ram, macos=sys.platform == "darwin")
|
|
125
149
|
|
|
126
150
|
|
|
127
|
-
def
|
|
128
|
-
"""
|
|
151
|
+
def ram_limited(specs: Specs) -> bool:
|
|
152
|
+
"""True when RAM is known and below the balanced/best comfort threshold."""
|
|
153
|
+
return 0 < specs.ram_gb < LOW_RAM_GB
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def recommend(
|
|
157
|
+
quality: str, specs: Specs, *, enforce_ram: bool = True
|
|
158
|
+
) -> tuple[EmbeddingChoice, RerankerChoice, list[str]]:
|
|
159
|
+
"""Pick an embedding + reranker combo; returns (embedding, reranker, notes).
|
|
160
|
+
|
|
161
|
+
enforce_ram=False (interactive callers whose user already confirmed the pick)
|
|
162
|
+
keeps the requested tier on low-RAM machines and warns instead of downgrading.
|
|
163
|
+
"""
|
|
129
164
|
if quality not in EMBEDDINGS:
|
|
130
165
|
raise RagxError(f"quality must be one of {'/'.join(QUALITY_TIERS)}, got {quality!r}")
|
|
131
166
|
notes: list[str] = []
|
|
132
167
|
tier = quality
|
|
133
|
-
if
|
|
134
|
-
|
|
135
|
-
|
|
168
|
+
if ram_limited(specs) and tier in ("balanced", "best"):
|
|
169
|
+
if enforce_ram:
|
|
170
|
+
notes.append(
|
|
171
|
+
f"{specs.ram_gb:.0f} GB RAM detected — dropping to the fast embedding tier"
|
|
172
|
+
)
|
|
173
|
+
tier = "fast"
|
|
174
|
+
else:
|
|
175
|
+
notes.append(
|
|
176
|
+
f"{specs.ram_gb:.0f} GB RAM detected — the {tier} tier may be slow on this machine"
|
|
177
|
+
)
|
|
136
178
|
if tier == "jina-nano":
|
|
137
179
|
notes.append("jina-nano is CC-BY-NC licensed — non-commercial use only")
|
|
138
180
|
return EMBEDDINGS[tier], RERANKER, notes
|
|
@@ -42,6 +42,15 @@ def test_recommend_downgrades_on_low_ram():
|
|
|
42
42
|
assert emb.tier == "best"
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def test_recommend_no_enforce_warns_instead_of_downgrading():
|
|
46
|
+
emb, _, notes = recommend("best", Specs(ram_gb=4.0, macos=False), enforce_ram=False)
|
|
47
|
+
assert emb.tier == "best"
|
|
48
|
+
assert any("may be slow" in n for n in notes)
|
|
49
|
+
# plenty of RAM: no warning either way
|
|
50
|
+
_, _, notes = recommend("best", BIG, enforce_ram=False)
|
|
51
|
+
assert notes == []
|
|
52
|
+
|
|
53
|
+
|
|
45
54
|
def test_recommend_rejects_unknown_quality():
|
|
46
55
|
with pytest.raises(RagxError):
|
|
47
56
|
recommend("ludicrous", BIG)
|
|
@@ -301,6 +310,120 @@ def test_models_rejects_unknown_engine(corpus):
|
|
|
301
310
|
assert result.exit_code == 2
|
|
302
311
|
|
|
303
312
|
|
|
313
|
+
def test_models_jina_auto_locks_embed_engine_when_unset(corpus, monkeypatch, tmp_path):
|
|
314
|
+
"""quality=jina-nano without --embed-engine no longer errors: the engine is
|
|
315
|
+
locked to llama-server (the only one that can serve it)."""
|
|
316
|
+
gguf = lmstudio.InstalledModel(
|
|
317
|
+
model_key="v5-nano-retrieval", type="embedding", format="gguf",
|
|
318
|
+
path="jinaai/jina-embeddings-v5-text-nano-retrieval-GGUF/v5-nano-retrieval-Q8_0.gguf",
|
|
319
|
+
size_bytes=1,
|
|
320
|
+
)
|
|
321
|
+
monkeypatch.setattr(lmstudio, "find_lms", lambda: "/fake/lms")
|
|
322
|
+
monkeypatch.setattr(lmstudio, "find_installed", lambda lms, fragment: gguf)
|
|
323
|
+
monkeypatch.setattr(lmstudio, "models_root", lambda: tmp_path / "lmsmodels")
|
|
324
|
+
result = runner.invoke(
|
|
325
|
+
app, ["models", "--quality", "jina-nano", "--rerank-engine", "llama-server",
|
|
326
|
+
"--yes", "--json"],
|
|
327
|
+
)
|
|
328
|
+
assert result.exit_code == 0, result.output
|
|
329
|
+
doc = json.loads(result.stdout)
|
|
330
|
+
assert doc["embedding"]["engine"] == "llama-server"
|
|
331
|
+
assert any("locked" in n for n in doc["notes"])
|
|
332
|
+
cfg = Config.load(corpus)
|
|
333
|
+
assert cfg.get("embeddings.provider") == "llama-server"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# --- interactive flow ---------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _script_prompts(monkeypatch, answers, confirms):
|
|
340
|
+
"""Feed scripted answers into the numbered pickers and confirms; a TTY stdin
|
|
341
|
+
makes run_flow take the interactive branch (prompts never touch real stdin)."""
|
|
342
|
+
from types import SimpleNamespace
|
|
343
|
+
|
|
344
|
+
monkeypatch.setattr("sys.stdin", SimpleNamespace(isatty=lambda: True))
|
|
345
|
+
answer_iter, confirm_iter = iter(answers), iter(confirms)
|
|
346
|
+
monkeypatch.setattr("typer.prompt", lambda *a, **k: next(answer_iter))
|
|
347
|
+
monkeypatch.setattr("typer.confirm", lambda *a, **k: next(confirm_iter))
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_pick_numbered_reprompts_until_valid(monkeypatch):
|
|
351
|
+
from ragx.cli import models_ui
|
|
352
|
+
|
|
353
|
+
answers = iter(["9", "abc", "2"])
|
|
354
|
+
monkeypatch.setattr("typer.prompt", lambda *a, **k: next(answers))
|
|
355
|
+
assert models_ui.pick_numbered(["a", "b", "c"], default=1) == 2
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_pick_tier_low_ram_defaults_and_override(monkeypatch):
|
|
359
|
+
from ragx.cli import models_ui
|
|
360
|
+
|
|
361
|
+
low = Specs(ram_gb=4.0, macos=False)
|
|
362
|
+
# decline the RAM override for "best", then settle on "fast"
|
|
363
|
+
_script_prompts(monkeypatch, answers=["3", "1"], confirms=[False])
|
|
364
|
+
assert models_ui.pick_tier(low) == "fast"
|
|
365
|
+
# confirm the override: the explicit choice is honored
|
|
366
|
+
_script_prompts(monkeypatch, answers=["3"], confirms=[True])
|
|
367
|
+
assert models_ui.pick_tier(low) == "best"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_models_interactive_flow_plans_then_writes(corpus, monkeypatch, capsys):
|
|
371
|
+
from ragx.cli.models_cmd import run_flow
|
|
372
|
+
from ragx.core.catalog import Specs as SpecsCls
|
|
373
|
+
|
|
374
|
+
installed = lmstudio.InstalledModel(
|
|
375
|
+
model_key="text-embedding-bge-m3", type="embedding", format="gguf",
|
|
376
|
+
path="gaianet/bge-m3-GGUF/x.gguf", size_bytes=1,
|
|
377
|
+
)
|
|
378
|
+
monkeypatch.setattr("ragx.cli.models_cmd.detect_specs", lambda: SpecsCls(32.0, True))
|
|
379
|
+
monkeypatch.setattr("shutil.which", lambda name: None)
|
|
380
|
+
monkeypatch.setattr(lmstudio, "find_lms", lambda: "/fake/lms")
|
|
381
|
+
monkeypatch.setattr(lmstudio, "find_installed", lambda lms, fragment: installed)
|
|
382
|
+
monkeypatch.setattr(
|
|
383
|
+
lmstudio, "download",
|
|
384
|
+
lambda lms, ref: pytest.fail("must not download an installed model"),
|
|
385
|
+
)
|
|
386
|
+
# tier 2 (balanced), embed engine 2 (lm-studio), rerank engine 2 (sentence-transformers)
|
|
387
|
+
_script_prompts(monkeypatch, answers=["2", "2", "2"], confirms=[True])
|
|
388
|
+
|
|
389
|
+
run_flow(corpus)
|
|
390
|
+
|
|
391
|
+
err = capsys.readouterr().err
|
|
392
|
+
assert "machine:" in err
|
|
393
|
+
assert "── embedding model" in err
|
|
394
|
+
assert "── plan" in err
|
|
395
|
+
assert "already downloaded ✓" in err
|
|
396
|
+
cfg = Config.load(corpus)
|
|
397
|
+
assert cfg.get("embeddings.model") == "text-embedding-bge-m3"
|
|
398
|
+
assert cfg.get("embeddings.provider") == "openai"
|
|
399
|
+
assert cfg.get("rerank.provider") == "sentence-transformers"
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def test_models_interactive_jina_skips_embed_engine_prompt(corpus, monkeypatch, tmp_path):
|
|
403
|
+
from ragx.cli.models_cmd import run_flow
|
|
404
|
+
from ragx.core.catalog import Specs as SpecsCls
|
|
405
|
+
|
|
406
|
+
gguf = lmstudio.InstalledModel(
|
|
407
|
+
model_key="v5-nano-retrieval", type="embedding", format="gguf",
|
|
408
|
+
path="jinaai/jina-embeddings-v5-text-nano-retrieval-GGUF/v5-nano-retrieval-Q8_0.gguf",
|
|
409
|
+
size_bytes=1,
|
|
410
|
+
)
|
|
411
|
+
monkeypatch.setattr("ragx.cli.models_cmd.detect_specs", lambda: SpecsCls(32.0, True))
|
|
412
|
+
monkeypatch.setattr("shutil.which", lambda name: None)
|
|
413
|
+
monkeypatch.setattr(lmstudio, "find_lms", lambda: "/fake/lms")
|
|
414
|
+
monkeypatch.setattr(lmstudio, "find_installed", lambda lms, fragment: gguf)
|
|
415
|
+
monkeypatch.setattr(lmstudio, "models_root", lambda: tmp_path / "lmsmodels")
|
|
416
|
+
# exactly ONE prompt expected (rerank engine) — an embed-engine prompt would
|
|
417
|
+
# exhaust the iterator and fail the test
|
|
418
|
+
_script_prompts(monkeypatch, answers=["1"], confirms=[True])
|
|
419
|
+
|
|
420
|
+
run_flow(corpus, quality="jina-nano")
|
|
421
|
+
|
|
422
|
+
cfg = Config.load(corpus)
|
|
423
|
+
assert cfg.get("embeddings.provider") == "llama-server"
|
|
424
|
+
assert cfg.get("rerank.provider") == "llama-server"
|
|
425
|
+
|
|
426
|
+
|
|
304
427
|
def test_init_routes_into_models_flow(tmp_path, monkeypatch):
|
|
305
428
|
called = {}
|
|
306
429
|
monkeypatch.setattr(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|