Repair source-backed question handling and verify the MCP delivery path

This commit is contained in:
Ján Pták 2026-11-09 21:19:43 +01:00
parent c2e516c1e5
commit 787cb6dfa3
19 changed files with 1374 additions and 32 deletions

View File

@ -0,0 +1,800 @@
{
"checked_at_utc": "2026-11-09T20:18:46.810794+00:00",
"state": "IMPLEMENTED_REGRESSION_VERIFIED_AWAITING_CLAUDE_UI_AND_JOINT_CLOSURE",
"scope": "Repair verification of known cases and operational components; NOT a new held-out accuracy measurement.",
"base_commit": "c2e516c1e538acf8fa5eabeebd48cdc039fb1a77",
"index_version": {
"version": "904884c1a86a45f39bbeccaec0e5e823",
"git_revision": "c3ec9f611e645a2fe14fe66d518eb0924bf9bb40",
"created_at_utc": "2026-11-08T20:49:39.997566+00:00",
"source_sha256": "201798bf276106986e7ef01b4d6357960cb495d09c9855b7df922c1df4fb35a7",
"documents_sha256": "8b7eea54f6233cdc9d16610296d32b9336ab0cbaa4b881872a088c05464960be",
"chunks_sha256": "8357d1e0641206990d26827b94f37916661595261f368e30f828bccd03171c95",
"embedding_model": "intfloat/multilingual-e5-small",
"publication_policy": "explicit-false-hidden-v1",
"embedding_fingerprint": "d066fb9a266a1a0c77c65da75dcf538cfab5c87d5c605ff26b1fd2da8a12aa92"
},
"content_version": "33c3062af9f1aee887e8950f3ec51b8f",
"checks": {
"full_pytest": {
"command": ".venv/bin/python -m pytest -q test tools -o faulthandler_timeout=45",
"passed": 1543,
"skipped": 90,
"note": "Skipped checks are not counted as passed. A separate live Neo4j scope test previously passed 5 cases."
},
"client_contract_regression": {
"passed": 104,
"note": "Exact metadata result exercised through actual packages.claude.client.query_backend, including coverage enum validation."
},
"known_live_question_regressions": {
"cases": 44,
"passed": 44,
"per_engine": 22,
"method": "Expected status, accepted delivery, mandatory anchor terms and selected forbidden substitutions. Followed by source-binding audit and AI inspection of teacher-style answers; not a fully independent semantic judge."
},
"post_contract_fix_metadata_replay": {
"cases": 14,
"passed": 14
},
"source_checkpoint_audit": {
"units": 6293,
"source_files": 72,
"whitespace_only_normalizations": 806,
"literal_errors": [],
"extraction_schema": 2,
"coverage": {
"decisions": {
"irrelevant": 463,
"uncertain": 419,
"structural": 877,
"knowledge": 4534
},
"units": 6293
}
},
"assertion_and_relation_invariants": {
"units": 6293,
"activated_relations": 2,
"errors": []
},
"returned_evidence_audit": {
"cases": 44,
"evidence_occurrences_checked": 85,
"evidence_source_files": 18,
"source_binding_errors": [],
"metadata_versions": [
"904884c1a86a45f39bbeccaec0e5e823"
],
"metadata_proofs": [
{
"person": "Ronald Zalacko",
"document_author": "Daniel Hladek",
"source_path": "pages/students/2023/ronald_zalacko/README.md",
"source_url": "https://zp.kemt.fei.tuke.sk/students/2023/ronald_zalacko",
"metadata_field": "author"
},
{
"person": "Ronald Zalacko",
"document_author": "Daniel Hladek",
"source_path": "pages/students/2023/ronald_zalacko/README.md",
"source_url": "https://zp.kemt.fei.tuke.sk/students/2023/ronald_zalacko",
"metadata_field": "author"
},
{
"person": "Yehor Piliavin",
"start_year": 2024,
"source_path": "pages/students/2024/yehor_piliavin/README.md",
"source_url": "https://zp.kemt.fei.tuke.sk/students/2024/yehor_piliavin",
"metadata_field": "literal_start_year",
"chunk_id": "pages/students/2024/yehor_piliavin/README.md::chunk-0",
"quote": "rok začiatku štúdia: 2024"
},
{
"person": "Yehor Piliavin",
"start_year": 2024,
"source_path": "pages/students/2024/yehor_piliavin/README.md",
"source_url": "https://zp.kemt.fei.tuke.sk/students/2024/yehor_piliavin",
"metadata_field": "literal_start_year",
"chunk_id": "pages/students/2024/yehor_piliavin/README.md::chunk-0",
"quote": "rok začiatku štúdia: 2024"
}
],
"checks": "Exact comparison to frozen checkpoint of owner, work, phase, time and source. Literal checkpoint text was separately checked against original Markdown. This is provenance validation, not a claim of perfect semantic completeness."
},
"runtime": {
"sqlite_integrity": "ok",
"foreign_key_errors": 0,
"counts": {
"chunks": 947,
"chunks_fts": 947
},
"metadata_versions_equal": true,
"content_graph_version_matches_checkpoint": true,
"content_graph_hash_matches_checkpoint": true,
"deployed_files_match_workspace": true,
"capabilities": {
"text_search": {
"ready": true,
"status": "ready",
"reason": null
},
"metadata_graph": {
"ready": true,
"status": "ready",
"reason": null
},
"knowledge": {
"ready": true,
"status": "ready",
"reason": null
},
"generation": {
"configured": true,
"ready": true,
"status": "ready",
"reason": null
}
}
},
"oauth": {
"passed": true,
"steps": [
"challenge and protected-resource discovery",
"S256 PKCE",
"dynamic client registration",
"owner login and consent",
"invalid password rejection",
"invalid verifier rejection",
"authorization code exchange and replay rejection",
"authenticated MCP discovery and readiness",
"refresh rotation",
"grant revocation"
],
"test_grants_revoked": true
},
"public_mcp": {
"url": "https://pressed-extra-graduation-readings.trycloudflare.com/mcp",
"tools": [
"zpwiki_rag",
"zpwiki_graphrag",
"zpwiki_health"
],
"checks": [
{
"tool": "zpwiki_health",
"status": "ok",
"expected": null,
"passed": true,
"seconds": 0.1
},
{
"tool": "zpwiki_rag",
"status": "ok",
"expected": "ok",
"passed": true,
"seconds": 0.8
},
{
"tool": "zpwiki_graphrag",
"status": "ok",
"expected": "ok",
"passed": true,
"seconds": 5.7
},
{
"tool": "zpwiki_graphrag",
"status": "no_answer",
"expected": "no_answer",
"passed": true,
"seconds": 2.8
},
{
"tool": "zpwiki_rag",
"status": "ok",
"expected": "ok",
"passed": true,
"seconds": 3.2
},
{
"tool": "zpwiki_graphrag",
"status": "ok",
"expected": "ok",
"passed": true,
"seconds": 9.9
}
],
"actual_claude_ui_verified_this_run": false
}
},
"implemented_steps": [
{
"step": "Consistent source and publication",
"verification": "Read-only local wiki revision matches the shared index; SQLite, metadata graph and content checkpoint/graph agree. No source wiki writes."
},
{
"step": "Scoped extraction",
"verification": "All 6293 units have a decision and source binding; actor, work, phase and time invariants checked. Uncertain facts remain uncertain. Activated arbitrary LLM concept relations are conservative and sparse."
},
{
"step": "Common typed question plan",
"verification": "Regression cases cover quote selectors, multiple people, style clauses, inflected/typo names, roles and unknown conditions; no arbitrary model Cypher."
},
{
"step": "Hybrid RAG",
"verification": "Content path uses independent FTS5/BM25 and E5 candidate ranks with RRF; a unit regression requires a semantic-only candidate to survive a lexical hit. Named finite scopes expand without dropping goals."
},
{
"step": "Local GraphRAG",
"verification": "Content path uses bounded Neo4j person/work/activity/evidence traversal with checkpoint validation. Metadata stays on the graph route; no model-generated Cypher."
},
{
"step": "Controlled prose and exact metadata",
"verification": "Descriptions use guarded synthesis; unsafe prose may fall back to source-backed extraction. Exact named metadata has deterministic source evidence and a tested MCP client contract."
},
{
"step": "Evaluation",
"verification": "Original consumed 96-question TEST is retained byte-for-byte. Current 44 known regressions and 14 contract replays do not replace an independent accuracy estimate."
}
],
"retrieval_routes_in_live_regression": {
"metadata_or_clarification": 18,
"bm25_e5_rrf_assertions": 12,
"bounded_local_graph": 13,
"content_checkpoint": 1
},
"synthesis_outcomes_in_live_regression": {
"not_applicable": 31,
"fallback": 7,
"accepted": 6
},
"case_results": [
{
"id": "holp_title",
"engine": "rag",
"query": "Aký je názov diplomovej práce Jána Holpa a z ktorého roku je?",
"passed": true,
"status": "ok",
"answer": "Ján Holp má diplomovú prácu s názvom **Systém získavania informácií v slovenskom jazyku** (rok práce: **2021**).\n\nZdroj: https://zp.kemt.fei.tuke.sk/students/2016/jan_holp",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2016/jan_holp"
]
},
{
"id": "quoted_project",
"engine": "rag",
"query": "Pri osobe Adrián Ondra over názov záznamu \"Bakalársky projekt 2027\" a pripoj evidovaný rok práce.",
"passed": true,
"status": "ok",
"answer": "Adrián Ondra má bakalársky projekt s názvom **Automatická tvorba myšlienkových máp** (rok práce: **2027**).\n\nZdroj: https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra"
]
},
{
"id": "holp_title",
"engine": "graphrag",
"query": "Aký je názov diplomovej práce Jána Holpa a z ktorého roku je?",
"passed": true,
"status": "ok",
"answer": "Ján Holp má diplomovú prácu s názvom **Systém získavania informácií v slovenskom jazyku** (rok práce: **2021**).\n\nZdroj: https://zp.kemt.fei.tuke.sk/students/2016/jan_holp",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2016/jan_holp"
]
},
{
"id": "quoted_project",
"engine": "graphrag",
"query": "Pri osobe Adrián Ondra over názov záznamu \"Bakalársky projekt 2027\" a pripoj evidovaný rok práce.",
"passed": true,
"status": "ok",
"answer": "Adrián Ondra má bakalársky projekt s názvom **Automatická tvorba myšlienkových máp** (rok práce: **2027**).\n\nZdroj: https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra"
]
},
{
"id": "goals",
"engine": "rag",
"query": "Aké sú ciele záznamu \"Bakalársky projekt 2027\" osoby Adrián Ondra? Zhrň všetky doložené body vlastnými slovami a nezamieňaj ich za hotové výsledky.",
"passed": true,
"status": "ok",
"answer": "**Adrián Ondra — Bakalársky projekt 2027**\n\nZadanie a ciele:\n\n- Vytvoriť vizualizáciu myšlienkovej mapy z automaticky extrahovaných entít. (navrhovaný cieľ)\n\n- Vytvoriť nástroj ktorý sa dá využiť pri vzdelávaní. (navrhovaný cieľ)\n\n- Vyhodnotiť kvalitu vytvoreného grafu. Ako veľmi je graf pravdivý vzhľadom na pôvvodný text. (navrhovaný cieľ)\n\nAdrián Ondra — Bakalársky projekt 2027: zdroj tieto body označuje ako možné/navrhované ciele, nie ako potvrdené splnené výsledky.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra"
]
},
{
"id": "goals",
"engine": "graphrag",
"query": "Aké sú ciele záznamu \"Bakalársky projekt 2027\" osoby Adrián Ondra? Zhrň všetky doložené body vlastnými slovami a nezamieňaj ich za hotové výsledky.",
"passed": true,
"status": "ok",
"answer": "**Adrián Ondra — Bakalársky projekt 2027**\n\nZadanie a ciele:\n\n- Vytvoriť vizualizáciu myšlienkovej mapy z automaticky extrahovaných entít. (navrhovaný cieľ)\n\n- Vytvoriť nástroj ktorý sa dá využiť pri vzdelávaní. (navrhovaný cieľ)\n\n- Vyhodnotiť kvalitu vytvoreného grafu. Ako veľmi je graf pravdivý vzhľadom na pôvvodný text. (navrhovaný cieľ)\n\nAdrián Ondra — Bakalársky projekt 2027: zdroj tieto body označuje ako možné/navrhované ciele, nie ako potvrdené splnené výsledky.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra"
]
},
{
"id": "discovery",
"engine": "rag",
"query": "Kto už vyskúšal hierarchickú sumarizáciu na problém s dlhým kontextom? Stačí jeden doložený príklad.",
"passed": true,
"status": "ok",
"answer": "**Simona Bobrovčanová**\n\n- Téma alebo názov doložený pri tej istej práci: Automatická sumarizácia slovenského textu\n\n- Bakalárska práca 2026; zaznamenaný výsledok; záznam 2026-02-27: Vyskúšaná hierarchická sumarizácia - text je rozdelený na časti a potom sa vykoná sumár zo sumárov. Funguje to \"dobre\".\n\nZobrazené sú príklady z obmedzeného výberu textových dôkazov; zoznam nemusí byť úplný. Výber môžeš zúžiť témou alebo obdobím.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2023/simona_bobrovcanova",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/simona_bobrovcanova"
]
},
{
"id": "discovery",
"engine": "graphrag",
"query": "Kto už vyskúšal hierarchickú sumarizáciu na problém s dlhým kontextom? Stačí jeden doložený príklad.",
"passed": true,
"status": "ok",
"answer": "**Simona Bobrovčanová**\n\n- Bakalárska práca 2026; zaznamenaný výsledok; záznam 2026-02-27: Vyskúšaná hierarchická sumarizácia - text je rozdelený na časti a potom sa vykoná sumár zo sumárov. Funguje to \"dobre\".\n\n- Bakalárska práca 2026; priebežný stav; záznam 2025-12-05: Vyzerá to tak, že problémom je dĺžka kontextu. Modely často vynechajú informácie na konci.\n\nZobrazené sú príklady z obmedzeného výberu textových dôkazov; zoznam nemusí byť úplný. Výber môžeš zúžiť témou alebo obdobím.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2023/simona_bobrovcanova\n\nPrechod grafom dosiahol limit výberu. Zúž otázku na prácu, tému alebo obdobie.",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/simona_bobrovcanova"
]
},
{
"id": "technologies",
"engine": "rag",
"query": "Aké technológie má Ronald Zalacko na frontende, backende a v databáze a čo je doložené o funkčnosti aplikácie?",
"passed": true,
"status": "ok",
"answer": "**Ronald Zalacko — Bakalárska práca 2026**\n\nZaznamenané výsledky (dátum záznamu: 2025-12-08):\n- Funkčnosť aplikácie je doložená tým, že aplikácia funguje. [1]\n\nZameranie (dátum záznamu: 2025-12-08):\n- Na fronte sa používa React s Vite JS, na backende Flask a v databáze Postgres so SQLAlchemy. [1]\n- Aplikácia využíva Websocket cez socket.io a JWT Token na autentifikáciu. [1]\n\nZdroje: [1](https://zp.kemt.fei.tuke.sk/students/2023/ronald_zalacko)\n\n[1]: https://zp.kemt.fei.tuke.sk/students/2023/ronald_zalacko",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/ronald_zalacko"
]
},
{
"id": "technologies",
"engine": "graphrag",
"query": "Aké technológie má Ronald Zalacko na frontende, backende a v databáze a čo je doložené o funkčnosti aplikácie?",
"passed": true,
"status": "ok",
"answer": "**Ronald Zalacko — Bakalárska práca 2026**\n\nZaznamenané výsledky (dátum záznamu: 2025-12-08):\n- Funkčnosť celej aplikácie je doložená tým, že aplikácia funguje. [1]\n\nZameranie (dátum záznamu: 2025-12-08):\n- Na fronte sa používa React s Vite JS, na backende Flask a v databáze Postgres so SQLAlchemy. [1]\n- Komunikácia v reálnom čase je riešená pomocou Websocket a socket.io. [1]\n- Autentifikácia je zabezpečená prostredníctvom JWT Token. [1]\n\nZdroje: [1](https://zp.kemt.fei.tuke.sk/students/2023/ronald_zalacko)\n\n[1]: https://zp.kemt.fei.tuke.sk/students/2023/ronald_zalacko",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/ronald_zalacko"
]
},
{
"id": "problem_remedy",
"engine": "graphrag",
"query": "S akým problémom pri dlhých textoch sa stretla Simona Bobrovčanová a aký prístup už vyskúšala na jeho riešenie?",
"passed": true,
"status": "ok",
"answer": "**Simona Bobrovčanová — Bakalárska práca 2026**\n\nRiešené problémy:\n\n- Vyzerá to tak, že problémom je dĺžka kontextu. Modely často vynechajú informácie na konci. (priebežný stav)\n\nMetódy a technológie:\n\n- Vyskúšaná hierarchická sumarizácia - text je rozdelený na časti a potom sa vykoná sumár zo sumárov. Funguje to \"dobre\". (zaznamenaný výsledok)\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2023/simona_bobrovcanova",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/simona_bobrovcanova"
]
},
{
"id": "problem_remedy",
"engine": "rag",
"query": "S akým problémom pri dlhých textoch sa stretla Simona Bobrovčanová a aký prístup už vyskúšala na jeho riešenie?",
"passed": true,
"status": "ok",
"answer": "**Simona Bobrovčanová — Bakalárska práca 2026**\n\nRiešené problémy:\n\n- Vyzerá to tak, že problémom je dĺžka kontextu. Modely často vynechajú informácie na konci. (priebežný stav)\n\nMetódy a technológie:\n\n- Vyskúšaná hierarchická sumarizácia - text je rozdelený na časti a potom sa vykoná sumár zo sumárov. Funguje to \"dobre\". (zaznamenaný výsledok)\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2023/simona_bobrovcanova",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/simona_bobrovcanova"
]
},
{
"id": "all_tasks",
"engine": "rag",
"query": "Aké úlohy dostal Denis Le Thanh podľa záznamu zo stretnutia 3.10.2025? Odlíš budúce úlohy od toho, čo už bolo urobené.",
"passed": true,
"status": "ok",
"answer": "**Denis Le Thanh — Bakalárska práca 2026**\n\nPlánované úlohy:\n\n- Naštudovať a vyskúšať PEFT-QLORA. (plán)\n\n- Vyskúšajte si skript pre dotrénovanie Slovak Mistral.(poslal som cez Teams) (plán)\n\n- Oboznámte sa s Huggingface TRL. (plán)\n\n- Oboznámte sa s knižnicou \"unsloth\". (plán)\n\n- Oboznámte sa s [odkaz neoverený] (plán)\n\n- Pracujte na teoretickej časti: opíšte základné pojmy, metódy a dátové množiny. Používajte google scholar a bibliografické odkazy. (plán)\n\n- Najprv pracujte s domácou GPU, ak nebude stačiť pracujte s Google Coolab, ak nebude stačiť požiadajte konzultanta. (plán)\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2023/denis_le_thanh\n\nRepozitárový odkaz zo zdroja sa nepodarilo verejne overiť, preto ho neuvádzam. Nedostupnosť ani HTTP 404 samy nerozlišujú zmazaný a súkromný repozitár.",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/denis_le_thanh"
]
},
{
"id": "all_tasks",
"engine": "graphrag",
"query": "Aké úlohy dostal Denis Le Thanh podľa záznamu zo stretnutia 3.10.2025? Odlíš budúce úlohy od toho, čo už bolo urobené.",
"passed": true,
"status": "ok",
"answer": "**Denis Le Thanh — Bakalárska práca 2026**\n\nPlánované úlohy:\n\n- Naštudovať a vyskúšať PEFT-QLORA. (plán)\n\n- Vyskúšajte si skript pre dotrénovanie Slovak Mistral.(poslal som cez Teams) (plán)\n\n- Oboznámte sa s Huggingface TRL. (plán)\n\n- Oboznámte sa s knižnicou \"unsloth\". (plán)\n\n- Oboznámte sa s [odkaz neoverený] (plán)\n\n- Pracujte na teoretickej časti: opíšte základné pojmy, metódy a dátové množiny. Používajte google scholar a bibliografické odkazy. (plán)\n\n- Najprv pracujte s domácou GPU, ak nebude stačiť pracujte s Google Coolab, ak nebude stačiť požiadajte konzultanta. (plán)\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2023/denis_le_thanh\n\nRepozitárový odkaz zo zdroja sa nepodarilo verejne overiť, preto ho neuvádzam. Nedostupnosť ani HTTP 404 samy nerozlišujú zmazaný a súkromný repozitár.",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/denis_le_thanh"
]
},
{
"id": "comparison",
"engine": "rag",
"query": "Porovnaj zadanie osoby Adrián Ondra pri \"Bakalársky projekt 2027\" so zadaním osoby Samuel Štucka pri \"Bakalársky projekt 2027\". Vysvetli rozdiel v cieľoch a uveď oba zdroje.",
"passed": true,
"status": "ok",
"answer": "| Oblasť | Adrián Ondra — Bakalársky projekt 2027 | Samuel Štucka — Bakalársky projekt 2027 |\n| --- | --- | --- |\n| Plánované úlohy | Adrián Ondra má vytvoriť vizualizáciu myšlienkovej mapy z automaticky extrahovaných entít. [1] Jeho cieľom je vyvinúť nástroj využiteľný pri vzdelávaní. [1] Plánuje vyhodnotiť kvalitu vytvoreného grafu a jeho pravdivosť vzhľadom na pôvodný text. [1] | Samuel Štucka má pripraviť čistý korpus novinových článkov s metainformáciami. [2] Korpus má uchovávať HTML aj obrázky bez reklám a rozhrania, pričom musí extrahovať údaje ako autor, zdroj, dátum vydania, sekcia, kľúčové slová, nadpis a perex. [2] Má implementovať pipeline na extrakciu a normalizáciu textu z rôznych zdrojov. [2] |\n\nAdrián Ondra — Bakalársky projekt 2027: zdroj tieto body označuje ako možné/navrhované ciele, nie ako potvrdené splnené výsledky.\n\nSamuel Štucka — Bakalársky projekt 2027: zdroj tieto body označuje ako možné/navrhované ciele, nie ako potvrdené splnené výsledky.\n\nZdroje: [1](https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra), [2](https://zp.kemt.fei.tuke.sk/students/2024/samuel_stucka)\n\n[1]: https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra\n[2]: https://zp.kemt.fei.tuke.sk/students/2024/samuel_stucka",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra",
"https://zp.kemt.fei.tuke.sk/students/2024/samuel_stucka"
]
},
{
"id": "comparison",
"engine": "graphrag",
"query": "Porovnaj zadanie osoby Adrián Ondra pri \"Bakalársky projekt 2027\" so zadaním osoby Samuel Štucka pri \"Bakalársky projekt 2027\". Vysvetli rozdiel v cieľoch a uveď oba zdroje.",
"passed": true,
"status": "ok",
"answer": "| Oblasť | Adrián Ondra — Bakalársky projekt 2027 | Samuel Štucka — Bakalársky projekt 2027 |\n| --- | --- | --- |\n| Plánované úlohy | Cieľom Adriána Ondru je vytvoriť vizualizáciu myšlienkovej mapy z automaticky extrahovaných entít. [1] Adrián Ondra plánuje vyvinúť nástroj určený na využitie pri vzdelávaní. [1] Plán Adriána Ondru zahŕňa vyhodnotenie kvality vytvoreného grafu a jeho pravdivosti vzhľadom na pôvodný text. [1] | Cieľom Samuela Štucku je pripraviť čistý korpus novinových článkov s metainformáciami. [2] Samuel Štucka plánuje, aby korpus uchovával HTML a obrázky z článkov bez reklám a rozhrania, pričom má extrahovať čo najviac metainformácií ako autor, zdroj, dátum vydania, sekcia, kľúčové slová, nadpis a perex, a zachovať odkazy na obrázky s ich titulkami a popisom. [2] Samuel Štucka má implementovať pipeline na extrakciu a normalizáciu textu z rôznych zdrojov. [2] |\n\nAdrián Ondra — Bakalársky projekt 2027: zdroj tieto body označuje ako možné/navrhované ciele, nie ako potvrdené splnené výsledky.\n\nSamuel Štucka — Bakalársky projekt 2027: zdroj tieto body označuje ako možné/navrhované ciele, nie ako potvrdené splnené výsledky.\n\nZdroje: [1](https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra), [2](https://zp.kemt.fei.tuke.sk/students/2024/samuel_stucka)\n\n[1]: https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra\n[2]: https://zp.kemt.fei.tuke.sk/students/2024/samuel_stucka",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra",
"https://zp.kemt.fei.tuke.sk/students/2024/samuel_stucka"
]
},
{
"id": "similar",
"engine": "rag",
"query": "Kto sa zaoberal obsahovo podobnou témou ako Ján Pták? Vysvetli konkrétne prepojenie a dolož obe osoby.",
"passed": true,
"status": "ok",
"answer": "**Oleh Poiasnik**\n\n- Diplomová práca 2027; podklad: Opíšte spôsoby vyhľadávania pomcou znalostných grafov.\n\n**Valerii Kutsenko**\n\n- Diplomová práca 2027; plán; záznam 27.4. (rok neuvedený): Získajte vzorový znalostný graf ktorý sa bude dať použiť pri výskume.\n\n- Diplomová práca 2027; plán; záznam 27.4. (rok neuvedený): Navrhnúť spôsob vyhľadávania s pomocou existujúceho znalostného grafu.\n\nPodklady k porovnávanej osobe:\n\n- Ján Pták; Diplomový projekt 2026: Agent pre manažment záverečných prác\n\n- Ján Pták; Diplomová práca 2027: Agent pre manažment záverečných prác\n\n- Ján Pták; Diplomová práca 2027: - Funguje vektorové a textové vyhľadávanie. Ale vyzerá to tak, že nevie synonymy.\n\n- Ján Pták; Diplomová práca 2027: - Pokračujte v GraphRAG - Naprogramujte načítanie existujúcich Markdown textov do Neo4j — vytvorte uzly pre každú záverečnú prácu, autora, tému a kľúčové slová. Vytvorte grafové relácie medzi uzlami (napr. rovnaký autor → rovnaká téma, rovnaký študijný program)\n\n- Ján Pták; Diplomová práca 2027: - Agenta rozdeľte na samostatné komponenty (nástroje - tools alebo skills). Každý nástroj dajte do osobitného adresára na GIte. Každý nástroj by sa mal z gitu dať nainštalovať do OpenWebUI.\n\n- Ján Pták; Diplomový projekt 2026: - Odovzdané nejaké zdrojové kódy na https://git.kemt.fei.tuke.sk/jp170na/dp-zp-agent - načítanie z markdown, indexovanie do SQLite a FastAPI.\n\nIde o obsahovú podobnosť podloženú uvedenými úryvkami, nie dôkaz spolupráce ani rovnakej fázy dokončenia.\n\nZobrazené sú príklady z obmedzeného výberu textových dôkazov; zoznam nemusí byť úplný. Výber môžeš zúžiť témou alebo obdobím.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/oleh_poiasnik, https://zp.kemt.fei.tuke.sk/students/2022/valerii_kutsenko, https://zp.kemt.fei.tuke.sk/students/2022/jan_ptak",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2022/oleh_poiasnik",
"https://zp.kemt.fei.tuke.sk/students/2022/valerii_kutsenko",
"https://zp.kemt.fei.tuke.sk/students/2022/jan_ptak"
]
},
{
"id": "shared_tags",
"engine": "rag",
"query": "Ktoré tagy spájajú osoby Yehor Piliavin a Kristián Sokolský? Uveď iba spoločný prienik a zdroje.",
"passed": true,
"status": "ok",
"answer": "Spoločné kľúčové slová: **alignment**, **llm**.\nDoložené pri: **Yehor Piliavin**, **Kristián Sokolský**.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2024/yehor_piliavin, https://zp.kemt.fei.tuke.sk/students/2024/kristian_sokolsky",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/yehor_piliavin",
"https://zp.kemt.fei.tuke.sk/students/2024/kristian_sokolsky"
]
},
{
"id": "shared_tags",
"engine": "graphrag",
"query": "Ktoré tagy spájajú osoby Yehor Piliavin a Kristián Sokolský? Uveď iba spoločný prienik a zdroje.",
"passed": true,
"status": "ok",
"answer": "Spoločné kľúčové slová: **LLM**, **alignment**.\nDoložené pri: **Kristián Sokolský**, **Yehor Piliavin**.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2024/kristian_sokolsky, https://zp.kemt.fei.tuke.sk/students/2024/yehor_piliavin",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/kristian_sokolsky",
"https://zp.kemt.fei.tuke.sk/students/2024/yehor_piliavin"
]
},
{
"id": "shared_years",
"engine": "rag",
"query": "V ktorých rokoch majú Ján Holp a Ondrej Megela obaja evidovanú prácu alebo projekt? Najprv uveď spoločné roky, potom príslušné práce s typom a názvom.",
"passed": true,
"status": "ok",
"answer": "Spoločný evidovaný rok je **2020**.\n\nJán Holp má v dostupných podkladoch tieto práce a projekty:\n\n1. **Diplomový projekt 2 2020** — názov v zdroji nie je uvedený.\n2. **Diplomový projekt 2020** — názov v zdroji nie je uvedený.\n\nOndrej Megela má v dostupných podkladoch tieto práce a projekty:\n\n3. **Bakalárska práca 2020** — **Neurónové jazykové modelovanie typu BERT**.\n4. **Bakalársky projekt 2020** — názov v zdroji nie je uvedený.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2016/jan_holp, https://zp.kemt.fei.tuke.sk/students/2018/ondrej_megela",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2016/jan_holp",
"https://zp.kemt.fei.tuke.sk/students/2018/ondrej_megela"
]
},
{
"id": "shared_years",
"engine": "graphrag",
"query": "V ktorých rokoch majú Ján Holp a Ondrej Megela obaja evidovanú prácu alebo projekt? Najprv uveď spoločné roky, potom príslušné práce s typom a názvom.",
"passed": true,
"status": "ok",
"answer": "Spoločný evidovaný rok je **2020**.\n\nOndrej Megela má v dostupných podkladoch tieto práce a projekty:\n\n1. **Bakalárska práca 2020** — **Neurónové jazykové modelovanie typu BERT**.\n2. **Bakalársky projekt 2020** — názov v zdroji nie je uvedený.\n\nJán Holp má v dostupných podkladoch tieto práce a projekty:\n\n3. **Diplomový projekt 2 2020** — názov v zdroji nie je uvedený.\n4. **Diplomový projekt 2020** — názov v zdroji nie je uvedený.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2018/ondrej_megela, https://zp.kemt.fei.tuke.sk/students/2016/jan_holp",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2018/ondrej_megela",
"https://zp.kemt.fei.tuke.sk/students/2016/jan_holp"
]
},
{
"id": "unknown",
"engine": "rag",
"query": "Aké práce má Xaver Neexistujúci?",
"passed": true,
"status": "no_answer",
"answer": "Meno „Xaver Neexistujúci“ sa nepodarilo spoľahlivo priradiť k osobe v dostupných podkladoch. Uveď presné celé meno.",
"source_urls": []
},
{
"id": "unknown",
"engine": "graphrag",
"query": "Aké práce má Xaver Neexistujúci?",
"passed": true,
"status": "no_answer",
"answer": "Meno „Xaver Neexistujúci“ sa nepodarilo spoľahlivo priradiť k osobe v dostupných podkladoch. Uveď presné celé meno.",
"source_urls": []
},
{
"id": "partial_name",
"engine": "rag",
"query": "Čo sa podarilo Samuelovi pri jeho práci?",
"passed": true,
"status": "ambiguous",
"answer": "Meno „Samuelovi“ nie je jednoznačné. Ktorú osobu máš na mysli? Možnosti: Samuel Širotník [pages/students/2018/samuel_sirotnik/README.md]; Samuel Horáni [pages/students/2019/samuel_horani/README.md]; Samuel Vasiľ [pages/students/2022/samuel_vasil/README.md]; Samuel Džurina [pages/students/2023/samuel_dzurina/README.md]; Samuel Štucka [pages/students/2024/samuel_stucka/README.md]. Uveď celé meno.",
"source_urls": []
},
{
"id": "partial_name",
"engine": "graphrag",
"query": "Čo sa podarilo Samuelovi pri jeho práci?",
"passed": true,
"status": "ambiguous",
"answer": "Meno „Samuelovi“ nie je jednoznačné. Ktorú osobu máš na mysli? Možnosti: Samuel Širotník [pages/students/2018/samuel_sirotnik/README.md]; Samuel Horáni [pages/students/2019/samuel_horani/README.md]; Samuel Vasiľ [pages/students/2022/samuel_vasil/README.md]; Samuel Džurina [pages/students/2023/samuel_dzurina/README.md]; Samuel Štucka [pages/students/2024/samuel_stucka/README.md]. Uveď celé meno.",
"source_urls": []
},
{
"id": "wrong_year",
"engine": "rag",
"query": "Akú prácu mal Ján Holp v roku 1999?",
"passed": true,
"status": "no_answer",
"answer": "Ján Holp: požadované údaje sa v dostupných podkladoch nenašli.\n\nZdroj: https://zp.kemt.fei.tuke.sk/students/2016/jan_holp",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2016/jan_holp"
]
},
{
"id": "wrong_year",
"engine": "graphrag",
"query": "Akú prácu mal Ján Holp v roku 1999?",
"passed": true,
"status": "no_answer",
"answer": "Ján Holp: v dostupných podkladoch sa nenašiel záznam práce alebo projektu z roku **1999**.",
"source_urls": []
},
{
"id": "similar",
"engine": "graphrag",
"query": "Kto sa zaoberal obsahovo podobnou témou ako Ján Pták? Vysvetli konkrétne prepojenie a dolož obe osoby.",
"passed": true,
"status": "ok",
"answer": "Ján Pták a Matej Ščišľak: doložená spoločná téma — GraphRAG, Neo4j. [1] [3]\n\nJán Pták a Oleh Poiasnik: doložená spoločná téma — znalostný graf. [1] [2]\n\nJán Pták a Valerii Kutsenko: doložená spoločná téma — znalostný graf. [1] [4]\n\nJán Pták a Maksym Zatirka: doložená spoločná téma — znalostný graf. [1] [6]\n\nJán Pták a Ján Malinovský: doložená spoločná téma — znalostný graf. [1] [5]\n\n**Ján Pták — Diplomová práca 2027**\n\nZaznamenaný priebeh (dátum záznamu: 2026-09-30):\n- Ján Pták začal pracovať na systéme GraphRAG a grafovej databáze Neo4J. [1]\n\nPlánované úlohy (dátum záznamu: 2026-11-05):\n- Ján Pták plánuje oboznámiť sa s pojmom znalostný graf a napísať o ňom vlastnými slovami. [1]\n\n**Oleh Poiasnik — Diplomový projekt 2026**\n\nZaznamenané výsledky (dátum záznamu: 14.5. (rok neuvedený)):\n- Oleh Poiasnik vytvoril pomocou LightRAG znalostný graf pre 500 liekov. [2]\n\n**Matej Ščišľak — Diplomová práca 2026**\n\nZaznamenané výsledky (dátum záznamu: 2025-03-30):\n- Matej Ščišľak vytvoril systém GraphRAG zložený z FAISS a Neo4J, kde Neo4J beží lokálne a komunikuje so serverom. [3]\n\n**Valerii Kutsenko — Diplomová práca 2027**\n\nZaznamenaný priebeh (dátum záznamu: 15.5. (rok neuvedený)):\n- Valerii Kutsenko zaindexoval anglické dáta z knihy Alice in Wonderland pomocou LightRAG a zostavil z nich znalostný graf. [4]\n\n**Ján Malinovský — Diplomový projekt 2026**\n\nZadanie a ciele:\n- Ján Malinovský má za úlohu zistiť, čo je to znalostný graf. [5]\n\n**Maksym Zatirka — Bakalárska práca 2026**\n\nZadanie a ciele:\n- Maksym Zatirka má za úlohu zostaviť znalostný graf pre slovenské právo a vložiť ho do vybranej grafovej databázy. [6]\n\nPrepojenie dokladá uvedenú tému alebo činnosť; nepreukazuje spoluprácu ani rovnaký stupeň dokončenia.\n\nPrechod grafom dosiahol limit výberu. Zúž otázku na prácu, tému alebo obdobie.\n\nZdroje: [1](https://zp.kemt.fei.tuke.sk/students/2022/jan_ptak), [2](https://zp.kemt.fei.tuke.sk/students/2022/oleh_poiasnik), [3](https://zp.kemt.fei.tuke.sk/students/2021/matej_scislak), [4](https://zp.kemt.fei.tuke.sk/students/2022/valerii_kutsenko), [5](https://zp.kemt.fei.tuke.sk/students/2022/jan_malinovsky), [6](https://zp.kemt.fei.tuke.sk/students/2023/maksym_zatirka)\n\n[1]: https://zp.kemt.fei.tuke.sk/students/2022/jan_ptak\n[2]: https://zp.kemt.fei.tuke.sk/students/2022/oleh_poiasnik\n[3]: https://zp.kemt.fei.tuke.sk/students/2021/matej_scislak\n[4]: https://zp.kemt.fei.tuke.sk/students/2022/valerii_kutsenko\n[5]: https://zp.kemt.fei.tuke.sk/students/2022/jan_malinovsky\n[6]: https://zp.kemt.fei.tuke.sk/students/2023/maksym_zatirka",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2022/jan_ptak",
"https://zp.kemt.fei.tuke.sk/students/2022/oleh_poiasnik",
"https://zp.kemt.fei.tuke.sk/students/2021/matej_scislak",
"https://zp.kemt.fei.tuke.sk/students/2022/valerii_kutsenko",
"https://zp.kemt.fei.tuke.sk/students/2022/jan_malinovsky",
"https://zp.kemt.fei.tuke.sk/students/2023/maksym_zatirka"
]
},
{
"id": "supervisor",
"engine": "rag",
"query": "Kto vedie bakalársku prácu Ronalda Zalacka a kto je pri profile uvedený ako autor dokumentu? Tieto roly odlíš.",
"passed": true,
"status": "ok",
"answer": "**Ronald Zalacko — Bakalárska práca 2026**\n\nNázov / téma zo zdroja: Skórovací online systém pre robotické súťaže\n\nVedúci v hlavičke profilu: Matúš Pleva.\n\nAutor dokumentu nie je automaticky vedúci ani konzultant práce.\n\nAutor dokumentu profilu Ronald Zalacko: Daniel Hladek.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2023/ronald_zalacko",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/ronald_zalacko"
]
},
{
"id": "supervisor",
"engine": "graphrag",
"query": "Kto vedie bakalársku prácu Ronalda Zalacka a kto je pri profile uvedený ako autor dokumentu? Tieto roly odlíš.",
"passed": true,
"status": "ok",
"answer": "**Ronald Zalacko — Bakalárska práca 2026**\n\nNázov / téma zo zdroja: Skórovací online systém pre robotické súťaže\n\nVedúci v hlavičke profilu: Matúš Pleva.\n\nAutor dokumentu nie je automaticky vedúci ani konzultant práce.\n\nAutor dokumentu profilu Ronald Zalacko: Daniel Hladek.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2023/ronald_zalacko",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/ronald_zalacko"
]
},
{
"id": "typo",
"engine": "rag",
"query": "Zisti názov práce alebo projektu osoby Sebastián Kiš a evidovaný rok. Pri oprave mena povedz, koho si rozpoznal.",
"passed": true,
"status": "ok",
"answer": "Rozpoznané meno: **Sebastián Kišš** (v otázke „Sebastián Kiš“).\n\nSebastián Kišš má v dostupných podkladoch tieto práce a projekty:\n\n1. **Bakalársky projekt 2027** — **Dáta pre vyhodnotenie multimodálnych jazykových modelov**.\n\nZdroj: https://zp.kemt.fei.tuke.sk/students/2024/sebastian_kiss",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/sebastian_kiss"
]
},
{
"id": "typo",
"engine": "graphrag",
"query": "Zisti názov práce alebo projektu osoby Sebastián Kiš a evidovaný rok. Pri oprave mena povedz, koho si rozpoznal.",
"passed": true,
"status": "ok",
"answer": "Rozpoznané meno: **Sebastián Kišš** (v otázke „Sebastián Kiš“).\n\nSebastián Kišš má v dostupných podkladoch tieto práce a projekty:\n\n1. **Bakalársky projekt 2027** — **Dáta pre vyhodnotenie multimodálnych jazykových modelov**.\n\nZdroj: https://zp.kemt.fei.tuke.sk/students/2024/sebastian_kiss",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/sebastian_kiss"
]
},
{
"id": "both_projects",
"engine": "rag",
"query": "Aké metódy používal Ján Holp pri oboch diplomových projektoch v roku 2020? Uveď obe práce samostatne.",
"passed": true,
"status": "ok",
"answer": "**Ján Holp — Diplomový projekt 2 2020**\n\nZaznamenané výsledky (dátum záznamu: 2020-11-13):\n- Vyriešil technické problémy s Cassandra a umožnil indexovanie z Cassandra do Elasticsearch. [1]\n\n**Ján Holp — Diplomový projekt 2020**\n\nZaznamenané výsledky (dátum záznamu: 2020-06-23):\n- Zabezpečil funkčnosť databázy Cassandra. [1]\n- Zabezpečil funkčnosť indexovania zo súboru. [1]\n\nZdroje: [1](https://zp.kemt.fei.tuke.sk/students/2016/jan_holp)\n\n[1]: https://zp.kemt.fei.tuke.sk/students/2016/jan_holp",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2016/jan_holp"
]
},
{
"id": "record_count",
"engine": "rag",
"query": "Koľko rôznych prác a projektov má evidovaných Rostyslav Rodzhuk? Spočítaj akademické záznamy.",
"passed": true,
"status": "ok",
"answer": "Počet evidovaných prác a projektov: **2**.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2023/rostyslav_rodzhuk",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/rostyslav_rodzhuk"
]
},
{
"id": "both_projects",
"engine": "graphrag",
"query": "Aké metódy používal Ján Holp pri oboch diplomových projektoch v roku 2020? Uveď obe práce samostatne.",
"passed": true,
"status": "ok",
"answer": "**Ján Holp — Diplomový projekt 2 2020**\n\nMetódy a technológie:\n\n- Vyriešené technické problémy s cassandrou. Vieme indexovať z Cassandra do ES. (zaznamenaný výsledok)\n\n**Ján Holp — Diplomový projekt 2020**\n\nMetódy a technológie:\n\n- funguje databáza Cassandra (zaznamenaný výsledok)\n\n- funguje indexovanie zo súboru. (zaznamenaný výsledok)\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2016/jan_holp",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2016/jan_holp"
]
},
{
"id": "time_roles",
"engine": "rag",
"query": "Pri Yehorovi Piliavinovi odlíš rok začiatku štúdia, rok bakalárskeho projektu a najnovší úplný dátum stretnutia. Znamená rok projektu dátum vykonania činnosti?",
"passed": true,
"status": "ok",
"answer": "**Yehor Piliavin — Bakalársky projekt 2027**\nPosledný záznam s úplným dátumom: 2026-10-15.\n\nExplicitný rok začiatku štúdia — Yehor Piliavin: 2024.\nRok práce, dátum záznamu a dátum vykonania činnosti sú odlišné údaje.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2024/yehor_piliavin",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/yehor_piliavin"
]
},
{
"id": "record_count",
"engine": "graphrag",
"query": "Koľko rôznych prác a projektov má evidovaných Rostyslav Rodzhuk? Spočítaj akademické záznamy.",
"passed": true,
"status": "ok",
"answer": "Rostyslav Rodzhuk má prácu (rok práce: **2026**).\nRostyslav Rodzhuk má prácu (rok práce: **2025**).\nPočet evidovaných prác a projektov: 2.\n\nZdroj: https://zp.kemt.fei.tuke.sk/students/2023/rostyslav_rodzhuk",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/rostyslav_rodzhuk"
]
},
{
"id": "time_roles",
"engine": "graphrag",
"query": "Pri Yehorovi Piliavinovi odlíš rok začiatku štúdia, rok bakalárskeho projektu a najnovší úplný dátum stretnutia. Znamená rok projektu dátum vykonania činnosti?",
"passed": true,
"status": "ok",
"answer": "**Yehor Piliavin — Bakalársky projekt 2027**\nPosledný záznam s úplným dátumom: 2026-10-15.\n\nExplicitný rok začiatku štúdia — Yehor Piliavin: 2024.\nRok práce, dátum záznamu a dátum vykonania činnosti sú odlišné údaje.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2024/yehor_piliavin",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/yehor_piliavin"
]
},
{
"id": "annotation",
"engine": "graphrag",
"query": "Kto sa venoval anotácii pomenovaných entít?",
"passed": true,
"status": "ok",
"answer": "**Than Trung Thanh**\n\n- Bakalársky projekt 2020; podklad: Pomocou nástroja Prodigy anotujte dostatočné množstvo textu pre výskyt pomenovaných entít.\n\nZobrazené sú príklady z obmedzeného výberu textových dôkazov; zoznam nemusí byť úplný. Výber môžeš zúžiť témou alebo obdobím.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2017/than_trung_thanh\n\nPrechod grafom dosiahol limit výberu. Zúž otázku na prácu, tému alebo obdobie.",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2017/than_trung_thanh"
]
},
{
"id": "annotation",
"engine": "rag",
"query": "Kto sa venoval anotácii pomenovaných entít?",
"passed": true,
"status": "ok",
"answer": "**Jakub Maruniak**\n\n- Diplomový projekt 2; zaznamenaný výsledok; záznam 2020-09-25: Oboznámenie sa procesom anotácie NER\n\n**Than Trung Thanh**\n\n- Bakalársky projekt 2020; podklad: Pomocou nástroja Prodigy anotujte dostatočné množstvo textu pre výskyt pomenovaných entít.\n\nZobrazené sú príklady z obmedzeného výberu textových dôkazov; zoznam nemusí byť úplný. Výber môžeš zúžiť témou alebo obdobím.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2016/jakub_maruniak, https://zp.kemt.fei.tuke.sk/students/2017/than_trung_thanh",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2016/jakub_maruniak",
"https://zp.kemt.fei.tuke.sk/students/2017/than_trung_thanh"
]
},
{
"id": "recognition",
"engine": "rag",
"query": "Kto sa venoval rozpoznávaniu pomenovaných entít?",
"passed": true,
"status": "ok",
"answer": "**Adrián Ondra**\n\n- Bakalársky projekt 2027; zaznamenaný výsledok; záznam 2026-11-05: Vyskúšaný model SlovakBERT-NER (Agarský) na rozpoznávanie pomenovaných entít.\n\n**Martin Wencel**\n\n- Bakalársky projekt 2020; zaznamenaný výsledok: Aplikácia vie robiť pomocou spacy tokenizácia, morfologická analýza, parsing a named entity recognition pre slovenčinu a angličtinu.\n\n**Samuel Horáni**\n\n- Bakalársky projekt 2021; úloha označená ako splnená; záznam 2021-12-17: [x] Pridajte schopnosť rozlišovať pomenované entity typu LOC. Spacy model by to mal vedieť.\n\nZobrazené sú príklady z obmedzeného výberu textových dôkazov; zoznam nemusí byť úplný. Výber môžeš zúžiť témou alebo obdobím.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra, https://zp.kemt.fei.tuke.sk/students/2018/martin_wencel, https://zp.kemt.fei.tuke.sk/students/2019/samuel_horani",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra",
"https://zp.kemt.fei.tuke.sk/students/2018/martin_wencel",
"https://zp.kemt.fei.tuke.sk/students/2019/samuel_horani"
]
},
{
"id": "recognition",
"engine": "graphrag",
"query": "Kto sa venoval rozpoznávaniu pomenovaných entít?",
"passed": true,
"status": "ok",
"answer": "**Adrián Ondra**\n\n- Bakalársky projekt 2027; zaznamenaný výsledok; záznam 2026-11-05: Vyskúšaný model SlovakBERT-NER (Agarský) na rozpoznávanie pomenovaných entít.\n\n**Martin Wencel**\n\n- Bakalársky projekt 2020; zaznamenaný výsledok: Aplikácia vie robiť pomocou spacy tokenizácia, morfologická analýza, parsing a named entity recognition pre slovenčinu a angličtinu.\n\nZobrazené sú príklady z obmedzeného výberu textových dôkazov; zoznam nemusí byť úplný. Výber môžeš zúžiť témou alebo obdobím.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra, https://zp.kemt.fei.tuke.sk/students/2018/martin_wencel\n\nPrechod grafom dosiahol limit výberu. Zúž otázku na prácu, tému alebo obdobie.",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2024/adrian_ondra",
"https://zp.kemt.fei.tuke.sk/students/2018/martin_wencel"
]
},
{
"id": "chatbot",
"engine": "graphrag",
"query": "Kto vytvoril chatbota?",
"passed": true,
"status": "ok",
"answer": "**Samuel Džurina**\n\n- Bakalárska práca 2026; zaznamenaný výsledok; záznam 2025-11-27: Vytvorený modelový OpenAI chatbot. Agent používa openai api, gpt3.5 turbo a vie odpovedať na otázky. Asi má problém s vyhľadávaním na internete.\n\nZobrazené sú príklady z obmedzeného výberu textových dôkazov; zoznam nemusí byť úplný. Výber môžeš zúžiť témou alebo obdobím.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2023/samuel_dzurina\n\nPrechod grafom dosiahol limit výberu. Zúž otázku na prácu, tému alebo obdobie.",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/samuel_dzurina"
]
},
{
"id": "chatbot",
"engine": "rag",
"query": "Kto vytvoril chatbota?",
"passed": true,
"status": "ok",
"answer": "**Samuel Džurina**\n\n- Bakalárska práca 2026; zaznamenaný výsledok; záznam 2025-11-27: Vytvorený modelový OpenAI chatbot. Agent používa openai api, gpt3.5 turbo a vie odpovedať na otázky. Asi má problém s vyhľadávaním na internete.\n\n- Bakalárska práca 2026; priebežný stav; záznam 2025-12-17: Je urobená prvá verzia chatbota - vie hľadať PDF a pristupovať na internet cez DUCKDUCK. Rozhranie je cez streamlit.\n\nZobrazené sú príklady z obmedzeného výberu textových dôkazov; zoznam nemusí byť úplný. Výber môžeš zúžiť témou alebo obdobím.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2023/samuel_dzurina",
"source_urls": [
"https://zp.kemt.fei.tuke.sk/students/2023/samuel_dzurina"
]
}
],
"remaining_limits": [
"Actual new Claude-chat presentation and follow-up need the user to check; protocol success alone does not verify that UI.",
"No new claim of 94% or general accuracy. The earlier TEST is consumed and its first-run conservative score remains unchanged; its model judge had documented reliability problems.",
"Selected global discovery/similarity answers are examples, not complete lists. Graph traversal and evidence budgets are enforced with concrete notices.",
"Conservative synthesis can return literal source wording when generated prose is not fully supported.",
"Only two arbitrary LLM-extracted concept-to-concept relations are activated in this checkpoint; the graph also uses many source-backed structural work/person/evidence/concept paths.",
"72 profile sources were checked automatically against Markdown; no human-reviewed source audit is claimed.",
"Quick Tunnel URLs are temporary; old DNS disappeared and the tunnel was restored. The laptop, Docker and tunnel must stay running."
],
"frozen_test_files_unchanged": [
"test.json",
"test_manifest.json",
"test_results.jsonl",
"test_summary.json",
"review.json"
],
"runtime_file_sha256": {
"tools/answers/answer_coverage.py": "746ab3b89ba65f263b63037be55ee02ee25aaaf685dae6e95f7bd6a5536fc6fa",
"tools/answers/rag_metadata.py": "a16486932e66aed571fd25cb59c5656892613488ea90afe80c41b0c1d15afcf1",
"tools/answers/checked_generation.py": "e36def33fb83e5c973fa632685b4782fa411a7eeb7151ef8c14007f58fa1f375",
"tools/answers/answer_delivery.py": "2520ab327670e35511fb578ef1a447f0c011fb04fa3a341a36696631a2ca499e",
"tools/answers/presentation.py": "954c65288ddc3c8f873fec3ef99176adb70785806043ac1814c7a01a267b4aad",
"tools/answers/content_selection.py": "8e09f4423f2fd3b253f5779f62849b11b9570a112a472ea07392468901e3d80e",
"tools/answers/synthesis.py": "d669136a99d71b9046119debc077f2ab51336ab8ffe1bcd5e5853cfa8ba8e748",
"tools/answers/graph_context.py": "5b14150b7f6903cd92c004d56943d949167be8859acf4f97aed14a713e59faff",
"tools/answers/model_client.py": "f595eb1b594364faff21ce2a6842c8c7e40a5ca4e6a2c3085cdc4a4bae28ae04",
"tools/answers/answer_errors.py": "d1541b5c31b5d64e0bfa729f5e9a29adddc4b55bdd24508490f39cfc0a44b240",
"tools/answers/context_policy.py": "526f5de0b9112e23abd76e403799f156d0150c83f03577251af23d1685991327",
"tools/answers/verified_rag.py": "b2e9777b269f2b19c16ad9e1aa86644f09a9ad96489358a1294e51d21025611e",
"tools/answers/answer_format.py": "533cfe3a30c835d22441d877df63fe08e366d01ed6d15474fe6fcff26bde259b",
"tools/answers/evidence_selection.py": "1007a85b8838e1eb3c34ee610a4f81dfff57410a90213e02b8ccde90a0fedbd2",
"tools/answers/__init__.py": "8b8ab7f996121e5e5b1b594cf08f4c3b4f16f1b434b74276c803a3a83681d2cb",
"tools/answers/rag_preparation.py": "98468b111c9f8f6d423f3c6a07300c3ad7a7e9fef5bae1d1385b91a04771e069",
"tools/answers/rag_evidence.py": "351b99eb5757991696dbe10dcb794cf02aea7c2232c2b7eabab4e3bf1a7ad687",
"tools/answers/reference_links.py": "eb59206bcb3b79912daafcd591e7adb504ec592e61ea32e4e133cb6a65fe0b57",
"tools/answers/render_answer.py": "953cae2cdbdbf05a1b0c1f2746c4dc5f0d85c9bcfd02c105df38a3ff1d2451af",
"tools/answers/question_plan.py": "d95b6f071845f4c2c8846bccc0f131cc80cf58cb0d28804f36e5f29ccf4b7387",
"tools/answers/question_intent.py": "2d009205f0168b80a0a90b0fc86e52a605ddb367d24ecb727387f6944f16669b",
"tools/knowledge/concepts.py": "de0d005ab298c3d86020b0cb09fc695852303d8a5f411d0a3828b58131713b8d",
"tools/knowledge/sources.py": "709c2201288a920e99578fe4e58e191a742deb5f15170104820680d156280c69",
"tools/knowledge/publication.py": "a5a29af22fd73a57f53da47c687200f0ad392d48ce94a23250f00c7a53731148",
"tools/knowledge/academic.py": "0ae9a611792b042c64c6ba9c3019bb95d8b51e4320b1df20844c8e9f6f006b60",
"tools/knowledge/__init__.py": "905a19d7cf5d95379800b2ad917ad759fa376f23d2f8a244527b9dc187555019",
"tools/knowledge/evidence.py": "b0eb7d3e0f6a97083829a9940b3a64dade96d4f98435eb5ebfb43adaf10fac18",
"tools/knowledge/graph_answers.py": "fd599b7ff65485ae11e77022cc007b99e5bd3b533383cc64dc5b7aa3dabf81fa",
"tools/knowledge/store.py": "fa22cd357df8c3cde4cd1b6a4f0a9b78d12721f3970ec896584205f8a5d5d7c1",
"tools/knowledge/extraction.py": "c1819afc5072a5999c985b4547809db997ecf3e1112b031534ae34ed295bd106",
"tools/knowledge/local_graph.py": "cefdc9de3d265a37636fdc2836c59e929743bddf17a3408d04668fa31b80dc4d",
"tools/knowledge/graph_projection.py": "dac3058008fb43c833d239b6a7ced27ef5419004cc768b246741eb2e1a69b12a",
"tools/knowledge/text_retrieval.py": "bcb2ee2c4de7c4c074e1677137a2e11995b36ffa934fc03de96b59710ad89307",
"tools/knowledge/query.py": "4b3c3333a9aae229de9b3ce83f61f5aa278cf82bebe7a1b0e8518f1ccf8e76ab",
"tools/knowledge/build.py": "4666b1cd2d42533b7289f5e2ecaac11c5cc16e09f3ea4b9a2714d17cb8c2f9f6",
"tools/knowledge/planned_answers.py": "e5820cb6c8d6e15315ae94e263e2886b0309b6c7eee8e09338750adbede2aa8c",
"tools/graphrag/graph_search.py": "eff65f570d3a52a9cb4132db8f3c4b59dc08840001ae1bfbab6c7ab5eda09bb0",
"tools/retrieval/person_names.py": "6e3eb293fee2d136f3b6e082a166dbd02cde9b36ab47777eb01a11404d63c1ac",
"tools/rag/llm_context.py": "8f195ff16639de4379c41108f7a3a456d90d985307ebde3691c1406b3f76dfa5"
}
}

