# ZP Agent Backend pre indexovanie, hybridné vyhľadávanie a RAG nad repozitárom záverečných prác `zpwiki`. Projekt načítava Markdown dokumenty, spracuje YAML metadata, rozdelí obsah na tokenové chunky a vytvorí SQLite index kombinujúci FTS5 fulltextové vyhľadávanie a embeddingy. Nad hybridným vyhľadávaním je vytvorená RAG vrstva, ktorá pripravuje kontext, zdroje a pravidlá pre odpoveď jazykového modelu. API je integrované so školským OpenWebUI ako OpenAPI Tool Server. Súčasná hlavná retrieval vetva používa klasický hybridný RAG: FTS5/BM25 + embeddingy + RRF. GraphRAG zatiaľ nie je implementovaný; je preň pripravený samostatný evaluačný dataset a bude sa dopĺňať ako ďalšia experimentálna vetva. ## Implementované - načítanie Markdown súborov a YAML front matter, - normalizácia názvov, autorov, tagov, kategórií a `published`, - tokenové chunkovanie pomocou `tiktoken`, - konfigurovateľná maximálna veľkosť chunku, overlap a minimálna veľkosť chunku, - zachovanie názvu dokumentu a hierarchie nadpisov v chunku, - SQLite databáza a FTS5 fulltextový index, - BM25 vyhľadávanie s podporou diakritiky a prefixových výrazov, - vyhľadávacie stratégie `all_terms`, `prefix_terms` a `any_term`, - embeddingy pre každý chunk pomocou modelu `intfloat/multilingual-e5-small`, - uloženie embeddingov priamo v SQLite, - vektorové vyhľadávanie pomocou cosine similarity, - hybridné vyhľadávanie FTS5 + embeddings pomocou RRF, - nižšia váha pre slabú `any_term` FTS stratégiu, - lexikálne zvýhodnenie presných alebo veľmi silných zhôd, - obmedzenie počtu výsledkov z jedného dokumentu pomocou `max_per_document`, - filtrovanie publikovaných dokumentov, - generovanie `source_url` pre dohľadateľnosť výsledkov, - RAG vrstva s pripraveným kontextom, zdrojmi a pravidlami pre grounded odpoveď, - rozšírenie RAG kontextu o začiatok rovnakej sekcie (`section-lead expansion`) bez globálneho zvýšenia `max_per_document`, - pravidlá proti používaniu neoverených informácií a zamieňaniu rôznych typov údajov, - no-answer správanie pri chýbajúcej alebo nedostatočne podloženej informácii, - FastAPI endpointy `/health`, `/rag`, `/search`, `/sync` a `/webhook/gitea`, - striktná validácia API vstupov, - autorizácia vyhľadávania pomocou `X-API-Key` alebo Bearer tokenu, - autorizácia `/sync` pomocou samostatného API kľúča, - CORS konfigurácia pre OpenWebUI, - bezpečnostné HTTP hlavičky, - integrácia s OpenWebUI cez OpenAPI Tool Server, - Gitea webhook s HMAC-SHA256 podpisom, limitom veľkosti payloadu a kontrolou udalosti a repozitára, - zámok proti súbežnému reindexovaniu, - atomická výmena databázy po úspešnom reindexovaní, - persistentná Hugging Face cache v Docker volume, - warm-up embeddingového modelu, - retrieval evaluácia pre FTS, embeddings a hybridný retrieval, - answer-level RAG evaluácia cez OpenWebUI model a RAG tool, - oddelené answer-level overrides bez úpravy frozen retrieval benchmarku, - príprava rozšírených datasetov pre extended RAG, GraphRAG, robustness a performance experimenty. ## Architektúra ```text zpwiki ↓ Markdown + YAML ↓ scan_zpwiki.py ↓ normalizované dokumenty ↓ build_chunks.py ↓ tokenové chunky ↓ build_sqlite_index.py ↓ SQLite ├── dokumenty ├── metadata ├── chunky ├── FTS5 └── embeddingy ↓ search_core.py / search_utils.py ↓ hybridné vyhľadávanie ├── FTS5 / BM25 ├── embeddingové vyhľadávanie └── RRF fusion + lexical anchor ↓ rag_utils.py ├── výber zdrojov ├── section-lead expansion ├── RAG kontext └── grounding pravidlá ↓ FastAPI /rag ↓ OpenWebUI / ZP Agent ↓ jazykový model ↓ odpoveď + source_url ``` Synchronizačná vetva: ```text Gitea push ↓ POST /webhook/gitea ↓ HMAC + repository + event validácia ↓ voliteľný git pull --ff-only ↓ rebuild_index.py ↓ nový SQLite index ↓ atomická výmena databázy ``` Evaluačná vetva: ```text evaluation/json_files/*.json ↓ retrieval evaluator / RAG answer evaluator ↓ runner ↓ metriky ↓ evaluation/results/ ``` `evaluation/results/` nie je v nižšie uvedenom stromčeku, pretože obsahuje generované výstupy experimentov. ## Štruktúra projektu Nižšie je funkčná štruktúra projektu. Testovacie súbory, generované výsledky, cache, `__pycache__`, Git interné súbory a podobné pomocné artefakty sú zámerne vynechané. ```text zp-agent/ ├── app/ │ ├── main.py │ ├── routes.py │ └── security.py │ ├── scripts/ │ ├── common.py │ ├── scan_zpwiki.py │ ├── build_chunks.py │ ├── build_sqlite_index.py │ ├── embedding_utils.py │ ├── rag_utils.py │ ├── rebuild_index.py │ ├── search_db.py │ ├── search_core.py │ └── search_utils.py │ ├── evaluation/ │ ├── evaluate_retrieval.py │ ├── retrieval_runner.py │ ├── metrics.py │ ├── evaluate_rag_answers.py │ ├── rag_runner.py │ ├── rag_metrics.py │ └── json_files/ │ ├── questions.json │ ├── questions_before_ambiguity_cleanup.json │ ├── questions_before_validation.json │ ├── rag_answer_overrides.json │ ├── questions_extended_2000.json │ ├── graphrag_questions.json │ ├── robustness_questions.json │ └── performance_queries.json │ ├── data/ │ ├── documents.json │ ├── chunks.json │ └── zp_index.sqlite │ ├── Dockerfile ├── docker-compose.yml ├── requirements.txt ├── requirements-dev.txt └── README.md ``` ### `app/` `app/main.py` - vytvára FastAPI aplikáciu, - nastavuje lifespan aplikácie, - validuje bezpečnostnú konfiguráciu pri štarte, - vykonáva warm-up embeddingovej vrstvy, - nastavuje CORS a bezpečnostné hlavičky, - pripája API routery, - ponecháva OpenAPI schému dostupnú pre OpenWebUI. Interaktívne Swagger/Redoc rozhranie môže byť v produkčnej konfigurácii vypnuté. Pre integráciu OpenWebUI je dôležitý endpoint: ```text /openapi.json ``` `app/routes.py` - definuje `/health`, - definuje verejne publikovaný RAG nástroj `/rag`, - obsahuje interné/zabezpečené `/search` a `/sync`, - obsluhuje `/webhook/gitea`, - vykonáva validáciu requestov a sanitizáciu chýb, - používa reindexovací zámok, - kontroluje webhook payload pred spracovaním. `app/security.py` - spracúva `SEARCH_API_KEY`, - spracúva `SYNC_API_KEY`, - spracúva `WEBHOOK_SECRET`, - vyžaduje dostatočne dlhé a navzájom odlišné tajomstvá, - používa bezpečné porovnávanie autentifikačných hodnôt, - validuje očakávaný Gitea repozitár a súvisiacu konfiguráciu. ### `scripts/` `common.py` Spoločné utility a konfiguračné funkcie používané indexovacou a vyhľadávacou vrstvou. `scan_zpwiki.py` - prechádza repozitár `zpwiki`, - načítava Markdown a YAML front matter, - normalizuje dokumentové metadata, - pripravuje dokumenty na ďalšie spracovanie. `build_chunks.py` - rozdeľuje dokumenty na tokenové chunky, - používa `tiktoken`, - zachováva nadpisy, kódové bloky a tabuľky, - podporuje tokenový overlap, - eviduje token count a hash obsahu chunku. `build_sqlite_index.py` - vytvára SQLite databázu, - ukladá dokumenty, chunky a metadata, - vytvára FTS5 index, - ukladá embeddingy do SQLite, - pripravuje databázu pre hybridný retrieval. `embedding_utils.py` - načítava embeddingový model, - vytvára embeddingy pre dokumenty a query, - normalizuje vektory, - poskytuje utility pre vektorové vyhľadávanie. `search_core.py` Nízkoúrovňové jadro vyhľadávania: - normalizácia query, - FTS5 vyhľadávanie, - embeddingové vyhľadávanie, - scoring, - RRF fusion, - lexical anchor, - metadata, - diverzifikácia výsledkov. `search_utils.py` Vyššia orchestration vrstva nad `search_core.py` a kompatibilné vyhľadávacie utility používané ostatnými časťami projektu. `search_db.py` CLI rozhranie na ručné vyhľadávanie nad lokálnym SQLite indexom. `rag_utils.py` - zostavuje RAG kontext, - pripravuje zdrojové metadata, - pridáva `source_url`, - aplikuje grounding pravidlá, - podporuje no-answer správanie, - rozširuje primárny chunk o začiatok rovnakej sekcie, - zachováva väzbu medzi primárnym chunkom a section-lead chunkom. `rebuild_index.py` Orchestruje kompletný rebuild: ```text scan → chunking → SQLite/FTS → embeddings → validácia → atomická výmena DB ``` ### `evaluation/` `evaluate_retrieval.py` CLI vstup pre retrieval benchmark. Umožňuje spúšťať DEV, TEST alebo celý dataset a používa strict dataset validáciu. `retrieval_runner.py` Spúšťa jednotlivé retrieval stratégie nad evaluačnými otázkami. `metrics.py` Počíta retrieval metriky, napríklad: ```text Hit@1 Hit@3 Hit@5 MRR Recall@5 ``` `evaluate_rag_answers.py` CLI vstup pre answer-level RAG evaluáciu. Hodnotí celý tok: ```text otázka → OpenWebUI model → ZP Agent tool → /rag → finálna odpoveď ``` Podporuje answer-level overrides, aby nebolo potrebné meniť frozen retrieval benchmark pri otázkach, ktoré sú pre generatívnu evaluáciu nejednoznačné. `rag_runner.py` Spúšťa jednotlivé answer-level prípady a komunikuje s OpenWebUI/API. `rag_metrics.py` Počíta answer-level metriky, napríklad: - prítomnosť očakávaných faktov, - správnosť `source_url`, - `should_answer`, - použitie toolu, - strict pass, - latency a tokenové údaje. ### `evaluation/json_files/` `questions.json` Hlavný frozen benchmark pre klasický retrieval a RAG. Po zmrazení benchmarku sa nemá meniť len preto, aby sa zlepšil výsledok answer-level evaluácie. `rag_answer_overrides.json` Obsahuje iba answer-level úpravy formulácie alebo očakávaní pri nejednoznačných otázkach. Retrieval benchmark tým ostáva nezmenený. `questions_before_ambiguity_cleanup.json` Historická záloha datasetu pred úpravami nejednoznačností. `questions_before_validation.json` Historická záloha datasetu pred validačnými úpravami. `questions_extended_2000.json` Rozšírený benchmark s presne 2000 otázkami. Je pripravený pre neskoršie rozsiahlejšie experimenty nad klasickým retrievalom a RAG. Obsahuje kombináciu: - faktických otázok, - parafráz, - otázok bez diakritiky, - preklepov, - krátkych query, - no-answer prípadov, - negatívnej verifikácie, - multi-document otázok, - porovnávania, - citation-oriented prípadov. Tento dataset je zatiaľ pripravený, ale nemá sa používať na priebežné ladenie frozen benchmarku. `graphrag_questions.json` Samostatný benchmark pripravený pre budúcu GraphRAG vetvu. Obsahuje 500 otázok zameraných na: - multi-hop vzťahy, - spoločné témy, - spoločné roky, - person → title → year, - person → topic → title, - person → category → author, - agregácie, - konjunktívnu disambiguáciu, - multi-document reasoning. Dataset obsahuje aj GraphRAG metadata ako: ```text graph.task graph.hop_count graph.start_entities graph.expected_entities graph.expected_relations graph.expected_paths ``` Dôležité: existencia tohto datasetu neznamená, že je GraphRAG už implementovaný. Dataset je pripravený na neskorší vývoj a porovnanie. `robustness_questions.json` Samostatný dataset s 200 otázkami pre testovanie robustnosti. Pokrýva napríklad: - chýbajúcu diakritiku, - preklepy, - nekonzistentnú kapitalizáciu a interpunkciu, - nerelevantný šum, - prompt injection, - konfliktné tvrdenia používateľa, - tlak na halucinovanie, - no-answer grounding, - ambiguity/disambiguation, - integritu citácií, - kombinovaný vstupný šum. Každá položka obsahuje aj `robustness` metadata s typom útoku, perturbáciou, očakávaným správaním a závažnosťou. `performance_queries.json` Samostatný workload s 200 query pre budúce performance a stress experimenty. Profily zahŕňajú: - baseline single-document query, - krátke query, - noisy query, - multi-document query, - no-answer query, - query s väčším retrieval limitom, - context-heavy query, - opakované hot query. Súbor samotný performance nemeria. Je to vstup pre budúci samostatný performance runner, ktorý bude volať `/rag` a merať napríklad: ```text mean latency median P50 P95 P99 requests/second error rate timeout rate cold vs warm latency ``` Stress režim bude nad rovnakým workloadom zvyšovať paralelizmus a sledovať správanie systému pri rastúcej záťaži. ## Konfigurácia V koreňovom priečinku vytvor `.env`: ```dotenv WEBHOOK_SECRET= SYNC_API_KEY= SEARCH_API_KEY=<ďalšia náhodná hodnota s minimálne 32 znakmi> EXPECTED_GITEA_REPOSITORY=KEMT/zpwiki WEBHOOK_PULL_GIT=false # Voliteľné EMBEDDING_MODEL=intfloat/multilingual-e5-small EMBEDDING_BATCH_SIZE=32 ``` Tajomstvá je možné vygenerovať príkazom: ```bash openssl rand -hex 32 ``` Súbor `.env` sa nesmie commitovať. Chunkovanie je možné konfigurovať pomocou premenných prostredia: ```dotenv CHUNK_MAX_TOKENS=450 CHUNK_OVERLAP_TOKENS=70 CHUNK_MIN_TOKENS=80 CHUNK_TOKEN_ENCODING=cl100k_base ``` ## Spustenie cez Docker ```bash docker compose up -d --build ``` Kontrola služby: ```bash curl http://127.0.0.1:8000/health ``` OpenAPI schéma: ```text http://127.0.0.1:8000/openapi.json ``` Logy: ```bash docker compose logs -f zp-agent-api ``` Zastavenie: ```bash docker compose down ``` Hugging Face cache je uložená v persistentnom Docker volume, takže sa embeddingový model pri bežnom reštarte kontajnera nemusí znova sťahovať. ## Reindexovanie Celý proces načíta dokumenty, vytvorí chunky, obnoví FTS5 index a vytvorí embedding pre každý chunk: ```bash docker compose run --rm zp-agent-api python scripts/rebuild_index.py ``` Vzniknú súbory: ```text data/documents.json data/chunks.json data/zp_index.sqlite ``` Databáza obsahuje dokumenty, chunky, FTS5 index, metadata a embeddingy. Pri automatickom reindexovaní sa používa zámok proti súbežnému spusteniu a atomická výmena databázy po úspešnom vytvorení nového indexu. ## Hybridné vyhľadávanie Vyhľadávanie kombinuje: ```text dotaz ├── FTS5 / BM25 └── embeddingové vyhľadávanie ↓ RRF fusion ↓ lexical anchor ↓ výsledky ``` FTS5 používa stratégie: ```text all_terms prefix_terms any_term ``` Presné lexikálne výsledky majú prednosť pred slabšími sémantickými zhodami. Pri slabšej `any_term` stratégii sa znižuje jej váha a väčší význam môže dostať embeddingové vyhľadávanie. Test z terminálu: ```bash docker compose run --rm zp-agent-api \ python scripts/search_db.py "rag agent" --limit 5 ``` Pred volaním API načítaj premenné z `.env`: ```bash set -a source .env set +a ``` Vyhľadávanie cez zabezpečené API: ```bash curl -X POST http://127.0.0.1:8000/search \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $SEARCH_API_KEY" \ -d '{ "query": "strojový preklad", "limit": 3, "published_only": false, "max_per_document": 1 }' ``` API používa hybridný engine: ```text hybrid_fts5_embeddings ``` `max_per_document=1` zabezpečí, že sa pri požiadavke na viac výsledkov preferujú rôzne dokumenty namiesto viacerých chunkov z rovnakého dokumentu. ## RAG Nad hybridným retrievalom je implementovaná RAG vrstva v `scripts/rag_utils.py`. Endpoint `/rag` vyhľadá relevantné dokumenty a pripraví: - textový kontext pre jazykový model, - metadata použitých zdrojov, - názov a autora dokumentu, - `source_url`, - interné retrieval informácie, - pravidlá pre grounded odpoveď. RAG pravidlá požadujú, aby model odpovedal iba podľa poskytnutých zdrojov. Zároveň rozlišujú údaje, ktoré sa môžu ľahko zameniť, napríklad autora dokumentu, osobu, o ktorej dokument pojednáva, rok začiatku štúdia a rok záverečnej práce. Ak sa relevantná informácia nenachádza priamo v primárnom chunke, RAG vrstva môže k rovnakému zdroju doplniť začiatok rovnakej sekcie. Táto section-lead expanzia umožňuje zachovať retrieval `max_per_document=1`, ale zároveň doplniť názov témy, rok alebo inú informáciu umiestnenú na začiatku sekcie. Pri nedostatočnej podpore má model odmietnuť informáciu domýšľať. Príklad: ```bash curl -X POST http://127.0.0.1:8000/rag \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $SEARCH_API_KEY" \ -d '{ "query": "V akom roku robil Ján Holp diplomovú prácu?", "limit": 5, "published_only": false, "max_per_document": 1 }' ``` ## OpenWebUI ZP Agent je pripojený do OpenWebUI ako OpenAPI Tool Server. OpenWebUI používa OpenAPI schému ZP Agent API a Bearer autentifikáciu pre vyhľadávací/RAG nástroj. Tok požiadavky: ```text používateľ ↓ OpenWebUI ↓ jazykový model ↓ ZP Agent /rag ↓ hybrid retrieval ↓ RAG kontext + zdroje ↓ jazykový model ↓ odpoveď ``` Výsledná odpoveď má byť grounded v ZP Wiki a má používať relevantný `source_url`. ## Retrieval evaluácia Frozen retrieval benchmark sa spúšťa napríklad: ```bash python evaluation/evaluate_retrieval.py \ --split dev \ --strict-dataset ``` Podporované režimy: ```text dev test all ``` Vyhodnocujú sa samostatne: ```text FTS vector hybrid ``` a metriky: ```text Hit@1 Hit@3 Hit@5 MRR Recall@5 ``` TEST split sa nemá používať na priebežné ladenie retrieval konfigurácie. ## RAG answer evaluácia Answer-level evaluácia používa: ```bash python evaluation/evaluate_rag_answers.py --split dev ``` Evaluuje kompletný model/tool/RAG tok a od retrieval benchmarku je oddelená. Ak je pôvodná otázka vhodná pre retrieval, ale nejednoznačná pre generatívnu odpoveď, jej answer-level formulácia sa upraví iba cez: ```text evaluation/json_files/rag_answer_overrides.json ``` Pôvodný frozen `questions.json` tým ostáva nezmenený. ## Manuálna synchronizácia Manuálne reindexovanie cez zabezpečený endpoint: ```bash curl -X POST http://127.0.0.1:8000/sync \ -H "Content-Type: application/json" \ -H "X-API-Key: $SYNC_API_KEY" \ -d '{"pull_git": false}' ``` ## Gitea webhook Endpoint: ```text POST /webhook/gitea ``` Webhook overuje: - HMAC-SHA256 podpis, - typ Gitea udalosti, - očakávaný repozitár, - maximálnu veľkosť payloadu, - stav reindexovacieho zámku. Očakávaný repozitár: ```dotenv EXPECTED_GITEA_REPOSITORY=KEMT/zpwiki ``` Ak je povolené: ```dotenv WEBHOOK_PULL_GIT=true ``` pred reindexovaním sa vykoná: ```bash git pull --ff-only ``` ## Bezpečnosť Vyhľadávanie podporuje API key: ```text X-API-Key: ``` aj Bearer autentifikáciu: ```text Authorization: Bearer ``` Endpoint `/sync` používa samostatný `SYNC_API_KEY` a webhook samostatný `WEBHOOK_SECRET`. Tajomstvá sa ukladajú do `.env`, ktorý nesmie byť súčasťou Git repozitára. Bezpečnostná vrstva zároveň kontroluje minimálnu dĺžku tajomstiev, odlišnosť jednotlivých secretov a používa bezpečné porovnávanie hodnôt. ## Testy Inštalácia testovacích závislostí: ```bash pip install -r requirements-dev.txt ``` Bežné automatizované testy: ```bash pytest -q ``` Kontrola syntaxe najdôležitejších upravovaných modulov: ```bash python -m py_compile \ scripts/rag_utils.py \ evaluation/evaluate_rag_answers.py \ evaluation/rag_runner.py \ evaluation/rag_metrics.py ``` Pred checkpointom je vhodné použiť aj: ```bash git diff --check git status ``` Pevný počet `pytest` výsledkov nie je v README uvádzaný, pretože sa s vývojom mení. Aktuálny stav sa má vždy potvrdiť novým spustením testov na konkrétnom commite. ## Pripravené, ale zatiaľ neaktívne experimenty Nasledujúce datasety sú pripravené, ale nemajú sa teraz používať na priebežné ladenie aktuálneho frozen benchmarku: ```text questions_extended_2000.json graphrag_questions.json robustness_questions.json performance_queries.json ``` Odporúčané poradie ich neskoršieho použitia: ```text stabilný klasický RAG ↓ extended benchmark ↓ robustness benchmark ↓ GraphRAG implementácia ↓ GraphRAG benchmark ↓ performance benchmark ↓ stress test ↓ finálne experimenty ``` ## Najbližší postup Najbližšia vývojová etapa je zámerne menšia a má uzavrieť existujúci klasický RAG pred otvorením ďalších experimentov. ### 1. Uzavretie retrieval/regresie Najprv sa má potvrdiť, že aktuálne zmeny neovplyvnili frozen retrieval baseline. Postup: ```text py_compile ↓ pytest ↓ frozen retrieval DEV strict ↓ porovnanie s uloženým baseline ↓ git diff --check ↓ retrieval lock ``` Do retrieval scoringu sa potom nemá zasahovať bez nového experimentálneho dôvodu. ### 2. Regresia section-lead RAG kontextu Treba potvrdiť, že section-lead expanzia: - opravuje prípady, kde primárny chunk chýba o názov/tému/rok, - nepridáva duplicitný chunk, - zostáva v rovnakom dokumente a sekcii, - nemení retrieval `max_per_document=1`, - nevytvára zbytočne veľký kontext. ### 3. `retry + backoff + resume` pre answer evaluator Pred spustením veľkého DEV answer benchmarku sa má evaluator doplniť tak, aby dlhý beh nebol znehodnotený jedným timeoutom alebo dočasnou chybou API. Plánované správanie: ```text request ↓ úspech ───────────────→ uložiť výsledok │ └─ timeout / 429 / 5xx ↓ retry ↓ exponential backoff ↓ retry limit ``` Resume mechanizmus má: - priebežne ukladať `.partial.json`, - pri novom spustení načítať existujúci partial výsledok, - overiť kompatibilitu modelu/datasetu/splitu, - preskočiť už úspešne dokončené otázky, - pokračovať od ďalšej otázky, - neprepisovať hotové výsledky bez explicitnej voľby, - po úspešnom dokončení vytvoriť finálny JSON/CSV výstup. Zároveň je vhodné: - rozlišovať retryable a permanentné chyby, - logovať číslo pokusu a dôvod retry, - mať konfigurovateľný maximálny počet pokusov, - mať konfigurovateľný počiatočný backoff, - používať mierny delay medzi otázkami, - validovať typy answer-level datasetových polí, - nenačítavať osobný OpenWebUI API kľúč z fallback súboru, ak má byť podľa bezpečnostnej politiky dostupný iba cez environment.