homelab-codex-ws/services/kb-query/README.md
oskar d9e9a4c204 docs(kb-query): confirm Cloudflare A-record for kb.kapala.org is live
Operator added the A-record manually; verified against Cloudflare's own
authoritative NS (which briefly still returned NXDOMAIN — not a propagation
lag, the record simply wasn't saved yet on first check) plus 8.8.8.8/1.1.1.1.
Both DNS layers (Cloudflare public/Tailscale + Pi-hole LAN override) and the
vhost now confirmed working end-to-end.
2026-07-23 18:37:11 +02:00

155 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# kb-query
FastAPI search API in front of the module-5 KB retrieval engine
(`packages/kb-retrieval/`). Runs on **PIHA**, bound to PIHA's LAN IP only
(`exposure: private`, same class as paperless/nextcloud — no public ingress
yet). This is a **search API, not chat**: no answer synthesis over results,
that's phase 5.
## Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
| `/` | GET | Search UI (Jinja2 shell + `/static/app.js`, no login yet — plan §8 OIDC is a later step) |
| `/static/*` | GET | UI assets (`app.js`, `style.css`) |
| `/healthz` | GET | `{"status": "ok", "sol_status": "up"\|"down"}``sol_status` is a live probe of Ollama@SOLARIA, no auth required (monitoring must reach it) |
| `/search?q=<text>&mode=cascade\|flat` | GET | `query_text -> embed -> cascade_query/flat_query -> results`, `mode` defaults to `cascade` |
`/search` response shape (module 5 phase 4 plan §4, `summary`/`summary_tags`
added in Krok 4 for the UI's per-envelope result header — additive, does not
change any field the plan §4 shape already defined):
```json
{
"query": "...", "mode": "cascade", "sol_status": "up",
"results": [
{"envelope_id": "paperless:119", "source": "paperless", "dist": 0.34,
"chunk_index": 2, "text": "...", "link": "https://paper.kapala.org/documents/119/details",
"summary": "...", "summary_tags": ["..."]},
{"envelope_id": "<Message-ID>", "source": "gmail", "dist": 0.44,
"chunk_index": 0, "text": "...", "subject": "...", "from": "...", "date": "...",
"link": null, "mail_ui_url": null, "summary": null, "summary_tags": []}
]
}
```
`dist` is never filtered server-side — the 0.45/0.55 colour thresholds (below)
are a frontend concern, not an API contract. `summary`/`summary_tags` come
from `document_summary` for `SUMMARY_MODEL`; `null`/`[]` when the envelope
has no summary yet.
## Frontend (Krok 4, plan §7)
One page, served from this same FastAPI process — no separate frontend
container, no node build step (plan §2 decision 4): `app/templates/index.html`
(Jinja2 shell) + `app/static/app.js` (vanilla JS, `fetch()` to `/search`) +
`app/static/style.css`. Wszystko po polsku.
- Pole zapytania + submit (Enter lub przycisk), przełącznik trybu
kaskada/flat (domyślnie kaskada — checkbox "tryb flat (debug)").
- Wyniki grupowane po `envelope_id` (dokument): nagłówek trafienia to
streszczenie dokumentu (`summary`, tor haiku) gdy dostępne, w przeciwnym
razie `envelope_id`; chunki są rozwijanymi fragmentami (`<details>`) pod
nagłówkiem, posortowane po `dist`.
- Kolorowanie progów (fazy 3, zweryfikowane bramką): `dist < 0.45` zielony,
`0.450.55` żółty (nadal renderowany, z wizualnym ostrzeżeniem), `> 0.55`
nigdy nie renderowany jako pojedynczy wynik. Jeśli po tym filtrze żadna
grupa nie zostaje nic do pokazania (wszystkie trafienia > 0.55, albo brak
trafień w ogóle), całość zastępuje komunikat "Brak odpowiedzi w KB dla
tego zapytania" z najlepszym (najniższym) zaobserwowanym `dist` w nawiasie.
- Źródło: Paperless → link "Otwórz w Paperless" (`link`); Gmail → metadane
(`subject`/`from`/`date`) + przycisk "Kopiuj Message-ID" (`envelope_id`
**jest** Message-ID, plan §2 decyzja 3) — nie ma dokąd linkować, więc
kopiowalny identyfikator zamiast martwego linku.
- Stopka pokazuje `sol_status` dyskretnie (odświeżane z `/healthz` przy
starcie strony i po każdym wyszukiwaniu).
## Embed path (current step)
Calls Ollama on SOLARIA directly per request — no cache, no circuit breaker,
no local-PIHA fallback yet (that state machine, plan §2 decision 2/§5, is a
separate later step). If SOLARIA is unreachable, `/search` returns **503**;
`/healthz` still answers (`sol_status: "down"`), same tolerance pattern as
`llm-gateway`.
## Startup invariant (hard-fail)
At startup, kb-query queries `document_chunk.model` and
`document_summary.embedding_model` for the set of models behind *active*
embeddings, and refuses to start (crash-loop, visible via container restarts)
if the configured `EMBED_MODEL` (default `bge-m3`) isn't in both sets. This
guards against querying with an embedding space that doesn't match what's
actually indexed — see `app/startup.py` for why the check reads
`document_summary.embedding_model` and not `.model` (the latter is the LLM
that *wrote* the summary, e.g. `claude-haiku-4-5`, not the embedder).
## Configuration
`.env`**gitignored**, copy from `env.example`. Required: `LAN_BIND_IP`,
`KB_DSN`. Optional: `OLLAMA_URL`, `EMBED_MODEL`, `SUMMARY_MODEL`.
## Deploy (PIHA)
1. `git pull` on PIHA (`~/homelab-codex-ws`).
2. `cp services/kb-query/env.example services/kb-query/.env` and fill in the
real `KB_DSN` password.
3. ```
docker compose -f services/kb-query/docker-compose.yml \
-f hosts/piha/runtime/kb-query/docker-compose.override.yml up -d --build
```
4. Verify: `services/kb-query/healthcheck.sh`, then from PIHA:
`curl "http://192.168.31.5:8230/search?q=test"` and open
`http://192.168.31.5:8230/` in a browser.
## Tests
```
pip install -e packages/kb-retrieval/
cd services/kb-query && pip install -r requirements.txt pytest pytest-asyncio && pytest
```
Unit tests mock the DB connection and Ollama HTTP session (no live DB/Ollama
required) — same style as `packages/kb-retrieval/tests/`. `tests/test_frontend.py`
drives `GET /`/`/static/*` through FastAPI's `TestClient` without entering it
as a context manager, so the DB-requiring `lifespan` never runs.
Frontend JS has its own pure-function tests (query-URL encoding, threshold
colouring, envelope grouping), run without a browser via Node's built-in
test runner: `node --test services/kb-query/tests/frontend/`.
## Ingress (`kb.kapala.org`, plan §8)
Wired up 2026-07-23 (`docs/sessions/2026-07-23-kb-f4-ingress.md`), **no code
change in this service** — pure infra step:
- npm@PIHA proxy host #35: `kb.kapala.org``http://192.168.31.5:8230`,
cert #49 (`*.kapala.org` wildcard, DNS-01 via Cloudflare, expires
2026-09-28) — same pattern as `paper.`/`vikunja.`/`ha.kapala.org`.
- Pi-hole Local DNS (`/etc/pihole/custom.list` on PIHA, runtime, not in Git):
`kb.kapala.org``192.168.31.5`. **This is the first `kapala.org` entry in
that file** — every other `kapala.org` vhost (paper/ha/immich/vikunja/forgejo)
has no LAN override and resolves via the public Cloudflare record
(Tailscale IP) even from LAN, a hairpin the plan assumed was already avoided
for those too. Not fixed here (out of this task's scope — no other vhosts
touched); worth a follow-up if it matters for those services.
- Cloudflare A record `kb.kapala.org``100.108.208.3` (Tailscale PIHA, DNS
only): added manually by the operator (no CF API token available in the
environment that did the rest of this step), verified against Cloudflare's
own authoritative NS and 8.8.8.8/1.1.1.1 — resolves everywhere now, both
LAN (via Pi-hole override) and Tailscale/public (via this record).
**No auth.** OIDC (plan §8: `authlib`, `/login`, `/auth/callback`) is explicitly
**not implemented** — confirmed no forward-auth/reverse-proxy-level auth pattern
exists anywhere in this repo (NPM community edition doesn't support it either);
the three precedents (paperless/nextcloud/vikunja) all do OIDC inside the app.
Building that is real service code (`authlib` dependency, session middleware,
Forgejo OAuth2 app registration) — deliberately deferred to a separate session,
decision confirmed with the operator 2026-07-23. Until then `kb.kapala.org` is
reachable by anyone on the LAN/tailnet with no login, same as before this vhost
existed.
## Out of scope for this step
- Local-PIHA embed fallback / circuit breaker (plan §2 decision 2, §5).
- OIDC login (see above) — separate session, needs `authlib` + Forgejo OAuth2 app.