Files
nanobot-runtime/skills/wiki/README.md
2026-09-10 12:33:37 +02:00

216 lines
11 KiB
Markdown
Raw Permalink 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.
# wiki — jak to funguje
Hybrid hledání ve tvých vlastních poznámkách: dva git repozitáře (`index`, `travel`) plus živý
workspace nanobota. Skill je **výhradně čtecí** — do tvých repů nikdy nezapisuje a klony drží
jen jako pracovní kopii. Index je derived artifact: kdykoli se dá smazat a postavit od nuly
(u dnešního objemu jsou to desítky sekund).
## Tři vrstvy, od nejlevnější
```text
grep ripgrep přes soubory na disku žádný index, nikdy zastaralé, vidí i kód
toc katalog `files` z databáze adresář → soubor → titulek + tagy
search FTS5 (BM25) + vec0 (KNN) → RRF parafráze, když neznáš slova
```
Není to fallback řetěz, kde se jde dolů, když horní vrstva zklame — je to volba podle **tvaru
otázky**: přesný string, hostname nebo identifikátor patří do `grep`, orientace („co o tom
vůbec mám?") do `toc`, parafráze do `search`.
`grep` je tedy základ, ne poslední záchrana. Soubory na disku beztak leží, takže nestojí nic
navíc, a nad kódem je exact match to hlavní — proto se zdrojáky vůbec neindexují (viz níž).
## Index nikdy nevzniká v tahu agenta
Indexuje cron každou minutu. Agent v tahu jen **čte** hotový index.
Proč takhle: `exec` tool má timeout 60 s a plný index je minuty. Indexace v odpovědi by
navíc při nedostupné Ollamě dělala z čerstvého dokumentu FTS-only výsledek.
Drtivá většina tiků neudělá nic a **nezapíše ani řádek**. Když je `log/wiki_sync_cron.log`
prázdný, je to správný stav, ne že cron neběží.
## Co se v tom tiku vlastně děje
1. Vezme lock `wiki/.sync.lock`. Když ho drží živý běh, skončí bez výpisu. Lock po mrtvém
procesu (`pid` neexistuje nebo je starší než 30 min) si vezme zpátky a zaloguje
`WARN stale lock, reclaiming`.
2. **Levná detekce změn, bez indexace.** U git zdrojů `git ls-remote` — zjistí remote `HEAD`
**bez** `fetch`, což je na minutovou kadenci ten správný nástroj; `fetch` teprve když se
revize liší. U workspace zdroje walk a porovnání `path` + `size` + `mtime`; sha256 se
počítá jen při neshodě.
3. Nic se nezměnilo a nic nečeká na vektor → konec.
4. Změněné soubory se rozřežou na chunky a pošlou do Ollamy po 32. Pak se **doberou všechny
chunky bez vektoru**, i když se žádný soubor nezměnil — tohle je cesta zpátky
z degradovaného režimu, když byla Ollama chvíli mimo.
5. Zapíše `indexed_rev` a `last_sync_at`, přidá coverage řádek a shrnutí do
`log/wiki_sync.log`.
Selhání sítě je **per zdroj**: timeout nebo chyba gitu zaloguje `WARN`, ten zdroj přeskočí
s nezměněnou `indexed_rev` a ostatní dojedou. Bez timeoutů by visící `ls-remote` držel lock
a zablokoval i workspace zdroj, který se sítí nemá nic společného.
## Proč hybrid
Každá polovina umí něco jiného a ani jedna neumí obojí:
| | Umí | Neumí |
|---|---|---|
| **BM25** (FTS5) | exact match, čísla, jména, zkratky | parafrázi, kde se slova nepřekrývají |
| **vektory** (`vec0`) | „elektroodpad ze stavebnic" → „Mindstorms po ukončení podpory" | přesné řetězce a čísla |
Výsledky slučuje **RRF** — sečte převrácené ranky z obou seznamů. Aby to mělo definovaný
význam, musí obě poloviny řadit **tutéž množinu**: proto je jedinou retrieval jednotkou chunk,
ne soubor. Kdyby BM25 řadil soubory a vektory chunky, merge by nešlo interpretovat.
Naměřeno na reálných poznámkách: RRF dává MRR 0,367 proti 0,257 (BM25 sám) a 0,261 (vektory
samy), takže merge opravdu vyhrává. Plná čísla, včetně srovnání čtyř modelů, jsou
v `.claude/tracking/plans/final-wiki-hybrid-rag-result.md` v trackovacím repu.
Ve výstupu `search` u každého hitu stojí, která polovina ho našla (`bm25 #2, vec #1`). Hit,
na kterém se poloviny **shodnou**, má výrazně větší cenu než ten, který vytáhly jen vektory.
## Česká flexe — na co narazíš
Dotazová vrstva lepí `*` na slova od tří znaků, aby pokryla české koncovky. **Funguje to jen
napůl** a je dobré vědět jak: wildcard je **prefixový**, takže pomůže jen tehdy, když je tvoje
slovo prefixem tvaru v dokumentu. Česká flexe ale mění koncovku, ne začátek.
Naměřeno nad `travel/packaging-list.md`:
| Dotaz | Chunků v tom souboru |
|---|---|
| `cestu*` | **0** |
| `cesty*` | 3 |
| `cest*` | 4 |
Prakticky: na *„co si vzít na cestu do zahraničí"* se doslovný seznam věcí na cestu nedostal
ani do top 10. Když víš, že něco máš, a `search` to nenajde, zkus **kmen slova** (`cest`,
`záloh`) nebo rovnou `grep`. Rozhodnutí, jestli to řešit systémově, je otevřená položka
v `todo.md` trackovacího repa — každá varianta rozšiřuje kandidátní pool, takže to není
zdarma.
## Index je nápověda, ne odpověď
Dvě věci, které se snadno přehlédnou:
- **Výřez v `search` není zdroj pravdy.** Je zkrácený a index může být o minutu starší než
disk. Než se z nálezu odpovídá, má se ten soubor přečíst čerstvý — `SKILL.md` to agentovi
říká, ale platí to i pro tebe.
- **Vektorová polovina vždycky něco vrátí.** KNN nemá dolní mez podobnosti, takže na jakýkoli
dotaz nabídne svých k nejbližších chunků. Pro retrieval je to záměr (posoudit relevanci je
na čtenáři), ale znamená to, že prázdný výsledek nikdy neznamená „nic nesedí" — znamená
prázdný index.
## Co se indexuje a co ne
Rozsah řídí `wiki/config.yaml` ve třech krocích: `paths` (whitelist — co v něm není, pro index
neexistuje) → `include` (whitelist přípon, `*.md`) → `exclude` (skalpel, vyhrává nad oběma).
Proč whitelist a ne blacklist: rozhoduje **směr selhání**. U blacklistu nový adresář *tiše
vstoupí* do indexu, u whitelistu *tiše chybí*. Index je komprimovaná kopie obsahu včetně
osobních věcí, takže „tiše zaindexováno" je horší porucha — a workspace se mění i bez tebe,
Dream do něj zapisuje sám.
Proti té jediné slabině whitelistu stojí záchranná síť: sync na konci zaloguje řádek
`coverage <zdroj>: markdown outside paths in …` s top-level adresáři, které markdown mají
a žádný zdroj je nepokrývá. Adresáře, které tam vidíš pořád (`backup`, `tmp`, `skills`, …),
jsou vědomě vynechané; zajímavý je **nový** přírůstek v tom seznamu.
**Indexuje se výhradně markdown**, a to bez jakékoli detekce typu obsahu — `main.py` se
netrefí do `include: ["*.md"]` a k chunkeru se nedostane. Důvod je měřený: markdown parser
dělá z Python komentáře `# TODO: …` nadpis a `unicode61` neumí identifikátory (`send` uvnitř
`SendAsync` je miss). Kód pokrývá `grep` — jede přes celý klon včetně `.txt` a `.mhtml`.
## Kde co leží
Relativně ke workspace (`~/.nanobot/workspace`):
| Cesta | Role |
|---|---|
| `wiki/config.yaml` | zdroje a model — **jediný verzovaný soubor** pod `wiki/` |
| `wiki/index.sqlite` | `chunks` + FTS5 + vektory; derived, gitignorovaný |
| `wiki/remote/<id>/` | non-bare klony git zdrojů (working tree, aby měl `grep` co číst) |
| `wiki/.sync.lock` | JSON `{pid, started_at}`, jen když sync běží |
| `log/wiki_sync.log` | jeden řádek na událost — co se naindexovalo, `WARN`, coverage |
| `log/wiki_sync_cron.log` | stdout cronu; **prázdný je správně** |
| `skills/wiki/scripts/` | `wiki_sync.py` (indexace) a `wiki_search.py` (dotazy) |
Klon je záměrně non-bare: `--mirror` nemá working tree, takže by `grep` neměl co číst.
Cena je dvojnásobek místa na disku, což je na těchhle repech nic.
## Ruční spuštění
```bash
cd ~/.nanobot/workspace
# to, co dělá cron
~/.local/bin/uv run skills/wiki/scripts/wiki_sync.py
# jen jeden zdroj
~/.local/bin/uv run skills/wiki/scripts/wiki_sync.py --source travel
# smazat index a postavit od nuly
~/.local/bin/uv run skills/wiki/scripts/wiki_sync.py --full
# dotazy
~/.local/bin/uv run skills/wiki/scripts/wiki_search.py search "zálohování" --limit 5
~/.local/bin/uv run skills/wiki/scripts/wiki_search.py toc --source travel
~/.local/bin/uv run skills/wiki/scripts/wiki_search.py toc --tag caj
~/.local/bin/uv run skills/wiki/scripts/wiki_search.py grep "WireGuard" --source index
```
`--full` potřebuješ jen po změně embedding modelu, jeho dimenzí, `query_prefix` nebo chunkeru
— tedy přesně tehdy, když dotaz začne hlásit `reindex needed`. Přeembeduje všechno, takže to
nepouštěj v tahu, kde na to někdo čeká.
Plná cesta k `uv` je tu proto, že v neinteraktivním SSH není v `PATH`. Crontab si `PATH`
nastavuje sám, takže tam stačí `uv run …`.
## Když něco nefunguje
| Hláška | Co to je |
|---|---|
| `note: embeddings unavailable … FTS-only results` | Ollama na `nvidia.hell` neodpovídá. Sync mezitím indexuje dál a vektory dosadí sám, až se vrátí. |
| `note: N chunks still awaiting vectors` | Sync je rozjezdu nebo byl degradovaný. Sémantická polovina je zatím neúplná. |
| `reindex needed: …` (exit 1) | Index byl postavený pod jiným embedding kontraktem než má config. Poznámky jsou v pořádku, řeší to `--full`. |
| `WARN <zdroj>: git … failed` | Ten zdroj se přeskočil, ostatní dojely. `indexed_rev` zůstala, takže příští tik to zkusí znovu. |
| `(no matches)` | Prázdný index — ne „nic nesedí" (viz výš). |
Všechno ostatní hledej v `log/wiki_sync.log`. Když je prázdný i po změně poznámek, problém je
v cronu, ne v hledání.
## Jak ověřit, že to funguje
**Smoke loop v konzoli** — projde celou smyčku od zápisu po nalezení:
```bash
cd ~/.nanobot/workspace
printf '# Smoke\n\nZrzavý jednorožec kontroluje wiki index.\n' > notes/_smoke.md
sleep 90
~/.local/bin/uv run skills/wiki/scripts/wiki_search.py search "zrzavý jednorožec" --limit 3
rm notes/_smoke.md # cron ho z indexu odklidí sám do minuty
```
**Dotaz do WebUI na každý zdroj.** Princip: vyber fakt, který model **nemůže** znát
z obecných znalostí — pak je konkrétní správná odpověď sama důkazem, že přišla z poznámek.
Když odpoví obecně a čísla vynechá nebo si je vymyslí, neprošel.
Příklady platné k 2026-09-09 (poznámky se mění, princip ne):
| Zdroj | Dotaz | Musí padnout |
|---|---|---|
| `workspace` | Kolik vody a čaje mám v receptu na Teh Tarik? | 700800 ml, dvě polévkové lžíce čaje |
| `travel` | Co si mám podle poznámek vzít na Sněžku? | rozdvojka do zásuvky, power banka, Sony ANC sluchátka |
| `index` | Co je projekt Sauter Bus? | RS485 proxy s odposlechem a modifikací provozu, micro:bit |
**Negativní kontrola** je nejcennější z celé sady: zeptej se na téma, které v poznámkách
prokazatelně není (např. pěstování bonsají). Správná odpověď je „nic o tom nemáš". Když
začne vyprávět, doplňuje si výsledky z obecných znalostí — a to je zrádnější vada než
nefunkční index, protože se tváří jako odpověď z poznámek.
Když si nejsi jistý, odkud odpověď je, dopiš **„ze kterého souboru to je?"**. Má přijít
konkrétní cesta, ne „z tvých poznámek".