homelab-codex-ws/kb/phases/kb-m5-faza4-fallback-dedup.md
oskar 0858e25c81 feat(kb): przenosiny type=phase do kb/phases/ (14 plikow, bez SPLIT)
Plany faz KB (modul 5 fazy 2/3/4/mailowa, moduly 0/2/3/4, documents-ingest,
raport fallback-dedup, eval retrieval-pilot) oraz prometheus-cutover-etap2,
subsystem-a-naprawa, okit-cloudflare.

git mv + frontmatter, tresc nietknieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:04 +02:00

152 lines
13 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.

---
okf: "0.1"
type: phase
visibility: private
status: active
updated: 2026-07-30
links: []
---
# Raport dedup: fallback embed SOLARIA→PIHA — e7625cd (master) vs 3d4ee38 (task/kb-f4-fallback)
**Data**: 2026-07-30 · **Worktree**: `task/kb-fallback-dedup` · **Status**: salvage **wykonany**
2026-07-30 — operator zatwierdził pełną listę S1S4 (sekcja „Rekomendacja zbiorcza");
zrealizowane na tym branchu: S1 (cherry-pick `--transport http` + README), S2 (session log
2026-07-27 z dopiskiem redakcyjnym), S3 (testy T1/T2/T3 w `test_embed_router.py`; T4
pominięty zgodnie z raportem), S4 (wynik kalibracji w override + README ollama-piha).
Weryfikacja: pytest kb-query 42/42 PASS; `retrieval_eval.py --transport http` przepuszczony
end-to-end przeciwko stubowi `/search` (raport + werdykt bramki generują się poprawnie;
kryterium 4 na stubie nie przechodzi wyłącznie dlatego, że `mail_queries` w `queries.yaml`
mają `expected_envelope: null` — placeholdery, artefakt danych, nie kodu).
Dwie równoległe sesje zaimplementowały ten sam krok planu (moduł 5 faza 4, §2 decyzja 2 / §5):
- **e7625cd** (2026-07-29, **ZMERGOWANY do master, wdrożony na PIHA**): `app/embed_router.py`
+ `test_embed_router.py` + `services/ollama-piha` + README z testami A/B/C.
- **3d4ee38** (2026-07-27, `origin/task/kb-f4-fallback`, **NIEZMERGOWANY**): `app/fallback.py`
+ `test_fallback.py` + własny `services/ollama-piha` + `retrieval_eval.py --transport http`
+ session log z kalibracją live.
Master = źródło prawdy. 3d4ee38 **nie będzie mergowany** — ten raport klasyfikuje jego deltę
i rekomenduje, co uratować.
---
## 1. Porównanie maszyn stanów (`embed_router.py` vs `fallback.py`)
Werdykt: **`embed_router.py` na masterze pokrywa 100% zachowań `fallback.py` i dodaje
kilka istotnych rzeczy ponad to. Nic nie zgubił.**
| Zachowanie (wymóg planu §2 D2 / zadania) | `fallback.py` (branch) | `embed_router.py` (master) |
|---|---|---|
| Health-check cache 30 s (probe `GET /api/tags` po wygaśnięciu) | ✅ `SolCircuitBreaker`, TTL 30 s | ✅ TTL 30 s (env `EMBED_HEALTH_TTL_S`) |
| Timeout probe'a | 0.5 s (stała) | 1.5 s (env; udokumentowane: absorbuje jitter Tailscale, spec zadania mówił „12 s") |
| Twardy timeout 3 s na nodze SOLARIA | ✅ `ClientTimeout(total=3)` przez nowy param `embed_chunk(timeout_s=...)` | ✅ `asyncio.wait_for(3 s)` wokół niezmienionego `embed_chunk`**bez ruszania pakietu** kb-retrieval; obejmuje też parsowanie odpowiedzi |
| One-shot switch (fail mid-embed → **ten sam request** z PIHA, breaker „down" na okno TTL) | ✅ | ✅ + łapie dodatkowo `ValueError` (brak `embedding` w odpowiedzi) i loguje `circuit open for 30s` |
| Noga PIHA bez twardego timeoutu (cold-load, ostatnia deska) | ✅ | ✅ (celowe, udokumentowane w docstringu) |
| Powrót na SOLARIĘ ≤ 30 s po wygaśnięciu TTL | ✅ | ✅ |
| Obsługa 503 (oba backendy padnięte → 503, nie gołe 500) | ⚠️ częściowa: tylko `aiohttp.ClientError` → 503 w `main.py`; **`TimeoutError` z nogi PIHA wyleciałby jako gołe 500** | ✅ dedykowany `EmbedBackendError` → 503; osobno pokryty przypadek „fallback nieskonfigurowany" |
| Inwariant embed-model (query model == `document_chunk.model`) | startowy DB-check + jedna stała `embed_model` na obie nogi (dowód strukturalny w teście) | to samo **plus** leniwa weryfikacja per-backend przy pierwszym użyciu: `/api/tags` backendu musi listować `EMBED_MODEL`, inaczej `ModelMismatchError` → głośne 500 (nigdy cichy embed w złej przestrzeni wektorowej) |
| `/healthz` dzieli cache z `/search` | ✅ `sol_status` | ✅ `sol_status` + dodatkowo `fallback_status` (up/down/unconfigured) |
| `embed_backend` w odpowiedzi `/search` | ❌ (tylko `sol_status`) | ✅ (potrzebne do debugowania jakości per backend) |
| Praca bez fallbacku (env nieustawiony) | placeholder `http://localhost:11434` (fail-closed, ale mylący `sol_status`) | jawny tryb `EMBED_FALLBACK_URL` unset → zachowanie sprzed Kroku 2 (503) |
| Parametryzacja (TTL/timeouty) | stałe modułowe | env vars z defaultami |
| Logging decyzji routingu | ❌ | ✅ (`kb-query.embed`, backend=…, elapsed_ms) |
Różnice czysto kosmetyczne: semantyka wygaśnięcia dokładnie na granicy TTL identyczna
(obie strony: `t == TTL` → wygasłe); branch trzymał breaker jako dataclass z injektowalnym
zegarem, master trzyma to samo wewnątrz `EmbedRouter` (też injektowalny zegar).
---
## 2. Pełna delta 3d4ee38 (git diff origin/master...origin/task/kb-f4-fallback) + klasyfikacja
| Plik | Rozmiar delty | Klasyfikacja | Rekomendacja | Uzasadnienie |
|---|---|---|---|---|
| `services/kb-query/app/fallback.py` | +113 | duplikat funkcji | **porzuć** | `embed_router.py` = nadzbiór (tabela §1) |
| `services/kb-query/app/main.py` | ±32 | duplikat | **porzuć** | master ma własną, pełniejszą integrację (mapowanie 503/500, env-schemat `EMBED_PRIMARY/FALLBACK_URL`) |
| `services/kb-query/app/search.py` | ±36 | duplikat | **porzuć** | identyczny pomysł (embed raz → `*_retrieve` z gotowym wektorem); master dodaje `embed_backend` |
| `services/kb-query/tests/test_fallback.py` | +221 | duplikat + **3 unikalne scenariusze** | **adaptuj** | luki testowe do przeniesienia do `test_embed_router.py` — patrz §3 |
| `services/kb-query/tests/test_search.py` | ±38 | duplikat | **porzuć** | master zaadaptował te same testy przez `_FakeRouter` (czystsze — nie dubluje maszyny stanów w testach searcha) |
| `services/kb-query/{README,env.example,service.yaml,docker-compose.yml}` | ~±58 | duplikat | **porzuć** | inny (porzucony) schemat env `OLLAMA_PIHA_URL`; master bogatszy (testy A/B/C, rename `OLLAMA_URL`→`EMBED_PRIMARY_URL`) |
| `packages/kb-retrieval/src/kb_retrieval/embed.py` (`timeout_s` w `embed_chunk`) | +16 | duplikat funkcji | **porzuć** | master osiąga twardy timeout przez `asyncio.wait_for` bez zmiany współdzielonego pakietu — mniejsza powierzchnia zmian, ten sam efekt |
| `jobs/documents-ingest/eval/retrieval_eval.py` (`--transport {direct,http}` + `--base-url`) | +109 | **unikalna wartość** | **cherry-pick** | plan §2 decyzja 6 / §9 (bramka HTTP-equivalence) — **na masterze w ogóle nie istnieje**; e7625cd nie tknął tego pliku, patch aplikuje się czysto; kod woła tylko `GET /search` i czyta `envelope_id`/`dist`/`source` — w pełni zgodny z odpowiedzią mastera |
| `jobs/documents-ingest/README.md` | +6 | **unikalna wartość** | **cherry-pick** | dokumentacja powyższego, idzie w parze |
| `docs/sessions/2026-07-27-kb-f4-fallback.md` | +189 | **unikalna wartość** | **adaptuj** | jedyny zapis: (1) znalezisko osieroconego natywnego `ollama.service` na PIHA + jego wyłączenie 2026-07-27 i backlog odinstalowania, (2) kalibracja live ollama-piha (GO: peak ~983 MiB, ~4.25.3 s/embed), (3) metodologia i wyniki bramki §9 (HTTP-equivalence 0 rozbieżności; sol-down Δ~3e-4), (4) rsync-deploy → dirty working tree na PIHA. Wciągnąć z dopiskiem redakcyjnym, że zmergowana implementacja to **inny kod** (e7625cd) i wyniki bramki wymagają powtórki |
| `services/ollama-piha/*` (5 plików) | +155 | duplikat | **porzuć** | wersja mastera lepsza: named volume `ollama_piha_models` (uzasadnienie uid-pattern PIHA), healthcheck sprawdza obecność `bge-m3`, bind tylko 127.0.0.1+LAN |
| `hosts/piha/runtime/ollama-piha/docker-compose.override.yml` | +13 | duplikat + **1 unikalny fakt** | **adaptuj (mikro)** | ten sam `mem_limit: 2560m`; ale komentarz brancha zawiera potwierdzony pomiar (peak ~983 MiB), a master wciąż mówi „Confirm/trim after live calibration" — dopisać wynik kalibracji do komentarza override'u i/lub sekcji „Calibration" w `services/ollama-piha/README.md` |
| `hosts/piha/services.yaml` | ±26 | duplikat | **porzuć** | master ma własny wpis `ollama-piha` + soft-dependency kb-query; drobna różnica (`offline_required: true` na branchu vs `false` na masterze) — master źródłem prawdy |
---
## 3. Pokrycie testowe — `test_embed_router.py` (13 testów) vs `test_fallback.py` (16 testów)
Scenariusze wspólne (pokryte po obu stronach): embed na SOLARIA gdy up; cache werdyktu
w TTL (bez re-probe); down z probe'a → PIHA bez próby embedu na SOLARIA; down cache'owany
(kolejne requesty omijają SOLARIĘ bez probe'a); **one-shot same-request switch** (wariant
connection-error); **powrót na SOLARIĘ ≤30 s** (test C — po wygaśnięciu TTL); oba
backendy padnięte → wyjątek; obie nogi z identycznym `embed_model` (na masterze mocniej:
weryfikacja `/api/tags` per backend).
Master ma ponadto testy, których branch nie miał: `ModelMismatchError` (fallback bez
bge-m3 → głośny błąd, nie cichy embed; primary bez bge-m3 → failover), brak fallbacku
skonfigurowanego → `EmbedBackendError`, `fallback_status` (up/unconfigured), sufiks
`:latest` spełnia gołą nazwę modelu.
**Luki mastera względem `test_fallback.py`** (kandydaci na salvage — pozycja „adaptuj" z §2):
| # | Scenariusz z `test_fallback.py` | Stan na masterze | Waga |
|---|---|---|---|
| T1 | One-shot switch przy **timeoutcie** mid-embed (nie tylko connection-error) — na masterze to inna ścieżka kodu (`asyncio.wait_for` → `TimeoutError`) i jest **nieprzetestowana** | brak | **wysoka** — to główny scenariusz produkcyjny („SOLARIA wisi", nie „SOLARIA odrzuca") |
| T2 | Po one-shot switchu breaker **zostaje** „down": kolejny request w tym samym oknie TTL idzie prosto na PIHA bez probe'a i bez próby SOLARIA | brak (master testuje cache „down" tylko z probe'a, nie z mid-embed failure) | średnia |
| T3 | Noga PIHA bez twardego timeoutu (strukturalna asercja) | brak (własność tylko udokumentowana) | niska — opcjonalnie |
| T4 | Wygaśnięcie cache dokładnie na granicy TTL (`t == 30 s`) | brak (master testuje 31 s) | niska — semantyka i tak identyczna, można pominąć |
---
## 4. Rekomendacja zbiorcza (lista do zatwierdzenia)
1. **S1 — cherry-pick**: `retrieval_eval.py --transport http --base-url` + akapit w
`jobs/documents-ingest/README.md` (plan §2 D6/§9; aplikuje się czysto, zero zależności
od porzuconego kodu brancha).
2. **S2 — adaptuj**: `docs/sessions/2026-07-27-kb-f4-fallback.md``docs/sessions/`
z dopiskiem redakcyjnym na górze (implementacja z tej sesji porzucona na rzecz
e7625cd; fakty operacyjne — ollama.service, kalibracja, metodologia bramki — pozostają
w mocy).
3. **S3 — adaptuj**: luki testowe T1 + T2 (T3 opcjonalnie) do `test_embed_router.py`.
4. **S4 — adaptuj (mikro)**: wynik kalibracji 2026-07-27 (peak ~983 MiB, ~4.25.3 s,
werdykt GO) do komentarza `hosts/piha/runtime/ollama-piha/docker-compose.override.yml`
i sekcji Calibration w `services/ollama-piha/README.md` — pomiar dotyczył kontenera
ollama-piha (ta sama konfiguracja: obraz, `OLLAMA_KEEP_ALIVE=0`, `mem_limit 2560m`),
więc **przenosi się** na wersję mastera; różni się tylko storage (bind vs named
volume), co nie wpływa na RAM/latencję.
5. **Porzuć** całą resztę delty (kolumna „porzuć" w §2).
---
## 5. Follow-up (po salvage)
- **(a) Sprzątanie brancha** (wykona Oskar): skasować `origin/task/kb-f4-fallback`
(3d4ee38) i worktree `~/homelab-codex-ws-kb-f4-fallback` po zakończeniu salvage.
- **(b) Powtórka testu sol-down na żywym masterze**: kalibracja i bramka z 2026-07-27
dotyczyły **innego kodu** (`fallback.py`, env `OLLAMA_PIHA_URL`) — na wdrożonym
e7625cd trzeba przejść testy A/B/C z `services/kb-query/README.md` oraz bramkę
`retrieval_eval.py --transport http` (po S1): HTTP-equivalence przy SOLARIA-up
(identyczne `dist`) i sol-down (Δ≤epsilon, kolejność top-k identyczna; baseline
z 27.07: Δ~3e-4). Symulacja wg README: `EMBED_PRIMARY_URL=http://192.0.2.1:11434`
(TEST-NET), nie dotykając SOLARII.
- **(c) Weryfikacja stanu PIHA po podwójnym deployu** (znalezisko z session loga):
sesja 27.07 wdrożyła kod brancha przez **rsync do `~/homelab-codex-ws` na PIHA**
(dirty working tree), a ollama-piha wystartował tam z **bind mountem**
`/opt/homelab/data/ollama-piha`; master definiuje **named volume**
`ollama_piha_models`. Sprawdzić: (1) working tree na PIHA czysty po merge'u e7625cd
i kontenery faktycznie zbudowane z kodu mastera, (2) który storage żywy kontener
naprawdę montuje; jeśli named volume — czy `bge-m3` jest w nim spullowany, a stary
katalog bind (`/opt/homelab/data/ollama-piha`, ~1.2 GB modelu) do skasowania,
(3) `.env` kb-query używa `EMBED_FALLBACK_URL` (schemat mastera), nie martwego
`OLLAMA_PIHA_URL` — szybki test: `/healthz` musi zwracać `fallback_status: "up"`,
nie `"unconfigured"`.
- **(d) Backlog z 27.07** (przetrwa tylko dzięki S2): natywny, osierocony
`ollama.service` na PIHA wyłączony (`systemctl disable --now`) 2026-07-27 —
odinstalować binarkę/unit po ~2 tygodniach ciszy (≈ 2026-08-10).