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

11 KiB
Raw Blame History

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ší

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í

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í:

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".