View File

@ -0,0 +1,240 @@
"""Previously failed user formulations are regressions, never a fresh TEST."""
import base64
import zlib
from types import SimpleNamespace
import numpy as np
import pytest
from tools.answers.question_plan import build_question_plan,QuestionPlan,PersonRequest
from tools.retrieval.person_names import resolve_person_names
from tools.knowledge.text_retrieval import retrieve_units
CATALOG=[{'kind':'Person','name':name,'source_document':f'pages/students/2024/{i}/README.md'}
for i,name in enumerate(['Adrián Ondra','Sebastián Kišš','Samuel Štucka','Samuel Džurina'])]
@pytest.mark.parametrize('suffix',[
'Zhrň všetky doložené body a nezamieňaj ich za hotové výsledky.',
'Vysvetli konkrétne prepojenie a dolož obe osoby.',
'Pri každej osobe príslušné práce uveď v bodoch.',
])
def test_presentation_clause_is_not_an_additional_unknown_identity(suffix):
r=resolve_person_names('Na čom pracoval Adrián Ondra? '+suffix,CATALOG)
assert r['status']=='ok' and [m['name'] for m in r['matches']]==['Adrián Ondra']
def test_inflected_partial_first_name_asks_for_real_candidates_without_model():
p=build_question_plan('Čo sa podarilo Samuelovi pri jeho práci?',CATALOG)
assert p.reason=='incomplete_person_name' and p.resolution['status']=='ambiguous'
assert {c['name'] for c in p.resolution['candidates']}=={'Samuel Štucka','Samuel Džurina'}
def test_one_short_surname_typo_still_requires_an_exact_given_name_anchor():
assert resolve_person_names('Akú prácu má Sebastián Kiš?',CATALOG)['matches'][0]['name']=='Sebastián Kišš'
assert resolve_person_names('Akú prácu má Sebastan Kš?',CATALOG)['status']!='ok'
@pytest.mark.parametrize('question',[
'Kto už vyskúšal hierarchickú sumarizáciu na problém s dlhým kontextom?',
'U koho je doložené vytvorenie crawlera pomocou Scrapy?',
'Ktorý študent sa venoval agentovi pre špeciálneho pedagóga? Bez domýšľania výsledkov.',
])
def test_global_discovery_does_not_require_a_known_student(question):
p=build_question_plan(question,CATALOG)
assert not p.clarification and p.spec.intent=='discovery' and not p.spec.people
def test_grounding_directive_does_not_add_a_results_facet_to_goals():
p=build_question_plan('Aké sú ciele projektu osoby Adrián Ondra? Nezamieňaj ich za hotové výsledky.',CATALOG)
assert p.spec.facets==['assignment'] and not p.clarification
def index_rows():
rows=[{'id':str(i),'person':'Eva Nová','source_path':'eva','line':i+1,
'text':'Všeobecná poznámka.' if i else 'Použitá technológia Cassandra.',
'concepts':[],'work_label':'Diplomová práca 2026','work_type':'diploma_thesis','work_year':2026}
for i in range(100)]
matrix=np.array([[0.,1.]]+[[1.,0.]]*99,dtype='<f4')
return {'version':'unit-v1','units':rows,'embedding_fingerprint':'fp','vector_shape':[100,2],
'vectors':base64.b64encode(zlib.compress(matrix.tobytes())).decode()}
def test_semantic_candidates_survive_a_successful_lexical_hit(monkeypatch):
import tools.rag.embedding_utils as embedding
monkeypatch.setattr(embedding,'model_profile',lambda _: {'fingerprint':'fp'})
monkeypatch.setattr(embedding,'embed_query',lambda _:np.array([1.,0.]))
plan=QuestionPlan(question='Cassandra',intent='discovery')
rows,scores,trace=retrieve_units(plan.question,index_rows(),plan)
assert '0' in {r['id'] for r in rows}
assert any(r['id']!='0' for r in rows) and scores['1']>scores['0']
assert trace['lexical_candidates']==1 and trace['vector_candidates']==80
def test_named_scope_expansion_preserves_every_goal_and_excludes_other_people(monkeypatch):
index=index_rows();index.pop('vectors')
index['units'].append({**index['units'][0],'id':'other','person':'Dana Iná','source_path':'dana'})
plan=QuestionPlan(question='Všetky ciele Evy',intent='facets',people=[PersonRequest(name='Eva Nová',source_document='eva')])
rows,_,trace=retrieve_units(plan.question,index,plan)
assert len(rows)==100 and all(r['person']=='Eva Nová' for r in rows)
assert trace['named_scope_expansion']
def metadata_payload(presentation='list',fields=None):
from tools.rag.rag_utils import build_context_text
source={'source_id':'S1','title':'Adrián Ondra','document_path':CATALOG[0]['source_document'],
'source_url':'https://example.test/adrian','tags':['rag','nlp'],
'text':'# Bakalársky projekt 2027\nAutomatická tvorba myšlienkových máp\nMožné ciele semestrálneho projektu:\n- Vytvoriť vizualizáciu.\n# Diplomový projekt 2029\nStav:\n- Vyskúšaný model.'}
plan=QuestionPlan(question='Zostav prehľad prác Adriána Ondru.',people=[PersonRequest(name='Adrián Ondra',source_document=source['document_path'])],
fields=fields if fields is not None else ['work_type','title','year'],presentation=presentation)
return {'sources':[source],'question_plan':plan.model_dump(),'context':build_context_text([source]),
'named_scope_complete':True,'index_version':{'version':'fixture'},'truncation_reasons':[]}
def test_exact_metadata_keeps_distinct_untitled_records_without_a_model():
from tools.answers.verified_rag import generate_verified_rag
result=generate_verified_rag('Zostav prehľad prác Adriána Ondru.',metadata_payload(),lambda _:pytest.fail('No model for exact complete records'))
assert result['status']=='ok' and len(result['claims'])==2
assert len({r['record_id'] for r in result['claims']})==2
assert 'myšlienkových máp' in result['answer'] and 'názov v zdroji nie je uvedený' in result['answer']
def test_exact_count_counts_records_not_source_windows_or_people():
from tools.answers.verified_rag import generate_verified_rag
payload=metadata_payload('count',[])
result=generate_verified_rag('Zostav prehľad prác Adriána Ondru.',payload,lambda _:pytest.fail('No model for exact count'))
assert result['count']==2 and 'prác a projektov' in result['answer']
@pytest.mark.parametrize('presentation',['list','count'])
def test_exact_metadata_is_accepted_by_the_actual_claude_client(monkeypatch,presentation):
from packages.claude import client
from tools.answers.verified_rag import generate_verified_rag
question='Zostav prehľad prác Adriána Ondru.'
result=generate_verified_rag(question,metadata_payload(presentation),lambda _:pytest.fail('No model'))
monkeypatch.setattr(client,'request_json',lambda *a,**kw:{**result,'query':question,'engine':'rag'})
delivered=client.query_backend(question,'rag',config=client.Config('http://localhost:8000','test'))
assert delivered['status']=='ok' and delivered['accepted_for_delivery']
assert delivered['answer']==result['answer']
def test_complete_scope_flag_cannot_override_context_clipping():
from tools.answers.rag_metadata import exact_metadata_answer
from tools.answers.rag_evidence import rag_evidence_context
payload=metadata_payload();payload['truncation_reasons']=['llm_context_budget']
assert exact_metadata_answer(payload,rag_evidence_context('Zostav prehľad prác Adriána Ondru.',payload)) is None
def test_document_authorship_is_not_interpreted_as_an_advisor_filter():
p=build_question_plan('Kto vedie bakalársku prácu Adriána Ondru a kto je autor dokumentu?',CATALOG)
assert p.spec.intent=='advisors' and not p.clarification
assert 'document_author' in p.spec.fields and not p.spec.conditions
def test_profile_metadata_has_its_own_time_role_and_snapshot(monkeypatch,tmp_path):
import sqlite3
from contextlib import nullcontext
import tools.sync.index_state as state
from tools.knowledge.academic import with_profile_metadata
db=tmp_path/'db.sqlite'
with sqlite3.connect(db) as conn:
conn.execute('CREATE TABLE documents(path,author,published)')
conn.execute('CREATE TABLE chunks(chunk_id,document_path,text,published)')
path=CATALOG[0]['source_document']
conn.execute('INSERT INTO documents VALUES (?,?,1)',(path,'Daniel Hladek'))
conn.execute('INSERT INTO chunks VALUES (?,?,?,1)',('c1',path,'rok začiatku štúdia: 2024'))
monkeypatch.setattr(state,'index_snapshot',lambda *a,**k:nullcontext())
monkeypatch.setattr(state,'read_version',lambda *a:{'version':'v'})
spec=QuestionPlan(question='Odlíš roly a roky.',people=[PersonRequest(name='Adrián Ondra',source_document=path)],fields=['document_author','start_year','year'])
plan=SimpleNamespace(spec=spec)
original={'status':'ok','accepted_for_delivery':True,'index_version':{'version':'v'},
'answer':'Vedúci: Matúš Pleva. Rok práce: 2027.\n\nZdroje: https://example.test/adrian','source_urls':['https://example.test/adrian']}
result=with_profile_metadata(original,plan,db)
assert 'Matúš Pleva' in result['answer'] and 'Autor dokumentu profilu Adrián Ondra: Daniel Hladek.' in result['answer']
assert 'začiatku štúdia — Adrián Ondra: 2024' in result['answer']
assert any(r.get('quote')=='rok začiatku štúdia: 2024' for r in result['metadata_evidence'])
with pytest.raises(RuntimeError,match='snapshot'):
with_profile_metadata({**original,'index_version':{'version':'other'}},plan,db)
def test_graph_work_count_is_not_the_number_of_authors():
from tools.graphrag import graph_search as gs
from tools.graphrag.tests.test_graph_search import FakeSession
from tools.graphrag.tests.test_graph_search_entity_v2 import catalog,thesis
q='Koľko prác má Alena Nová?'
bound=build_question_plan(q,catalog())
rows=[{**thesis(),'thesis_id':'a','heading':'Bakalárska práca 2024'},
{**thesis(),'thesis_id':'b','heading':'Bakalárska práca 2025','year':2025}]
session=FakeSession({gs.CATALOG_QUERY:catalog(),gs.THESIS_FACTS_QUERY:rows})
result=gs.search_graph_with_session(session,q,question_plan=bound.spec)
assert result['aggregation']['count']==2 and result['aggregation']['people']==['Alena Nová']
assert result['aggregation']['count_domain']=='academic_records'
def test_declarative_stack_is_a_method_description_without_promoting_its_phase():
from tools.answers.content_selection import facets_for
q='Aké technológie má študent na frontende, backende a čo je doložené o funkčnosti?'
assert 'methods' in facets_for('Frontend React, backend Flask, DB Postgres.','other',q,'unknown')
assert 'methods' not in facets_for('Frontend bude React, backend bude Flask.','task',q,'planned')
assert 'results' in facets_for('Aplikácia funguje.','progress',q,'result')
@pytest.mark.parametrize('extra_work,expected', [('',True),('\n# Diplomová práca 2029\nStav:\n- Funguje systém.',False)])
def test_profile_header_role_is_qualified_and_never_broadcast_to_multiple_works(extra_work,expected):
from tools.knowledge.sources import profile_units
from tools.knowledge.academic import answer_academic
path=CATALOG[0]['source_document']
units=profile_units({'title':'Adrián Ondra','path':path,'content':'Vedúci: Matúš Pleva\n# Bakalárska práca 2027\nStav:\n- Funguje aplikácia.'+extra_work},include_records=True)
bound=build_question_plan('Kto vedie bakalársku prácu Adriána Ondru?',CATALOG)
index={'index_version':{'version':'v'},'version':'content','academic_schema':1,'units':units}
result=answer_academic(bound.spec.question,index,engine='rag',mode='advisors',plan=bound)
assert ('Matúš Pleva' in result['answer']) is expected
if expected:
assert 'v hlavičke profilu' in result['answer']
assert any(r['kind']=='supervisor' and r.get('work_id') is None for r in result['evidence'])
@pytest.mark.parametrize('consultant_profile',[False,True])
def test_mentioning_a_consultant_does_not_turn_the_supervisor_into_a_consultant(consultant_profile):
consultant={'kind':'DocumentAuthor','name':'Kristián Sopkovič'}
if consultant_profile:consultant.update(kind='Person',source_document='pages/students/2020/kristian/README.md')
catalog=[{'kind':'DocumentAuthor','name':'Daniel Hladek'}, consultant]
plan=build_question_plan('Ktoré práce vedie Daniel Hladek a konzultuje Kristián Sopkovič?',catalog)
assert [(p.name,p.role) for p in plan.spec.people]==[('Daniel Hladek','supervisor'),('Kristián Sopkovič','consultant')]
def test_created_chatbot_is_not_satisfied_by_a_future_assignment():
from tools.knowledge.query import activity_supported
q='Kto vytvoril chatbota?'
assert not activity_supported(q,{'text':'Vytvorte chatbota.','phase':'assignment'})
assert activity_supported(q,{'text':'Vytvorený modelový chatbot.','phase':'result'})
def test_ner_is_not_proved_by_generic_form_slot_recognition_or_model_concepts():
from tools.knowledge.query import activity_supported
q='Kto sa venoval rozpoznávaniu pomenovaných entít?'
row={'text':'Nedarí sa rozpoznať typ priznania za psa vo Form.','phase':'progress','concepts':['NER']}
assert not activity_supported(q,row)
assert activity_supported(q,{'text':'Vyskúšaný model SlovakBERT-NER na rozpoznávanie pomenovaných entít.','phase':'result'})
def test_possible_goals_keep_the_literal_qualification_for_prose_rendering():
from tools.knowledge.sources import profile_units
from tools.knowledge.planned_answers import answer_facets
rows=profile_units({'title':'Adrián Ondra','path':CATALOG[0]['source_document'],
'content':'# Bakalársky projekt 2027\nMožné ciele semestrálneho projektu:\n- Vytvoriť vizualizáciu.'},include_records=True)
bound=build_question_plan('Aké sú ciele projektu Adriána Ondru?',CATALOG)
index={'version':'c','index_version':{'version':'v'},'units':rows}
result=answer_facets(bound,index,rows,engine='rag',limit=5)
assert 'Vytvoriť vizualizáciu' in result['answer']
assert any('možné/navrhované ciele' in note for note in result['presentation_notes'])
def test_comparing_time_roles_does_not_dump_the_meeting_agenda():
from tools.knowledge.sources import profile_units
from tools.knowledge.academic import answer_academic
path=CATALOG[0]['source_document']
rows=profile_units({'title':'Adrián Ondra','path':path,'content':'# Bakalársky projekt 2027\nStretnutie 5.11.2026\nStav:\n- Implementovaný nevyžiadaný detail.'},include_records=True)
p=build_question_plan('Odlíš rok začiatku štúdia Adriána Ondru, rok práce a najnovší dátum stretnutia.',CATALOG)
result=answer_academic(p.spec.question,{'index_version':{'version':'v'},'version':'c','academic_schema':1,'units':rows},engine='rag',mode='meeting',plan=p)
assert '2026-11-05' in result['answer'] and '2027' in result['answer']
assert 'nevyžiadaný detail' not in result['answer']

