homelab-codex-ws/services/kb-query/README.md
oskar fbe81f9bf5 docs(kb-query): ingress kb.kapala.org live — npm vhost + Pi-hole DNS, OIDC deferred
Runtime steps (not in Git, logged here): npm@PIHA proxy host #35
(kb.kapala.org -> 192.168.31.5:8230, cert #49 *.kapala.org wildcard, same
pattern as paper./vikunja.kapala.org); Pi-hole custom.list split-horizon
entry added and verified (first kapala.org entry in that file — the other
kapala.org vhosts turned out to have no LAN override at all, a plan
assumption that didn't hold). Cloudflare A-record left for the operator (no
API token available here). OIDC intentionally not built: confirmed no
forward-auth pattern exists anywhere in this repo, and building authlib
OIDC into kb-query is real service code out of scope for an infra-only
task — operator decided to leave kb.kapala.org without auth for now.
2026-07-23 18:38:34 +02:00

154 lines
7.6 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): pending manual step by the operator (no CF API token available in
the environment that did this step) — without it, `kb.kapala.org` resolves
only on the LAN (Pi-hole), not over Tailscale from outside.
**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.