Usage¶
Three ways to drive the system: the CLI, the warm HTTP API, and the console. They share one pipeline and one config.
Agent-facing query surface
The warm API exposes live capability and provider discovery, stable source lookup,
and bounded comparison trees in addition to single /search and /query calls.
Fetch /schema and /providers at runtime instead of copying preset or backend
lists into an agent prompt.
CLI¶
python main.py index # build indexes from data/*.jsonl
python main.py index --append <file>.jsonl # add a source family (auto-rebuilds sparse)
python main.py index --rebuild-into <name> [--embedding <provider>] [--limit N] # re-embed the active collection's corpus into a NEW collection; --embedding = an embedding.providers name or local (default embedding.provider); resumable, never writes the active one; --limit N = a smoke build, left unservable
python main.py stamp [--collection NAME] [--force] # verify which embedder built a collection (re-embeds a sample), then record it; :8051 refuses a mismatch
python main.py ingest-pdfs [--pages "1-50,60"] [--chunking heading|fixed|document|none] [--include-files "a.pdf,b.pdf"] [--output data/<name>_chunks.jsonl]
python main.py ingest-notebooks [--include-path "<scope>"] [--include-files "a.py,b.ipynb"] [--output data/<name>_chunks.jsonl] [--force-domain ml] [--force-tags "a,b"]
python main.py ingest-code --include-path "<subtree>" --output data/<name>_chunks.jsonl [--include-files "x.sql"] [--force-domain swe] [--force-tags "a,b"] # a scoped run needs its own output
python main.py ingest-md --include-path "<scope>" --output data/<name>_chunks.jsonl [--force-domain nlp] [--force-tags "a,b"] # scoped md parse (guarded)
python main.py ingest-canvas [--include-path "<scope>" --output data/<name>_chunks.jsonl] [--context-depth 0|1|2] [--max-chunk-size N] [--force-tags "a,b"] # Obsidian .canvas files; a scoped run needs its own output
python main.py ocr-studio-ingest --doc "<vault-relative PDF or image>" --output data/ocrstudio_<name>_chunks.jsonl [--pages done|all] # one document's OCR Studio pages
python main.py graph-embed [--limit N] # rebuild graph mode's own seed index from scratch (needs graph.embedding_provider)
python main.py stamp-provenance --dry-run|--yes # metadata-only: mark stored rows the provenance rule calls generated (--yes: servers down; :8051 refuses to start under drop/route until it has run once)
python main.py route-pointers [--out PATH] # build route mode's pointer sidecar (each generated chunk -> the primary pages it cites); restart :8051 afterwards
python main.py relocate --map rename_map.jsonl [--dry-run|--verify] # metadata-only move of indexed files to new vault paths, no re-embed (servers down, then rebuild_bm25.py)
python main.py fetch-web --urls "https://…" [--backend auto|requests|crawl4ai|scrapling|crawlee] [--format md|pdf]
python main.py convert-files --files "report.docx" [--ocr-pages "1-4,9"] # markitdown → .md
python main.py query "<question>" [--preset code|concept|synthesis] [--top-k N] [--max-tokens N]
python main.py chat # interactive REPL
python main.py bench run --configs ladder # score labelled questions per pipeline configuration (also: report | split | validate | sample)
OCR engines (--ocr-engine, also per job in the console): auto — probe for
Tesseract and use it if present (the default); tesseract — pin the classic
in-process binary; paddle — a PaddleOCR service reached over HTTP
(pdf.paddle_ocr), much stronger on skewed, multi-column and non-Latin scans and
still seconds per page on CPU; vlm — a document vision model over HTTP
(pdf.vlm_ocr) for the hardest material; none — no OCR, scanned pages stay
sparse. The two HTTP lanes run out-of-process on purpose, so heavy OCR
dependencies never enter the query environment and can be shut down when idle.
Run GET /api/import/ocr_scan (the 🔎 button) first — it reports which pages
actually lack extractable text, so you only OCR those.
The paddle lane itself picks its engine with
pdf.paddle_ocr.pipeline: ocr (the default — PP-OCRv6 detect-and-recognise,
plain text, seconds per page) and vl (PaddleOCR-VL, a document VLM that
returns markdown with LaTeX formulas and real tables, at roughly 30x the cost
for no gain on ordinary prose). Reach for vl on a chapter where the equations
and tables are the content; leave it alone for scanned prose. It needs the
GPU image, and a sidecar that cannot serve it says so rather than quietly
returning the other engine's output.
Chunking strategies (--chunking, also selectable per run in the console): heading —
paragraph packing that follows the text's structure (default); fixed — strict sliding
window, for OCR'd / wall-of-text sources; document — element-aware packing that never
cuts inside a fenced code block, table, or list (one element larger than the chunk size
on its own falls back to the sliding window); none — no splitting at all (one chunk
per section; embedding models truncate very long inputs, so use deliberately). Small
sections chunk identically under every strategy.
HTTP API (:8051)¶
The warm endpoint keeps the indexes and models hot, so queries never pay startup cost.
curl -s -X POST http://127.0.0.1:8051/compare \
-H "Content-Type: application/json" \
-d '{
"q": "What does the retention policy say about backups?",
"mode": "search",
"branches": [
{"id": "baseline", "label": "Config baseline", "auto_preset": false},
{"id": "concept", "preset": "concept"},
{"id": "lexical", "rerank": "lexical"}
]
}'
Every /query response identifies what actually ran. Every source has a stable
evidence id, and generated answers report the resolved backend/model rather than
leaving the caller to infer them from config:
{
"sources": [
{
"id": "<stable-evidence-id>",
"origin_id": "<retrieval-id>",
"lookup_available": true,
"n": 1,
"label": "<source label>",
"cited": true
}
],
"retrieval": {
"preset": "code",
"rerank_top_k": 10,
"hyde_used": false,
"reranker_model": "<resolved-reranker>"
},
"generation": {
"backend": "<provider-registry-name>",
"protocol": "<wire-protocol>",
"model": "<resolved-model>",
"usage": {}
}
}
GET /history returns the last /search + /query calls (newest first, in-memory):
the question, the knobs the caller explicitly set, the full retrieval echo of what
actually ran, confidence and timing — so an agent tuning hyperparameters can see what
it already tried instead of re-deriving it.
GET /compare/options returns ready-to-post /compare branches for each
dimension (presets, rerank methods, generation backends) together with the live
branch caps — so building a comparison never means joining /schema and
/providers by hand.
GET /chunks/{chunk_id} fetches the current evidence record behind a stable id returned
from /search, /query, or /compare when lookup_available is true.
Parent-expanded sections use a parent:<id> evidence id and also report the indexed
child as origin_id, so overlap is computed from the text each branch actually
received. Live Omnisearch excerpts use a content-derived live:<hash> evidence id and
set lookup_available to false because they are not stored index records.
GET /schema advertises the current request fields, branch limits, preset registry,
and endpoint map.
Comparison-tree semantics¶
POST /compare accepts a question, mode: search|query, and a bounded list of named
branches. Read the live /schema for the current limits. A branch can override the
same retrieval controls as /search; query-mode branches may additionally override
the configured provider and model.
The response contains every branch plus comparison: common and branch-unique source
ids, per-branch ranks, rank spread, and pairwise overlap. It intentionally does not
compare raw scores, because cross-encoder logits, lexical scores, HTTP reranker scores,
and fused RRF values are on different scales.
Branches that differ only by provider/model reuse one exact evidence set. This makes a generation-backend comparison about answer behavior instead of retrieval noise. A generation failure stays on that branch; a retrieval/reranker failure marks the branch as a retrieval error without discarding successful siblings.
fetch-web --format pdf prints the fully rendered page through headless Chromium
(LaTeX, tables and highlighted code exactly as the site shows them) instead of
converting to markdown — the right lane for math- or code-heavy sources, and the
output ingests through the PDF lane with real page numbers. It needs Playwright's
Chromium once: python -m playwright install chromium.
For the markdown path, --backend auto tries crawl4ai, then scrapling, then plain
requests, using whatever is installed. crawlee is opt-in: auto never picks it.
It renders the page with Crawlee's PlaywrightCrawler in the same Chromium, then the
HTML goes through the same markitdown conversion as every other backend. Asking for
it without the package installed, or when the crawl fails, is an error, never a quiet
switch to another backend.
Presets and per-query knobs¶
Named override bundles in config.yaml, selectable per query with no restart — the warm
pipeline is never mutated:
retrieval:
presets:
code: {rerank_top_k: 10, use_hyde: false, dense_top_k: 40, sparse_top_k: 40, boost_code: true}
concept: {rerank_top_k: 5, use_hyde: false}
synthesis: {rerank_top_k: 10, use_hyde: false, dense_top_k: 30, sparse_top_k: 30, parent_context: true, neighbor_context: true}
| Knob | Effect |
|---|---|
preset |
Apply a named bundle (code / concept / synthesis). |
auto_preset |
false suppresses implicit code-intent preset selection for a config-only baseline. Explicit preset still wins. |
top_k / rerank_top_k |
How many reranked chunks reach the generator. |
dense_top_k / sparse_top_k |
Candidate-pool width per lane before fusion. |
use_hyde / hype |
Toggle query expansion. |
rerank_instruction |
The ranking criterion in plain language: "prefer worked procedures over definitions". Applied only to the text the reranker scores against, so it reorders the candidate pool and can never remove from it. Ignored by lexical / none / laya; the retrieval echo reports whether it was actually applied. Config default: retrieval.rerank_instruction. |
rerank |
Rerank method for this call: cross_encoder (in-process semantic scoring), http (configured external /v1/rerank service), lexical (model-free query-term coverage), none (raw fused order), or laya (experimental: a fine-tuned scorer, refused unless retrieval.laya.enabled and never swapped for another method). Config default: retrieval.rerank_mode. |
lane_weights / lanes / metadata_boost |
Fusion controls. lane_weights rescales each lane's RRF term; lanes restricts the call to a subset of the lanes (one outside the list does not run); metadata_boost forces the course/domain/tag boost on or off. See Lane weights. |
scope_lanes / max_per_file / decompose |
scope_lanes forces the scope lanes on or off; max_per_file caps how many chunks of one source file reach the final list; decompose splits a multi-part question into separate searches with one LLM call. See Ask and search. |
gate / gate_threshold / gate_scorer |
The optional post-rerank relevance gate: chunks scoring below the threshold are dropped, and when none pass /query abstains with the fixed "nothing relevant" answer and no LLM call. Off by default. See Relevance gate. |
parent_context / neighbor_context |
E2 small-to-big, post-rerank: swap note chunks for their full section / append a PDF hit's adjacent pages. Carried by the synthesis preset; per-call override beats preset beats config. |
generated_mode |
drop keeps generated chunks (maps, summaries an LLM wrote) out of this call's evidence; route does too and lets them find the primary pages they cite, each such source carrying found_via. off is config only (retrieval.generated_mode). Unset follows the configured mode. See Route mode. |
source_file / pages |
Restrict every lane to one file's primary chunks, and with pages: [first, last] to a page range of it. GET /outline lists the files. Need retrieval.sparse_backend: bm25s; the echo's file_filter says whether anything matched. |
max_tokens |
Cap the answer length. |
provider / model |
/query and query-mode /compare only: select a configured provider and optionally override its default model. Endpoints and secrets cannot be supplied per request. |
A knob with a bad value (an unknown lane, a gate with no threshold, a refused rerank mode) is
answered with HTTP 200 and an error field rather than a 400; a wrong type is a 422. See
When a knob is wrong.
max_tokens and citations
A very small max_tokens can truncate the citation footer and drop the answer's
confidence to UNKNOWN. Leave enough room (a few hundred tokens) for a cited answer.
Reranker failures are retrieval failures
A response beginning Reranking failed: did not reach generation. Preserve the
underlying model error and inspect GET /config for the active reranker. In
particular, BAAI/bge-reranker-base has a 512-token input limit; configuring it
above that limit is invalid. Do not relabel this as a generation-provider outage.
Generation providers¶
Provider definitions live in the providers: registry in config.yaml. A request can
name a configured backend, but it cannot inject a URL or secret. GET /providers returns
the active backend plus each configured backend's protocol, endpoint, default model,
secret environment-variable name, and readiness flags; it never returns secret values.
available means the required key is present and type-compatible, not that a remote
endpoint has been contacted.
Where a vendor sells both a subscription and pay-as-you-go API access, the two
credentials are usually different products and are not interchangeable. A provider
entry can declare api_key_prefix so the wrong key type is rejected before a
query runs, rather than silently billing the wrong account. Put a key only in the
environment variable the registry declares, or store it through the console's
provider-key API. Restart the query service after changing a provider secret.
An entry marked vision: true declares that its model reads images, and only
such an entry is ever sent one: the Graph tab's suggest-label attaches an image
file node for it alone, and the LLM client refuses images for any other entry
rather than letting an endpoint drop them without a word. Set it only after
checking that the endpoint's model accepts images, and not on a router whose
model can change from one request to the next.
The Corpus Ledger console (:8052)¶
Open http://127.0.0.1:8052. Every tab follows one design, drawn from the active theme's tokens: panels open with a small kicker line above their heading, actions carry icons from one icon set, and every confirmation or prompt is an in-page dialog in the theme, never a browser pop-up. The header names the active vault, the console reopens on the tab you left (the address bar carries it too, so a link opens that tab), and any list that grows with use scrolls inside its card. Tabs:
- Ledger — the corpus at a glance: chunk, document and vector counts, disk use, the Index health card (below), each domain's share, every chunk file (with when it last changed, sorted newest first, oldest first or by chunk count), and recent jobs.
- Query — the presets, the query history (kept per vault) and the question box sit
above three sub-tabs: Answer, Compare and Graph (the last only while Graph
RAG is switched on). Answer is Ask / Search with its knobs in labeled groups (pool sizes, extra lanes
HyDE/HyPE/Omnisearch, rerank method, E2 parents/neighbors), plus copy and
.mdexport (ready to paste into any Markdown editor). The Markdown + LaTeX Preview toggle renders the answer and every source chunk — math, tables and fenced code included. Compare, the Query comparison tree, keeps the same knobs (its branches start from them) and fans the current question across live-config presets and generation backends or the built-in rerank baselines, with evidence-only and generated-answer modes. - Documents — search,
#tagfilter, retag (domain / group label / tags — metadata-only, no re-embed), and delete from the index. - Vault — browse the mounted vault tree with each file's index status, stage domain and tag edits for indexed files and apply them in one pass (index metadata only, with one BM25 rebuild queued), and find any file by name. Vault files are never modified.
- Ingest — a numbered flow: 1 · Add documents (upload to the inbox; every
routable type — pdf, md, code, notebook — rides a
default | customlane, and every file can carry its own ⚙ settings — domain and tags for all kinds, chunking for pdf/md, OCR engine and page subset for PDFs — plus a destination folder: the file moves to its vault home when the job is queued, before parsing, so the index records the final path (blank = stays in the inbox, archives to_ingested). The batch defaults (domain / tags / chunking / destination, each with a 📁 picker) fill in for any file without its own ⚙ value. 👁 previews any md/PDF, with PDF page numbers visible for OCR-range picking. An inbox file's × asks, then deletes it from disk for good — nothing indexed is touched — unlike the OCR tab's ×, which moves a file to the vault's.trash); 2 · Fetch & convert (pull web links as.mdvia markitdown or as a printed.pdfof the rendered page via headless Chromium — LaTeX/tables/code intact; convert any upload — pdf/docx/pptx/xlsx/html — to.md, with optional per-page OCR; outputs stage in_convertedwith preview until promoted, and a staged file's × also asks, then deletes it for good); 3 · Jobs designer — one card per queued job (kind badge, lane, file chips, effective-settings line): files sharing identical effective settings batch together, differing ⚙ settings split automatically; then Queue the plan (everything shown) or one lane at a time (custom only / default only). Vault-wide passes live under Advanced in the same panel. Routing is kind-aware:.py/.R/.ipynb/.Rmdgo to the notebook lane (ingest-notebooksowns them — it has the Pythonast/# %%cell splitter), while.js/.ts/.sql/.go/… go to the code lane. 4 · Graph & canvases previews and queues the canvas lane for a scope, with its own chunking, node depth and metadata; the preview reports how much text a node depth duplicates before anything is written. - OCR — hand-guided OCR of one PDF or image from the vault, or an import (a typed
page range limits the work, and every page keeps its real number): per-page clean-up
(deskew, contrast, threshold, denoise, a healing brush), regions drawn by hand or
proposed from the page layout or the PaddleOCR sidecar's lines, each region OCR'd by its
own engine and language, a quality report per engine over the pages marked done, ingest
of those pages (append, or replace what is indexed), and a zip export of the corrected
markdown with its figures. The × on an imported or inbox row asks, then takes
the file out of the queue by moving it into the vault's
.trashunder its own sub-path, refreshes the list, and closes the document if it was open (its unsaved page edits are saved first). Nothing is deleted: moving it back restores it with its OCR work. While the document is indexed the × is refused, and the message lists the indexed files with a shortcut to them in the Documents tab; remove them from the index first. - Graph — every
.canvasin the vault: open one, edit its nodes, edges and labels, and save the edits into the file. A save is refused if the file changed on disk since it was opened, and the previous version is kept incanvas.backup_dir, outside the vault. Re-index removes the canvas's chunks from the index and its chunk files, then ingests it into its own chunk file and appends that (and rebuilds the graph seed index when one is configured). Graph-mode querying is a separate, experimental panel in the Query tab (Graph mode).- Suggest label, on a selected edge, asks a language model for that edge's label
from the canvas as the editor holds it, unsaved edits included: both endpoint nodes
(file nodes read from the vault, long ones summarised first), the arrow's
direction, the canvas title, the groups and the labels around the edge. Pick the
backend (an entry marked vision also sees image nodes; one without a usable key is
listed but disabled, naming the missing variable or a wrong key type) and,
optionally, a model and a line of guidance; the defaults are
canvas.augment.providerandcanvas.augment.model, else the generation backend. The suggestion fills the label box. Beside it: the relation, confidence and reason, alternative labels to apply in one click, tag chips that add or remove a#tagin the label, any warnings, the backend and routed model with the calls and tokens used, and what the model saw. Every change can be undone. A failed call shows the failing stage, the HTTP status, the backend and model, the error, and the model's raw reply. Only Save writes the file (Suggest label). #tagsin labels. Write them into a label (#regression). Withcanvas.label_tags: true(off by default), the next ingest of that canvas also stamps them astagson the chunks of both nodes the edge joins: metadata only, with ids and text unchanged. The edge editor says which applies.- Remove from index (shown with nothing selected, when the canvas has indexed
chunks) asks first, then deletes its chunks from the index as the Documents tab
does and queues the BM25 rebuild, plus
graph_embedwhen a graph embedder is set. It then offers a:8051restart, which runs only once those jobs have finished. The.canvasfile is never written, and unsaved edits in the editor survive. - Graph embedder, a panel under the canvas list, shows graph mode's own seed
index: its verdict and the reason, what it was built with and how complete it is,
a note when graph mode itself is off, and the embedders to choose from (an
unavailable one is listed with its reason). Its button saves the choice through
Settings, builds the index through the job queue (
graph_embed), follows the job, and then offers the restart. Choosing the main embedder only saves the setting, and no new build starts while one is queued or running.
- Suggest label, on a selected edge, asks a language model for that edge's label
from the canvas as the editor holds it, unsaved edits included: both endpoint nodes
(file nodes read from the vault, long ones summarised first), the arrow's
direction, the canvas title, the groups and the labels around the edge. Pick the
backend (an entry marked vision also sees image nodes; one without a usable key is
listed but disabled, naming the missing variable or a wrong key type) and,
optionally, a model and a line of guidance; the defaults are
- Jobs — watch long-running ingest / maintenance jobs: the active vault's by default, every vault's with all vaults.
- Eval — the review queue for the bench's question sets
(Eval review queue): progress per suite against its quota,
the questions filtered by suite, status and split, and one question at a time with its
gold texts and validator findings, to verify, reject or edit. Check retrieval runs
the question through
/searchwith HyDE off. - Settings — appearance, kept in the browser: dark and light preset shelves
with the built-in themes (the warm Ledger pair + Material mint dark/light) and your
own saved presets, each renamed or deleted from its chip; the header's theme button
cycles through every theme on both shelves. The theme designer edits one row per
design token (the colours, each with a colour picker; the three fonts; the corner
radii), bound both ways to a box of the same CSS variables; Export current → CSS
writes the active theme into that box. An edit starts from the theme in use and
previews on the page at once; nothing is kept until Save as preset, under a name,
on the dark or light shelf. Export all themes and Import
themes… move every saved preset as one JSON file, and an import merges without
overwriting. Font pickers for headings / body / mono (system serif and sans
choices like Times New Roman, Georgia, Arial — or any installed font by name) apply
over every theme. Then a vault switcher that remembers
every vault ever opened together with its own settings (index paths and collection,
every lane's chunk file, figure and backup folders, the inbox and tree root, and the
embedder) and swaps the whole set atomically; and the editable config surface with 📁 folder pickers: vault root,
Chroma / BM25 / chunks paths, an Embedding panel (the model, picked
by name, and the collection it searches, kept consistent: choosing a model switches to
a collection this vault built with it, choosing a collection switches to the model
that built it, fields the provider ignores are greyed out, and a verdict line says
whether
:8051will start), cross-encoder models, default rerank & chunking, generation endpoint, and the Graph tab's suggest-label backend and model, label-tag indexing and graph seed embedder. Saves rewriteconfig.yamlin place (comments preserved); nothing hot-applies — the response says which service to restart. Panels sit under the fields: Generation backends, listing theproviders:registry with each backend's endpoint/model and whether its API-key environment variable is set and type-compatible (never the value), plus controls to activate a backend or write its declared key to the gitignored.env; Reranker, which suggests known-good cross-encoders with their measured cost and states what PyTorch can actually reach — acudadevice on a CPU-only torch build would otherwise fail silently into a much slower path; and OCR, which reports what each OCR engine can do right now and can warm a vision-OCR model before the first ingest page. A Fetch engines panel lists each web-import package with its installed version andrequirements.txtpin; Check fetches the latest release and dry-runs the upgrade so every package it would change shows first, and Update queues that upgrade (plus its browser installs) as anengine_updatejob in the Jobs tab. - Info — diagrams of the query and the ingestion pipelines. Hovering, focusing or
clicking a stage shows what it does, which
config.yamlkeys control it, and whether it is on in the live config. A Workspace map has one card per tab, saying what each is for.
Ops niceties: the header's ⟳ restart button relaunches the warm query API in one
click (and webui.auto_restart_rag in Settings does it automatically after every
successful index-changing job); the Ledger shows an Index health card — ✓ in sync
when the dense (Chroma) and sparse (BM25) indexes hold the same corpus, ✗ drifted with
the fix spelled out (the BM25 count comes from a sidecar written at build time — no
giant unpickle); and the Query tab keeps a restorable history of your last 20
queries with their knobs.
For agents¶
manage_api exposes GET /api/schema — a machine-readable, permission-tiered map of
every management operation (read / mutating / destructive) so an agent can discover what
it may call before it calls it. The query service has its own GET /schema: pair the
two maps rather than assuming a copied capability list. Ask/search/compare/inspect
evidence or change warm retrieval defaults → :8051; manage the corpus,
persistent install settings, or provider secrets → :8052.
The complete endpoint listing for both services is the
API reference; the patterns for driving them efficiently from an LLM
agent are in Agent integration. An agent that does not know the vault
can start from GET /outline (what is indexed, by domain and course, with chunk
and page counts) before it searches, and the noetrix-navigator skill
(SKILL_NAVIGATOR.md) turns a multi-hop question into the right calls.
Agents that speak MCP can use the MCP server instead of raw HTTP. On
Windows, rag mcp starts it over stdio for a client on the same machine (it
prints nothing, because stdout is the protocol channel); python mcp_server.py
--http serves it over HTTP on mcp.port. Start :8051 and :8052 first.
When the query API will not come back¶
GET /health on :8051 reports one of three states:
state |
Meaning | What to do |
|---|---|---|
ready |
Serving. | — |
loading |
Indexes and models still loading. | Wait. |
failed |
The pipeline could not be built from the current config. | Act; waiting cannot fix it. |
A failed service keeps answering: every retrieval endpoint returns 503 carrying
the same reason, instead of refusing the connection and leaving the caller to
guess. The console's restart button surfaces the startup log automatically, and
GET /api/service/log on :8052 returns it on demand.
The usual cause is the setting changed most recently — a cross-encoder id that
cannot be downloaded, an embedding model with a typo, an index path that moved.
Before switching cross-encoder, POST /api/rerank/check (the Check button on
each model row in Settings → Reranker) reports whether the id resolves and
whether the weights are already cached, and where. Those are exactly the two
conditions that decide whether the restart after the switch will succeed, and it
downloads nothing to find out.
A stale Hugging Face login looks exactly like a missing model
If a token is present but expired or revoked — in HF_TOKEN, or in the
stored login file under your cache directory — the hub answers 401 and
huggingface_hub surfaces that as repository not found. Every public
cross-encoder then appears not to exist, and the download at the next
:8051 start fails the same way with the same misleading message.
The preflight retries anonymously and, when that succeeds, says explicitly which credential the hub rejected and where it came from. Fix or remove it.
Where 'cached' is looked up
There is no single model cache. sentence-transformers honours
SENTENCE_TRANSFORMERS_HOME over HF_HOME, while HF_HUB_CACHE can be
set machine-wide to override HF_HOME for huggingface_hub itself — so
those can legitimately be three different directories. The preflight checks
all of them and reports the snapshot path it found, and it requires an actual
weights file: a metadata-only directory is a failed download, not a cached
model.
Updating the scraper pins¶
The web-fetch lane (fetch-web, and the console's Fetch & convert panel) sits on a
few scraper packages that requirements.txt pins exactly. They move together and a
loose upgrade tends to break at call time rather than at install time, so a bump is a
deliberate edit of the pins, never a pip install -U.
| Pin | Why it is spelled that way |
|---|---|
scrapling[fetchers] |
The [fetchers] extra is required. Bare scrapling imports, but its Fetcher dies on the missing curl_cffi / browserforge the first time it is called. |
crawl4ai |
The browser-rendering backend; it drives both playwright and patchright. |
playwright, patchright |
patchright is the patched Playwright driver that crawl4ai and scrapling's stealth fetcher use. Keep the two on the same release and bump them together; on one release both want the same Chromium build. |
markitdown[pdf,docx,pptx,xlsx] |
The HTML / any-file to markdown converter. Never [all]: that adds the Azure, audio-transcription and YouTube extras nothing here uses. |
crawlee[playwright,beautifulsoup] |
The opt-in crawlee backend. It drives PlaywrightCrawler; [beautifulsoup] adds the HTML-only crawler, which the backend does not use yet. |
After installing a new set, download both browsers, Playwright's first:
On the desktop rag.bat points PLAYWRIGHT_BROWSERS_PATH at the G: cache.
To check that a new set resolves together before touching the venv, compile the whole file for the runtime's Python, and again for Windows, the real runtime (a wheel that exists for Linux can be missing there). The output files are throwaways, so delete them rather than commit them; a failure names the conflicting pin:
uv pip compile requirements.txt --python-version 3.11 -o resolved-linux.txt
uv pip compile requirements.txt --python-version 3.11 --python-platform windows -o resolved-windows.txt
The Docker image installs the same requirements.txt, with
deploy/docker/constraints.txt pinning the transitive ML packages, so a new set reaches
it on the next image build (see Docker deployment). Its
Dockerfile, deploy/docker/Dockerfile, downloads both browsers in the same order and
sets CRAWLEE_DISABLE_BROWSER_SANDBOX=1: Crawlee, unlike plain Playwright, keeps
Chromium's sandbox on, and the sandbox needs user namespaces that a default container
does not grant, whether it runs as root or not.