View File

@ -19,7 +19,7 @@ def generate_checked_answer(question: str, context: dict, prompt: str, complete)
aggregation = context.get("aggregation")
if aggregation is not None:
sent["aggregation"] = {
k: v for k, v in aggregation.items() if k in {"count", "people", "complete"}
k: v for k, v in aggregation.items() if k in {"count", "count_domain", "people", "complete"}
}
policy = "\nDo claims nevkladaj source_urls ani evidence_documents; source_urls patrí iba na vrchnú úroveň odpovede."
if (

View File

@ -137,13 +137,14 @@ def facets_for(text: str, role: str, question: str, phase: str | None = None) ->
) and role not in {"assignment", "task"}:
facets.append("problems")
if re.search(
r"\b(?:metod\w*|technolog\w*|nastroj\w*|pouzi\w*|vyuziv\w*|pomocou|implementovan\w*|natrenovan\w* model\w*|indexov\w*|databaz\w*|webov\w* rozhrani\w*)\b",
r"\b(?:metod\w*|technolog\w*|nastroj\w*|pouzi\w*|vyuziv\w*|pomocou|implementovan\w*|natrenovan\w* model\w*|indexov\w*|databaz\w*|webov\w* rozhrani\w*|frontend\w*|backend\w*|vyskus\w*|sumarizac\w*)\b",
n,
):
actual_use = actual_methods_requested(question)
actual_statement = not re.search(NOT_COMPLETED, n) and (
re.search(COMPLETED, n)
re.search(COMPLETED, n) or re.search(r'\bvyskus(?:any|ana|ane|al|ala|ali)\b',n)
or re.search(r"\b(?:pouziva\w*|pouzil\w*|vyuziva\w*|vyuzil\w*|vieme indexov\w*)\b", n)
or (not re.search(PLANNED,n) and re.search(r'\b(?:frontend|backend)\s+\w+',n))
)
if not actual_use or (role not in {"assignment", "task"} and actual_statement):
facets.append("methods")

