204 lines
12 KiB
Markdown
204 lines
12 KiB
Markdown
|
|
# Sesja 2026-07-27 — KB faza 4: fallback embed SOLARIA→PIHA (krok 3, ostatni element rdzenia)
|
|||
|
|
|
|||
|
|
> **Dopisek redakcyjny (2026-07-30, dedup — `docs/kb/modules/05-fallback-dedup-raport.md`):**
|
|||
|
|
> implementacja kodu z tej sesji (`app/fallback.py`, branch `task/kb-f4-fallback`, 3d4ee38)
|
|||
|
|
> została **porzucona** — do mastera weszła równoległa, szersza implementacja tego samego
|
|||
|
|
> kroku planu (e7625cd, `app/embed_router.py`, 2026-07-29) i to ona biega na PIHA. Ten log
|
|||
|
|
> wciągnięto do repo, bo dokumentuje fakty operacyjne niezależne od porzuconego kodu:
|
|||
|
|
> znalezisko i wyłączenie osieroconego natywnego `ollama.service` na PIHA (§3, z backlogiem
|
|||
|
|
> odinstalowania ≈2026-08-10), kalibrację live ollama-piha z werdyktem GO (§4 — konfiguracja
|
|||
|
|
> kontenera identyczna na masterze, pomiar przenosi się) oraz metodologię i baseline bramki
|
|||
|
|
> §9 (§5, Δ~3e-4). Wyniki bramki i testu sol-down dotyczyły kodu z brancha — na wdrożonym
|
|||
|
|
> masterze wymagają powtórki (raport dedup, follow-up (b)). Sekcje o deployu (§6) i
|
|||
|
|
> "Do zrobienia przez operatora" pkt 1–2 opisują stan sprzed merge'a e7625cd — historyczne.
|
|||
|
|
> Z delty brancha uratowano ponadto: `retrieval_eval.py --transport http` (plan §2 D6/§9)
|
|||
|
|
> i luki testowe T1/T2 przeniesione do `test_embed_router.py`.
|
|||
|
|
|
|||
|
|
**Zakres**: `docs/kb/modules/05-faza4-plan.md` §2 decyzja 2 / §5 — aktywny fallback
|
|||
|
|
embedu, ostatni brakujący element rdzenia fazy 4 (frontend i ingress LIVE od
|
|||
|
|
2026-07-22/23, `docs/sessions/2026-07-23-kb-f4-ingress.md`). Zero zmian w schemacie
|
|||
|
|
DB, zero zmian w `kb_retrieval`'s retrieval logice — wyłącznie warstwa embed + health.
|
|||
|
|
|
|||
|
|
Praca wykonana w task worktree (`task/kb-f4-fallback`, `.claude/skills/worktree-aware`).
|
|||
|
|
Zgodnie z ustaleniem na starcie sesji (patrz "Ustalenia proceduralne" niżej): kod
|
|||
|
|
napisany i przetestowany lokalnie w worktree, produkcyjne kroki (kalibracja, deploy,
|
|||
|
|
live-test) wykonane po jawnej zgodzie operatora, z osobnym potwierdzeniem przed
|
|||
|
|
każdym kolejnym krokiem dotykającym PIHA/SOLARIĘ.
|
|||
|
|
|
|||
|
|
## Ustalenia proceduralne
|
|||
|
|
|
|||
|
|
Zadanie wprost wymagało kroków produkcyjnych (kalibracja RAM/latencji na żywym PIHA,
|
|||
|
|
symulacja sol-down dotykająca SOLARII, deploy, push) — sprzeczne z ogólną dyscypliną
|
|||
|
|
`worktree-aware` ("nigdy nie uruchamiaj deployów/healthchecków przeciw produkcji z
|
|||
|
|
worktree"). Zamiast rozstrzygać to samodzielnie, zapytano operatora:
|
|||
|
|
1. Recon read-only (bez zmian stanu) — zgoda bez pytania.
|
|||
|
|
2. Właściwe kroki produkcyjne (kalibracja, deploy, live-test, push) — operator
|
|||
|
|
potwierdził jawnie ("Yes, proceed with all of it") po zobaczeniu pełnego zakresu.
|
|||
|
|
|
|||
|
|
## 1. Kod (warstwa embed + health, zero zmian retrievalu/DB)
|
|||
|
|
|
|||
|
|
- **`packages/kb_retrieval/embed.py`**: `embed_chunk` dostał opcjonalny `timeout_s`
|
|||
|
|
(domyślnie `None`, zero zmiany zachowania istniejących wołań) — potrzebny do
|
|||
|
|
twardego 3 s timeoutu na nodze SOLARIA bez zmiany zachowania nogi PIHA.
|
|||
|
|
- **`services/kb-query/app/fallback.py`** (nowy): `SolCircuitBreaker` (cache 30 s,
|
|||
|
|
zegar wstrzykiwalny do testów) + `resolve_sol_status` (probe `/api/tags`, 500 ms) +
|
|||
|
|
`embed_with_fallback` (SOLARIA z twardym 3 s timeoutem → jednorazowe przełączenie na
|
|||
|
|
PIHA **w tym samym requeście** przy timeout/błędzie → PIHA bez dodatkowego
|
|||
|
|
timeoutu). Dokładnie maszyna stanów z planu §2 decyzja 2.
|
|||
|
|
- **`app/search.py`**: `run_search` liczy embedding raz przez `embed_with_fallback`,
|
|||
|
|
potem woła `flat_retrieve`/`cascade_retrieve`/`hybrid_retrieve` (niskopoziomowe
|
|||
|
|
funkcje `kb_retrieval`, biorą gotowy wektor) zamiast `flat_query`/`cascade_query`/
|
|||
|
|
`hybrid_query` (które embedują same) — dzięki temu decyzja fallbacku żyje wyłącznie
|
|||
|
|
w warstwie HTTP kb-query, zero zmiany w `kb_retrieval`. `sol_status` w odpowiedzi to
|
|||
|
|
teraz realny wynik, nie zahardkodowane `"up"`.
|
|||
|
|
- **`app/main.py`**: `/healthz` i `/search` dzielą jeden `SolCircuitBreaker`
|
|||
|
|
(`app.state.sol_breaker`) — oba endpointy zawsze zgadzają się co do aktualnego
|
|||
|
|
stanu. Nowy env `OLLAMA_PIHA_URL` (domyślnie `http://localhost:11434` — celowo
|
|||
|
|
"inertny" placeholder, fail-closed, dopóki operator nie ustawi realnego adresu).
|
|||
|
|
- **Inwariant modelu**: **nie dodano** drugiego, per-request sprawdzenia w DB —
|
|||
|
|
`EMBED_MODEL` to jedna stała wątkowana przez obie nogi `embed_with_fallback`,
|
|||
|
|
więc startowy check (`app/startup.py`, niezmieniony) pokrywa obie ścieżki z
|
|||
|
|
konstrukcji. Dodanie drugiego DB-checka chroniłoby przed scenariuszem, który nie
|
|||
|
|
może wystąpić (CLAUDE.md: nie dodawaj walidacji dla scenariuszy, które nie mogą się
|
|||
|
|
zdarzyć) — zamiast tego nowy test (`test_both_legs_use_identical_embed_model`)
|
|||
|
|
strukturalnie dowodzi, że obie nogi w tym samym requeście dostają identyczny
|
|||
|
|
`embed_model`.
|
|||
|
|
- **`jobs/documents-ingest/eval/retrieval_eval.py`**: dodano `--transport
|
|||
|
|
{direct,http}` + `--base-url` (plan §2 decyzja 6 / §9) — dotąd nieistniejące (tylko
|
|||
|
|
ręczny smoke-test, `docs/sessions/2026-07-23-kb-f4-ingress.md` follow-up). Tryb
|
|||
|
|
`http` woła trzy `GET /search` (flat/cascade/hybrid) na żywym kb-query zamiast
|
|||
|
|
embedować+odpytywać lokalnie; `envelope.source` do kryterium 4 bierze się z pola
|
|||
|
|
`source` w odpowiedzi JSON, nie z osobnego zapytania do DB. Nie da się swipe'ować
|
|||
|
|
N przez HTTP (kb-query serwuje jeden N per request) — tryb http raportuje tylko
|
|||
|
|
przy `--gate-n`.
|
|||
|
|
|
|||
|
|
## 2. Nowy serwis `services/ollama-piha`
|
|||
|
|
|
|||
|
|
Klon wzorca `services/ollama` (`owner_node: piha` zamiast `solaria`, bez rezerwacji
|
|||
|
|
GPU — PIHA to arm64 bez akceleracji), `OLLAMA_KEEP_ALIVE=0` (model ładowany tylko na
|
|||
|
|
czas requestu). `mem_limit: 2560m` (tentatywny wg planu, potwierdzony pomiarem —
|
|||
|
|
patrz §3). Wpisany do `hosts/piha/services.yaml` (`depends_on.local` kb-query →
|
|||
|
|
`[kb-postgres, ollama-piha]`, fallback nie jest twardą zależnością na starcie).
|
|||
|
|
|
|||
|
|
## 3. Znalezisko: osierocony natywny `ollama.service` na PIHA
|
|||
|
|
|
|||
|
|
Podczas pierwszej próby deployu `ollama-piha` (bind `127.0.0.1:11434`) — konflikt
|
|||
|
|
portu. Okazało się, że PIHA ma **natywny (nie-Docker) systemd `ollama.service`**
|
|||
|
|
(v0.6.1, `enabled`, działający od 2026-06-22, PATH env wskazujący na użytkownika
|
|||
|
|
`/home/pi/...`), o którym nic nie wiadomo w repo — plan §1.2 wprost zakładał "PIHA:
|
|||
|
|
brak Ollamy", co okazało się nieaktualne/błędne. To realna sprzeczność planu z
|
|||
|
|
rzeczywistością → STOP, pytanie do operatora zamiast cichej decyzji.
|
|||
|
|
|
|||
|
|
Weryfikacja przed jakąkolwiek akcją: `journalctl -u ollama --since "7 days ago"` —
|
|||
|
|
**tylko własne, właśnie wykonane** zapytania probe (`/api/version`, `/api/tags`),
|
|||
|
|
`total blobs: 0` od startu (nigdy nic nie pobrano). Operator potwierdził: martwy
|
|||
|
|
balast, `sudo systemctl disable --now ollama.service` (**disable, nie uninstall** —
|
|||
|
|
odwracalne). Port 11434 zwolniony, `ollama-piha` wystartował normalnie.
|
|||
|
|
|
|||
|
|
**Backlog**: PIHA host-level shadow — natywny `ollama.service` wyłączony
|
|||
|
|
2026-07-27; odinstalować binarkę/unit po ~2 tygodniach jeśli nic się nie posypie.
|
|||
|
|
|
|||
|
|
## 4. Kalibracja (plan §5, gate) — **werdykt: GO**
|
|||
|
|
|
|||
|
|
Zmierzone na żywym PIHA pod normalnym obciążeniem (kb-postgres, paperless, Immich,
|
|||
|
|
HA, Forgejo działające, nie okno nocnej ciszy), 3 kolejne wywołania `/api/embeddings`
|
|||
|
|
po `ollama pull bge-m3`:
|
|||
|
|
|
|||
|
|
| Wywołanie | Latencja |
|
|||
|
|
|---|---|
|
|||
|
|
| 1 (pierwsze, zimny start) | 5.25 s |
|
|||
|
|
| 2 | 4.41 s |
|
|||
|
|
| 3 | 4.16 s |
|
|||
|
|
|
|||
|
|
Brak przyspieszenia między wywołaniami — zgodnie z projektem (`OLLAMA_KEEP_ALIVE=0`
|
|||
|
|
zwalnia model po każdym requeście, `ollama ps` pokazuje zero rezydentnych modeli
|
|||
|
|
między wywołaniami).
|
|||
|
|
|
|||
|
|
RAM: baseline idle ~66 MiB, szczyt podczas burst ~983 MiB (`docker stats`, próbkowane
|
|||
|
|
co 0.3 s w trakcie 3 wywołań) — komfortowo w granicach ceilingu `2560m`. `free -h`
|
|||
|
|
systemowe: `available` nie spadło poniżej ~1.3 GiB w trakcie, osiadło na ~4.2 GiB po
|
|||
|
|
(dla porównania: przed startem eksperymentu `available` = 3.7 GiB).
|
|||
|
|
|
|||
|
|
**Werdykt**: oba kryteria planu spełnione (latencja pojedyncze sekundy, nie
|
|||
|
|
dziesiątki; RAM ze sporym zapasem) → **włączony jako domyślny fallback**, bez flagi
|
|||
|
|
`KB_QUERY_LOCAL_FALLBACK_ENABLED`.
|
|||
|
|
|
|||
|
|
## 5. Bramka jakościowa (plan §9)
|
|||
|
|
|
|||
|
|
Wszystko uruchomione z `~/kb/venv` na PIHA (istniejący venv z poprzednich sesji,
|
|||
|
|
`aiohttp`/`asyncpg`/`yaml` już obecne) przeciw żywej bazie + żywemu kb-query.
|
|||
|
|
|
|||
|
|
**HTTP-equivalence** (`--transport http` vs `--transport direct`, SOLARIA up, ten sam
|
|||
|
|
`--gate-n 10`): oba PASS, **0 rozbieżności** w `dist` na wszystkich zapytaniach
|
|||
|
|
(`flat_top1_dist`, `hybrid_top1_dist`, `cascade[10].top1_dist`) — identyczne bit w
|
|||
|
|
bit, jak wymagał plan (nie ±epsilon, bo to ten sam kod, HTTP to tylko opakowanie).
|
|||
|
|
|
|||
|
|
**Live sol-down fallback test**: symulacja przez `OLLAMA_URL=http://solaria:1`
|
|||
|
|
(zły port, zgodnie z rekomendacją planu — zero dotknięcia SOLARII/innych
|
|||
|
|
konsumentów Ollamy) w `.env` kb-query, restart kontenera. `/healthz` →
|
|||
|
|
`sol_status: "down"`. `/search` → 200, wyniki z PIHA, ~4.3 s (zgodnie z kalibracją).
|
|||
|
|
Pełna bramka `retrieval_eval.py --transport http` z SOLARIA-down: **PASS** —
|
|||
|
|
identyczny wzorzec hit@3 co na SOLARII, `dist` w granicach epsilon:
|
|||
|
|
|
|||
|
|
| Zapytanie | dist (SOLARIA) | dist (PIHA fallback) | Δ |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| 1 | 0.341780 | 0.341509 | 0.000271 |
|
|||
|
|
| 2 | 0.324808 | 0.324858 | 0.00005 |
|
|||
|
|
| 3 | 0.428898 | 0.429184 | 0.000286 |
|
|||
|
|
| 4 | 0.448199 | 0.447903 | 0.000296 |
|
|||
|
|
| 5 | 0.386901 | 0.386816 | 0.000085 |
|
|||
|
|
| N (negative control) | 0.598301 | 0.598017 | 0.000283 |
|
|||
|
|
| N2 (negative control borderline) | 0.529772 | 0.529530 | 0.000242 |
|
|||
|
|
|
|||
|
|
Maksymalna rozbieżność: **~3e-4** — rząd wielkości mniejszy niż oczekiwany przez plan
|
|||
|
|
(1e-3–1e-2), kolejność top-k identyczna, wynik bramki (`gate.passed`) identyczny.
|
|||
|
|
Kb-query przywrócony do normalnej konfiguracji po teście (`.env` z prawdziwym
|
|||
|
|
`OLLAMA_URL`, restart), `/healthz` z powrotem `sol_status: "up"`.
|
|||
|
|
|
|||
|
|
## 6. Deploy
|
|||
|
|
|
|||
|
|
Kod nie był jeszcze zmergowany do `master` (dyscyplina worktree: agent nigdy nie
|
|||
|
|
mergeuje/pushuje `master`) — deploy przez standardowy `deploy-node.sh`
|
|||
|
|
niedostępny bez mastera. Zamiast tego: `rsync` zmienionych plików
|
|||
|
|
(`packages/kb-retrieval`, `services/kb-query`, `services/ollama-piha`,
|
|||
|
|
`hosts/piha/runtime/ollama-piha`, `hosts/piha/services.yaml`,
|
|||
|
|
`jobs/documents-ingest/eval/retrieval_eval.py` + README) do żywego checkoutu
|
|||
|
|
`~/homelab-codex-ws` na PIHA (bez zmiany brancha — working tree pozostaje na
|
|||
|
|
`master` z niescommitowanym diffem 1:1 identycznym z tą gałęzią), potem
|
|||
|
|
standardowy `docker compose ... up -d --build` z tego miejsca. Efekt: realny,
|
|||
|
|
działający deploy, ale **repo na PIHA ma dziś dirty working tree** — wymaga domknięcia
|
|||
|
|
(patrz "Do zrobienia przez operatora" niżej).
|
|||
|
|
|
|||
|
|
Zweryfikowane: `kb-query` (healthy), `ollama-piha` (healthy, `bge-m3` w wolumenie),
|
|||
|
|
`curl https://kb.kapala.org/healthz` → `200 {"sol_status":"up"}`,
|
|||
|
|
`curl https://kb.kapala.org/search?q=test` → `200`.
|
|||
|
|
|
|||
|
|
## Stan na koniec sesji
|
|||
|
|
|
|||
|
|
| Element | Status |
|
|||
|
|
|---|---|
|
|||
|
|
| `packages/kb-retrieval` — `embed_chunk(timeout_s=...)` | ✅ kod + testy |
|
|||
|
|
| `services/kb-query/app/fallback.py` — maszyna stanów | ✅ kod + testy (38/38 kb-query, 25/25 kb-retrieval) |
|
|||
|
|
| `services/ollama-piha` — nowy serwis GitOps | ✅ zdefiniowany, ✅ LIVE na PIHA |
|
|||
|
|
| Natywny `ollama.service` na PIHA (osierocony) | ✅ wyłączony (nie odinstalowany) |
|
|||
|
|
| Kalibracja RAM/latencja | ✅ zmierzone — werdykt GO |
|
|||
|
|
| `retrieval_eval.py --transport http` | ✅ zaimplementowane, ✅ PASS na żywo |
|
|||
|
|
| Live sol-down fallback test | ✅ PASS, Δ~3e-4 |
|
|||
|
|
| Deploy kb-query + ollama-piha na PIHA | ✅ LIVE, working tree PIHA dirty (patrz niżej) |
|
|||
|
|
| Merge do `master` | ⛔ nie wykonany (dyscyplina worktree — operator) |
|
|||
|
|
|
|||
|
|
## Do zrobienia przez operatora
|
|||
|
|
|
|||
|
|
1. **Merge** `task/kb-f4-fallback` → `master` (`scripts/dev/agent.sh merge` albo
|
|||
|
|
ręcznie) — branch popchnięty do `origin/task/kb-f4-fallback` (patrz commit poniżej).
|
|||
|
|
2. Na PIHA: `cd ~/homelab-codex-ws && git status` będzie dirty (diff identyczny z tym
|
|||
|
|
commitem, bo już wdrożony ad-hoc przez `rsync` w tej sesji) — po mergu do mastera,
|
|||
|
|
`git checkout -- .` (working tree już ma dokładnie tę treść) albo zwyczajnie
|
|||
|
|
`git pull` po mergu powinien wylądować "already up to date"/no-op, bo pliki na
|
|||
|
|
dysku już są zgodne z tym co przyjdzie z mastera. **Zweryfikować** `git diff` jest
|
|||
|
|
puste po pull, nie zakładać.
|
|||
|
|
3. Backlog: natywny `ollama.service` na PIHA wyłączony `systemctl disable --now`
|
|||
|
|
2026-07-27 (§3 wyżej) — jeśli nic się nie posypie przez ~2 tygodnie, odinstalować
|
|||
|
|
binarkę/unit całkiem.
|
|||
|
|
4. OIDC dla kb-query nadal odłożone (decyzja z 2026-07-23) — nie w zakresie tej sesji.
|