ragdesk 0.1.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.
- ragdesk-0.1.0/.env.example +30 -0
- ragdesk-0.1.0/.github/workflows/ci.yml +32 -0
- ragdesk-0.1.0/.github/workflows/eval.yml +42 -0
- ragdesk-0.1.0/.github/workflows/release.yml +85 -0
- ragdesk-0.1.0/.gitignore +17 -0
- ragdesk-0.1.0/AGENTS.md +406 -0
- ragdesk-0.1.0/LICENSE +201 -0
- ragdesk-0.1.0/PKG-INFO +479 -0
- ragdesk-0.1.0/README.md +445 -0
- ragdesk-0.1.0/SECURITY.md +60 -0
- ragdesk-0.1.0/docs/auth-design.html +14849 -0
- ragdesk-0.1.0/docs/auth-design.json +143 -0
- ragdesk-0.1.0/docs/embedder-comparison.md +94 -0
- ragdesk-0.1.0/docs/eval.json +6 -0
- ragdesk-0.1.0/docs/sources.md +225 -0
- ragdesk-0.1.0/docs/vector-scale.md +110 -0
- ragdesk-0.1.0/fixtures/docs/auth.md +19 -0
- ragdesk-0.1.0/fixtures/docs/deploy.md +17 -0
- ragdesk-0.1.0/fixtures/docs/oncall.md +16 -0
- ragdesk-0.1.0/fixtures/golden.jsonl +7 -0
- ragdesk-0.1.0/fixtures/golden_corpus.jsonl +24 -0
- ragdesk-0.1.0/fixtures/golden_multiturn.jsonl +5 -0
- ragdesk-0.1.0/fixtures/golden_repo.jsonl +12 -0
- ragdesk-0.1.0/pyproject.toml +94 -0
- ragdesk-0.1.0/scripts/bench.py +166 -0
- ragdesk-0.1.0/scripts/build_sidecar.sh +46 -0
- ragdesk-0.1.0/scripts/embedder_compare.py +161 -0
- ragdesk-0.1.0/scripts/eval_badge.py +61 -0
- ragdesk-0.1.0/scripts/eval_ci.sh +31 -0
- ragdesk-0.1.0/scripts/make_golden.py +187 -0
- ragdesk-0.1.0/scripts/public_corpus.py +200 -0
- ragdesk-0.1.0/scripts/rerank_compare.py +106 -0
- ragdesk-0.1.0/scripts/rerank_pool_sweep.py +56 -0
- ragdesk-0.1.0/scripts/topic_map.py +186 -0
- ragdesk-0.1.0/scripts/vector_scale_bench.py +447 -0
- ragdesk-0.1.0/src/ragdesk/__init__.py +3 -0
- ragdesk-0.1.0/src/ragdesk/__main__.py +4 -0
- ragdesk-0.1.0/src/ragdesk/answer.py +178 -0
- ragdesk-0.1.0/src/ragdesk/archive.py +39 -0
- ragdesk-0.1.0/src/ragdesk/buildenv.py +94 -0
- ragdesk-0.1.0/src/ragdesk/chunk.py +93 -0
- ragdesk-0.1.0/src/ragdesk/cli.py +1012 -0
- ragdesk-0.1.0/src/ragdesk/complete.py +167 -0
- ragdesk-0.1.0/src/ragdesk/confluence.py +368 -0
- ragdesk-0.1.0/src/ragdesk/credentials.py +73 -0
- ragdesk-0.1.0/src/ragdesk/defaults.py +48 -0
- ragdesk-0.1.0/src/ragdesk/email_source.py +335 -0
- ragdesk-0.1.0/src/ragdesk/embed.py +343 -0
- ragdesk-0.1.0/src/ragdesk/envfile.py +63 -0
- ragdesk-0.1.0/src/ragdesk/evaluate.py +288 -0
- ragdesk-0.1.0/src/ragdesk/gdrive.py +308 -0
- ragdesk-0.1.0/src/ragdesk/github.py +257 -0
- ragdesk-0.1.0/src/ragdesk/gitlab.py +169 -0
- ragdesk-0.1.0/src/ragdesk/htmlutil.py +29 -0
- ragdesk-0.1.0/src/ragdesk/index.py +399 -0
- ragdesk-0.1.0/src/ragdesk/llm.py +370 -0
- ragdesk-0.1.0/src/ragdesk/mcp.py +248 -0
- ragdesk-0.1.0/src/ragdesk/msgraph.py +295 -0
- ragdesk-0.1.0/src/ragdesk/notes.py +117 -0
- ragdesk-0.1.0/src/ragdesk/notion.py +231 -0
- ragdesk-0.1.0/src/ragdesk/oauth.py +85 -0
- ragdesk-0.1.0/src/ragdesk/obsidian.py +79 -0
- ragdesk-0.1.0/src/ragdesk/office.py +266 -0
- ragdesk-0.1.0/src/ragdesk/ollama.py +60 -0
- ragdesk-0.1.0/src/ragdesk/presets.py +54 -0
- ragdesk-0.1.0/src/ragdesk/rerank.py +225 -0
- ragdesk-0.1.0/src/ragdesk/s3.py +329 -0
- ragdesk-0.1.0/src/ragdesk/screen.py +226 -0
- ragdesk-0.1.0/src/ragdesk/search.py +296 -0
- ragdesk-0.1.0/src/ragdesk/serve.py +2915 -0
- ragdesk-0.1.0/src/ragdesk/settings.py +59 -0
- ragdesk-0.1.0/src/ragdesk/store.py +1108 -0
- ragdesk-0.1.0/src/ragdesk/symbols.py +239 -0
- ragdesk-0.1.0/src/ragdesk/topics.py +124 -0
- ragdesk-0.1.0/src/ragdesk/tui.py +229 -0
- ragdesk-0.1.0/src/ragdesk/vectors.py +357 -0
- ragdesk-0.1.0/src/ragdesk/vision.py +182 -0
- ragdesk-0.1.0/src/ragdesk/web.py +171 -0
- ragdesk-0.1.0/tests/test_buildenv.py +41 -0
- ragdesk-0.1.0/tests/test_complete.py +55 -0
- ragdesk-0.1.0/tests/test_confluence.py +221 -0
- ragdesk-0.1.0/tests/test_credentials.py +91 -0
- ragdesk-0.1.0/tests/test_defaults.py +77 -0
- ragdesk-0.1.0/tests/test_email.py +224 -0
- ragdesk-0.1.0/tests/test_embed.py +98 -0
- ragdesk-0.1.0/tests/test_envfile.py +29 -0
- ragdesk-0.1.0/tests/test_gdrive.py +99 -0
- ragdesk-0.1.0/tests/test_github.py +191 -0
- ragdesk-0.1.0/tests/test_gitlab.py +84 -0
- ragdesk-0.1.0/tests/test_llm.py +269 -0
- ragdesk-0.1.0/tests/test_mcp.py +165 -0
- ragdesk-0.1.0/tests/test_msgraph.py +132 -0
- ragdesk-0.1.0/tests/test_notes.py +68 -0
- ragdesk-0.1.0/tests/test_notion.py +137 -0
- ragdesk-0.1.0/tests/test_obsidian.py +92 -0
- ragdesk-0.1.0/tests/test_office.py +238 -0
- ragdesk-0.1.0/tests/test_pipeline.py +605 -0
- ragdesk-0.1.0/tests/test_presets.py +63 -0
- ragdesk-0.1.0/tests/test_rerank.py +97 -0
- ragdesk-0.1.0/tests/test_retrieval.py +585 -0
- ragdesk-0.1.0/tests/test_s3.py +266 -0
- ragdesk-0.1.0/tests/test_screen.py +56 -0
- ragdesk-0.1.0/tests/test_serve.py +1696 -0
- ragdesk-0.1.0/tests/test_settings.py +38 -0
- ragdesk-0.1.0/tests/test_symbols.py +80 -0
- ragdesk-0.1.0/tests/test_topic_map.py +69 -0
- ragdesk-0.1.0/tests/test_topics.py +57 -0
- ragdesk-0.1.0/tests/test_tui.py +117 -0
- ragdesk-0.1.0/tests/test_ui.py +40 -0
- ragdesk-0.1.0/tests/test_vectors.py +208 -0
- ragdesk-0.1.0/tests/test_vision.py +164 -0
- ragdesk-0.1.0/tests/test_web.py +93 -0
- ragdesk-0.1.0/uv.lock +1619 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ragdesk build credentials — copy to `.env` (gitignored) and fill in.
|
|
2
|
+
#
|
|
3
|
+
# Everything in this file is baked into release builds at build time:
|
|
4
|
+
# python -m ragdesk.buildenv # .env -> src/ragdesk/_build_env.py (gitignored)
|
|
5
|
+
# uv build # wheel/DMG now carries the values
|
|
6
|
+
#
|
|
7
|
+
# Policy (see SECURITY.md): only non-confidential values belong here.
|
|
8
|
+
# - GitHub device flow: client ID only (no secret exists).
|
|
9
|
+
# - Google Drive (Desktop app type): client ID + secret are not confidential —
|
|
10
|
+
# Google documents that installed apps cannot keep secrets; PKCE protects
|
|
11
|
+
# the flow.
|
|
12
|
+
# - Atlassian 3LO: the secret IS a real credential. Never bake it into a
|
|
13
|
+
# public artifact; the baker refuses it. Users connect with an API token or
|
|
14
|
+
# bring their own Atlassian app (stored locally, 0600).
|
|
15
|
+
|
|
16
|
+
# --- GitHub (OAuth App with "Enable Device Flow" ticked) ----------------------
|
|
17
|
+
RAGDESK_GITHUB_CLIENT_ID=
|
|
18
|
+
|
|
19
|
+
# --- Google Drive (GCP project -> Drive API -> OAuth client, type Desktop) ---
|
|
20
|
+
GDRIVE_CLIENT_ID=
|
|
21
|
+
GDRIVE_CLIENT_SECRET=
|
|
22
|
+
|
|
23
|
+
# --- Atlassian (optional, local-only; never commit the secret) ---------------
|
|
24
|
+
# RAGDESK_ATLASSIAN_CLIENT_ID=
|
|
25
|
+
# RAGDESK_ATLASSIAN_CLIENT_SECRET=
|
|
26
|
+
|
|
27
|
+
# --- Microsoft Graph / OneDrive / SharePoint (public client app, no secret) ---
|
|
28
|
+
# Azure app registration: "Allow public client flows" = Yes,
|
|
29
|
+
# delegated permissions: Files.Read.All, Sites.Read.All, offline_access.
|
|
30
|
+
# RAGDESK_MS_CLIENT_ID=
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v5
|
|
13
|
+
- uses: astral-sh/setup-uv@v7
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
enable-cache: true
|
|
17
|
+
- name: Cache the embedding model
|
|
18
|
+
uses: actions/cache@v6
|
|
19
|
+
with:
|
|
20
|
+
path: ~/.cache/huggingface
|
|
21
|
+
key: hf-embeddinggemma-${{ runner.os }}-v1
|
|
22
|
+
- run: uv sync --all-groups --extra onnx
|
|
23
|
+
- run: uv run ruff check .
|
|
24
|
+
- run: uv run ruff format --check . # style is enforced, not just linted
|
|
25
|
+
- run: uv run pytest
|
|
26
|
+
- name: Retrieval eval (fixtures, offline embedder — fast smoke test)
|
|
27
|
+
run: |
|
|
28
|
+
uv run ragdesk --embedder hash --db .ragdesk/eval.db index fixtures/docs
|
|
29
|
+
uv run ragdesk --embedder hash --db .ragdesk/eval.db eval \
|
|
30
|
+
--golden fixtures/golden.jsonl --min-recall 0.8
|
|
31
|
+
- name: Retrieval eval (repo golden, real embedder, gated at 0.8)
|
|
32
|
+
run: scripts/eval_ci.sh --gate
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Eval
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: "0 6 1 * *" # monthly, so the badge cannot quietly rot
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
badge:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v5
|
|
18
|
+
- uses: astral-sh/setup-uv@v7
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
enable-cache: true
|
|
22
|
+
- name: Cache the embedding model
|
|
23
|
+
uses: actions/cache@v6
|
|
24
|
+
with:
|
|
25
|
+
path: ~/.cache/huggingface
|
|
26
|
+
key: hf-embeddinggemma-${{ runner.os }}-v1
|
|
27
|
+
- run: uv sync --all-groups --extra onnx
|
|
28
|
+
- name: Score the repo golden set
|
|
29
|
+
run: scripts/eval_ci.sh --db .ragdesk/eval-badge.db > /tmp/eval.json
|
|
30
|
+
- name: Refresh docs/eval.json and the README row
|
|
31
|
+
run: uv run python scripts/eval_badge.py /tmp/eval.json
|
|
32
|
+
- name: Commit only when a number moved
|
|
33
|
+
run: |
|
|
34
|
+
if git diff --quiet; then
|
|
35
|
+
echo "numbers unchanged — nothing to publish"
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
git config user.name "ragdesk eval"
|
|
39
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
40
|
+
git add docs/eval.json README.md
|
|
41
|
+
git commit -m "eval: refresh the published retrieval numbers [skip ci]"
|
|
42
|
+
git push
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
# Tag pushes do not run ci.yml, and publishing is irreversible: gate here.
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v5
|
|
16
|
+
- uses: astral-sh/setup-uv@v7
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
enable-cache: true
|
|
20
|
+
- run: uv sync --all-groups --extra onnx
|
|
21
|
+
- run: uv run ruff check .
|
|
22
|
+
- run: uv run ruff format --check .
|
|
23
|
+
- run: uv run pytest
|
|
24
|
+
|
|
25
|
+
build:
|
|
26
|
+
needs: test
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v5
|
|
30
|
+
- uses: astral-sh/setup-uv@v7
|
|
31
|
+
with:
|
|
32
|
+
python-version: "3.12"
|
|
33
|
+
- run: uv build
|
|
34
|
+
- uses: actions/upload-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
|
|
39
|
+
publish:
|
|
40
|
+
needs: build
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
environment: pypi
|
|
43
|
+
permissions:
|
|
44
|
+
id-token: write
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/download-artifact@v4
|
|
47
|
+
with:
|
|
48
|
+
name: dist
|
|
49
|
+
path: dist/
|
|
50
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
51
|
+
|
|
52
|
+
desktop:
|
|
53
|
+
# The DMG is attached to the GitHub Release for people who want the app
|
|
54
|
+
# without a toolchain. It is NOT notarized (that needs a paid Apple
|
|
55
|
+
# Developer account), so Gatekeeper will warn on first open — the release
|
|
56
|
+
# notes say so, and building from source stays the supported path.
|
|
57
|
+
needs: test
|
|
58
|
+
runs-on: macos-14
|
|
59
|
+
permissions:
|
|
60
|
+
contents: write
|
|
61
|
+
env:
|
|
62
|
+
# Ad-hoc signing in CI: no developer certificate lives in the repo.
|
|
63
|
+
APPLE_SIGNING_IDENTITY: "-"
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v5
|
|
66
|
+
- uses: actions/setup-node@v4
|
|
67
|
+
with:
|
|
68
|
+
node-version: "20"
|
|
69
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
70
|
+
- uses: astral-sh/setup-uv@v7
|
|
71
|
+
with:
|
|
72
|
+
python-version: "3.12"
|
|
73
|
+
- run: npm ci
|
|
74
|
+
working-directory: desktop
|
|
75
|
+
- run: npm run tauri build
|
|
76
|
+
working-directory: desktop
|
|
77
|
+
- name: Attach the DMG to the release
|
|
78
|
+
run: |
|
|
79
|
+
tag="${GITHUB_REF_NAME}"
|
|
80
|
+
gh release view "$tag" >/dev/null 2>&1 || \
|
|
81
|
+
gh release create "$tag" --title "$tag" --generate-notes
|
|
82
|
+
gh release upload "$tag" \
|
|
83
|
+
desktop/src-tauri/target/release/bundle/dmg/*.dmg --clobber
|
|
84
|
+
env:
|
|
85
|
+
GH_TOKEN: ${{ github.token }}
|
ragdesk-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.ragdesk/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
.DS_Store
|
|
11
|
+
|
|
12
|
+
# local build credentials (never commit)
|
|
13
|
+
.env
|
|
14
|
+
src/ragdesk/_build_env.py
|
|
15
|
+
|
|
16
|
+
# bundled runtime (scripts/build_sidecar.sh regenerates it)
|
|
17
|
+
desktop/src-tauri/sidecar/
|
ragdesk-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
# ragdesk — agent notes
|
|
2
|
+
|
|
3
|
+
Personal, local-first RAG over your own sources. Core is **plain Python** with
|
|
4
|
+
two runtime dependencies (numpy, pypdf); models are reached through a local
|
|
5
|
+
Ollama server.
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
- `src/ragdesk/` — package. `chunk` (paragraph-aware + overlap), `embed`
|
|
10
|
+
(HashingEmbedder for CI, OllamaEmbedder, OnnxEmbedder = EmbeddingGemma int8
|
|
11
|
+
with query/doc prompts; shared `tokenize`), `store` (SQLite: FTS5 + float32
|
|
12
|
+
vectors + fail-closed embedder guard; keeps the chosen local roots in `meta`
|
|
13
|
+
for the per-path Indexed breakdown via `local_paths()`; `duplicate_clusters`
|
|
14
|
+
groups docs by exact chunk-hash containment — a copied or sliced file shares
|
|
15
|
+
chunks, a topical neighbour does not, which document-level cosine cannot
|
|
16
|
+
separate; `web_pages()` is the bookmark list), `search` (BM25 +
|
|
17
|
+
dense + RRF; `retrieve` adds the optional rerank stage), `rerank`
|
|
18
|
+
(LexicalReranker baseline; FastEmbedReranker + OnnxReranker = multilingual
|
|
19
|
+
gte behind the `onnx` extra), `index` (incremental local files; `iter_files`
|
|
20
|
+
prunes `SKIP_DIRS` during the walk so `target/`/`node_modules/` are never
|
|
21
|
+
traversed; `index_document` takes optional `metadata` merged over any
|
|
22
|
+
front-matter; one bad file becomes a skip with the reason, never a dead
|
|
23
|
+
watcher), `office` (PDF/DOCX/PPTX/XLSX text extraction: docx/pptx/xlsx via
|
|
24
|
+
zip+XML with zero deps — sheets keep `r<row>` refs, shared + inline strings;
|
|
25
|
+
PDFs via pypdf (one of the two runtime dependencies); XML with a DTD is refused;
|
|
26
|
+
scanned PDFs return empty and are skipped, no OCR), `vision` (image OCR through Apple's
|
|
27
|
+
on-device Vision framework via the `vision` extra: `en-US` + `vi-VT`, images
|
|
28
|
+
upscaled 2× before recognition, header carries file name + Spotlight capture
|
|
29
|
+
date; non-macOS or no extra → images skip as before), `github` / `gitlab` / `confluence` / `gdrive` / `notion` /
|
|
30
|
+
`msgraph` (OneDrive + SharePoint, device flow) connectors, `email_source`
|
|
31
|
+
(read-only email: `index_mbox` iterates one document per message,
|
|
32
|
+
`sync_imap` uses `select(readonly=True)` + `BODY.PEEK` so nothing is ever
|
|
33
|
+
marked read; headers + text body only, attachment names are listed not
|
|
34
|
+
parsed; credentials under the `email` provider), `web` (same-host
|
|
35
|
+
HTML crawl, capped pages/depth; `save_page` = one page, failures raise),
|
|
36
|
+
`archive`
|
|
37
|
+
(shared repo-tarball extraction), `htmlutil` (shared HTML→text),
|
|
38
|
+
`evaluate` (recall@5 / nDCG@10 / MRR), `answer` (Ollama LLM: non-stream +
|
|
39
|
+
stream, grounding gate), `credentials` (0600 store under `~/.config/ragdesk/`),
|
|
40
|
+
`envfile` (.env loader for dev), `buildenv` (bakes .env into a gitignored
|
|
41
|
+
`_build_env.py` for release builds), `mcp` (stdio MCP server exposing
|
|
42
|
+
search/document/sources to Claude Code & co), `serve` (loopback JSON API:
|
|
43
|
+
status/search/ask/index/sync + connections connect/disconnect + optional
|
|
44
|
+
static UI; hosts the auto-index timer), `settings` (app config in
|
|
45
|
+
`~/.config/ragdesk/settings.json`, 0600: `auto_index_hours`, default 1,
|
|
46
|
+
0 = off), `llm` (answer backends + the no-double-download ladder: `auto`
|
|
47
|
+
reuses a running Ollama that already has the preset model, else MLX
|
|
48
|
+
in-process from the shared HF cache; `--llm`/`RAGDESK_LLM` override),
|
|
49
|
+
`presets` (RAM tiers; each carries an Ollama tag and an `llm_mlx` repo;
|
|
50
|
+
`POST /api/settings {preset}` applies a tier live — rerank and LLM swap, the
|
|
51
|
+
shared embedder means no re-index; the choice persists in settings.json and
|
|
52
|
+
beats the built-in default, with `--preset` still winning at launch),
|
|
53
|
+
`cli`.
|
|
54
|
+
- UI identity (`desktop/src/styles.css`, tokens at the top): the "catalog
|
|
55
|
+
drawer" — grey-green card stock (`--paper` desk, `--sheet` working sheet,
|
|
56
|
+
`--card` insets), **violet library ink** (`--stamp`) for actions and stamps,
|
|
57
|
+
**amber** (`--lamp`) for anything live or cited, Iowan/Charter display + SF
|
|
58
|
+
interface + SF Mono for paths and figures. Structural devices carry meaning:
|
|
59
|
+
the transcript is a ruled ledger with entry numbers in the gutter, the active
|
|
60
|
+
rail tab is the pulled drawer, the Indexed stats are a ledger with
|
|
61
|
+
proportional bars, and the composer is an index card with a violet top edge.
|
|
62
|
+
Keep it restrained — hairlines and one shadow, no per-element shadows, no
|
|
63
|
+
gradient decoration, and `prefers-reduced-motion` disables all motion.
|
|
64
|
+
`main.ts` also owns the command palette (⌘K/⌘N/⌘1-4/?; documents come from
|
|
65
|
+
`/api/search`), which must keep every `$("id")` it touches present in
|
|
66
|
+
`index.html` (`tests/test_ui.py` enforces that). One more rule with a
|
|
67
|
+
precedent: citations have ONE stored shape (`search.hit_to_dict`) — the
|
|
68
|
+
terminal chats once wrote a thinner one and the GUI crashed on a missing
|
|
69
|
+
`cosine` when it replayed them, so any writer must use the shared helper.
|
|
70
|
+
- `desktop/` — Tauri 2 shell (card-catalog UI; tabs: Chat, Sources, Indexed, Settings). Retrieval-only search lives in `ragdesk search` and `/api/search`;
|
|
71
|
+
the Chat tab keeps its source list under every answer. Rust spawns `ragdesk serve`
|
|
72
|
+
with `--db $HOME/.ragdesk/index.db`; a watchdog thread respawns it if it
|
|
73
|
+
dies (skipping the respawn when another instance owns the port) and the
|
|
74
|
+
child's output goes to `~/.ragdesk/serve.log`; env overrides: `RAGDESK_BIN`,
|
|
75
|
+
`RAGDESK_DB`, `RAGDESK_LLM_MODEL`, `RAGDESK_PROJECT`. Browser mode:
|
|
76
|
+
`ragdesk serve --ui desktop/dist`.
|
|
77
|
+
- Terminal surfaces (`tui.py`, `screen.py`): `ragdesk chat` is the line REPL
|
|
78
|
+
(streaming + citations, history shared with the GUI); `ragdesk chat --server
|
|
79
|
+
URL` attaches to a running server over its local API — no db, no embedder,
|
|
80
|
+
no model load, the server owns all three; `ragdesk tui` is the full-screen
|
|
81
|
+
Textual app behind the optional `tui` extra (`uv run --extra tui pytest
|
|
82
|
+
tests/test_screen.py`; the test skips itself without the extra).
|
|
83
|
+
- Watcher + auto-index: `watch_loop` re-indexes the recorded local roots every
|
|
84
|
+
`settings.watch_seconds` (default 60, 0 = off, UI card "Watch for changes")
|
|
85
|
+
and `auto_index_loop` re-checks everything every `auto_index_hours` as the
|
|
86
|
+
safety net; both take the mtime fast path, so a quiet pass is one stat per
|
|
87
|
+
file. `POST /api/settings {watch_seconds}` clamps to 0-3600 and `/api/status`
|
|
88
|
+
exposes it.
|
|
89
|
+
- Auto-index: `serve` runs a 60s timer; when `auto_index_hours` (Settings tab,
|
|
90
|
+
default 1, 0 = off) has elapsed since `auto_index_last`, it re-indexes the
|
|
91
|
+
recorded local roots via `run_auto_index` (connectors stay manual until
|
|
92
|
+
their sync params are persisted).
|
|
93
|
+
- LLM wizard: `/api/llm/setup` {kind: mlx|ollama} starts a background download
|
|
94
|
+
(`run_llm_setup`); progress + options ride on `/api/status.llm_setup`
|
|
95
|
+
(job: running/progress/detail/error), the Settings tab renders them, and a
|
|
96
|
+
finished job clears `state.llm` so the next ask re-resolves the ladder.
|
|
97
|
+
`llm_setup_options` respects an explicit `--llm mlx:<repo>` override.
|
|
98
|
+
- Ask pipeline: `ask`/`ask_stream` parse `folder:`/`source:` filters, build a
|
|
99
|
+
fingerprint (embedder + model + corpus revision) and try the exact cache key,
|
|
100
|
+
then the semantic cache (`store.cache_nearest`, cosine ≥ 0.88 — calibrated:
|
|
101
|
+
paraphrases score 0.91+, different intents ≤ 0.35), then retrieve → inject up
|
|
102
|
+
to 3 similar memories (`store.memories`, cosine ≥ 0.35) and the last 3 turns
|
|
103
|
+
→ `answer*()`; every exchange is recorded in `chats`/`messages`, refusals and
|
|
104
|
+
cache replays included. Chat/cache/memory tables live in the same SQLite
|
|
105
|
+
file; `/api/chats` and `/api/memories` (+ `/api/memories/extract`) back the
|
|
106
|
+
UI. A semantic hit reports `cached_question` so the UI can say what it matched.
|
|
107
|
+
- Answer engines: `llm.llm_preference` (Settings/wizard) picks Ollama, MLX, or an
|
|
108
|
+
OpenAI-compatible endpoint; host/model live in settings.json and the API key
|
|
109
|
+
in credentials (`openai`). `auto` ladder: ollama-with-model → configured
|
|
110
|
+
endpoint → MLX.
|
|
111
|
+
- Diagrams: `answer.wants_diagram` detects intent (EN + VI words) and adds
|
|
112
|
+
`DIAGRAM_NOTE` to the prompt, which pins the allowed Mermaid vocabulary;
|
|
113
|
+
the UI strips the fence from the prose and renders the figure with mermaid
|
|
114
|
+
(strict security, themed from the CSS tokens) plus SVG download. Small-model
|
|
115
|
+
keyword slips (`subregion`) are repaired before rendering.
|
|
116
|
+
- Wizard: `/api/status.onboarded` gates the first-run overlay; `system_info()`
|
|
117
|
+
reports RAM + suggested preset; `/api/settings {onboarded}` marks it done.
|
|
118
|
+
- Chunking: `chunk_text` is paragraph-aware and records `line_start` per chunk
|
|
119
|
+
(citations show `file:line`). **Symbol-aware code chunking was tried three
|
|
120
|
+
ways and rejected — do not rebuild it without new evidence.** Fresh-db A/B on
|
|
121
|
+
the 12-query scoped golden (plain = recall 1.000 / nDCG 0.819 / MRR 0.757):
|
|
122
|
+
1. per-symbol segmentation → 0.743 nDCG / 0.656 MRR (fragmentation raises BM25
|
|
123
|
+
term density, small keyword-rich chunks crowd out the right document);
|
|
124
|
+
2. symbol header inside each chunk → 0.788 / 0.715 (test files carry the
|
|
125
|
+
symbol in their own names and out-rank the implementation);
|
|
126
|
+
3. a symbol lane over definition sites (LIKE on names) → 0.523 / 0.419 (loose
|
|
127
|
+
substring matching adds a weak lane and dilutes RRF).
|
|
128
|
+
The path lane already answers "where is X defined". Real symbol intelligence
|
|
129
|
+
is a call-graph index (defs + references + callers), a separate feature —
|
|
130
|
+
not a chunking trick.
|
|
131
|
+
- Tuning tool: `scripts/bench.py` indexes once per chunking config and sweeps
|
|
132
|
+
rerankers/weights over the same db (`--db`). Measured 2026-09-15 on the
|
|
133
|
+
scoped golden: 600-char chunks rank better but lose recall; the multilingual
|
|
134
|
+
`onnx` reranker lifts recall 0.833 → 0.917 while the English fastembed one
|
|
135
|
+
drops it to 0.750; lane weights change nothing. Chunking stays 1000 chars
|
|
136
|
+
(settings `chunk_chars`/`chunk_overlap`), weights stay equal, and the quality
|
|
137
|
+
preset keeps `rerank: onnx`.
|
|
138
|
+
- Metadata: `index.parse_front_matter` reads a flat `--- key: value ---` header
|
|
139
|
+
(md/txt), `documents.metadata` stores it as JSON, `parse_filters` turns any
|
|
140
|
+
unrecognised `key:value` (minus URL schemes) into `Filters.meta`, and
|
|
141
|
+
`_filter_sql` matches it through `json_extract` with a sanitised key. Hits
|
|
142
|
+
carry the metadata for citation chips. `search.metadata_factor` is the soft
|
|
143
|
+
boost (measure first — see the symbol-lane lesson): `authority: canonical`
|
|
144
|
+
(or authoritative/official/high/true/yes/1) multiplies the fused score by
|
|
145
|
+
`META_BOOST` 1.05, `status: draft` (or deprecated/archived/superseded/
|
|
146
|
+
obsolete/wip) by `META_PENALTY` 0.90; no tag = 1.0, so the boost is opt-in
|
|
147
|
+
and cannot re-rank an untagged corpus. `hybrid_search` decodes every lane's
|
|
148
|
+
metadata JSON into the same dict shape (bm25/path lanes used to leak the raw
|
|
149
|
+
string into `Hit.metadata`). Measured 2026-09-15: whole-repo index (93 docs /
|
|
150
|
+
2408 chunks, absolute paths) bench none+onnx and the 24-query corpus copy
|
|
151
|
+
both identical before/after (0.917/0.752/0.694 and 0.542/0.557/0.549);
|
|
152
|
+
`tests/test_retrieval.py` proves the reorder canonical > plain > draft.
|
|
153
|
+
- Quiet indexing: `embed.session_options(threads)` builds every ONNX session
|
|
154
|
+
(embedder *and* reranker) with `intra_op_num_threads`/`inter_op_num_threads=1`
|
|
155
|
+
when capped; `settings.embed_threads` (0 = all cores) is read at session build
|
|
156
|
+
and `embed.set_thread_override` lets `--embed-threads N` win for one process.
|
|
157
|
+
`POST /api/settings {embed_threads}` unloads the embedder so the toggle takes
|
|
158
|
+
effect immediately. Measured on the 651-chunk repo subset, cold index:
|
|
159
|
+
all cores 97s @ ~580% CPU, 4 threads 136s @ ~390%, 2 threads 256s @ ~200% —
|
|
160
|
+
`OMP_NUM_THREADS` does **not** work with the Python wheel, the code change is
|
|
161
|
+
the only way to cap it.
|
|
162
|
+
- Embedding cache (`store.embed_cache`, key = sha1 of embedder name + dim + exact
|
|
163
|
+
chunk text): `index._embed_with_cache` embeds only what is new, so a one-line
|
|
164
|
+
edit costs one embedding instead of a whole document's worth and a duplicated
|
|
165
|
+
file costs zero model calls. Measured on the 48-doc / 648-chunk repo subset:
|
|
166
|
+
cold index 96.8s, wipe-the-documents-and-re-index 0.2s, and the golden
|
|
167
|
+
numbers unchanged (0.917 / 0.783) — the cache must never alter a result, only
|
|
168
|
+
the work it takes to produce one. It lives in the same SQLite file (one vector
|
|
169
|
+
per chunk, ~3KB, pruned to 50k rows FIFO) and is disposable: delete the rows
|
|
170
|
+
and the next index run re-embeds.
|
|
171
|
+
- Rejected after measuring: a **`tiny` LLM preset** (Qwen3.5-2B-4bit, ~1.2GB RAM
|
|
172
|
+
vs the 4B's 2.5GB). The deterministic proxy looked *better* (grounded 1.000 vs
|
|
173
|
+
0.953) but reading the answers showed the drop: the 2B opened the second
|
|
174
|
+
question by re-answering the first (context bleed), missed the named flags and
|
|
175
|
+
was visibly less structured. Lesson: `ground_answer` measures sentence overlap
|
|
176
|
+
with the cited chunks, so a short extractive answer scores high — **always read
|
|
177
|
+
the answers before shipping a model swap**, and keep the 4B.
|
|
178
|
+
- Ranking extras: `diversify` caps chunks per document (2 by default, backfills
|
|
179
|
+
when a query is dominated by one file) and `recency_factor` adds a mild
|
|
180
|
+
freshness nudge to the RRF score (RECENCY_WEIGHT).
|
|
181
|
+
- Smart retrieval (`serve._smart_retrieval` + `parse_smart_retrieval`): ONE
|
|
182
|
+
local-model call that rewrites follow-ups, drafts the HyDE text and proposes
|
|
183
|
+
up to two sub-queries (extra dense lanes). Runs only when it can pay off
|
|
184
|
+
(history present, multi-part question, or hyde enabled) and is a no-op
|
|
185
|
+
whenever no model resolves.
|
|
186
|
+
- Trust surface: `POST /api/feedback {message_id, value}` (▲/▼ stored on the
|
|
187
|
+
message), `GET /api/feedback/golden` (rated answers exported as a golden
|
|
188
|
+
JSONL), `POST /api/verify {message_id}` (`ground_answer_detail`: per-sentence
|
|
189
|
+
grounded/loose verdicts against the stored citations, stopword-free overlap).
|
|
190
|
+
- Library surfaces: `GET /api/duplicates` (chunk-hash clusters, display-only —
|
|
191
|
+
ranking is untouched by design), `POST /api/save {url}` + `GET /api/bookmarks`
|
|
192
|
+
(one page saved with `web.save_page`, `metadata.url` keeps the real address),
|
|
193
|
+
and the CLI mirrors: `ragdesk save <url>`, `ragdesk completions
|
|
194
|
+
bash|zsh|fish`, `ragdesk man` (all in `complete.py`, generated from the
|
|
195
|
+
argparse tree so they cannot drift; `complete._subparsers` reads argparse
|
|
196
|
+
internals on purpose).
|
|
197
|
+
- Email surfaces: `POST /api/connections/email {host, port, user, password}`
|
|
198
|
+
(validated by `email_source.whoami` before saving), `POST /api/sync/email
|
|
199
|
+
{folder, limit}` (saved credentials; `limit` = newest N messages) and
|
|
200
|
+
`POST /api/sync/email-mbox {path}`; CLI mirrors as `ragdesk email --mbox
|
|
201
|
+
FILE` / `--imap HOST --user U` (password prompted, never an argv default).
|
|
202
|
+
The Sources tab has the card; `credentials.email` holds the app password.
|
|
203
|
+
- Call graph (`symbols.py`): `parse_symbol_question` recognises "who calls X",
|
|
204
|
+
"callers of X", "where is X defined" and the VI equivalents (returns
|
|
205
|
+
`(name, certain)`; a plain lowercase word is only trusted when a definition
|
|
206
|
+
exists). `find_symbol` scans the *stored chunk text* of code extensions for
|
|
207
|
+
definitions (`chunk.SYMBOL_RE`), call sites (`X(`) and mentioning files — no
|
|
208
|
+
AST, no index-time table, no re-index needed on old corpora; line numbers are
|
|
209
|
+
chunk-relative (a file with runs of blank lines can be off by a line or two).
|
|
210
|
+
`serve._symbol_lookup` runs before retrieval and returns a deterministic
|
|
211
|
+
answer + `lanes="symbol"` hits, so symbol questions never reach the LLM and
|
|
212
|
+
never touch the RRF lanes (graph stays at the navigation layer). The ask
|
|
213
|
+
response carries `symbol` and the UI shows a "call graph" badge.
|
|
214
|
+
- Judge (`evaluate.judge_answer` + `JUDGE_PROMPT`): an opt-in LLM grader next
|
|
215
|
+
to the overlap proxy — one call per answer asking for
|
|
216
|
+
`{"supported", "total", "unsupported"}`; `parse_judge_reply` tolerates prose
|
|
217
|
+
around the JSON and any unparseable reply stays `judged: False` (out of the
|
|
218
|
+
aggregate, reported as coverage). `eval --judge` implies `--answers` and
|
|
219
|
+
prints `judge faithfulness` + how many answers were judged.
|
|
220
|
+
- Portable bundle (`serve.run_export` / `run_import`): a zip with
|
|
221
|
+
`manifest.json` + a SQLite-consistent `index.db` snapshot under
|
|
222
|
+
`<db dir>/bundles/`; import validates the manifest, refuses a major-version
|
|
223
|
+
gap or a different embedder (fail-closed: a silent mismatch would poison
|
|
224
|
+
every future query), takes a safety snapshot, then uses the SQLite backup API
|
|
225
|
+
to overwrite the live database in place. CLI `ragdesk export` / `import`,
|
|
226
|
+
API `/api/export`, `/api/bundles`, `/api/import`, UI card in Settings.
|
|
227
|
+
Credentials are never included in a bundle.
|
|
228
|
+
- Scope chip (UI): "Only this folder" on a citation sets a pending scope; the
|
|
229
|
+
next questions are sent as `folder:"<dir>"` (quoted filter values — see
|
|
230
|
+
`_FILTER_RE` in search.py), which is why paths with spaces work.
|
|
231
|
+
- Topic map picture (`scripts/topic_map.py`): numpy PCA over `_doc_vectors()`
|
|
232
|
+
(script, not the stdlib core), colours from `topics.cluster_documents`,
|
|
233
|
+
capped at `PER_CLUSTER` dots so a 200-copy pile cannot squash the map;
|
|
234
|
+
writes a self-contained HTML with hover tooltips.
|
|
235
|
+
- Topic map (`topics.py`): `cluster_documents` is greedy leader clustering over
|
|
236
|
+
`store._doc_vectors()` (the same average chunk vectors related-documents uses;
|
|
237
|
+
pure Python, `MAX_DOCS` 2000 before it needs numpy) at cosine ≥ 0.75;
|
|
238
|
+
`cluster_labels` attaches the most distinctive terms (cluster rate minus the
|
|
239
|
+
corpus rate, sampled to 20k chunks) so the label is not just the commonest
|
|
240
|
+
words. `GET /api/topics` powers the Indexed tab's Topics card, which shows
|
|
241
|
+
multi-document clusters open, collapses single-document topics, and caps
|
|
242
|
+
paths per cluster. Display only — never a ranking lane.
|
|
243
|
+
- Connector auto-sync: `settings.sync_jobs` holds `{id, provider, params}` entries
|
|
244
|
+
(id = sha1 of provider+params, so the same sync is one job); the UI's
|
|
245
|
+
"Keep in sync (hourly)" checkbox on a sync form posts to `/api/sync-jobs`
|
|
246
|
+
after a successful sync. `SYNC_HANDLERS` in serve.py dispatches all nine
|
|
247
|
+
connectors — github, gitlab, confluence, gdrive, msgraph, notion, email,
|
|
248
|
+
web, s3 — pulling credentials from the same places the manual endpoints do
|
|
249
|
+
(email's password from `credentials`, tokens resolved by each connector).
|
|
250
|
+
`run_auto_index` runs local roots first, then every job, and returns a
|
|
251
|
+
`connectors` list; a failing job is reported in it, never fatal.
|
|
252
|
+
`GET /api/sync-jobs`, `POST /api/sync-jobs`, `/api/sync-jobs/delete`;
|
|
253
|
+
`/api/status.sync_jobs` feeds the Settings card.
|
|
254
|
+
- S3 (`s3.py`): hand-rolled **SigV4 over stdlib** (`hashlib`/`hmac`/`urllib` +
|
|
255
|
+
`xml.etree`) instead of boto3 or the aws CLI — a non-technical user should
|
|
256
|
+
paste one key into the app (or read a public bucket with no key at all), not
|
|
257
|
+
install a second tool. Key details: the canonical URI is the path **as sent**
|
|
258
|
+
(S3 does not normalize it — matching botocore's `S3SigV4Auth`, cross-checked
|
|
259
|
+
2026-09-16 on query strings, encoded keys with spaces, Unicode, `+` and
|
|
260
|
+
custom endpoints), `x-amz-content-sha256` is the empty-payload hash, and
|
|
261
|
+
**S3 XML carries a default namespace** so tags are stripped before parsing
|
|
262
|
+
(finding `Contents` without that step silently returns zero objects — the
|
|
263
|
+
first live run against a public bucket caught it). Credentials resolve
|
|
264
|
+
explicit → `AWS_*` env → saved connection; custom endpoints are path-style.
|
|
265
|
+
`/api/connections/s3` validates by listing before saving, `/api/sync/s3`
|
|
266
|
+
indexes, `/api/status.s3` drives the card, `ragdesk s3 BUCKET` mirrors it.
|
|
267
|
+
Verified live against the public `noaa-ghcn-pds` bucket: anonymous list,
|
|
268
|
+
fetch, extract and search with no key stored.
|
|
269
|
+
- Email attachments (`email_source.attachments`): up to 10 per message, each
|
|
270
|
+
decoded and pushed through `extract_bytes` (so OCR and sheet row-refs work);
|
|
271
|
+
indexed as their own documents at `<base>::<message-key>::NN-<name>` with
|
|
272
|
+
`kind: attachment`, while the message body keeps `[attachment: name]` lines.
|
|
273
|
+
`IndexStats.attachments` counts them and flows into every sync summary.
|
|
274
|
+
- Reliability surfaces: `GET /api/health` (embedder match, last local run + `skipped_samples` from `store.last_index_report()`, oldest documents, db
|
|
275
|
+
size, never-index patterns + docs still matching them), `POST
|
|
276
|
+
/api/never-index {patterns}` (saves `settings.never_index` AND prunes matching
|
|
277
|
+
documents — redaction, not just prevention; `index_paths` skips matching files
|
|
278
|
+
as `never-index pattern`), and `GET /api/backups` + `POST /api/backup` +
|
|
279
|
+
`POST /api/restore {path}` (`run_backup`/`restore_backup` use the SQLite
|
|
280
|
+
backup API so they are safe while the app runs; a restore takes a safety
|
|
281
|
+
snapshot first, and same-second snapshots get a `-N` suffix — without it the
|
|
282
|
+
safety copy silently overwrote the backup being restored).
|
|
283
|
+
- **FTS wedge (fixed 2026-09-15, live incident):** a `chunks_fts` row whose
|
|
284
|
+
chunk was gone collided with the next chunk id once ids were reused
|
|
285
|
+
("constraint failed" on insert), every re-index of that file failed, and the
|
|
286
|
+
exception killed the watch + auto-index threads for the session. `Store._prune_orphan_fts_rows` repairs on every open (cheap count check),
|
|
287
|
+
`index_paths` turns a per-file failure into a skip with the reason, and the
|
|
288
|
+
serve loops log a failed pass instead of dying. If indexing "stops" again,
|
|
289
|
+
check `~/.ragdesk/serve.log` first.
|
|
290
|
+
- Corrections: `corrections` table (`question`, `answer`, embedding of the
|
|
291
|
+
question; `corrections_revision` for invalidation). The UI's Fix button under
|
|
292
|
+
an answer opens an editor and `POST /api/corrections {question, answer}`;
|
|
293
|
+
`GET /api/corrections` + `POST /api/corrections/delete` back the Settings
|
|
294
|
+
card. On ask, `serve._corrections_for` tries every rewrite variant
|
|
295
|
+
(`search_query` + `sub_queries`) and injects the nearest correction at
|
|
296
|
+
cosine ≥ `CORRECTION_MIN_COSINE` 0.88 (same calibration as the semantic
|
|
297
|
+
cache) — measured: a follow-up's raw text scored 0.416, its sub-query 0.896,
|
|
298
|
+
so a single raw lookup is not enough. The block rides right before the
|
|
299
|
+
question (`answer.CORRECTION_HEADER`, `ANSWER_PROMPT_VERSION` v4) because a
|
|
300
|
+
4B model ignored it mid-prompt but followed it near the question — verify any
|
|
301
|
+
wording change against the live model, not just a unit test. The API echoes
|
|
302
|
+
`correction` (the matched question) on ask/ask_stream so the UI shows a "your
|
|
303
|
+
fix" badge; the fingerprint carries `corrections_revision`, so adding a
|
|
304
|
+
correction invalidates cached answers instead of replaying an uncorrected one.
|
|
305
|
+
- Retrieval lanes (`search.hybrid_search`): BM25 (FTS5), dense cosine, path
|
|
306
|
+
tokens (file names), plus an optional HyDE dense lane — RRF-fused. HyDE text
|
|
307
|
+
comes from `settings.hyde` (Settings toggle, off by default; measured:
|
|
308
|
+
recall@5 0.889 → 1.000 on the repo golden set at +2-4s per question) and is
|
|
309
|
+
skipped silently whenever no LLM resolves.
|
|
310
|
+
- Parent-child: `index.group_parents` groups child chunks (~4k chars) and
|
|
311
|
+
`Hit.context` returns the parent for prompts; old rows are backfilled on
|
|
312
|
+
Store open (`_backfill_parents`, no re-embedding) and fall back to the child.
|
|
313
|
+
- Eval: `evaluate` groups per-category metrics, `category_metrics` powers
|
|
314
|
+
`--min-recall-category name=value` gates, and `ground_answer` is the
|
|
315
|
+
deterministic faithfulness proxy (sentence overlap against cited chunks +
|
|
316
|
+
citation range checks) used by `eval --answers`. Golden rows may carry
|
|
317
|
+
`"history": [["user", "…"], ["assistant", "…"]]` for follow-ups; passing
|
|
318
|
+
`rewrite_for(question, history)` makes the harness search the standalone
|
|
319
|
+
rewrite too, and `eval --rewrite` prints the raw baseline next to it (the
|
|
320
|
+
rewriter is `cli._standalone_rewrite`, the same prompt `serve` uses; it needs
|
|
321
|
+
a resolvable local model or exits 2). `fixtures/golden_multiturn.jsonl`
|
|
322
|
+
scores 0.926 → 1.000 nDCG / 0.900 → 1.000 MRR (raw → rewrite, Qwen3.5-4B MLX,
|
|
323
|
+
2026-09-15). **Lesson: `SMART_RETRIEVAL_PROMPT` holds a JSON example, so its
|
|
324
|
+
braces must be doubled for `.format()` — the un-doubled prompt raised
|
|
325
|
+
`KeyError: '"standalone"'`, the bare `except` in `_smart_retrieval` swallowed
|
|
326
|
+
it, and follow-up rewriting was silently off; those handlers now print the
|
|
327
|
+
reason to stderr.**
|
|
328
|
+
- Connectors ingest payloads through `index.extract_bytes(data, name)`: one
|
|
329
|
+
dispatcher for PDFs, office files, images (OCR) and plain text. Never decode
|
|
330
|
+
raw bytes to text at a call site — `is_indexable` admits those types now, so
|
|
331
|
+
a local decode would index image bytes as mojibake.
|
|
332
|
+
- Long jobs report progress through `state.activity` (owner-token guarded so
|
|
333
|
+
concurrent runs cannot clobber each other): index/sync endpoints pass a
|
|
334
|
+
progress callback into `index_paths`/`sync_github`/`sync_gitlab`, auto-index
|
|
335
|
+
claims the slot too, and `/api/status.activity` feeds the rail + sync cards.
|
|
336
|
+
- `ask_stream` commits headers first and then emits `{"status": …}` lines
|
|
337
|
+
(searching → HyDE → loading the model → thinking) before the deltas, so the
|
|
338
|
+
UI shows phases + elapsed seconds and can abort with the Stop button.
|
|
339
|
+
- Idle unload: the serve timer releases the embedder session and the LLM after
|
|
340
|
+
`idle_unload_minutes` (Settings; 0 = never) of no POSTs; ONNX workspace
|
|
341
|
+
memory does not respond to arena/batch tuning, so dropping the session is
|
|
342
|
+
the honest way to give RAM back.
|
|
343
|
+
- Connections: GitHub has three paths (device code with `RAGDESK_GITHUB_CLIENT_ID`
|
|
344
|
+
or a saved client ID, `gh` login, or a pasted token); Confluence and GDrive
|
|
345
|
+
connect flows validate before saving to `~/.config/ragdesk/credentials.json`
|
|
346
|
+
(`0600`; override the dir with `RAGDESK_CONFIG_DIR` for tests).
|
|
347
|
+
- Credentials policy: `.env` (gitignored) → `python -m ragdesk.buildenv` bakes
|
|
348
|
+
non-confidential values into `src/ragdesk/_build_env.py` (gitignored) for
|
|
349
|
+
release artifacts. The Atlassian 3LO secret is refused by the baker — it stays
|
|
350
|
+
per-user. User-facing setup guides: `docs/sources.md`.
|
|
351
|
+
- `fixtures/` — tiny corpus + two golden sets (fixtures, repo) for offline CI.
|
|
352
|
+
- `tests/` — pytest; always uses `HashingEmbedder` (never requires Ollama or
|
|
353
|
+
network). Connector tests monkeypatch HTTP.
|
|
354
|
+
|
|
355
|
+
## Commands
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
uv sync --all-groups
|
|
359
|
+
uv run pytest
|
|
360
|
+
uv run ruff check .
|
|
361
|
+
uv run ragdesk --embedder hash --db /tmp/eval.db index fixtures/docs
|
|
362
|
+
uv run ragdesk --embedder hash --db /tmp/eval.db eval --golden fixtures/golden.jsonl
|
|
363
|
+
# repo golden (real numbers; --embedder onnx downloads ~0.3GB on first run)
|
|
364
|
+
# NB: the golden scopes with folder:dtduc-git/ragdesk, so index an absolute path
|
|
365
|
+
uv run ragdesk --embedder hash --db /tmp/eval-repo.db index "$PWD"
|
|
366
|
+
uv run ragdesk --embedder hash --db /tmp/eval-repo.db eval --golden fixtures/golden_repo.jsonl
|
|
367
|
+
# desktop shell (bundles a self-contained Python runtime — scripts/build_sidecar.sh
|
|
368
|
+
# runs automatically as beforeBuildCommand: standalone CPython + ragdesk[onnx,mlx,vision]
|
|
369
|
+
# into desktop/src-tauri/sidecar/, ~470MB, gitignored. The shell prefers
|
|
370
|
+
# <Resources>/python/bin/python3 -m ragdesk over PATH; module invocation on
|
|
371
|
+
# purpose — a console-script shebang bakes in build-machine paths. Verify a
|
|
372
|
+
# build with: env -i HOME=$HOME PATH=/usr/bin:/bin <app>/Contents/Resources/python/bin/python3 -m ragdesk --db /tmp/t.db serve --port 8770)
|
|
373
|
+
cd desktop && npm install && npm run tauri build
|
|
374
|
+
# macOS: sign with a stable identity so the Documents/Desktop/Downloads TCC
|
|
375
|
+
# grant survives rebuilds — an ad-hoc build gets a new identity every time and
|
|
376
|
+
# macOS re-asks for folder access on the next run
|
|
377
|
+
APPLE_SIGNING_IDENTITY="Apple Development: <you> (<TEAMID>)" npm run tauri build
|
|
378
|
+
# release build with baked credentials
|
|
379
|
+
cp .env.example .env # fill non-confidential values (see SECURITY.md)
|
|
380
|
+
uv run python -m ragdesk.buildenv && uv build
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Conventions
|
|
384
|
+
|
|
385
|
+
- Core stays dependency-free (argparse/sqlite3/urllib). New runtime deps need a
|
|
386
|
+
good reason; ONNX/reranker deps belong in an optional extra.
|
|
387
|
+
- `HashingEmbedder` is for tests/CI only — never quote its numbers as quality.
|
|
388
|
+
- Never default the reranker to a CC-BY-NC model (jina-reranker-v2 is
|
|
389
|
+
non-commercial). Defaults: `BAAI/bge-reranker-base` (MIT, fastembed) and
|
|
390
|
+
`onnx-community/gte-multilingual-reranker-base` (Apache-2.0, `--rerank onnx`).
|
|
391
|
+
- Embedder/dimension mismatch must stay fail-closed (`Store.ensure_embedder`).
|
|
392
|
+
- Sources are read-only. No telemetry, ever.
|
|
393
|
+
- Eval numbers published in README must be reproducible from the repo.
|
|
394
|
+
|
|
395
|
+
## Roadmap order
|
|
396
|
+
|
|
397
|
+
sources (Notion → GitLab → OneDrive/SharePoint → S3) → multilingual reranker →
|
|
398
|
+
MCP server → release v0.1.0 (DMG notarization + release workflow + PyPI
|
|
399
|
+
trusted publisher) → eval badge automation → Windows/Linux builds.
|
|
400
|
+
|
|
401
|
+
## Notes
|
|
402
|
+
|
|
403
|
+
- If code is ever reused from OpsRAG (Apache-2.0), add the required NOTICE
|
|
404
|
+
attribution before release.
|
|
405
|
+
- PyPI name `ragdesk` verified free (2026-09-14); re-check the similarity
|
|
406
|
+
checker at publish time.
|