View File

@ -7,10 +7,10 @@ FACET_PATTERNS = {
"assignment": r"\b(?:zadani\w*|ciel\w*)\b",
"plans": r"\b(?:planovan\w*|dalsi\w*\s+uloh\w*|uloh\w*)\b",
"problems": r"\b(?:problem\w*|tazkost\w*|prekazk\w*|chyb\w*)\b",
"methods": r"\b(?:metod\w*|technolog\w*|nastroj\w*|pouziv\w*|pouzil\w*)\b",
"methods": r"\b(?:metod\w*|technolog\w*|nastroj\w*|pouziv\w*|pouzil\w*|pristup\w*|frontend\w*|backend\w*)\b",
"experiments": r"\b(?:experiment\w*|merani\w*|testovani\w*|vyhodnocov\w*)\b",
"progress": r"\b(?:priebezn\w*\s+)?stav\w*\b",
"results": r"\b(?:vysledk\w*|dosiahn\w*)\b",
"results": r"\b(?:vysledk\w*|dosiahn\w*|funkcnost\w*)\b",
}
FACET_LABELS = {
"assignment": "Zadanie a ciele",
@ -25,6 +25,7 @@ FACET_LABELS = {
def requested_facets(question: str) -> list[str]:
text = normalized(re.sub(r'[„“"«][^„“”"»]+[“”"»]', " ", question))
text = re.sub(r'\bnezamienaj\s+(?:ich|to|ciele|zadanie|plany|ulohy)\s+za\s+(?:hotove\s+)?vysled\w*|\bbez domyslania vysled\w*',' ',text)
matches = [
(m.start(), key)
for key, pattern in FACET_PATTERNS.items()
@ -54,7 +55,8 @@ def facet_mode(question: str) -> bool:
def actual_methods_requested(question: str) -> bool:
return bool(
re.search(
r"\b(?:skutocne|pouziva\w*|pouzil\w*|vyuzival\w*|implementoval\w*)\b|"
r"\b(?:skutocne|pouziva\w*|pouzil\w*|vyuzival\w*|implementoval\w*|vyskus(?:al|ala|ali)\w*)\b|"
r"\bake technolog\w* ma\b|"
r"\b(?:pouzit\w*|aplikovan\w*|vyuzit\w*)\s+(?:metod\w*|technolog\w*|nastroj\w*)\b",
normalized(question),
)

View File

@ -126,17 +126,24 @@ def direct_spec(question, resolution):
selector=parse_work_selector(q)
mode=intent(q)
facets=requested_facets(q)
global_discovery=bool(re.search(r'\b(?:kto|ktori studenti|ktory student|u koho)\b',n)
and re.search(r'\b(?:venov\w*|zaober\w*|pracov\w*|vyskus\w*|vytvor\w*|pouz\w*|ries\w*|implement\w*)\b',n))
if mode not in {'similar','supervisor','advisors','completion','meeting','period'} and global_discovery:
mode='discovery'
# Facet questions without an academic lifecycle intent share one execution
# path, including GraphRAG's content evidence. Explicit metadata stays metadata.
if (not mode or mode=='progress' and facets) and content_question(q):mode='facets'
years=requested_years(q)
people=[]
staff_query=bool(re.search(r'\b(?:pod veden\w*|vedie|viedol|veduc\w*|skolitel\w*|konzult(?:ant|oval)\w*)\b',n))
staff_query=bool(re.search(r'\b(?:pod veden\w*|vedie|viedol|veduc\w*|skolitel\w*|konzult\w*)\b',n))
for m in resolution.get('matches',[]):
role='document_author' if not m.get('source_document') else 'student'
explicit_staff=bool(re.search(r'(?:pod vedenim|vedie|viedol|veduca|veduci|konzultant)\s+'+re.escape(normalized(m['name'])),n))
explicit_staff=bool(re.search(r'(?:pod vedenim|vedie|viedol|veduca|veduci|konzult\w*)\s+'+re.escape(normalized(m['name'])),n))
if staff_query and (not m.get('source_document') or explicit_staff):
role='consultant' if re.search(r'\bkonzult(?:ant|oval)\w*',n) else 'supervisor'
name=re.escape(normalized(m['name']))
consultant=bool(re.search(r'\bkonzult\w*(?:\s+s)?\s+'+name+r'\b',n)
or re.search(r'\b'+name+r'\s+konzult\w*\b',n))
role='consultant' if consultant else 'supervisor'
people.append(PersonRequest(name=m['name'],role=role,source_document=m.get('source_document')))
from tools.retrieval.work_selector import QUOTED,HEADING,label_key
types_text=QUOTED.sub(lambda m:m[0] if HEADING.fullmatch(label_key(m[1])) else ' ',selector.positive_text)
@ -178,7 +185,8 @@ def direct_spec(question, resolution):
if any(len(m[1])!=4 for m in re.finditer(r'\brok(?:u|och)?\s+(\d+)\b',n)):unsupported.append('invalid_year')
full_dates=re.findall(r'\d{1,2}\.\d{1,2}\.[12]\d{3}',q)
if len(full_dates)<2 and re.search(r'\b(?:kvartal\w*|minuly rok|minulom roku|v marci|v januari|v februari|v aprili|v maji|v juni|v juli|v auguste|v septembri|v oktobri|v novembri|v decembri)\b',n):unsupported.append('period_needs_dates')
if re.search(r'\bbez\b',n) and not (selector.unnumbered or selector.excluded_numbers or selector.excluded_labels):unsupported.append('exclusion')
exclusion_text=re.sub(r'\bbez\s+(?:domyslania|dohadov|spekulacii|nepodlozenych tvrdeni)(?:\s+vysledkov)?\b',' ',n)
if re.search(r'\bbez\b',exclusion_text) and not (selector.unnumbered or selector.excluded_numbers or selector.excluded_labels):unsupported.append('exclusion')
if unresolved_followup(q):unsupported.append('missing_followup_context')
if re.search(r'\bsemest\w*\b',n) and len(re.findall(r'\d{1,2}\.\d{1,2}\.[12]\d{3}',q))<2:unsupported.append('enrollment_not_established' if re.search(r'\b(?:aktual\w*|teraz|v akom|v ktorom)\b',n) else 'period_needs_dates')
code=unsupported_constraint(q)
@ -203,6 +211,7 @@ def direct_spec(question, resolution):
conditions=[]
for pattern,kind in [(r'\btag\w*\s+[^,;?]+','tag'),(r'\bklucov\w*\s+slov\w*\s+[^,;?]+','tag'),
(r'\bkategori\w*\s+[^,;?]+','category'),(r'\bautor\w* dokument\w*[^,;?]*','document_author')]:
if kind=='document_author' and not any(p.role=='document_author' for p in people):continue
conditions.extend(Condition(kind=kind,quote=m[0]) for m in re.finditer(pattern,n))
if mode and mode!='metadata' and conditions:unsupported.append('unsupported_content_join')
conditions.extend(Condition(kind='current_status',quote=m[0]) for m in re.finditer(r'\b(?:aktual\w*|teraz|dnes)\b',n))
@ -222,7 +231,7 @@ def direct_spec(question, resolution):
work.years = [year for year in work.years if year not in study_values | verification_values]
if year_verification:
conditions.append(Condition(kind='year_verification', quote=q))
fields=[field for field,pattern in [('title',r'\bnaz(?:ov|v)\w*'),('year',r'\b(?:ktoreho roku|ktorych rokoch|rok\w*)'),('work_type',r'\btyp\w*'),('keyword',r'\b(?:tag\w*|klucov\w*)'),('category',r'\bkategor\w*'),('document_author',r'\bautor\w* dokument\w*'),('supervisor',r'\b(?:vedie|veduc\w*|veden\w*)'),('consultant',r'\bkonzult(?:ant|oval)\w*'),('status',r'\b(?:ukoncil\w*|obhaj\w*|skoncil\w*)'),('date',r'\b(?:kedy|datum\w*)')] if re.search(pattern,n)]
fields=[field for field,pattern in [('title',r'\bnaz(?:ov|v)\w*'),('year',r'\b(?:ktoreho roku|ktorych rokoch|rok\w*)'),('work_type',r'\btyp\w*'),('keyword',r'\b(?:tag\w*|klucov\w*)'),('category',r'\bkategor\w*'),('document_author',r'\bautor\w* dokument\w*'),('supervisor',r'\b(?:vedie|veduc\w*|veden\w*)'),('consultant',r'\bkonzult\w*'),('status',r'\b(?:ukoncil\w*|obhaj\w*|skoncil\w*)'),('date',r'\b(?:kedy|datum\w*)')] if re.search(pattern,n)]
if study_start:
fields.append('start_year')
return QuestionPlan(fields=fields,question=q,intent=mode or 'metadata',people=people,works=works,years=years,period=period,

View File

@ -603,6 +603,7 @@ def rag_evidence_context(question: str, payload: dict) -> dict:
if project_number(question) is None
or (scope is not None and project_number(label or "") == project_number(question))
]
labels={(scope,text):label for scope,label,text in academic_windows_metadata(source['body'])}
unscoped = []
for section, text in windows:
titles = []
@ -652,7 +653,7 @@ def rag_evidence_context(question: str, payload: dict) -> dict:
if (
lead
and re.match(
r"^(?:Cie[lľe]+\s*:|Zadanie\s*:|Myšlienky\s*:|1[.)]\s)", next_line, re.I
r"^(?:(?:Možné\s+)?Cie[lľe]+\b|Zadanie\s*:|Myšlienky\s*:|1[.)]\s)", next_line, re.I
)
and not re.match(r"^(?:[-*+]\s|\d+[.)]\s|#{1,6}\s)", lead)
and not normalized(lead).startswith(
@ -676,6 +677,9 @@ def rag_evidence_context(question: str, payload: dict) -> dict:
if title and len(normalized(title).split()) >= 3:
titles.append(title)
record = {**base, "work_type": kind}
if labels.get((section,text)):
from tools.knowledge.sources import identifier
record.update(work_label=labels[(section,text)],record_id=identifier(source['document_path'],labels[(section,text)]))
if year is not None:
record["year"] = year
for title in titles or [None]:
@ -811,7 +815,8 @@ def rag_evidence_context(question: str, payload: dict) -> dict:
requirements["project_number"] = project_number(question)
requirements["exact_work_label"] = selector.labels[0] if len(selector.labels) == 1 else None
if plan:
facts=[row for row in facts if not row.get("work_type") or any((not w.work_type or w.work_type==row.get("work_type")) and (not w.years or row.get("year") in w.years) for w in plan.works if w.person in {None,row["person"]})]
facts=[row for row in facts if not row.get('work_type') or plan.matches_work(
row['person'],row.get('work_label'),row.get('work_type'),row.get('year'))]
if requested:
facts = [row for row in facts if row["person"] in requested]
requirements["available_fields"] = {

View File

@ -0,0 +1,68 @@
"""Exact metadata answers from delivered text, never from Neo4j or a gold set."""
import re
from tools.answers.context_policy import normalized,citation_check
from tools.answers.question_plan import QuestionPlan
from tools.answers.render_answer import render_graph_answer
def exact_metadata_answer(payload,context):
if not payload.get('question_plan'):return None
spec=QuestionPlan.model_validate(payload['question_plan'])
people=[p.name for p in spec.people if p.role=='student']
if spec.intent!='metadata' or not people or any(p.role!='student' for p in spec.people):return None
if set(spec.fields)&{'supervisor','consultant','status','date'}:return None
q=normalized(spec.question)
shared=bool(re.search(r'\b(?:spoloc\w*|prienik|obaja|spajaj\w*)\b',q) and len(people)>1)
keyword=shared and bool(set(spec.fields)&{'keyword','category','document_author'})
if spec.conditions and not keyword:return None
sources={s.get('title'):s for s in context['sources'] if s.get('document_path') in {p.source_document for p in spec.people}}
if not set(people)<=set(sources):return None
if not keyword and (not payload.get('named_scope_complete') or 'llm_context_budget' in payload.get('truncation_reasons',[])):
return None
facts=context['facts']
record_map={}
for row in facts:
if row.get('record_id') and row['person'] in people and spec.matches_work(
row['person'],row.get('work_label'),row.get('work_type'),row.get('year')):
old=record_map.get(row['record_id'],{})
if old.get('title') and row.get('title') and normalized(old['title'])!=normalized(row['title']):return None
record_map[row['record_id']]={**row,**{k:v for k,v in old.items() if v is not None}}
records=list(record_map.values())
claims=[];extra={};status='ok'
if keyword:
fields=set(spec.fields)&{'keyword','category','document_author'}
for field in fields:
sets=[{r[field] for r in facts if r['person']==p and r.get(field) is not None} for p in people]
for value in sorted(set.intersection(*sets)):
extra.setdefault('shared_properties',[]).append({'field':field,'value':value,'people':people})
claims.extend({'person':p,field:value} for p in people)
if not claims:status='no_answer'
elif shared and 'year' in spec.fields:
years=set.intersection(*[{r['year'] for r in records if r['person']==p and r.get('year') is not None} for p in people])
records=[r for r in records if r.get('year') in years]
extra['academic_year_comparison']={'people':people,'shared_years':sorted(years)}
if not years:status='no_answer'
if not keyword:
listing=spec.presentation in {'list','count'} or shared or 'work_type' in spec.fields and spec.scope=='across_works' or 'title' in spec.fields and any(not r.get('title') for r in records)
for row in records:
keys=['person','record_id','work_label','work_type','title','year'] if listing else ['person','work_type',*(k for k in ['title','year'] if k in spec.fields)]
claims.append({k:row[k] for k in keys if k in row})
if not claims:status='no_answer'
urls=[sources[p]['source_url'] for p in people]
out={'status':status,'claims':claims,'source_urls':list(dict.fromkeys(urls))}
rendered_context={**context,**extra,'complete':True,'truncated':False,'requested_people':people}
if spec.presentation=='count':
# Every record has an explicit heading in the fully delivered named
# profile scope. Never count top-k hits, meetings or taxonomy years.
count=len({r['record_id'] for r in records})
status='ok'
out.update(status='ok',count=count)
out['answer']='Počet evidovaných prác a projektov: **'+str(count)+'**.\n\nZdroje: '+', '.join(out['source_urls'])
else:out=render_graph_answer(out,rendered_context)
if not citation_check(out,set(urls))['citations_ok']:raise ValueError('Exact metadata citations rejected')
return {**out,'accepted_for_delivery':True,'attempts':[],'complete':True,'truncated':False,
'truncation_reasons':[],'index_version':context['index_version'],
'completeness_scope':'explicit_named_profiles',
'answer_coverage':{'status':'supported' if status=='ok' else 'not_found',
'scope':'count' if spec.presentation=='count' else 'list' if keyword or listing else 'fact',
'warnings':[],'limitations':[]}}

View File

@ -35,7 +35,9 @@ def prepare_named_content(db_file, question: str, payload: dict) -> dict:
from tools.answers.question_plan import QuestionPlan, build_question_plan
plan=QuestionPlan.model_validate(payload['question_plan']) if payload.get('question_plan') else None
selector = parse_work_selector(question)
if not content_question(question) and not selector.specific:
metadata_scope=bool(plan and plan.intent=='metadata' and any(p.role=='student' for p in plan.people)
and (set(plan.fields)&{'title','year','work_type'} or plan.presentation=='count'))
if not content_question(question) and not selector.specific and not metadata_scope:
return payload
if re.search(
r"\b(?:s tagom|s klucovym slovom|z kategorie|s autorom dokumentu)\b", normalized(question)
@ -128,10 +130,16 @@ def prepare_named_content(db_file, question: str, payload: dict) -> dict:
)
if not selector.matches(label):
continue
if metadata_scope and (scope,normalized(label)) in records:
continue
records[(scope, normalized(label))] = label
if (scope, normalized(label), text) not in seen:
seen.add((scope, normalized(label), text))
frames.append("Sekcia: " + label + "\n" + text)
# Exact metadata needs every record's source heading
# and introduction, not all subsequent meeting notes.
body='\n'.join(line for line in text.splitlines() if line.strip()) if metadata_scope else text
if metadata_scope:body='\n'.join(body.splitlines()[:6])
frames.append("Sekcia: " + label + "\n" + body)
if selector.multiple and (
any(
label not in {label_key(v) for v in records.values()}
@ -202,4 +210,5 @@ def prepare_named_content(db_file, question: str, payload: dict) -> dict:
"named_subjects": [p["title"] for p in named],
"work_scopes": scopes,
"question_plan": plan.model_dump(),
"named_scope_complete": True,
}

View File

@ -122,8 +122,9 @@ def render_graph_answer(answer: dict, context: dict) -> dict:
lines.append(fact_sentences(extra))
lines.append("")
if out.get("count") is not None:
lines.append(f'Počet vyhovujúcich osôb: {out["count"]}.')
aggregation = context.get("aggregation") or {}
label='Počet evidovaných prác a projektov' if aggregation.get('count_domain')=='academic_records' else 'Počet vyhovujúcich osôb'
lines.append(label+f': {out["count"]}.')
if aggregation.get("complete") is True and out["count"] == aggregation.get("count"):
# A full total is backed by the inspected candidate set, not
# just the one person left visible after a presentation cap.

View File

@ -808,6 +808,9 @@ def generate_verified_rag(question: str, payload: dict, complete) -> dict:
"truncation_reasons": [],
"index_version": context.get("index_version"),
}
from tools.answers.rag_metadata import exact_metadata_answer
exact=exact_metadata_answer(payload,context)
if exact is not None:return exact
if (
context["requirements"].get("facets")
and context["requirements"]["response_mode"] == "quotes"

View File

@ -1841,6 +1841,15 @@ def search_graph_with_session(
kinds={work.work_type for work in spec.works if work.work_type}
constraints['thesis_types']=sorted(k for k in kinds if k.endswith('_thesis'))
constraints['work_types']=sorted(k for k in kinds if not k.endswith('_thesis'))
constraints['mentions_thesis']=bool(constraints['thesis_types'])
constraints['mentions_work']=bool(constraints['work_types'])
constraints['mentions_count']=spec.presentation=='count'
constraints['mentions_work_listing']=bool(
spec.presentation=='list' and spec.scope=='across_works'
and not set(spec.fields)&{'keyword','category','document_author','start_year'}
or 'work_type' in spec.fields and spec.scope=='across_works')
if spec.presentation=='count':
constraints['mentions_academic_year']=True # fetch academic records, not meetings
constraints["published_only"] = published_only
# Explicit tag filters are resolved separately. Incidental names such as the
# tag 'zp' in 'ZP Wiki' must never become additional query constraints.
@ -2022,6 +2031,8 @@ def search_graph_with_session(
normalize_for_match(clean_query[: match.start()]),
)
]
from tools.retrieval.work_selector import HEADING,label_key
quoted_titles=[title for title in quoted_titles if not HEADING.fullmatch(label_key(title))]
if quoted_titles and (thesis_specific or work_specific) and not thesis_ids and not work_ids:
blocked_reasons.append("unknown_title")
constraints["unresolved_title"] = True
@ -2201,8 +2212,12 @@ def search_graph_with_session(
aggregation = None
if constraints["mentions_count"] and not blocked:
matching = sorted({str(row["person"]) for row in thesis_rows + work_rows})
record_count=bool(re.search(r'\b(?:kolko|pocet)\s+(?:roznych\s+|evidovanych\s+)?(?:prac\w*|projekt\w*)\b',normalize_for_match(clean_query)))
records={row.get('thesis_id') or row.get('work_id') or row.get('thesis_element_id') or row.get('work_element_id') for row in thesis_rows+work_rows}
records.discard(None)
aggregation = {
"count": len(matching),
"count": len(records) if record_count else len(matching),
"count_domain":'academic_records' if record_count else 'people',
"people": matching,
"candidate_count": len(person_ids),
"complete": not (entity_limit_reached or fetch_capped),

View File

@ -12,6 +12,45 @@ import re
from tools.answers.context_policy import normalized
from tools.knowledge.sources import advisor_name
def with_profile_metadata(result,plan,db_file):
"""Keep indexed document authorship and literal study start separate from work events."""
import sqlite3
from tools.sync.index_state import index_snapshot,read_version
from tools.rag.search_core import make_source_url
if result.get('status')!='ok' or not result.get('accepted_for_delivery'):return result
with index_snapshot(db_file.parent,db_file=db_file):
if read_version(db_file.parent)!=result['index_version']:raise RuntimeError('Authorship snapshot changed')
with sqlite3.connect(db_file.resolve().as_uri()+'?mode=ro',uri=True) as conn:
rows=[]
for person in plan.spec.people:
if person.role!='student' or not person.source_document:continue
row=conn.execute('SELECT author FROM documents WHERE path=? AND COALESCE(published,1)=1',
(person.source_document,)).fetchone()
if 'document_author' in plan.spec.fields and row and row[0]:rows.append({'person':person.name,'document_author':row[0],
'source_path':person.source_document,'source_url':make_source_url(person.source_document),'metadata_field':'author'})
if 'start_year' in plan.spec.fields:
starts={}
for chunk_id,text in conn.execute('SELECT chunk_id,text FROM chunks WHERE document_path=? AND COALESCE(published,1)=1',(person.source_document,)):
for line in text.splitlines():
match=re.fullmatch(r'rok zaciatku studia ([12]\d{3})',normalized(line))
if match:starts[int(match[1])]=(chunk_id,line)
if len(starts)==1:
year=next(iter(starts));chunk_id,quote=starts[year]
rows.append({'person':person.name,'start_year':year,'source_path':person.source_document,
'source_url':make_source_url(person.source_document),'metadata_field':'literal_start_year','chunk_id':chunk_id,'quote':quote})
if not rows:return result
body,separator,sources=result['answer'].rpartition('\n\nZdroje:')
if not separator:body=result['answer']
lines=[('Autor dokumentu profilu '+r['person']+': '+r['document_author']+'.') if 'document_author' in r else
('Explicitný rok začiatku štúdia — '+r['person']+': '+str(r['start_year'])+'.') for r in rows]
if 'start_year' in plan.spec.fields and 'year' in plan.spec.fields:
lines.append('Rok práce, dátum záznamu a dátum vykonania činnosti sú odlišné údaje.')
urls=list(dict.fromkeys([*result['source_urls'],*(r['source_url'] for r in rows)]))
answer=body+'\n\n'+'\n'.join(lines)+'\n\nZdroje: '+', '.join(urls)
return {**result,'answer':answer,'metadata_evidence':rows,
'source_urls':urls}
from tools.retrieval.person_names import resolve_person_names
from tools.retrieval.work_selector import parse_work_selector
@ -33,7 +72,7 @@ def academic_intent(question):
return "meeting"
if re.search(r"\bkto\b.*\b(?:vedie|viedol|viedla)\b", text):
return "advisors"
if re.search(r"\b(?:veduc\w*|skolitel\w*|konzult(?:ant|oval)\w*)\b", text):
if re.search(r"\b(?:veduc\w*|skolitel\w*|konzult\w*)\b", text):
return "advisors"
return None
@ -205,7 +244,7 @@ def answer_academic(question, index, *, engine, limit=5, units=None, mode=None,
role_filter = bool(
staff
and re.search(
r"\b(?:pod vedenim|vedie|viedol|veden\w*|veduc\w*|skolitel\w*|konzult(?:ant|oval)\w*)\b", text
r"\b(?:pod vedenim|vedie|viedol|veden\w*|veduc\w*|skolitel\w*|konzult\w*)\b", text
)
)
if role_filter:
@ -276,6 +315,13 @@ def answer_academic(question, index, *, engine, limit=5, units=None, mode=None,
reason="completion_year_scope",
)
selected_paths = {m["source_document"] for m in students}
profile_roles=defaultdict(list)
for row in rows:
if row['kind'] in {'supervisor','consultant'} and not row.get('work_id'):
profile_roles[row['source_path']].append(row)
profile_work_ids=defaultdict(set)
for row in index['units']:
if row['kind']=='work_record' and row.get('work_id'):profile_work_ids[row['source_path']].add(row['work_id'])
groups = defaultdict(list)
for r in rows:
if plan and not plan.spec.matches_work(r['person'],r.get('work_label'),r.get('work_type'),r.get('work_year')):continue
@ -301,7 +347,7 @@ def answer_academic(question, index, *, engine, limit=5, units=None, mode=None,
groups[(r["source_path"], r.get("work_label"))].append(r)
requested_role = (
"consultant"
if re.search(r"\bkonzult(?:ant|oval)\w*\b", text) and not re.search(r"\bveduc\w*|\bveden\w*", text)
if re.search(r"\bkonzult\w*\b", text) and not re.search(r"\bveduc\w*|\bveden\w*", text)
else "supervisor"
)
if role_filter:
@ -412,6 +458,9 @@ def answer_academic(question, index, *, engine, limit=5, units=None, mode=None,
else f'**{marker["person"]}** — záznam: {marker["date_label"]}; rok nie je uvedený.'
)
evidence.append(marker)
if (plan and {'start_year','date'}<=set(plan.spec.fields) and not plan.spec.facets
and not re.search(r'\bo com\b|\bco sa\b',text)):
continue # A comparison of time roles does not ask for the meeting agenda.
notes = [
r
for r in part
@ -481,7 +530,12 @@ def answer_academic(question, index, *, engine, limit=5, units=None, mode=None,
("supervisor", "Vedúci práce"),
("consultant", "Konzultant"),
]:
requested=set(plan.spec.fields)&{'supervisor','consultant'} if plan else set()
if requested and role not in requested:continue
roles = [r for r in part if advisor_name(r, role)]
if not roles and len(profile_work_ids[key[0]])==1:
roles=[r for r in profile_roles[key[0]] if advisor_name(r,role)]
if roles:name_label='Vedúci v hlavičke profilu' if role=='supervisor' else 'Konzultant v hlavičke profilu'
names = list(dict.fromkeys(advisor_name(r, role) for r in roles))
lines.append(
name_label

View File

@ -175,7 +175,7 @@ def retrieve_local_graph(question, index, bound, *, limit=5):
'supervisors':[store.identifier(normalized(p.name)) for p in spec.people if p.role=='supervisor'],
'consultants':[store.identifier(normalized(p.name)) for p in spec.people if p.role=='consultant'],
'date_field':period.field if period else None,'start':period.start if period else None,'end':period.end if period else None,
'profile_records':not any(w.work_type or w.years or w.labels or w.numbers or
'profile_records':spec.intent=='advisors' or not any(w.work_type or w.years or w.labels or w.numbers or
w.excluded_labels or w.excluded_numbers or w.unnumbered for w in spec.works),
'budget':EVIDENCE_LIMIT+1,'peers':min(61,max(11,limit*3+1))}
settings=get_neo4j_settings()

View File

@ -46,6 +46,16 @@ def answer_facets(bound, index, units, *, engine, limit):
keys=[part[i] for i in range(max((len(v) for v in by_person.values()),default=0))
for part in by_person.values() if i<len(part)]
requested=plan.facets or ['assignment','results']
# The checkpoint preserves uncertain/proposed goals as planned, not as an
# established assignment. Use the literal preceding goal heading to make
# those proposals available without changing their recorded phase.
goal_headers={}
tentative_goals=set()
for row in units:
if row.get('section_scope')=='work' and re.fullmatch(r'(?:mozne |predbezne )?ciele(?: semestralneho projektu)?',normalized(row['text'])):
goal_headers.setdefault((row['person'],row.get('work_label')),[]).append(row['line'])
if normalized(row['text']).startswith(('mozne ','predbezne ')):
tentative_goals.add((row['person'],row.get('work_label')))
lines=[];evidence=[];missing=False;truncated=len(keys)>limit;size=0;notes=[]
cells=defaultdict(list)
for key in keys[:limit]:
@ -58,8 +68,23 @@ def answer_facets(bound, index, units, *, engine, limit):
for row in groups[key]:
if row['kind'] in {'reference','work_record','meeting','supervisor','consultant','lifecycle','topic'}:continue
role={'assignment':'assignment','planned':'task','result':'progress','progress':'progress'}.get(row['phase'],'other')
if facet in facets_for(row['text'],role,plan.question,row['phase']):candidates.append(row)
candidates.sort(key=lambda r:(-unit_score(r['text'], {'result':'progress','progress':'progress','planned':'task'}.get(r['phase'],r['phase']),facet),r['line']))
proposal=(facet=='assignment' and row['phase']=='planned' and row.get('section_scope')=='work'
and any(n<row['line'] for n in goal_headers.get(key,[])))
if re.fullmatch(r'(?:predbezne )?(?:zadanie|ciele)',normalized(row['text'])):continue
if facet in facets_for(row['text'],role,plan.question,row['phase']) or proposal:candidates.append(row)
if facet=='methods' and re.search(r'\b(?:frontend\w*|backend\w*)\b',normalized(plan.question)):
candidates=[r for r in candidates if re.search(r'\b(?:frontend\w*|backend\w*)\b',normalized(r['text']))
or 'databaz' in normalized(r['text']) and not re.search(r'\[[xX]\]',r['text'])]
if facet=='results' and 'funkcnost' in normalized(plan.question):
candidates=[r for r in candidates if re.search(r'\b(?:funguje|funguju|funkcn\w*)\b',normalized(r['text']))]
from tools.knowledge.query import _terms
question_terms=_terms(plan.question)
def score(row):
text_terms=_terms(row['text'])
overlap=sum(any(a.startswith(b) or b.startswith(a) for a in text_terms) for b in question_terms)
checklist=bool(re.match(r'\s*[-*]?\s*\[[xX]\]',row['text']))
return unit_score(row['text'],{'result':'progress','progress':'progress','planned':'task'}.get(row['phase'],row['phase']),facet)+12*overlap-35*checklist
candidates.sort(key=lambda r:(-score(r),r['line']))
if facet=="assignment":candidates.sort(key=lambda r:r["line"])
distinct={}
for row in candidates:distinct.setdefault(normalized(row['text']),row)
@ -68,7 +93,7 @@ def answer_facets(bound, index, units, *, engine, limit):
if not candidates:
notes.append(person+' — '+label+': '+FACET_LABELS[facet].lower()+': v podkladoch chýbajú doložené údaje.')
lines.append('V podkladoch k tejto práci chýbajú doložené údaje.');cells[(key,facet)].append('V podkladoch chýbajú doložené údaje.');missing=True;continue
take=16 if facet=='assignment' else 2 if plan.presentation=='brief' else 5
take=2 if plan.presentation=='brief' and facet!='assignment' else len(candidates)
if len(candidates)>take:truncated=True
for row in candidates[:take]:
if size+len(row['text'])>18000 or work_size+len(row['text'])>work_budget:
@ -76,9 +101,13 @@ def answer_facets(bound, index, units, *, engine, limit):
size+=len(row['text']);work_size+=len(row['text'])
quote=re.sub(r'^\s*(?:[-*+]\s+|\d+[.)]\s+)','',row['text'])
phase={'assignment':'zadanie','planned':'plán','progress':'priebežný stav','result':'zaznamenaný výsledok'}.get(row['phase'],'podklad')
if facet=='assignment' and row['phase']=='planned':phase='navrhovaný cieľ'
lines.append('- '+quote+' ('+phase+')')
cells[(key,facet)].append(quote+' ('+phase+')')
if row['id'] not in used:evidence.append(row);used.add(row['id'])
if facet=='assignment' and candidates and key in tentative_goals:
note=person+' — '+label+': zdroj tieto body označuje ako možné/navrhované ciele, nie ako potvrdené splnené výsledky.'
notes.append(note);lines.append(note)
if not cells[(key,facet)]:lines.append('Doložené výňatky sa nezmestili do tohto výpisu.')
if plan.presentation in {'comparison','table'} and keys:
from tools.answers.presentation import comparison_table

View File

@ -161,9 +161,17 @@ def activity_supported(question, row):
compares the workflow to NER. These are distinct research tasks in this wiki.
"""
wording = normalized(question)
achieved=re.search(r'\b(?:vytvoril\w*|implementoval\w*|nasadil\w*|pouzil\w*|vyhodnotil\w*|vyskus(?:al|ala|ali)\w*)\b',wording)
if achieved and row.get('phase') not in {'result','progress'}:
return False
named_entities = bool(
re.search(r"\bpomenovan\w* entit\w*\b|\bnamed entit\w*\b|\bner\b", wording)
)
if named_entities:
literal=row['text']+' '+' '.join(u['text'] for u in row.get('context_quotes',[]))
if 'ner' not in concept_families(literal) and not re.search(
r'\bpomenovan\w* entit\w*\b|\bnamed entit\w*\b',normalized(literal)):
return False
if named_entities and re.search(r'\brozpoznav\w*|\brozpoznava\w*|\bnamed entity recognition\b|\bner\b', wording):
text = normalized(row['text'])
if re.search(r'\banot\w*\b', text) and not re.search(
@ -1035,6 +1043,11 @@ def deliver_content(question: str, *, engine: str, limit: int, db_file, complete
if (note["source_path"], note.get("work_label"), note["text"]) not in existing
),
]
retrieval_trace=None
retrieved_relevance=None
if engine=='rag' and plan and mode not in {'advisors','completion','supervisor'}:
from tools.knowledge.text_retrieval import retrieve_units
units,retrieved_relevance,retrieval_trace=retrieve_units(question,index,plan.spec,limit=limit)
def traced(result):
if not include_diagnostics:
return result
@ -1044,7 +1057,8 @@ def deliver_content(question: str, *, engine: str, limit: int, db_file, complete
pool=[row for row in pool if (not people or row['person'] in people)
and plan.spec.matches_work(row['person'],row.get('work_label'),row.get('work_type'),row.get('work_year'))]
return {**result,'retrieval_diagnostics':{
'strategy':'bounded_local_graph' if local is not None else 'content_checkpoint',
**(retrieval_trace or {}),
'strategy':'bounded_local_graph' if local is not None else (retrieval_trace or {}).get('strategy','content_checkpoint'),
'stage':'loaded_scoped_units_before_answer_selection',
'candidate_ids':list(dict.fromkeys(row['id'] for row in pool)),
'candidate_count':len(pool)}}
@ -1080,8 +1094,8 @@ def deliver_content(question: str, *, engine: str, limit: int, db_file, complete
from tools.knowledge.sources import with_work_context
units = with_work_context(units)
relevance = None
if local is None and mode in {"discovery", "similar"} and index.get("vectors"):
relevance = retrieved_relevance
if local is None and relevance is None and mode in {"discovery", "similar"} and index.get("vectors"):
import base64
import zlib
import numpy as np
@ -1129,6 +1143,9 @@ def deliver_content(question: str, *, engine: str, limit: int, db_file, complete
plan=plan,
local_graph=local,
)
if mode in {'advisors','meeting'} and plan and set(plan.spec.fields)&{'document_author','start_year'}:
from tools.knowledge.academic import with_profile_metadata
result=with_profile_metadata(result,plan,db_file)
if local is not None:
from tools.knowledge.graph_answers import graph_details
result = graph_details(result,local)

View File

@ -0,0 +1,81 @@
"""Independent FTS5/BM25 and E5 candidates over witnessed source assertions.
The checkpoint is an evidence store, not an alternative to text retrieval.
Named finite scopes are expanded after ranking so a bounded candidate list
cannot silently remove an assignment goal or one side of a comparison.
"""
from functools import lru_cache
import base64
import sqlite3
import threading
import zlib
from tools.answers.context_policy import normalized
_lock = threading.RLock()
@lru_cache(maxsize=2)
def lexical_index(version, texts):
connection = sqlite3.connect(':memory:', check_same_thread=False)
connection.execute('CREATE VIRTUAL TABLE evidence USING fts5(id UNINDEXED, text, tokenize="unicode61 remove_diacritics 2")')
connection.executemany('INSERT INTO evidence VALUES (?,?)',texts)
return connection
def retrieve_units(question, index, spec, *, limit=5):
from tools.knowledge.query import _terms
from tools.knowledge.evidence import claim_eligible
people={p.name for p in spec.people if p.role=='student'}
similar=spec.intent=='similar'
def in_scope(row):
if not spec.matches_work(row['person'],row.get('work_label'),row.get('work_type'),row.get('work_year')):return False
if spec.period and spec.period.field!='work_year':
value=row.get(spec.period.field)
if not value or not spec.period.start<=value<=spec.period.end:return False
return similar or not people or row['person'] in people
candidates=[r for r in index['units'] if in_scope(r)]
eligible={r['id'] for r in candidates}
texts=tuple((r['id'],r['text']+' '+' '.join(r.get('concepts',[]))) for r in index['units'])
connection=lexical_index(index['version'],texts)
terms=_terms(question)
if similar:
# The reference's actual substantive topics seed the peer search.
from tools.knowledge.concepts import substantive_concept
terms={normalized(c) for r in candidates if r['person'] in people and claim_eligible(r)
for c in r.get('concepts',[]) if substantive_concept(c)} or terms
expression=' OR '.join('"'+term.replace('"','""')+'"*' for term in sorted(terms))
with _lock:
lexical=[r[0] for r in connection.execute(
'SELECT id FROM evidence WHERE evidence MATCH ? ORDER BY bm25(evidence),id',
(expression,)) if r[0] in eligible] if expression else []
semantic={}
if index.get('vectors'):
import numpy as np
from tools.rag.embedding_utils import embed_query,model_profile,embedding_model_name
if model_profile(embedding_model_name())['fingerprint']!=index['embedding_fingerprint']:
raise RuntimeError('Content embeddings belong to another model')
matrix=np.frombuffer(zlib.decompress(base64.b64decode(index['vectors'])),dtype='<f4').reshape(index['vector_shape'])
if len(matrix)!=len(index['units']):raise ValueError('Invalid content vector count')
anchors=[i for i,r in enumerate(index['units']) if r['person'] in people and in_scope(r) and claim_eligible(r)] if similar else []
vector=np.mean(matrix[anchors],axis=0) if anchors else embed_query(question)
norm=float(np.linalg.norm(vector))
if not norm:raise ValueError('Invalid query/reference embedding')
scores=matrix @ (vector/norm)
semantic={r['id']:float(s) for r,s in zip(index['units'],scores) if r['id'] in eligible}
vector_rank=sorted(semantic,key=lambda key:(-semantic[key],key))
budget=min(256,max(80,limit*16))
fused={}
for ranking in (lexical[:budget],vector_rank[:budget]):
for position,key in enumerate(ranking,1):fused[key]=fused.get(key,0)+1/(60+position)
ranked=sorted(fused,key=lambda key:(-fused[key],key))[:budget]
selected=set(ranked)
# Whole named source/work scopes are finite, explicit evidence expansion;
# this is not an assumption that a global top-k result is exhaustive.
if people:
selected.update(r['id'] for r in candidates if r['person'] in people)
rows=[r for r in candidates if r['id'] in selected]
rows.sort(key=lambda r:(-fused.get(r['id'],0),r['source_path'],r['line']))
return rows,semantic,{'strategy':'bm25_e5_rrf_assertions','candidate_count':len(rows),
'lexical_candidates':len(lexical[:budget]),'vector_candidates':len(vector_rank[:budget]),
'named_scope_expansion':bool(people),'bounded':not people and len(candidates)>len(rows)}

View File

@ -37,6 +37,7 @@ FIELDS = (
"clarification",
"named_subjects",
"work_scopes",
"named_scope_complete",
"question_plan",
"resolved_query",
"name_resolution",

View File

@ -73,7 +73,7 @@ def _score(words: list[str], canonical: list[str]) -> int | None:
if (
not actual.isalpha()
or not expected.isalpha()
or min(len(actual), len(expected)) < 4
or min(len(actual), len(expected)) < 3
):
return None
cap = 2 if min(len(actual), len(expected)) >= 8 else 1
@ -189,7 +189,7 @@ def resolve_person_names(query: str, catalog: list[dict]) -> dict:
}
author_lookup = bool(
re.search(
r"\b(?:autor\w*|dokument\w*|vedie|viedol|veduc\w*|skolitel\w*|konzult(?:ant|oval)\w*)\b", normalize(query)
r"\b(?:autor\w*|dokument\w*|vedie|viedol|veduc\w*|skolitel\w*|konzult\w*)\b", normalize(query)
)
)
people = [
@ -345,7 +345,14 @@ def resolve_person_names(query: str, catalog: list[dict]) -> dict:
and not token[0].isupper()
and not tokens[i + 1][0].isupper()
)
comparison_subject = bool(selected and pair and re.search(r"\b(?:a|porovnaj)\s*$", prefix))
# A conjunction introduces another identity only while enumerating
# names. "Ján Holp ... a nezamieňaj ich" is a presentation request.
adjacent_name = any(right <= token.start() and re.fullmatch(
r"\s*(?:,|a)\s*", query[right:token.start()]) for _,right,_ in selected)
comparison_subject = bool(selected and pair and (adjacent_name or re.fullmatch(r"porovnaj\s*",prefix)))
identity_role = bool(re.search(r"\b(?:osob\w*|student\w*|kandidat\w*|autor(?:a|om)?)\s*$",prefix)) and not reference
owner_slot = role and not selected and not re.match(
r'^(?:kto|ktori|ktory|u koho)\b', normalize(query))
# Lower-case unknown entries in an explicit candidate list must still
# fail closed, even if they follow a comma or a known person.
list_subject = explicit_list and any(
@ -353,8 +360,8 @@ def resolve_person_names(query: str, catalog: list[dict]) -> dict:
for _, right, _ in selected
)
if not (
role
or (pair and words[i] in first_names)
owner_slot or (identity_role and pair)
or words[i] in first_names
or upper_pair
or comparison_subject
or list_subject