diff --git a/docs/kb/modules/05-fallback-dedup-raport.md b/docs/kb/modules/05-fallback-dedup-raport.md new file mode 100644 index 0000000..175c2b2 --- /dev/null +++ b/docs/kb/modules/05-fallback-dedup-raport.md @@ -0,0 +1,135 @@ +# Raport dedup: fallback embed SOLARIA→PIHA — e7625cd (master) vs 3d4ee38 (task/kb-f4-fallback) + +**Data**: 2026-07-30 · **Worktree**: `task/kb-fallback-dedup` · **Status**: recon zakończony, +czeka na decyzję operatora co do listy salvage (sekcja „Rekomendacja zbiorcza"). + +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ł „1–2 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.2–5.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.2–5.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).