diff --git a/app/routes.py b/app/routes.py index 78d4e63..ab23916 100644 --- a/app/routes.py +++ b/app/routes.py @@ -162,6 +162,7 @@ class AnswerRequest(StrictRequestModel): engine: Literal["rag", "graphrag"] = "rag" limit: int = Field(default=5, ge=1, le=20) include_diagnostics: bool = False + context_token: str | None = Field(default=None,min_length=1,max_length=16000) @router.post("/answer", dependencies=[Depends(require_search_api_key)], include_in_schema=False) @@ -172,6 +173,7 @@ def verified_answer(request: AnswerRequest) -> dict: engine=request.engine, limit=request.limit, include_diagnostics=request.include_diagnostics, + **({'context_token':request.context_token} if request.context_token else {}), ) except Exception: logger.exception("Príprava overenej odpovede zlyhala.") diff --git a/experiments/conversation_context/verification.json b/experiments/conversation_context/verification.json new file mode 100644 index 0000000..c02505a --- /dev/null +++ b/experiments/conversation_context/verification.json @@ -0,0 +1,1129 @@ +{ + "checked_at_utc": "2026-11-10T00:52:20.163245+00:00", + "state": "BLOCK_1_IMPLEMENTED_AND_DEPLOYED_AWAITING_CLAUDE_UI_AND_JOINT_CLOSURE", + "scope": "Question understanding and explicit conversational references. Not the later prose-quality/proposal-generation block or a new accuracy TEST.", + "base_commit": "787cb6dfa38bd2ef410eb6925f46ae1bf3b39c31", + "design": { + "plan": "Shared typed person/role/work/year/period/meeting selector plus requested answer mode.", + "context": "Explicit optional context_token passed by the client. HMAC-SHA256 with domain-separated existing SEARCH_API_KEY, 24-hour expiry, source-index version and source-ID checks. No shared last-user/last-person state or chat database.", + "audit": "Original query is echoed unchanged; resolved_query records only backend interpretation.", + "retrieval": "Exact meeting conditions constrain RAG evidence selection and parametrized Neo4j traversal before final scope checks.", + "followups": [ + "v danom stretnutí", + "vtedy", + "pri tej druhej práci", + "daj obe", + "new aspect", + "new meeting year", + "supervisor role with a new work year" + ], + "fail_closed": [ + "missing context", + "several possible meetings/works", + "different person", + "unknown new person", + "invalid signature", + "expiry", + "different source version", + "scope mismatch" + ], + "answer_modes": [ + "fact", + "description", + "comparison", + "similarity", + "proposal" + ], + "proposal": "Recognized explicitly and returned as proposal_not_available, rather than accepting unrelated factual excerpts as a proposal. Generation belongs to block 2." + }, + "verification": { + "full_regression": { + "command": ".venv/bin/python -m pytest -q test tools -o faulthandler_timeout=45", + "passed": 1582, + "skipped": 90, + "note": "After the full run, one additional displayed-work-option restriction was covered by the final focused suite and public MCP checks." + }, + "final_focused": { + "command": ".venv/bin/python -m pytest -q test/test_conversation_context.py", + "passed": 35 + }, + "known_live_regressions": { + "passed": 28, + "total": 28, + "cases": [ + { + "id": "latest", + "engine": "rag", + "query": "Kedy mal Jakub Schwarc posledné stretnutie a o čom bolo?", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Jakub Schwarc — Diplomová práca 2027**\nPosledný záznam s úplným dátumom: 2026-11-05.\n\n- Priebežný stav: napísané skripty na spúštanie a prípravu dát a na vyhodnotenie.\n\n- Priebežný stav: Máme prístup na HPC\n\n- Priebežný stav: hľadanie optimálneho LR pomocou optuna na hruska.kemt.fei.tuke.sk - je to dosť pomalé\n\n- Priebežný stav: skúšanie SK bench. Niektoré repozitáre nf HF hub zatiaľ nefungovali.\n\n- Plán: Spustite skript na vyhodnotnotenie na HPC. Pozrite si https://github.com/slovak-nlp/sk-bench/blob/main/container/slurm/README.md\n\n- Plán: Spustite experiment na dotrénovanie na HPC\n\n- Plán: sk bench vyskúšať ešte raz.\n\n- Plán: Nakopírujte si dáta na HPC. Použite projektový adresár /projects/cisloprojektu . Môžete použit scp. Potrebujete tunel.\n\nProfil obsahuje aj záznamy bez úplného dátumu; nemožno spoľahlivo určiť, či sú novšie. Rok nedopĺňam z roku práce.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/jakub_schwarc", + "resolved_query": null, + "has_context": true, + "evidence_scope": [ + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomový projekt 2026", + "record_date": null, + "meeting_id": "bbdd7a83d033172418b263d1afc79b80" + } + ] + }, + { + "id": "meeting_followup", + "engine": "rag", + "query": "Na čom pracoval Jakub Schwarc v danom stretnutí?", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Jakub Schwarc — Diplomová práca 2027**\nDátum požadovaného stretnutia: 2026-11-05.\n\n- Priebežný stav: napísané skripty na spúštanie a prípravu dát a na vyhodnotenie.\n\n- Priebežný stav: Máme prístup na HPC\n\n- Priebežný stav: hľadanie optimálneho LR pomocou optuna na hruska.kemt.fei.tuke.sk - je to dosť pomalé\n\n- Priebežný stav: skúšanie SK bench. Niektoré repozitáre nf HF hub zatiaľ nefungovali.\n\n- Plán: Spustite skript na vyhodnotnotenie na HPC. Pozrite si https://github.com/slovak-nlp/sk-bench/blob/main/container/slurm/README.md\n\n- Plán: Spustite experiment na dotrénovanie na HPC\n\n- Plán: sk bench vyskúšať ešte raz.\n\n- Plán: Nakopírujte si dáta na HPC. Použite projektový adresár /projects/cisloprojektu . Môžete použit scp. Potrebujete tunel.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/jakub_schwarc", + "resolved_query": "Na čom pracoval Jakub Schwarc na stretnutí 5.11.2026? Pri práci \"Diplomová práca 2027\".", + "has_context": true, + "evidence_scope": [ + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + } + ] + }, + { + "id": "phrase_typo", + "engine": "rag", + "query": "ma com v danom stretnuti pracoval jakub schwarc", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Jakub Schwarc — Diplomová práca 2027**\nDátum požadovaného stretnutia: 2026-11-05.\n\n- Priebežný stav: napísané skripty na spúštanie a prípravu dát a na vyhodnotenie.\n\n- Priebežný stav: Máme prístup na HPC\n\n- Priebežný stav: hľadanie optimálneho LR pomocou optuna na hruska.kemt.fei.tuke.sk - je to dosť pomalé\n\n- Priebežný stav: skúšanie SK bench. Niektoré repozitáre nf HF hub zatiaľ nefungovali.\n\n- Plán: Spustite skript na vyhodnotnotenie na HPC. Pozrite si https://github.com/slovak-nlp/sk-bench/blob/main/container/slurm/README.md\n\n- Plán: Spustite experiment na dotrénovanie na HPC\n\n- Plán: sk bench vyskúšať ešte raz.\n\n- Plán: Nakopírujte si dáta na HPC. Použite projektový adresár /projects/cisloprojektu . Môžete použit scp. Potrebujete tunel.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/jakub_schwarc", + "resolved_query": "na čom na stretnutí 5.11.2026 pracoval jakub schwarc Pri práci \"Diplomová práca 2027\".", + "has_context": true, + "evidence_scope": [ + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + } + ] + }, + { + "id": "relative_time", + "engine": "rag", + "query": "Aké úlohy mal vtedy?", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Jakub Schwarc — Diplomová práca 2027**\nDátum požadovaného stretnutia: 2026-11-05.\n\n- Plán: Spustite skript na vyhodnotnotenie na HPC. Pozrite si https://github.com/slovak-nlp/sk-bench/blob/main/container/slurm/README.md\n\n- Plán: Spustite experiment na dotrénovanie na HPC\n\n- Plán: sk bench vyskúšať ešte raz.\n\n- Plán: Nakopírujte si dáta na HPC. Použite projektový adresár /projects/cisloprojektu . Môžete použit scp. Potrebujete tunel.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/jakub_schwarc", + "resolved_query": "Aké úlohy mal na stretnutí 5.11.2026? Osoba: Jakub Schwarc. Na stretnutí 5.11.2026. Pri práci \"Diplomová práca 2027\".", + "has_context": true, + "evidence_scope": [ + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + } + ] + }, + { + "id": "old_explicit_date", + "engine": "rag", + "query": "Na čom pracoval Jakub Schwarc na stretnutí 1. 10. 2026?", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Jakub Schwarc — Diplomová práca 2027**\nDátum požadovaného stretnutia: 2026-10-01.\n\n- Zaznamenaný výsledok: vytvorený repozitár s konfiguráciami\n\n- Priebežný stav: prípravených 10 experimentov s eval a loss priebehom.\n\n- Zaznamenaný výsledok: porovnanie AYA a Alpaca dotrénovania. Alpaca vyzerá lepšie.\n\n- Priebežný stav: vyskúšaná AYA collection dataset - zbehlo 86 percent datasetu. Kolísala evaluácia.\n\n- Plán: Pripravte skript, ktorý zozbiera a pripraví trénovacie dáta, ktoré mame k dispozícii. Dáta rozdeľte na 3 časti. train, test, eval. Test a eval by mali byť tak, nastavené, aby vyhodnotenie na nich netrovalo príáliš dlho. Stačí iba jedna mnoižina ak sú dáta podobné.\n\n- Plán: pripravte skript, ktorý vyhodnotí výsledný model. Na vyhodnotenie použite \"eval\" časť dát.\n\n- Plán: použite vyhodnotenie SK-bench.\n\n- Plán: Použite iný multilinguálny model na dotrénovanie - napr. OLMO.\n\nZobrazený je obmedzený výber. Zúž otázku na študenta, konkrétnu prácu alebo obdobie, prípadne zvýš limit.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/jakub_schwarc", + "resolved_query": "Na čom pracoval Jakub Schwarc na stretnutí 1.10.2026?", + "has_context": true, + "evidence_scope": [ + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + } + ] + }, + { + "id": "missing_context", + "engine": "rag", + "query": "Na čom pracoval Jakub Schwarc v danom stretnutí?", + "passed": true, + "status": "ambiguous", + "reason": "missing_meeting_context", + "answer": "Ktoré stretnutie máš na mysli? Uveď študenta a dátum alebo presné označenie stretnutia.", + "resolved_query": null, + "has_context": true, + "evidence_scope": [] + }, + { + "id": "wrong_context_person", + "engine": "rag", + "query": "Na čom vtedy pracoval Ján Holp?", + "passed": true, + "status": "ambiguous", + "reason": "context_subject_mismatch", + "answer": "Odkaz patrí k inej osobe. Uveď pre aktuálneho študenta konkrétnu prácu alebo dátum stretnutia.", + "resolved_query": null, + "has_context": true, + "evidence_scope": [] + }, + { + "id": "unknown_new_person", + "engine": "rag", + "query": "Aké práce má xaver neexistujuci?", + "passed": true, + "status": "no_answer", + "reason": "unknown_person", + "answer": "Meno „xaver neexistujuci“ sa nepodarilo spoľahlivo priradiť k osobe v dostupných podkladoch. Uveď presné celé meno.", + "resolved_query": null, + "has_context": false, + "evidence_scope": [] + }, + { + "id": "forged_context", + "engine": "rag", + "query": "Na čom vtedy pracoval?", + "passed": true, + "status": "error", + "reason": "invalid_context", + "answer": "Kontext predchádzajúcej otázky sa nepodarilo overiť. Zopakuj úplnú otázku s osobou, prácou a prípadným dátumom.", + "resolved_query": null, + "has_context": false, + "evidence_scope": [] + }, + { + "id": "proposal_route", + "engine": "rag", + "query": "Čo by sa dalo spojiť z práce Jána Ptáka a Jakuba Schwarca?", + "passed": true, + "status": "error", + "reason": "proposal_not_available", + "answer": "Otázka žiada návrh možného prepojenia, nie iba existujúce fakty. Návrhový režim ešte nie je dostupný; výpis podkladov preto nevydávam za hotový návrh.", + "resolved_query": null, + "has_context": false, + "evidence_scope": [] + }, + { + "id": "ambiguous_work", + "engine": "rag", + "query": "Aké metódy používal Ján Holp pri diplomovom projekte v roku 2020?", + "passed": true, + "status": "ambiguous", + "reason": "ambiguous_work", + "answer": "Ktorú prácu osoby Ján Holp máš na mysli? V indexe sú tieto možnosti: Diplomový projekt 2020; Diplomový projekt 2 2020. Uveď presné označenie projektu.", + "resolved_query": null, + "has_context": true, + "evidence_scope": [] + }, + { + "id": "both_works", + "engine": "rag", + "query": "Daj obe.", + "passed": true, + "status": "ok", + "reason": null, + "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", + "resolved_query": "Aké metódy používal Ján Holp pri diplomovom projekte v roku 2020? Pýtam sa na obe práce: \"Diplomový projekt 2020\" aj \"Diplomový projekt 2 2020\".", + "has_context": true, + "evidence_scope": [ + { + "person": "Ján Holp", + "work": "Diplomový projekt 2 2020", + "record_date": "2020-11-13", + "meeting_id": "756526bbb8f178e89e178eb8414d71a2" + }, + { + "person": "Ján Holp", + "work": "Diplomový projekt 2020", + "record_date": "2020-06-23", + "meeting_id": "bdd04ff26753878f8926fd23a48bebe8" + }, + { + "person": "Ján Holp", + "work": "Diplomový projekt 2020", + "record_date": "2020-06-23", + "meeting_id": "bdd04ff26753878f8926fd23a48bebe8" + } + ] + }, + { + "id": "second_work", + "engine": "rag", + "query": "Pri tej druhej práci.", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Ján Holp — Diplomový projekt 2 2020**\n\nZaznamenané výsledky (dátum záznamu: 2020-11-13): Boli vyriešené technické problémy s Cassandra a umožnené indexovanie z Cassandra do Elasticsearch. [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", + "resolved_query": "Aké metódy používal Ján Holp pri diplomovom projekte v roku 2020? Pýtam sa na prácu: \"Diplomový projekt 2 2020\".", + "has_context": true, + "evidence_scope": [ + { + "person": "Ján Holp", + "work": "Diplomový projekt 2 2020", + "record_date": "2020-11-13", + "meeting_id": "756526bbb8f178e89e178eb8414d71a2" + } + ] + }, + { + "id": "fresh_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", + "reason": null, + "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", + "resolved_query": null, + "has_context": true, + "evidence_scope": [] + }, + { + "id": "latest", + "engine": "graphrag", + "query": "Kedy mal Jakub Schwarc posledné stretnutie a o čom bolo?", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Jakub Schwarc — Diplomová práca 2027**\nPosledný záznam s úplným dátumom: 2026-11-05.\n\n- Priebežný stav: napísané skripty na spúštanie a prípravu dát a na vyhodnotenie.\n\n- Priebežný stav: Máme prístup na HPC\n\n- Priebežný stav: hľadanie optimálneho LR pomocou optuna na hruska.kemt.fei.tuke.sk - je to dosť pomalé\n\n- Priebežný stav: skúšanie SK bench. Niektoré repozitáre nf HF hub zatiaľ nefungovali.\n\n- Plán: Spustite skript na vyhodnotnotenie na HPC. Pozrite si https://github.com/slovak-nlp/sk-bench/blob/main/container/slurm/README.md\n\n- Plán: Spustite experiment na dotrénovanie na HPC\n\n- Plán: sk bench vyskúšať ešte raz.\n\n- Plán: Nakopírujte si dáta na HPC. Použite projektový adresár /projects/cisloprojektu . Môžete použit scp. Potrebujete tunel.\n\nProfil obsahuje aj záznamy bez úplného dátumu; nemožno spoľahlivo určiť, či sú novšie. Rok nedopĺňam z roku práce.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/jakub_schwarc", + "resolved_query": null, + "has_context": true, + "evidence_scope": [ + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomový projekt 2026", + "record_date": null, + "meeting_id": "bbdd7a83d033172418b263d1afc79b80" + } + ] + }, + { + "id": "meeting_followup", + "engine": "graphrag", + "query": "Na čom pracoval Jakub Schwarc v danom stretnutí?", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Jakub Schwarc — Diplomová práca 2027**\nDátum požadovaného stretnutia: 2026-11-05.\n\n- Priebežný stav: napísané skripty na spúštanie a prípravu dát a na vyhodnotenie.\n\n- Priebežný stav: Máme prístup na HPC\n\n- Priebežný stav: hľadanie optimálneho LR pomocou optuna na hruska.kemt.fei.tuke.sk - je to dosť pomalé\n\n- Priebežný stav: skúšanie SK bench. Niektoré repozitáre nf HF hub zatiaľ nefungovali.\n\n- Plán: Spustite skript na vyhodnotnotenie na HPC. Pozrite si https://github.com/slovak-nlp/sk-bench/blob/main/container/slurm/README.md\n\n- Plán: Spustite experiment na dotrénovanie na HPC\n\n- Plán: sk bench vyskúšať ešte raz.\n\n- Plán: Nakopírujte si dáta na HPC. Použite projektový adresár /projects/cisloprojektu . Môžete použit scp. Potrebujete tunel.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/jakub_schwarc", + "resolved_query": "Na čom pracoval Jakub Schwarc na stretnutí 5.11.2026? Pri práci \"Diplomová práca 2027\".", + "has_context": true, + "evidence_scope": [ + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + } + ] + }, + { + "id": "phrase_typo", + "engine": "graphrag", + "query": "ma com v danom stretnuti pracoval jakub schwarc", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Jakub Schwarc — Diplomová práca 2027**\nDátum požadovaného stretnutia: 2026-11-05.\n\n- Priebežný stav: napísané skripty na spúštanie a prípravu dát a na vyhodnotenie.\n\n- Priebežný stav: Máme prístup na HPC\n\n- Priebežný stav: hľadanie optimálneho LR pomocou optuna na hruska.kemt.fei.tuke.sk - je to dosť pomalé\n\n- Priebežný stav: skúšanie SK bench. Niektoré repozitáre nf HF hub zatiaľ nefungovali.\n\n- Plán: Spustite skript na vyhodnotnotenie na HPC. Pozrite si https://github.com/slovak-nlp/sk-bench/blob/main/container/slurm/README.md\n\n- Plán: Spustite experiment na dotrénovanie na HPC\n\n- Plán: sk bench vyskúšať ešte raz.\n\n- Plán: Nakopírujte si dáta na HPC. Použite projektový adresár /projects/cisloprojektu . Môžete použit scp. Potrebujete tunel.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/jakub_schwarc", + "resolved_query": "na čom na stretnutí 5.11.2026 pracoval jakub schwarc Pri práci \"Diplomová práca 2027\".", + "has_context": true, + "evidence_scope": [ + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + } + ] + }, + { + "id": "relative_time", + "engine": "graphrag", + "query": "Aké úlohy mal vtedy?", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Jakub Schwarc — Diplomová práca 2027**\nDátum požadovaného stretnutia: 2026-11-05.\n\n- Plán: Spustite skript na vyhodnotnotenie na HPC. Pozrite si https://github.com/slovak-nlp/sk-bench/blob/main/container/slurm/README.md\n\n- Plán: Spustite experiment na dotrénovanie na HPC\n\n- Plán: sk bench vyskúšať ešte raz.\n\n- Plán: Nakopírujte si dáta na HPC. Použite projektový adresár /projects/cisloprojektu . Môžete použit scp. Potrebujete tunel.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/jakub_schwarc", + "resolved_query": "Aké úlohy mal na stretnutí 5.11.2026? Osoba: Jakub Schwarc. Na stretnutí 5.11.2026. Pri práci \"Diplomová práca 2027\".", + "has_context": true, + "evidence_scope": [ + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-11-05", + "meeting_id": "d7385f9350e8a9ecd2b125e333c68e82" + } + ] + }, + { + "id": "old_explicit_date", + "engine": "graphrag", + "query": "Na čom pracoval Jakub Schwarc na stretnutí 1. 10. 2026?", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Jakub Schwarc — Diplomová práca 2027**\nDátum požadovaného stretnutia: 2026-10-01.\n\n- Zaznamenaný výsledok: vytvorený repozitár s konfiguráciami\n\n- Priebežný stav: prípravených 10 experimentov s eval a loss priebehom.\n\n- Zaznamenaný výsledok: porovnanie AYA a Alpaca dotrénovania. Alpaca vyzerá lepšie.\n\n- Priebežný stav: vyskúšaná AYA collection dataset - zbehlo 86 percent datasetu. Kolísala evaluácia.\n\n- Plán: Pripravte skript, ktorý zozbiera a pripraví trénovacie dáta, ktoré mame k dispozícii. Dáta rozdeľte na 3 časti. train, test, eval. Test a eval by mali byť tak, nastavené, aby vyhodnotenie na nich netrovalo príáliš dlho. Stačí iba jedna mnoižina ak sú dáta podobné.\n\n- Plán: pripravte skript, ktorý vyhodnotí výsledný model. Na vyhodnotenie použite \"eval\" časť dát.\n\n- Plán: použite vyhodnotenie SK-bench.\n\n- Plán: Použite iný multilinguálny model na dotrénovanie - napr. OLMO.\n\nZobrazený je obmedzený výber. Zúž otázku na študenta, konkrétnu prácu alebo obdobie, prípadne zvýš limit.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/jakub_schwarc", + "resolved_query": "Na čom pracoval Jakub Schwarc na stretnutí 1.10.2026?", + "has_context": true, + "evidence_scope": [ + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + }, + { + "person": "Jakub Schwarc", + "work": "Diplomová práca 2027", + "record_date": "2026-10-01", + "meeting_id": "528179c268355b28fb9590a21f71850a" + } + ] + }, + { + "id": "missing_context", + "engine": "graphrag", + "query": "Na čom pracoval Jakub Schwarc v danom stretnutí?", + "passed": true, + "status": "ambiguous", + "reason": "missing_meeting_context", + "answer": "Ktoré stretnutie máš na mysli? Uveď študenta a dátum alebo presné označenie stretnutia.", + "resolved_query": null, + "has_context": true, + "evidence_scope": [] + }, + { + "id": "wrong_context_person", + "engine": "graphrag", + "query": "Na čom vtedy pracoval Ján Holp?", + "passed": true, + "status": "ambiguous", + "reason": "context_subject_mismatch", + "answer": "Odkaz patrí k inej osobe. Uveď pre aktuálneho študenta konkrétnu prácu alebo dátum stretnutia.", + "resolved_query": null, + "has_context": true, + "evidence_scope": [] + }, + { + "id": "unknown_new_person", + "engine": "graphrag", + "query": "Aké práce má xaver neexistujuci?", + "passed": true, + "status": "no_answer", + "reason": "unknown_person", + "answer": "Meno „xaver neexistujuci“ sa nepodarilo spoľahlivo priradiť k osobe v dostupných podkladoch. Uveď presné celé meno.", + "resolved_query": null, + "has_context": false, + "evidence_scope": [] + }, + { + "id": "forged_context", + "engine": "graphrag", + "query": "Na čom vtedy pracoval?", + "passed": true, + "status": "error", + "reason": "invalid_context", + "answer": "Kontext predchádzajúcej otázky sa nepodarilo overiť. Zopakuj úplnú otázku s osobou, prácou a prípadným dátumom.", + "resolved_query": null, + "has_context": false, + "evidence_scope": [] + }, + { + "id": "proposal_route", + "engine": "graphrag", + "query": "Čo by sa dalo spojiť z práce Jána Ptáka a Jakuba Schwarca?", + "passed": true, + "status": "error", + "reason": "proposal_not_available", + "answer": "Otázka žiada návrh možného prepojenia, nie iba existujúce fakty. Návrhový režim ešte nie je dostupný; výpis podkladov preto nevydávam za hotový návrh.", + "resolved_query": null, + "has_context": false, + "evidence_scope": [] + }, + { + "id": "ambiguous_work", + "engine": "graphrag", + "query": "Aké metódy používal Ján Holp pri diplomovom projekte v roku 2020?", + "passed": true, + "status": "ambiguous", + "reason": "ambiguous_work", + "answer": "Ktorú prácu osoby Ján Holp máš na mysli? V indexe sú tieto možnosti: Diplomový projekt 2 2020; Diplomový projekt 2020. Uveď presné označenie projektu.", + "resolved_query": null, + "has_context": true, + "evidence_scope": [] + }, + { + "id": "both_works", + "engine": "graphrag", + "query": "Daj obe.", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Ján Holp — Diplomový projekt 2 2020**\n\nZaznamenané výsledky (dátum záznamu: 2020-11-13):\n- Pri práci na projekte sa vyriešili technické problémy s Cassandra a umožnilo sa indexovanie z Cassandra do ES. [1]\n\n**Ján Holp — Diplomový projekt 2020**\n\nZaznamenané výsledky (dátum záznamu: 2020-06-23):\n- V rámci projektu bola zavedená a funkčná databáza Cassandra. [1]\n- Bolo implementované a funkčné indexovanie 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", + "resolved_query": "Aké metódy používal Ján Holp pri diplomovom projekte v roku 2020? Pýtam sa na obe práce: \"Diplomový projekt 2 2020\" aj \"Diplomový projekt 2020\".", + "has_context": true, + "evidence_scope": [ + { + "person": "Ján Holp", + "work": "Diplomový projekt 2 2020", + "record_date": "2020-11-13", + "meeting_id": "756526bbb8f178e89e178eb8414d71a2" + }, + { + "person": "Ján Holp", + "work": "Diplomový projekt 2020", + "record_date": "2020-06-23", + "meeting_id": "bdd04ff26753878f8926fd23a48bebe8" + }, + { + "person": "Ján Holp", + "work": "Diplomový projekt 2020", + "record_date": "2020-06-23", + "meeting_id": "bdd04ff26753878f8926fd23a48bebe8" + } + ] + }, + { + "id": "second_work", + "engine": "graphrag", + "query": "Pri tej druhej práci.", + "passed": true, + "status": "ok", + "reason": null, + "answer": "**Ján Holp — Diplomový projekt 2020**\n\nZaznamenané výsledky (dátum záznamu: 2020-06-23): Pri diplomovom projekte bola implementovaná databáza Cassandra. [1] Bolo realizované indexovanie 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", + "resolved_query": "Aké metódy používal Ján Holp pri diplomovom projekte v roku 2020? Pýtam sa na prácu: \"Diplomový projekt 2020\".", + "has_context": true, + "evidence_scope": [ + { + "person": "Ján Holp", + "work": "Diplomový projekt 2020", + "record_date": "2020-06-23", + "meeting_id": "bdd04ff26753878f8926fd23a48bebe8" + }, + { + "person": "Ján Holp", + "work": "Diplomový projekt 2020", + "record_date": "2020-06-23", + "meeting_id": "bdd04ff26753878f8926fd23a48bebe8" + } + ] + }, + { + "id": "fresh_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", + "reason": null, + "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", + "resolved_query": null, + "has_context": true, + "evidence_scope": [] + } + ], + "note": "Expected ambiguity/error behavior is tested as such; passing an unavailable proposal-mode case does not claim proposal generation is implemented. No independent accuracy estimate." + }, + "public_mcp": { + "public_url": "https://pressed-extra-graduation-readings.trycloudflare.com/mcp", + "tools_schema_has_context": true, + "results": [ + { + "engine": "rag", + "case": "latest", + "passed": true + }, + { + "engine": "rag", + "case": "relative_meeting_tasks", + "passed": true, + "answer": "**Jakub Schwarc — Diplomová práca 2027**\nDátum požadovaného stretnutia: 2026-11-05.\n\n- Plán: Spustite skript na vyhodnotnotenie na HPC. Pozrite si https://github.com/slovak-nlp/sk-bench/blob/main/container/slurm/README.md\n\n- Plán: Spustite experiment na dotrénovanie na HPC\n\n- Plán: sk bench vyskúšať ešte raz.\n\n- Plán: Nakopírujte si dáta na HPC. Použite projektový adresár /projects/cisloprojektu . Môžete použit scp. Potrebujete tunel.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/jakub_schwarc" + }, + { + "engine": "rag", + "case": "missing_context", + "passed": true + }, + { + "engine": "graphrag", + "case": "latest", + "passed": true + }, + { + "engine": "graphrag", + "case": "relative_meeting_tasks", + "passed": true, + "answer": "**Jakub Schwarc — Diplomová práca 2027**\nDátum požadovaného stretnutia: 2026-11-05.\n\n- Plán: Spustite skript na vyhodnotnotenie na HPC. Pozrite si https://github.com/slovak-nlp/sk-bench/blob/main/container/slurm/README.md\n\n- Plán: Spustite experiment na dotrénovanie na HPC\n\n- Plán: sk bench vyskúšať ešte raz.\n\n- Plán: Nakopírujte si dáta na HPC. Použite projektový adresár /projects/cisloprojektu . Môžete použit scp. Potrebujete tunel.\n\nZdroje: https://zp.kemt.fei.tuke.sk/students/2022/jakub_schwarc" + }, + { + "engine": "graphrag", + "case": "missing_context", + "passed": true + } + ] + }, + "oauth": { + "passed": true, + "checks": [ + "discovery", + "S256 PKCE", + "DCR", + "owner login and consent", + "code replay rejection", + "MCP tool discovery", + "refresh rotation", + "test grant revocation" + ], + "test_grants_revoked": true + }, + "skill_packages": { + "version": "1.9.0", + "reproducible_archives_match_current_source": true, + "manifest": { + "packages": [ + { + "archive": "zpwiki-rag.zip", + "name": "rag", + "sha256": "f55d72cdf1bb751807221ade986ac582098ee41c3e49a2ac3b291742935f13f2" + }, + { + "archive": "zpwiki-graphrag.zip", + "name": "graphrag", + "sha256": "78ec35715f31a2be7ca5c155506a499415fa49e644ed6309ea680bb46ab943f8" + } + ], + "schema_version": 1 + } + }, + "runtime": { + "api_files_match_workspace": true, + "mcp_files_match_workspace": true, + "sqlite_integrity": "ok", + "foreign_key_errors": 0, + "metadata_versions_equal": true, + "content_checkpoint_and_graph_match": true, + "index_version": { + "version": "815b67d4b9c54cbcb5d6bd87251fd2ef", + "git_revision": "c3ec9f611e645a2fe14fe66d518eb0924bf9bb40", + "created_at_utc": "2026-11-09T20:49:43.773353+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": "9440b8501af042b871e40de3fdce8899", + "capabilities_ready": { + "text_search": true, + "metadata_graph": true, + "knowledge": true, + "generation": false + } + }, + "wiki": { + "revision": "c3ec9f611e645a2fe14fe66d518eb0924bf9bb40", + "local_worktree_clean": true, + "wiki_writes_performed": false + } + }, + "remaining_limits": [ + "User must update both Claude Skills and refresh the connector tool schema before testing the browser UI. Actual browser behavior is not verified by protocol tests alone.", + "The host must pass its own latest answer token explicitly. The server has no independent access to the entire human/Claude conversation; tokens are not a substitute for host-side question fidelity.", + "Natural meeting prose and actual recommendation generation belong to the next block. This change does not claim those are complete.", + "Unknown/ambiguous references need a concrete clarification; no unrestricted search is substituted.", + "Quick Tunnel still requires the running laptop/Docker and may get a new address after renewal.", + "No new independent accuracy score or 94% claim is made. Existing consumed TEST artifacts are unchanged." + ], + "references": [ + "https://modelcontextprotocol.io/specification/2025-06-18/server/tools", + "https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" + ], + "runtime_sha256": { + "tools/answers/answer_coverage.py": "746ab3b89ba65f263b63037be55ee02ee25aaaf685dae6e95f7bd6a5536fc6fa", + "tools/answers/rag_metadata.py": "a16486932e66aed571fd25cb59c5656892613488ea90afe80c41b0c1d15afcf1", + "tools/answers/checked_generation.py": "e36def33fb83e5c973fa632685b4782fa411a7eeb7151ef8c14007f58fa1f375", + "tools/answers/answer_delivery.py": "9dc14e563d441ac813bf76d92be87635fa7dfbc30bcfa832c218410636270cf3", + "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": "3bc883d4e58d1f74ea2527f93867fe04231a09d24964dfec4436c97a1eaeddc4", + "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/conversation.py": "24e77e208d164de06e7294ba9a0618a8b546861893e1bd2cc9f42f0ee079c2b3", + "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": "e30f4f9f67723bb17a627469d0800621600cef05810747bded973157351517ed", + "tools/answers/question_intent.py": "a044a75ab04c57c11763b78775cc63673debed8f543523ee279e89db1f204a5b", + "tools/knowledge/concepts.py": "de0d005ab298c3d86020b0cb09fc695852303d8a5f411d0a3828b58131713b8d", + "tools/knowledge/sources.py": "709c2201288a920e99578fe4e58e191a742deb5f15170104820680d156280c69", + "tools/knowledge/publication.py": "a5a29af22fd73a57f53da47c687200f0ad392d48ce94a23250f00c7a53731148", + "tools/knowledge/academic.py": "6d39cdd747b5ece8803605f0713a9afeeee1813d3e49bb9ff2ac7e0ae4c3b73d", + "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": "58af4bd5b888756728739e26d8bc9f4cba1aaf75853c17e11453d4f38fa29f1e", + "tools/knowledge/graph_projection.py": "dac3058008fb43c833d239b6a7ced27ef5419004cc768b246741eb2e1a69b12a", + "tools/knowledge/text_retrieval.py": "946911f6b1aac8ccf1861c33b75b574df1deca810787b3bd0b2c708a9dd20f71", + "tools/knowledge/query.py": "ea372294da8e8f928328b467cfb7a652ee9b693bff42f346f6f1a4267de10b73", + "tools/knowledge/build.py": "4666b1cd2d42533b7289f5e2ecaac11c5cc16e09f3ea4b9a2714d17cb8c2f9f6", + "tools/knowledge/planned_answers.py": "e5820cb6c8d6e15315ae94e263e2886b0309b6c7eee8e09338750adbede2aa8c", + "tools/graphrag/graph_search.py": "eff65f570d3a52a9cb4132db8f3c4b59dc08840001ae1bfbab6c7ab5eda09bb0", + "tools/retrieval/person_names.py": "c4f492e75acfc205ef6172bba7ea69d6424a60f4554cd79529edaab4b28238d7", + "tools/rag/llm_context.py": "8f195ff16639de4379c41108f7a3a456d90d985307ebde3691c1406b3f76dfa5", + "app/routes.py": "55b2178834dc0e98cf832c4c87ef07304a99b0ebffc9d4b41093fbfaaed10235", + "app/main.py": "38ae3f77e8a0ddec414459da1ec17d5247d2f9457bc7ceb2af17d442658a1d28" + }, + "mcp_sha256": { + "client.py": "b1ce6d41714396afb57e87149c20415451ff16d97fe0d5d5afbe1332374ae516", + "mcp_server.py": "c5f37ea54687af8950ac73df29b9d047939af04528bc3572b8b47bb04d2d5513", + "oauth_provider.py": "85e00facfe9c772b174b57f125986a329e3b1bc99f72f4a7f553b763fbea166b" + } +} diff --git a/packages/claude/client.py b/packages/claude/client.py index 2f6362e..3f353e0 100644 --- a/packages/claude/client.py +++ b/packages/claude/client.py @@ -93,6 +93,10 @@ class Config: def error_result(reason): messages = { + 'invalid_context':'Kontext predchádzajúcej otázky sa nepodarilo overiť. Zopakuj úplnú otázku s osobou, prácou a prípadným dátumom.', + 'stale_context':'Dáta alebo kontext konverzácie sa zmenili. Zopakuj úplnú otázku; starý výber stretnutia nepoužijem.', + 'expired_context':'Kontext predchádzajúcej otázky už vypršal. Zopakuj úplnú otázku.', + 'proposal_not_available':'Otázka žiada návrh možného prepojenia, nie iba existujúce fakty. Návrhový režim ešte nie je dostupný; výpis podkladov preto nevydávam za hotový návrh.', "reference_unavailable": "Zdrojový repozitár sa nepodarilo verejne overiť. Odpoveď založenú iba na tomto odkaze neuvádzam.", "unauthorized": "Prístup nebol autorizovaný. Skontroluj prihlásenie konektora a jeho konfiguráciu.", "forbidden": "Pre tento nástroj nemáš potrebné oprávnenie.", @@ -188,7 +192,7 @@ def validate_metadata(value): raise ClientError("invalid_backend_response") -def query_backend(query: str, engine: str, *, mode="answer", limit=5, config=None): +def query_backend(query: str, engine: str, *, mode="answer", limit=5, config=None, context_token=None): """Return only approved answer fields, or explicitly marked retrieval context.""" try: if ( @@ -208,8 +212,11 @@ def query_backend(query: str, engine: str, *, mode="answer", limit=5, config=Non if not 1 <= len(query) <= 500: raise ClientError("invalid_query") config = config or Config.load() + if context_token is not None and (mode!='answer' or not isinstance(context_token,str) or not 1<=len(context_token)<=16000): + raise ClientError('invalid_query') endpoint = "/answer" if mode == "answer" else "/" + engine body = {"query": query.strip(), "limit": limit} + if context_token is not None:body['context_token']=context_token if mode == "answer" or engine == "rag": body["include_diagnostics"] = False if mode == "answer": @@ -223,6 +230,7 @@ def query_backend(query: str, engine: str, *, mode="answer", limit=5, config=Non # Raw model output, diagnostics and rejected text must not escape. reason = value.get("reason") safe_reasons = { + 'invalid_context','stale_context','expired_context','proposal_not_available', "model_unavailable", "answer_capacity_exceeded", "answer_rejected", @@ -277,6 +285,14 @@ def query_backend(query: str, engine: str, *, mode="answer", limit=5, config=Non raise ClientError("invalid_backend_response") if "knowledge_version" in value and not isinstance(value["knowledge_version"], str): raise ClientError("invalid_backend_response") + if 'conversation_context' in value: + context=value['conversation_context'] + if (not isinstance(context,dict) or not isinstance(context.get('token'),str) + or not 1<=len(context['token'])<=16000 + or any(not isinstance(context.get(k),list) for k in ['work_options','meeting_options'])): + raise ClientError('invalid_backend_response') + if 'resolved_query' in value and not isinstance(value['resolved_query'],str): + raise ClientError('invalid_backend_response') fields |= { "query", "answer", @@ -287,6 +303,7 @@ def query_backend(query: str, engine: str, *, mode="answer", limit=5, config=Non "answer_coverage", } fields.add("knowledge_version") + fields |= {'conversation_context','resolved_query'} else: if not isinstance(value.get("sources"), list): raise ClientError("invalid_backend_response") @@ -359,6 +376,7 @@ def answer_for_presentation(result: dict) -> dict: Never change an incomplete result into a claim of completeness. """ fields = { + 'conversation_context','resolved_query', "status", "reason", "query", @@ -381,11 +399,12 @@ def main(engine=None): parser.add_argument("--mode", choices=["answer", "retrieve"], default="answer") parser.add_argument("--limit", type=int, default=5) parser.add_argument("--health", action="store_true") + parser.add_argument('--context-token',help='Opaque conversation token from this chat\'s previous tool result') args = parser.parse_args() result = ( backend_health() if args.health - else query_backend(args.query, engine or args.engine, mode=args.mode, limit=args.limit) + else query_backend(args.query, engine or args.engine, mode=args.mode, limit=args.limit, context_token=args.context_token) ) if not args.health and args.mode == "answer": result = answer_for_presentation(result) diff --git a/packages/claude/dist/packages.json b/packages/claude/dist/packages.json index 6b33982..25e5a7f 100644 --- a/packages/claude/dist/packages.json +++ b/packages/claude/dist/packages.json @@ -3,12 +3,12 @@ { "archive": "zpwiki-rag.zip", "name": "rag", - "sha256": "c7a207660004c1e4e7ae7d507006df231c666d13c53067bd5896a022ef44b64d" + "sha256": "f55d72cdf1bb751807221ade986ac582098ee41c3e49a2ac3b291742935f13f2" }, { "archive": "zpwiki-graphrag.zip", "name": "graphrag", - "sha256": "1f340a0298625b7dd5f427e8501dde8ec781cc8d72e7a4f50b9ad4b21de6b1fa" + "sha256": "78ec35715f31a2be7ca5c155506a499415fa49e644ed6309ea680bb46ab943f8" } ], "schema_version": 1 diff --git a/packages/claude/dist/zpwiki-graphrag.zip b/packages/claude/dist/zpwiki-graphrag.zip index ea83b5b..5cfb309 100644 Binary files a/packages/claude/dist/zpwiki-graphrag.zip and b/packages/claude/dist/zpwiki-graphrag.zip differ diff --git a/packages/claude/dist/zpwiki-rag.zip b/packages/claude/dist/zpwiki-rag.zip index 2f66ed6..d4de847 100644 Binary files a/packages/claude/dist/zpwiki-rag.zip and b/packages/claude/dist/zpwiki-rag.zip differ diff --git a/packages/claude/graphrag/SKILL.md b/packages/claude/graphrag/SKILL.md index fc7fb8c..b1c068e 100644 --- a/packages/claude/graphrag/SKILL.md +++ b/packages/claude/graphrag/SKILL.md @@ -20,9 +20,17 @@ Send misspelled, unaccented or reversed names unchanged: the backend resolves th against actual source identities. Preserve its recognized-name notice. If it offers several people, ask the user to select a full name from those choices; never select one yourself, invent a person, or omit an unresolved person from a comparison. -For a short reply selecting the immediately preceding options, reconstruct that original -full question and apply only the selection. Preserve every condition; if context is missing, -ask for it. Compare the returned `query` against that resolved full question. +For a follow-up such as "v danom stretnutí", "vtedy", "pri tej druhej práci" or "daj obe", +pass the user's current question unchanged in `query` and copy `conversation_context.token` +from this chat's immediately preceding tool result into optional `context_token`. +The backend resolves and validates the source identities, exact work and meeting against the +same index version. Do not decode, invent or modify the token, use another chat's token, +or replace it with retrieved wiki text. Never send a token for an unrelated standalone question. +If no token is available, ask for a complete question with the person, work and meeting date; +do not guess the missing date or choose one of several previous works/meetings yourself. +For standalone questions omit `context_token`. Compare returned `query` to the exact current +user question; `resolved_query` records only the backend-validated interpretation. +Never present the token or conversation_context as part of the answer. The content graph also supports progress, dated outcomes and similar-topic questions when its extraction is ready. Preserve any backend guidance instead of inventing graph evidence. Explicit source-backed supervisors, consultants, meeting records and completion outcomes are also @@ -34,10 +42,6 @@ dated meetings. Do not infer missing years or today's status from an old source Do not turn a task into a result, a work year into a meeting date, or shared topics into collaboration. For multiple explicit commands in one message, handle each question separately in order; keep all conditions belonging to each question together. -For a follow-up omitting a person or supervisor, reconstruct the full question only when -that subject is unambiguous in the immediately preceding exchange. Preserve the new year -and every remaining condition. Otherwise ask for the name. Document authorship does not -establish supervision. 1. With a ZP Wiki MCP connector, load its current tool definition before calling it. When tools are deferred, use the host's tool search (for example `tool_search`) to find @@ -50,6 +54,7 @@ establish supervision. run `python scripts/query.py "the complete question"` relative to this directory. Keep every condition in the same query (maximum 500 characters): names, work type, year, title, keyword, category and document author have different meanings. + For script follow-ups, add `--context-token` with the unchanged token from the previous result. 3. Default mode calls `/answer`. If `accepted_for_delivery=true`, present `answer` unchanged, including citations and completeness notices. Add no new factual claims. Claude must follow this presentation rule; the skill itself cannot enforce the UI output. diff --git a/packages/claude/mcp_server.py b/packages/claude/mcp_server.py index b65d917..f9594bd 100644 --- a/packages/claude/mcp_server.py +++ b/packages/claude/mcp_server.py @@ -24,18 +24,15 @@ QUESTION_POLICY = ( "Use the exact loaded tool identifier and schema. For zpwiki_rag and zpwiki_graphrag, " "the question belongs in the query argument, never an argument named question. " "A tool-not-loaded or invalid-argument error is a host invocation error, not a DNS failure. " - "For a follow-up omitting a person or supervisor, inherit the subject only from an unambiguous " - "immediately preceding exchange and reconstruct one full question with the new year and all constraints. " - "Otherwise ask for the name. Never infer supervision from document authorship. " "For a complete question, copy the latest user question verbatim after removing only its leading " "/rag or /graphrag command. Keep names, work types, years, negations, comparisons and every condition. " "Never reuse a previous question or an example, paraphrase it or split its conditions across searches. " "If the user supplies multiple explicit commands, handle each exact question separately in order. " - "For a short reply selecting options from the immediately preceding clarification, reconstruct the original " - "full question and apply only that selection. Preserve its person, year, work type and requested aspects. " - "Use exact quoted work headings for a selected work; if the user chooses both, ask about both works in one full query. " - "If that context is missing, ask for it instead of guessing. " - "Before calling, compare the query with the current complete question or its explicitly resolved clarification. " + "For any follow-up, keep the current query verbatim and pass conversation_context.token from this chat's " + "immediately preceding tool response as context_token. Never decode, edit, fabricate or reuse another chat's token. " + "The backend validates and resolves the person, work and exact meeting; resolved_query is its audit field. " + "Without that token, ask for the full person/work/date rather than guessing a referent. " + "For standalone questions omit context_token. Never display conversation_context in the answer. " "Respect the explicitly selected engine. " "Send name typos and spelling variants unchanged; the backend resolves actual catalog identities. " "Keep its recognized-name notice, and ask the user to select when several people are offered. " @@ -79,26 +76,30 @@ def tool_result(result): ) -async def zpwiki_rag(query: str, limit: int = 5) -> CallToolResult: +async def zpwiki_rag(query: str, limit: int = 5, context_token: str | None = None) -> CallToolResult: """Verified ZP Wiki text answer. Keep the full question, names, work type and year. Pass the full question in query, not question. Optional limit is an integer. + For a follow-up copy conversation_context.token from the last result into context_token. Present the accepted answer with its citations; add no guesses or history commentary. """ return tool_result( - answer_for_presentation(await asyncio.to_thread(query_backend, query, "rag", limit=limit)) + answer_for_presentation(await asyncio.to_thread(query_backend, query, "rag", limit=limit, + **({'context_token':context_token} if context_token is not None else {}))) ) -async def zpwiki_graphrag(query: str, limit: int = 5) -> CallToolResult: +async def zpwiki_graphrag(query: str, limit: int = 5, context_token: str | None = None) -> CallToolResult: """Verified graph answer: relationships, related topics, progress, dated outcomes, years and counts in ZP Wiki. Pass the full question in query, not question. Optional limit is an integer. + For a follow-up copy conversation_context.token from the last result into context_token. Present the accepted answer with its citations; no_answer means no supporting evidence. """ return tool_result( answer_for_presentation( - await asyncio.to_thread(query_backend, query, "graphrag", limit=limit) + await asyncio.to_thread(query_backend, query, "graphrag", limit=limit, + **({'context_token':context_token} if context_token is not None else {})) ) ) diff --git a/packages/claude/rag/SKILL.md b/packages/claude/rag/SKILL.md index 83b38b6..65a572c 100644 --- a/packages/claude/rag/SKILL.md +++ b/packages/claude/rag/SKILL.md @@ -24,16 +24,17 @@ Send misspelled, unaccented or reversed names unchanged: the backend resolves th against actual source identities. Preserve its recognized-name notice. If it offers several people, ask the user to select a full name from those choices; never select one yourself, invent a person, or omit an unresolved person from a comparison. -For a short reply selecting the immediately preceding options, reconstruct that original -full question and apply only the selection. Preserve the person, year, work type and -requested aspects. For one work, use its exact quoted heading; for "both", request both -works in one query and keep their evidence separate. Do not search the words "both" alone. -If the preceding question/options are unavailable, ask for them rather than guessing. -Compare the returned `query` against this resolved full question in that case. -For a follow-up that omits the person or supervisor (for example "but which ones in 2026?"), -carry over that subject only when it is unambiguous in the immediately preceding exchange. -Send one complete question preserving the new year and all remaining conditions. If the subject -is unclear, ask for the name; never send a subjectless search or infer supervision from authorship. +For a follow-up such as "v danom stretnutí", "vtedy", "pri tej druhej práci" or "daj obe", +pass the user's current question unchanged in `query` and copy `conversation_context.token` +from this chat's immediately preceding tool result into optional `context_token`. +The backend resolves and validates the source identities, exact work and meeting against the +same index version. Do not decode, invent or modify the token, use another chat's token, +or replace it with retrieved wiki text. Never send a token for an unrelated standalone question. +If no token is available, ask for a complete question with the person, work and meeting date; +do not guess the missing date or choose one of several previous works/meetings yourself. +For standalone questions omit `context_token`. Compare returned `query` to the exact current +user question; `resolved_query` records only the backend-validated interpretation. +Never present the token or conversation_context as part of the answer. For multiple explicit commands in one message, handle each question separately in order; keep all conditions belonging to each question together. @@ -48,6 +49,7 @@ keep all conditions belonging to each question together. `python scripts/query.py "the complete question"` relative to this skill's directory. The question limit is 500 characters. Preserve names, work type, year and all constraints. Do not split a combined factual condition across independent searches. + For script follow-ups, add `--context-token` with the unchanged token from the previous result. 3. Default mode requests `/answer`. When `accepted_for_delivery=true`, present its `answer` unchanged as the verified answer. Do not add factual claims or replace its citations. This is a presentation instruction, not a guarantee enforced by the Claude interface. diff --git a/test/test_conversation_context.py b/test/test_conversation_context.py new file mode 100644 index 0000000..35ce3e2 --- /dev/null +++ b/test/test_conversation_context.py @@ -0,0 +1,245 @@ +"""Multi-turn reference resolution with real source IDs, no paid model calls.""" +import copy +import json +import time + +import pytest + +from tools.answers.conversation import issue_context,resolve_question,seal,unseal,ContextError +from tools.answers.question_plan import build_question_plan,QuestionPlan,MeetingRequest,validate_model_plan,PlanRejected +from tools.knowledge.sources import profile_units +from tools.knowledge.academic import answer_academic + +CATALOG=[{'name':'Jakub Schwarc','kind':'Person','source_document':'pages/students/2022/jakub_schwarc/README.md'}, + {'name':'Ján Holp','kind':'Person','source_document':'pages/students/2016/jan_holp/README.md'}] +VERSION={'version':'v1'} + + +@pytest.fixture +def data(monkeypatch): + monkeypatch.setenv('SEARCH_API_KEY','context-fixture-only-key') + a=profile_units({'title':'Jakub Schwarc','path':CATALOG[0]['source_document'],'content': + '# Diplomová práca 2027\nStretnutie 5.11.2026\nStav:\n- Napísané skripty na vyhodnotenie.\n' + 'Úlohy:\n- Spustite experiment na HPC.\nStretnutie 1.10.2026\nStav:\n- Porovnané modely.\n' + '# Diplomový projekt 2026\nStretnutie 10.6.2026\nStav:\n- Vyskúšaný starší model.\n' + 'Stretnutie 27.2.\nStav:\n- Iná poznámka bez roku.'},include_records=True) + b=profile_units({'title':'Ján Holp','path':CATALOG[1]['source_document'],'content': + '# Diplomový projekt 2 2020\nStav:\n- Funguje indexovanie Cassandra.\n' + '# Diplomový projekt 2020\nStav:\n- Funguje databáza Cassandra.'},include_records=True) + return {'version':'content-v1','index_version':VERSION,'academic_schema':1,'units':a+b} + + +def latest(data): + q='Kedy mal Jakub Schwarc posledné stretnutie a o čom bolo?' + p=build_question_plan(q,CATALOG,index_version=VERSION) + result=answer_academic(q,data,engine='rag',mode='meeting',plan=p) + context=issue_context(q,result,CATALOG,data,VERSION) + assert len(context['meeting_options'])==1 + return context + + +@pytest.mark.parametrize('q',[ + 'Na čom pracoval Jakub Schwarc v danom stretnutí?', + 'ma com v danom stretnuti pracoval jakub schwarc', + 'Na čom vtedy pracoval?', +]) +@pytest.mark.parametrize('engine',['rag','graphrag']) +def test_followup_keeps_exact_meeting_work_person_and_record_year(data,q,engine): + resolved=resolve_question(q,token=latest(data)['token'],catalog=CATALOG,index=data,version=VERSION) + assert not resolved.clarification + plan=build_question_plan(resolved.query,CATALOG,meeting=resolved.meeting,index_version=VERSION) + assert plan.spec.intent=='meeting' and plan.spec.period.start=='2026-11-05' + assert not any(2026 in w.years for w in plan.spec.works) + rows=[r for r in data['units'] if plan.spec.matches_evidence(r)] + reply=answer_academic(resolved.query,data,engine=engine,mode='meeting',plan=plan,units=rows) + assert reply['status']=='ok' and 'Napísané skripty' in reply['answer'] + assert 'Porovnané modely' not in reply['answer'] and 'starší model' not in reply['answer'] + assert '2026-10-01' not in reply['answer'] and 'Diplomový projekt 2026' not in reply['answer'] + assert 'Dátum požadovaného stretnutia: 2026-11-05' in reply['answer'] + + +@pytest.mark.parametrize('q',['Na čom vtedy pracoval?','Na čom pracoval Jakub Schwarc v danom stretnutí?','Daj obe.','Pri tej druhej práci.']) +def test_reference_without_context_asks_instead_of_global_search(data,q): + r=resolve_question(q,catalog=CATALOG,index=data,version=VERSION) + assert r.clarification and r.reason in {'missing_meeting_context','missing_followup_context'} + + +def test_another_person_cannot_borrow_a_meeting(data): + r=resolve_question('Na čom vtedy pracoval Ján Holp?',token=latest(data)['token'],catalog=CATALOG,index=data,version=VERSION) + assert r.reason=='context_subject_mismatch' + + +def test_fresh_standalone_question_does_not_inherit_old_subject_or_meeting(data): + q='Aký je názov diplomovej práce Jána Holpa a z ktorého roku je?' + r=resolve_question(q,token=latest(data)['token'],catalog=CATALOG,index=data,version=VERSION) + assert r.query==q and r.meeting is None + + +def test_unknown_new_person_does_not_get_replaced_by_previous_person(data): + q='Aké práce má xaver neexistujuci?' + r=resolve_question(q,token=latest(data)['token'],catalog=CATALOG,index=data,version=VERSION) + assert r.query==q and 'Schwarc' not in r.query + + +@pytest.mark.parametrize('date',['1.10.2026','1. 10. 2026','2026-10-01']) +def test_date_spelling_variants_preserve_exact_same_record_scope(data,date): + p=build_question_plan('Na čom pracoval Jakub Schwarc na stretnutí '+date+'?',CATALOG) + assert p.spec.meeting.selection=='exact' and p.spec.period.start=='2026-10-01' + + +def test_implicit_new_aspect_still_belongs_to_previous_meeting(data): + r=resolve_question('A aké úlohy dostal?',token=latest(data)['token'],catalog=CATALOG,index=data,version=VERSION) + p=build_question_plan(r.query,CATALOG,meeting=r.meeting) + rows=[r for r in data['units'] if p.spec.matches_evidence(r)] + reply=answer_academic(r.query,data,engine='rag',mode='meeting',plan=p,units=rows) + assert 'Spustite experiment na HPC' in reply['answer'] and 'Napísané skripty' not in reply['answer'] + + +def test_vtedy_after_a_predicate_is_not_an_unknown_person(data): + r=resolve_question('Aké úlohy mal vtedy?',token=latest(data)['token'],catalog=CATALOG,index=data,version=VERSION) + assert not r.clarification and r.meeting.ids + p=build_question_plan(r.query,CATALOG,meeting=r.meeting) + assert not p.clarification and p.spec.facets==['plans'] + + +def test_year_followup_changes_record_period_without_reusing_old_meeting_id(data): + r=resolve_question('A v roku 2025?',token=latest(data)['token'],catalog=CATALOG,index=data,version=VERSION) + p=build_question_plan(r.query,CATALOG) + assert not p.clarification and p.spec.intent=='meeting' and r.meeting is None + assert p.spec.meeting.selection=='latest' and p.spec.period.field=='record_date' + assert p.spec.period.start=='2025-01-01' and p.spec.period.end=='2025-12-31' + + +def test_staff_role_is_preserved_in_a_year_followup(data): + catalog=CATALOG+[{'kind':'DocumentAuthor','name':'Daniel Hladek'}] + q='Ktoré práce vedie Daniel Hladek?' + result={'status':'ok','accepted_for_delivery':True,'index_version':VERSION,'answer':'Výber vedených prác.'} + context=issue_context(q,result,catalog,data,VERSION) + r=resolve_question('A ktoré v roku 2026?',token=context['token'],catalog=catalog,index=data,version=VERSION) + p=build_question_plan(r.query,catalog) + assert p.spec.people[0].role=='supervisor' and p.spec.people[0].name=='Daniel Hladek' + assert p.spec.works[0].years==[2026] + + +def test_final_scope_check_rejects_true_facts_from_wrong_person_or_meeting(data): + from tools.answers.conversation import answer_matches_scope + old=next(r for r in data['units'] if r.get('record_date')=='2026-10-01' and r['kind']!='meeting') + wrong={'status':'ok','accepted_for_delivery':True,'evidence':[old],'claims':[]} + q='Na čom pracoval Jakub Schwarc na stretnutí 5.11.2026?' + assert not answer_matches_scope(q,wrong,CATALOG) + q='Aký je názov diplomovej práce Jána Holpa?' + assert not answer_matches_scope(q,wrong,CATALOG) + + +@pytest.mark.parametrize('change,reason',[ + ('signature','invalid_context'),('version','stale_context'),('expiry','expired_context'),('source','invalid_context')]) +def test_invalid_expired_stale_and_missing_source_references_fail_closed(data,change,reason): + token=latest(data)['token'];version=VERSION + if change=='signature':token=token[:-3]+'xxx' + if change=='version':version={'version':'new-version'} + if change=='expiry': + payload=unseal(token,VERSION);payload['expires']=int(time.time())-1;token=seal(payload) + if change=='source':data['units']=[u for u in data['units'] if u['kind']!='meeting'] + with pytest.raises(ContextError,match=reason): + resolve_question('Na čom vtedy pracoval?',token=token,catalog=CATALOG,index=data,version=version) + + +def test_multiple_previous_meetings_require_a_specific_selection(data): + context=latest(data);payload=unseal(context['token'],VERSION) + marker=next(r for r in data['units'] if r['kind']=='meeting' and r['date']=='2026-10-01') + payload['meetings'].append({'id':marker['id'],'person':marker['person'],'work_id':marker['work_id'], + 'label':marker['work_label'],'date':marker['date'],'date_label':marker['date_label']}) + r=resolve_question('Na čom vtedy pracoval?',token=seal(payload),catalog=CATALOG,index=data,version=VERSION) + assert r.reason=='ambiguous_meeting_reference' and '2026-11-05' in r.clarification and '2026-10-01' in r.clarification + + +def test_both_cannot_select_an_undisplayed_old_work_from_a_meeting_answer(data): + context=latest(data) + assert len(context['work_options'])==1 + r=resolve_question('Daj obe.',token=context['token'],catalog=CATALOG,index=data,version=VERSION) + assert r.reason=='ambiguous_work_reference' + + +def work_context(data): + q='Aké metódy používal Ján Holp pri diplomovom projekte v roku 2020?' + result={'status':'ambiguous','reason':'ambiguous_work','accepted_for_delivery':True,'index_version':VERSION, + 'answer':'Ktorú prácu? Diplomový projekt 2 2020; Diplomový projekt 2020.'} + return issue_context(q,result,CATALOG,data,VERSION) + + +def test_both_reconstructs_one_complete_question_with_original_aspects(data): + r=resolve_question('Daj obe.',token=work_context(data)['token'],catalog=CATALOG,index=data,version=VERSION) + p=build_question_plan(r.query,CATALOG) + assert not p.clarification and p.spec.facets==['methods'] + assert p.spec.scope=='selected_works' and len(p.spec.works[0].labels)==2 and p.spec.works[0].years==[2020] + + +def test_second_work_selection_keeps_original_request_but_only_selected_work(data): + r=resolve_question('Pri tej druhej práci.',token=work_context(data)['token'],catalog=CATALOG,index=data,version=VERSION) + p=build_question_plan(r.query,CATALOG) + assert not r.clarification and p.spec.facets==['methods'] + assert p.spec.matches_work('Ján Holp','Diplomový projekt 2020','diploma_project',2020) + assert not p.spec.matches_work('Ján Holp','Diplomový projekt 2 2020','diploma_project',2020) + + +def test_second_selection_can_follow_a_successful_both_answer(data): + from tools.knowledge.planned_answers import answer_facets + both=resolve_question('Daj obe.',token=work_context(data)['token'],catalog=CATALOG,index=data,version=VERSION) + plan=build_question_plan(both.query,CATALOG) + result=answer_facets(plan,data,data['units'],engine='rag',limit=5) + assert result['status']=='ok' + context=issue_context(both.query,result,CATALOG,data,VERSION) + second=resolve_question('Pri tej druhej práci.',token=context['token'],catalog=CATALOG,index=data,version=VERSION) + plan=build_question_plan(second.query,CATALOG) + result=answer_facets(plan,data,data['units'],engine='rag',limit=5) + assert result['status']=='ok' and len({r['work_id'] for r in result['evidence']})==1 + assert all(r['work_label']==context['work_options'][1]['label'] for r in result['evidence']) + + +def test_new_aspect_inherits_person_work_type_and_year(data): + r=resolve_question('A aké výsledky?',token=work_context(data)['token'],catalog=CATALOG,index=data,version=VERSION) + p=build_question_plan(r.query,CATALOG) + assert p.spec.facets==['results'] and p.spec.people[0].name=='Ján Holp' + assert p.spec.works[0].work_type=='diploma_project' and p.spec.works[0].years==[2020] + + +def test_exact_date_is_not_silently_replaced_by_latest(data): + q='Na čom pracoval Jakub Schwarc na stretnutí 1.10.2026?' + p=build_question_plan(q,CATALOG) + assert p.spec.intent=='meeting' and p.spec.meeting.selection=='exact' + assert p.spec.period.start==p.spec.period.end=='2026-10-01' + rows=[r for r in data['units'] if r.get('record_date')=='2026-10-01'] + reply=answer_academic(q,data,engine='rag',mode='meeting',plan=p,units=rows) + assert 'Porovnané modely' in reply['answer'] and 'Napísané skripty' not in reply['answer'] + + +def test_model_cannot_drop_meeting_or_turn_proposal_into_metadata(data): + p=build_question_plan('Na čom pracoval Jakub Schwarc na stretnutí 1.10.2026?',CATALOG).spec + with pytest.raises(PlanRejected,match='meeting'): + validate_model_plan(p.model_copy(update={'meeting':None}),p) + p=build_question_plan('Čo by sa dalo spojiť z práce Jána Holpa a Jakuba Schwarca?',CATALOG).spec + assert p.intent=='proposal' and p.answer_mode=='proposal' + with pytest.raises(PlanRejected,match='answer mode'): + validate_model_plan(p.model_copy(update={'answer_mode':'fact'}),p) + + +def test_same_meaning_typo_and_paraphrase_use_same_route(data): + from tools.answers.conversation import normalize_question + for q in ['na com pracoval Jakub Schwarc','ma com pracoval Jakub Schwarc']: + p=build_question_plan(normalize_question(q),CATALOG) + assert p.spec.intent=='progress' and p.spec.answer_mode=='description' + + +def test_client_transports_context_without_rewriting_query(monkeypatch): + from packages.claude import client + captured={} + def request(config,endpoint,body): + captured.update(body) + return {'status':'ambiguous','reason':'test','query':body['query'],'engine':'rag', + 'accepted_for_delivery':True,'answer':'Vyber stretnutie.','claims':[],'source_urls':[], + 'complete':False,'truncated':False,'truncation_reasons':[],'index_version':VERSION, + 'resolved_query':'Overená úplná otázka.','conversation_context':{'token':'opaque','work_options':[],'meeting_options':[]}} + monkeypatch.setattr(client,'request_json',request) + result=client.query_backend('Daj obe.','rag',config=client.Config('http://localhost:8000','test'),context_token='opaque') + assert captured['query']=='Daj obe.' and captured['context_token']=='opaque' + assert client.answer_for_presentation(result)['conversation_context']['token']=='opaque' diff --git a/test/test_question_delivery.py b/test/test_question_delivery.py index d6632e0..1800a98 100644 --- a/test/test_question_delivery.py +++ b/test/test_question_delivery.py @@ -17,6 +17,14 @@ QUESTIONS = [ ] +@pytest.mark.parametrize('engine',['rag','graphrag']) +def test_context_token_crosses_real_http_without_model_rewriting(backend,engine): + result=client.query_backend('Daj obe.',engine,context_token='opaque-context') + assert result['accepted_for_delivery'] and result['query']=='Daj obe.' + body=backend['requests'][-1][1] + assert body['query']=='Daj obe.' and body['context_token']=='opaque-context' + + @pytest.mark.parametrize("engine", ["rag", "graphrag"]) def test_latest_question_and_all_conditions_cross_http_unchanged(backend, engine): for question in QUESTIONS: diff --git a/tools/answers/answer_delivery.py b/tools/answers/answer_delivery.py index 3bf7025..f0fab7b 100644 --- a/tools/answers/answer_delivery.py +++ b/tools/answers/answer_delivery.py @@ -71,18 +71,47 @@ def deliver_answer( complete=None, db_file: Path = DB_FILE, include_diagnostics: bool = False, + context_token: str | None = None, ) -> dict: from tools.answers.reference_links import checked_repository_links + from tools.answers.conversation import resolve_question, issue_context, answer_matches_scope, ContextError + from tools.answers.question_plan import planning_catalog + from tools.knowledge.store import load_index + from tools.sync.index_state import read_version - result = _deliver_answer( - question, - engine=engine, - limit=limit, - complete=complete, - db_file=db_file, - include_diagnostics=include_diagnostics, - ) - return checked_repository_links(result) + catalog=[];index=None;version=None + try: + with index_snapshot(db_file.parent,db_file=db_file): + version=read_version(db_file.parent) + catalog=planning_catalog(db_file) + index=load_index(db_file.parent,version) + except (OSError,ValueError,RuntimeError): + if context_token: + return public_failure({'reason':'stale_context','engine':engine}) + try: + resolved=resolve_question(question,token=context_token,catalog=catalog,index=index,version=version) + except ContextError as error: + return public_failure({'reason':str(error),'engine':engine}) + if resolved.clarification: + result=clarification_result(resolved.clarification,resolved.reason,engine=engine,index_version=version) + else: + result = _deliver_answer( + resolved.query, + engine=engine, + limit=limit, + complete=complete, + db_file=db_file, + include_diagnostics=include_diagnostics, + meeting=resolved.meeting, + expected_version=version if context_token else None, + ) + if catalog and not answer_matches_scope(resolved.query,result,catalog,resolved.meeting): + result=public_failure({'reason':'answer_rejected','engine':engine}) + result=checked_repository_links(result) + if resolved.query!=question.strip():result['resolved_query']=resolved.query + conversation=issue_context(resolved.query,result,catalog,index,version) + if conversation:result['conversation_context']=conversation + return result def _deliver_answer( @@ -93,6 +122,8 @@ def _deliver_answer( complete=None, db_file: Path = DB_FILE, include_diagnostics: bool = False, + meeting=None, + expected_version=None, ) -> dict: if not _capacity.acquire(blocking=False): return public_failure({"reason": "answer_capacity_exceeded", "engine": engine}) @@ -103,12 +134,14 @@ def _deliver_answer( from tools.knowledge.query import deliver_content, ContentModelUnavailable, ContentAnswerRejected try: with index_snapshot(db_file.parent, db_file=db_file) as planned_version: + if expected_version is not None and expected_version!=planned_version: + return public_failure({'reason':'stale_context','engine':engine}) catalog = planning_catalog(db_file) plan_model = complete or (lambda messages: complete_with_configured_model( messages, output_schema=QuestionPlan.model_json_schema())) verify_model = complete or (lambda messages: complete_with_configured_model( messages, output_schema=PlanMeaning.model_json_schema())) - plan = build_question_plan(question, catalog, complete=plan_model,verify_complete=verify_model,index_version=planned_version) + plan = build_question_plan(question, catalog, complete=plan_model,verify_complete=verify_model,index_version=planned_version,meeting=meeting) except PlanModelUnavailable: return public_failure({'reason':'model_unavailable','engine':engine}) except PlanRejected: @@ -121,6 +154,8 @@ def _deliver_answer( value['name_resolution']=plan.resolution if include_diagnostics:value['question_plan']=plan.model_dump() return value + if plan.spec.intent=='proposal': + return public_failure({'reason':'proposal_not_available','engine':engine}) question = plan.spec.question try: content_result = deliver_content( diff --git a/tools/answers/answer_errors.py b/tools/answers/answer_errors.py index 482ef7a..b8ef4e9 100644 --- a/tools/answers/answer_errors.py +++ b/tools/answers/answer_errors.py @@ -1,6 +1,10 @@ """Public failure wording; transport/model exception text is never a UI message.""" MESSAGES = { + 'invalid_context':'Kontext predchádzajúcej otázky sa nepodarilo overiť. Zopakuj úplnú otázku s osobou, prácou a prípadným dátumom.', + 'stale_context':'Dáta alebo kontext konverzácie sa zmenili. Zopakuj úplnú otázku; starý výber stretnutia nepoužijem.', + 'expired_context':'Kontext predchádzajúcej otázky už vypršal. Zopakuj úplnú otázku.', + 'proposal_not_available':'Otázka žiada návrh možného prepojenia, nie iba existujúce fakty. Návrhový režim ešte nie je dostupný; výpis podkladov preto nevydávam za hotový návrh.', "reference_unavailable": "Zdrojový repozitár sa nepodarilo verejne overiť. Odpoveď založenú iba na tomto odkaze neuvádzam.", "content_index_unavailable": "Obsahové vyhľadávanie momentálne nie je pripravené alebo sa jeho výstup " "nepodarilo overiť. Treba overiť extrakciu pre aktuálnu verziu dát; " diff --git a/tools/answers/conversation.py b/tools/answers/conversation.py new file mode 100644 index 0000000..bf8a229 --- /dev/null +++ b/tools/answers/conversation.py @@ -0,0 +1,263 @@ +"""Stateless, signed conversation references; source text is never chat memory. + +The caller explicitly returns a token from its own previous tool response. No +global last-person/last-meeting state is kept, and nothing is written to wiki. +""" +from __future__ import annotations + +import base64 +from dataclasses import dataclass +import hashlib +import hmac +import json +import os +import re +import time + +from tools.answers.context_policy import normalized +from tools.answers.question_intent import normalize_question +from tools.answers.question_plan import MeetingRequest, direct_spec +from tools.retrieval.person_names import resolve_person_names + +MAX_TOKEN = 16000 +LIFETIME = 24 * 60 * 60 +MEETING_REF = re.compile(r'\b(?:vtedy|(?:v|na|o|z|pri) (?:danom|tomto|tom|tomto istom) stretnuti)\b') +WORK_REF = re.compile(r'\b(?:(?:tej|tu|t[uú]) (?:druhej|druhu|prvej|prvu)|(?:pri|o) druhej) (?:praci|pracu|projekte)\b') +BOTH = re.compile(r'^(?:(?:daj|uved|ukaz|vypis|porovnaj|chcem|prosim)\s+)?(?:obe|oba|obidv\w*)(?:\s+(?:prace|projekty))?[.!?]?$') + + +class ContextError(ValueError): + pass + + +@dataclass +class ResolvedQuestion: + query: str + meeting: MeetingRequest | None = None + clarification: str | None = None + reason: str | None = None + + +def _key(): + # Domain-separated from API authentication; no additional .env secret. + key = os.getenv('SEARCH_API_KEY', '') + return hashlib.sha256(('zp-conversation-v1\0'+key).encode()).digest() if key else None + + +def _encode(value): + return base64.urlsafe_b64encode(value).decode().rstrip('=') + + +def _decode(value): + return base64.b64decode(value+'='*(-len(value)%4),altchars=b'-_',validate=True) + + +def seal(payload): + key = _key() + if key is None: + return None # Local library tests need no private API configuration. + body = _encode(json.dumps(payload,ensure_ascii=False,separators=(',',':')).encode()) + token = body+'.'+_encode(hmac.digest(key,body.encode(),'sha256')) + return token if len(token)<=MAX_TOKEN else None + + +def unseal(token, version): + try: + if not isinstance(token,str) or not 1<=len(token)<=MAX_TOKEN or _key() is None: + raise ValueError() + body,signature=token.split('.') + if not hmac.compare_digest(_decode(signature),hmac.digest(_key(),body.encode(),'sha256')): + raise ValueError() + value=json.loads(_decode(body)) + if value.get('schema')!=1 or not isinstance(value.get('question'),str):raise ValueError() + if value['expires']20 or len(meetings)>12:return None + payload={'schema':1,'version':version['version'],'expires':int(time.time())+LIFETIME, + 'question':question,'people':[p.model_dump() for p in spec.people], + 'works':works,'meetings':meetings} + token=seal(payload) + return {'token':token,'work_options':works,'meeting_options':meetings} if token else None + + +def resolve_question(question, *, token=None, catalog, index, version): + query=normalize_question(question) + n=normalized(query) + context=unseal(token,version) if token else None + year_followup=re.fullmatch(r'(?:(?:a|ale)(?: co)?\s+)?v roku ([12]\d{3})[.?]?',n) + if context and year_followup: + old=direct_spec(context['question'],resolve_person_names(context['question'],catalog)) + if old.intent=='meeting' and old.meeting and old.meeting.selection=='latest': + query=re.sub(r'\bv roku [12]\d{3}\b','',context['question'],flags=re.I).rstrip(' .?')+' v roku '+year_followup[1]+'?' + n=normalized(query) + meeting_ref=bool(MEETING_REF.search(n)) + work_ref=WORK_REF.search(n) + both=bool(BOTH.fullmatch(n)) + explicit_date=bool(re.search(r'\b\d{1,2}\.\s*\d{1,2}\.\s*[12]\d{3}\b',query)) + # An explicit date is stronger than a redundant word such as "vtedy". + needs_context=meeting_ref and not explicit_date or bool(work_ref) or both + short_followup=bool(context and (explicit_date or re.match(r'^(?:a |ale |čo |co |aké |ake |kedy |čo sa |co sa )',n))) + if needs_context and context is None: + return ResolvedQuestion(query,clarification=( + 'Ktoré stretnutie máš na mysli? Uveď študenta a dátum alebo presné označenie stretnutia.' if meeting_ref + else 'Ktoré práce máš na mysli? Uveď osobu a presné označenia prác.'), + reason='missing_meeting_context' if meeting_ref else 'missing_followup_context') + if not context or not (needs_context or short_followup):return ResolvedQuestion(query) + # Revalidate references against the captured published generation, not just + # a plausible name, an unsigned caller dictionary or a model's assertions. + by_id={r['id']:r for r in (index or {}).get('units',[])} + for marker in context['meetings']: + row=by_id.get(marker['id']) + if not row or row['kind']!='meeting' or row['person']!=marker['person'] or row.get('work_id')!=marker['work_id']: + raise ContextError('invalid_context') + for work in context['works']: + if not any(r.get('work_id')==work['id'] and r['person']==work['person'] and r.get('work_label')==work['label'] for r in by_id.values()): + raise ContextError('invalid_context') + actual=resolve_person_names(query,catalog) + if actual['status']!='ok':return ResolvedQuestion(query) + named=[m['name'] for m in actual.get('matches',[])] + previous={p['name'] for p in context['people']} + if needs_context and named and not set(named)<=previous: + return ResolvedQuestion(query,clarification='Odkaz patrí k inej osobe. Uveď pre aktuálneho študenta konkrétnu prácu alebo dátum stretnutia.',reason='context_subject_mismatch') + if both or work_ref: + works=context['works'] + if both and len(works)!=2 or work_ref and len(works)<2: + return ResolvedQuestion(query,clarification='Predchádzajúce možnosti neurčujú jednoznačný výber. Uveď presné názvy požadovaných prác.',reason='ambiguous_work_reference') + chosen=works if both else [works[0 if 'prv' in work_ref[0] else 1]] + if len({w['person'] for w in chosen})!=1: + return ResolvedQuestion(query,clarification='Uveď osobu a presné označenie každej vyberanej práce.',reason='ambiguous_work_reference') + selection_only=bool(work_ref and WORK_REF.sub('',n).strip(' .?!') in {'','pri','o','daj','ukaz'}) + if both or selection_only: + from tools.retrieval.work_selector import QUOTED,label_key + old_labels={label_key(w['label']) for w in works} + query=QUOTED.sub(lambda m:'' if label_key(m[1]) in old_labels else m[0],context['question']) + else: + query=re.sub(r'\b(?:pri\s+)?(?:tej\s+)?(?:druhej|prvej)\s+(?:práci|praci|projekte)\b', + 'pri vybranej práci',query,flags=re.I) + query+=' Pýtam sa na '+('obe práce: ' if both else 'prácu: ')+ ' aj '.join('"'+w['label']+'"' for w in chosen)+'.' + if not named and not (both or selection_only):query+=' Osoba: '+chosen[0]['person']+'.' + elif not named and len(previous)==1: + if re.fullmatch(r'\d{1,2}\.\s*\d{1,2}\.\s*[12]\d{3}[.?]?',query): + query='Na čom pracoval na stretnutí '+query.rstrip('?')+'?' + person=context['people'][0] + prefix={'supervisor':'Pod vedením ','consultant':'Konzultant ', + 'document_author':'Autor dokumentu '}.get(person['role'],'Osoba: ') + query+=' '+prefix+person['name']+'.' + elif not named and previous: + return ResolvedQuestion(query,clarification='Na ktorú z predchádzajúcich osôb otázka nadväzuje? Uveď celé meno.',reason='ambiguous_person_reference') + meeting=None + from tools.retrieval.question_constraints import requested_years + inherited_meeting=bool(not named and not explicit_date and not requested_years(question) and not work_ref and not both and short_followup + and context['meetings'] and direct_spec(context['question'],resolve_person_names(context['question'],catalog)).intent=='meeting') + if (meeting_ref or inherited_meeting) and not explicit_date: + markers=context['meetings'] + if len(markers)!=1: + options='; '.join(m['person']+' — '+str(m['label'])+' — '+str(m['date'] or m['date_label']) for m in markers) + return ResolvedQuestion(query,clarification='Ktoré stretnutie máš na mysli? '+(options+'. ' if options else '')+'Uveď konkrétny dátum alebo označenie.',reason='ambiguous_meeting_reference') + marker=markers[0] + meeting=MeetingRequest(selection='exact',ids=[by_id[marker['id']].get('meeting_id') or marker['id']]) + label=marker['date'] or marker['date_label'] + if marker['date']: + y,m,d=marker['date'].split('-');label=f'{int(d)}.{int(m)}.{y}' + # Match on original Unicode spelling without replacing unrelated words. + query=re.sub(r'\bvtedy\b|\b(?:v|na|o|z|pri)\s+(?:danom|tomto|tom|tomto istom)\s+stretnut[ií]\b', + 'na stretnutí '+label,query,flags=re.I) + if inherited_meeting:query+=' Na stretnutí '+label+'.' + query+=' Pri práci "'+marker['label']+'".' if marker['label'] else '' + if not both and meeting is None and len(previous)==1 and not named: + query=_inherit_constraints(query,context,catalog) + if len(query)>500: + return ResolvedQuestion(query,clarification='Nadväzujúca otázka je príliš dlhá. Uveď stručne osobu, prácu, dátum a požadovanú informáciu.',reason='resolved_query_too_long') + return ResolvedQuestion(query,meeting=meeting) + + +def _inherit_constraints(query, context, catalog): + """Carry previous explicit constraints, allowing explicit new replacements.""" + from tools.answers.rag_evidence import WORK_LABELS + old=direct_spec(context['question'],resolve_person_names(context['question'],catalog)) + new=direct_spec(query,resolve_person_names(query,catalog)) + if len(old.people)!=1:return query + prior=old.works[0] if old.works else None + current=new.works[0] if new.works else None + if prior and current: + if not (current.work_type or current.labels or current.numbers): + if prior.labels:query+=' Pri práci '+ ' alebo '.join('"'+v+'"' for v in prior.labels)+'.' + elif prior.work_type:query+=' Typ práce: '+WORK_LABELS[prior.work_type]+'.' + if prior.years and not new.years and new.period is None: + query+=' Rok práce: '+', '.join(str(y) for y in prior.years)+'.' + if prior.excluded_labels and not current.excluded_labels: + query+=' Nie '+ ' ani '.join('"'+v+'"' for v in prior.excluded_labels)+'.' + for condition in old.conditions: + if condition.kind in {'tag','category','document_author','exclusion','current_status'} and not any(c.kind==condition.kind for c in new.conditions): + query+=' '+condition.quote+'.' + if old.period and not new.period: + def label(value): + y,m,d=value.split('-');return f'{int(d)}.{int(m)}.{y}' + query+=' Obdobie od '+label(old.period.start)+' do '+label(old.period.end)+'.' + return query + + +def answer_matches_scope(question, result, catalog, meeting=None): + """Necessary final identity/meeting check, independent of prose quality.""" + if result.get('status')!='ok' or not result.get('accepted_for_delivery'):return True + resolution=resolve_person_names(question,catalog) + if resolution['status']!='ok':return False + spec=direct_spec(question,resolution,meeting=meeting) + names={p.name for p in spec.people if p.role=='student'} + rows=result.get('evidence',[]) + if names and spec.intent not in {'similar','discovery'}: + if any(r.get('person') and r['person'] not in names for r in [*rows,*result.get('claims',[])]):return False + if spec.meeting: + if not rows:return False + if any(not spec.matches_evidence(r) for r in rows):return False + if spec.period and spec.period.field=='record_date' and any( + not r.get('record_date') or not spec.period.start<=r['record_date']<=spec.period.end for r in rows):return False + return True diff --git a/tools/answers/question_intent.py b/tools/answers/question_intent.py index a87c615..6637fd3 100644 --- a/tools/answers/question_intent.py +++ b/tools/answers/question_intent.py @@ -23,6 +23,15 @@ FACET_LABELS = { } +def normalize_question(question: str) -> str: + """Only a narrow phrase typo; identities, dates and conditions stay literal.""" + text=re.sub(r'^ma\s+(?:čom|com)\b','na čom',question.strip(),flags=re.I) + text=re.sub(r'(? 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) @@ -65,6 +74,8 @@ def actual_methods_requested(question: str) -> bool: def unresolved_followup(question: str) -> bool: return bool( + re.fullmatch(r'(?:(?:a|ale)(?: co)?\s+)?v roku [12]\d{3}[.?]?',normalized(question)) or + re.search(r'\b(?:vtedy|(?:v|na|o|z|pri) (?:danom|tomto|tom|tomto istom) stretnuti|(?:tej|pri) druhej praci)\b',normalized(question)) or re.fullmatch( r"(?:(?:daj|uved|ukaz|vypis|porovnaj|chcem|prosim)\s+)?" r"(?:obe|oba|obidv\w*|vsetky|prvy|druhy)(?:\s+(?:prace|projekty))?[.!?]?", diff --git a/tools/answers/question_plan.py b/tools/answers/question_plan.py index 50461ec..ab10111 100644 --- a/tools/answers/question_plan.py +++ b/tools/answers/question_plan.py @@ -21,7 +21,7 @@ from tools.retrieval.question_constraints import unsupported_constraint, request from tools.retrieval.work_selector import parse_work_selector, WorkSelector WorkType = Literal['bachelor_thesis','diploma_thesis','doctoral_thesis','bachelor_project','diploma_project','team_project','research_project','scientific_project'] -Intent = Literal['metadata','facets','progress','timeline','period','discovery','similar','advisors','meeting','completion','supervisor','unsupported'] +Intent = Literal['metadata','facets','progress','timeline','period','discovery','similar','advisors','meeting','completion','supervisor','proposal','unsupported'] Facet = Literal['assignment','plans','problems','methods','experiments','progress','results'] class Strict(BaseModel): @@ -63,6 +63,13 @@ class Condition(Strict): kind: Literal['tag','category','document_author','quantity','exclusion','current_status','year_verification','other'] quote: str +class MeetingRequest(Strict): + selection: Literal['latest','exact','all'] + ids: list[str] = Field(default_factory=list, max_length=12) + + def matches(self, row): + return not self.ids or (row.get('meeting_id') or (row.get('id') if row.get('kind')=='meeting' else None)) in self.ids + class QuestionPlan(Strict): question: str intent: Intent = 'metadata' @@ -70,6 +77,8 @@ class QuestionPlan(Strict): works: list[WorkRequest] = Field(default_factory=list) years: list[int] = Field(default_factory=list) period: Period | None = None + meeting: MeetingRequest | None = None + answer_mode: Literal['fact','description','comparison','similarity','proposal'] = 'fact' conditions: list[Condition] = Field(default_factory=list) fields: list[Literal['title','year','start_year','work_type','keyword','category','document_author','supervisor','consultant','status','date']] = Field(default_factory=list) facets: list[Facet] = Field(default_factory=list) @@ -83,6 +92,10 @@ class QuestionPlan(Strict): return False return not filters or any(w.matches(label,kind,year) for w in filters) + def matches_evidence(self, row): + return (self.matches_work(row['person'],row.get('work_label'),row.get('work_type'),row.get('work_year')) + and (self.meeting is None or self.meeting.matches(row))) + class PlannedQuestion(Strict): """Server-only bindings; not part of the model's writable output schema.""" spec: QuestionPlan @@ -118,17 +131,25 @@ def planning_catalog(db_file): return catalog -def direct_spec(question, resolution): +def direct_spec(question, resolution, *, meeting=None): from tools.knowledge.query import intent, invalid_question, _period from tools.knowledge.academic import academic_intent q=resolution.get('resolved_query') or question n=normalized(q) selector=parse_work_selector(q) mode=intent(q) + meeting_words=bool(re.search(r'\b(?:stretnut\w*|konzultaci\w*)\b',n)) + if meeting_words and (meeting or re.search(r'\d{1,2}\.\s*\d{1,2}\.\s*[12]\d{3}',q)): + mode='meeting' + meeting=meeting or MeetingRequest(selection='exact') + elif mode=='meeting' and re.search(r'\b(?:posledn\w*|naposledy|najnov\w*)\b',n): + meeting=meeting or MeetingRequest(selection='latest') + elif meeting_words and re.search(r'\b(?:ake|ktore|vsetky) stretnut\w*\b',n): + mode='meeting';meeting=MeetingRequest(selection='all') 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: + if mode not in {'similar','supervisor','advisors','completion','meeting','period','proposal'} 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. @@ -179,6 +200,7 @@ def direct_spec(question, resolution): else 'count' if re.search(r'\b(?:kolko|pocet)\b',n) else 'brief' if re.search(r'\bstrucn\w*|\bjednoducho\b',n) else 'list' if scope=='across_works' or selector.multiple else 'overview') unsupported=[] + if meeting_words and meeting is None:unsupported.append('missing_meeting_context') if mode in {'progress','timeline','similar'} and not people:unsupported.append('missing_subject') if mode and mode!='metadata' and presentation=='count':unsupported.append('content_count_not_established') if invalid_question(q):unsupported.append('unintelligible_question') @@ -187,14 +209,14 @@ def direct_spec(question, resolution): 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') 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 unresolved_followup(q) and meeting is None: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) if code and not (code=='work_event_date' and academic_intent(q)=='completion'): if code!='year_range':unsupported.append(code) period=None has_dates=bool(re.search(r'\d{1,2}\.\d{1,2}\.[12]\d{3}',q)) - if has_dates or code=='year_range' or mode in {'period','timeline','completion'} or mode in {'progress','facets'} and years and not TYPE_PATTERN.search(types_text): + if has_dates or code=='year_range' or mode in {'period','timeline','completion'} or mode in {'progress','facets','meeting'} and years and not TYPE_PATTERN.search(types_text): try: bounds=_period(q) if bounds: @@ -234,7 +256,10 @@ def direct_spec(question, resolution): 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') + answer_mode=('proposal' if mode=='proposal' else 'comparison' if presentation in {'comparison','table'} + else 'similarity' if mode=='similar' else 'description' if mode and mode!='metadata' else 'fact') return QuestionPlan(fields=fields,question=q,intent=mode or 'metadata',people=people,works=works,years=years,period=period, + meeting=meeting,answer_mode=answer_mode, conditions=conditions,facets=facets,scope=scope,presentation=presentation,unsupported=list(dict.fromkeys(unsupported))) @@ -247,6 +272,9 @@ def _model_json(response): def validate_model_plan(candidate, baseline, *, allow_work_binding=False): """Schema validity does not authorize a changed identity, filter or time role.""" if candidate.question!=baseline.question:raise PlanRejected('Question rewrite') + if candidate.meeting!=baseline.meeting:raise PlanRejected('Changed meeting selection') + if candidate.answer_mode!=baseline.answer_mode and baseline.answer_mode in {'proposal','comparison','similarity'}: + raise PlanRejected('Changed requested answer mode') if candidate.people!=baseline.people:raise PlanRejected('Changed identities or roles') if candidate.years!=baseline.years or candidate.period!=baseline.period: raise PlanRejected('Changed temporal filters') @@ -280,14 +308,17 @@ def validate_model_plan(candidate, baseline, *, allow_work_binding=False): return candidate -def build_question_plan(question, catalog, *, complete=None, verify_complete=None, index_version=None): +def build_question_plan(question, catalog, *, complete=None, verify_complete=None, index_version=None, meeting=None): + from tools.answers.question_intent import normalize_question + question=normalize_question(question) resolution=resolve_person_names(question,catalog or []) if catalog is not None else { 'status':'ok','matches':[],'resolved_query':question,'notices':[]} - spec=direct_spec(question,resolution) + spec=direct_spec(question,resolution,meeting=meeting) bound=PlannedQuestion(spec=spec,resolution=resolution,index_version=index_version) if spec.unsupported: code=spec.unsupported[0] messages={'missing_subject':'Uveď celé meno študenta, ktorého prácu mám vyhľadať.', + 'missing_meeting_context':'Ktoré stretnutie máš na mysli? Uveď konkrétny dátum alebo požiadaj výslovne o posledné stretnutie.', 'content_count_not_established':'Obsahové vyhľadávanie poskytuje doložené prípady, nie overený úplný počet. Požiadaj o príklady alebo počet podľa evidovaných metadát.', 'unintelligible_question':'Otázke nerozumiem. Uveď študenta, tému alebo výsledok.', 'missing_followup_context':'Uveď osobu a presné označenia prác alebo úplnú otázku. Samotné „obe“ bez predchádzajúcich možností neviem spoľahlivo vyhľadať.', diff --git a/tools/claude/build_claude_packages.py b/tools/claude/build_claude_packages.py index 319debc..0e73fb2 100644 --- a/tools/claude/build_claude_packages.py +++ b/tools/claude/build_claude_packages.py @@ -12,7 +12,7 @@ import zipfile ROOT = Path(__file__).resolve().parents[2] BASE = ROOT / "packages/claude" ENGINES = ("rag", "graphrag") -PACKAGE_VERSION = "1.8.3" +PACKAGE_VERSION = "1.9.0" def json_bytes(value): diff --git a/tools/knowledge/academic.py b/tools/knowledge/academic.py index 2d8c24c..bfcb8d8 100644 --- a/tools/knowledge/academic.py +++ b/tools/knowledge/academic.py @@ -449,12 +449,21 @@ def answer_academic(question, index, *, engine, limit=5, units=None, mode=None, ) continue latest = max((r["date"] for r in dated), default=None) - chosen = [r for r in markers if r.get("date") == latest] if latest else markers + meeting_selection=plan.spec.meeting if plan else None + exact=bool(meeting_selection and meeting_selection.selection in {'exact','all'}) + if exact: + chosen=[r for r in markers if meeting_selection.matches(r)] + if meeting_selection.selection=='exact' and len(chosen)>1 and len({r.get('work_id') for r in chosen})>1: + return respond('Rovnakému dátumu zodpovedá viac prác. Uveď presné označenie práce.',status='ambiguous',reason='ambiguous_meeting_work') + else: + chosen = [r for r in markers if r.get("date") == latest] if latest else markers for marker in chosen[:limit]: + heading='Dátum požadovaného stretnutia' if exact else 'Posledný záznam s úplným dátumom' + displayed_date=marker.get('date') if exact else latest lines.append( f'**{marker["person"]} — {marker.get("work_label") or "profil"}**\n' - f"Posledný záznam s úplným dátumom: {latest}." - if latest + f"{heading}: {displayed_date}." + if displayed_date else f'**{marker["person"]}** — záznam: {marker["date_label"]}; rok nie je uvedený.' ) evidence.append(marker) @@ -471,6 +480,11 @@ def answer_academic(question, index, *, engine, limit=5, units=None, mode=None, from tools.answers.content_selection import unit_score notes = [r for r in notes if not r["text"].rstrip().endswith(":")] + if plan and plan.spec.facets: + from tools.answers.content_selection import facets_for + notes=[r for r in notes if set(plan.spec.facets)&set(facets_for(r['text'], + {'assignment':'assignment','planned':'task','result':'progress','progress':'progress'}.get(r['phase'],'other'), + plan.spec.question,r['phase']))] notes.sort( key=lambda r: ( -unit_score( @@ -503,7 +517,7 @@ def answer_academic(question, index, *, engine, limit=5, units=None, mode=None, if len(chosen) > limit: omitted = True partial = [r for r in part if r["kind"] == "meeting" and not r.get("date")] - if partial: + if partial and not exact: lines.append( "Profil obsahuje aj záznamy bez úplného dátumu; nemožno spoľahlivo určiť, " "či sú novšie. Rok nedopĺňam z roku práce." diff --git a/tools/knowledge/local_graph.py b/tools/knowledge/local_graph.py index a9c2a24..08d9a36 100644 --- a/tools/knowledge/local_graph.py +++ b/tools/knowledge/local_graph.py @@ -24,6 +24,7 @@ WITH p,w,e WHERE (p.source_path IN $profiles OR EXISTS { MATCH (w)-[:SUPERVISED_BY]->(v:KnowledgeAdvisor {graph_scope:$scope}) WHERE v.id IN $supervisors } OR EXISTS { MATCH (w)-[:CONSULTED_BY]->(v:KnowledgeAdvisor {graph_scope:$scope}) WHERE v.id IN $consultants }) AND ((w.id IN $works AND e.work_id=w.id) OR ($profile_records AND e.work_id IS NULL)) +AND (size($meeting_ids)=0 OR e.id IN $meeting_ids OR e.meeting_id IN $meeting_ids) AND ($date_field IS NULL OR ($start <= e[$date_field] AND e[$date_field] <= $end)) OPTIONAL MATCH (w)-[:HAS_ACTIVITY]->(a:KnowledgeActivity {graph_scope:$scope})-[:SUPPORTED_BY]->(e) OPTIONAL MATCH (e)-[:ABOUT]->(c:KnowledgeConcept {graph_scope:$scope}) @@ -123,7 +124,7 @@ def seed_ids(question, index, spec): terms = _terms(question); families = concept_families(question) candidates = [u for u in index['units'] if u.get('work_id') and claim_eligible(u) and u.get('annotation_status')=='knowledge' and not administrative_only(u['text']) - and spec.matches_work(u['person'],u.get('work_label'),u.get('work_type'),u.get('work_year')) + and spec.matches_evidence(u) and activity_supported(question,u)] if spec.period and spec.period.field!='work_year': candidates=[u for u in candidates if u.get(spec.period.field) and @@ -172,6 +173,7 @@ def retrieve_local_graph(question, index, bound, *, limit=5): ids=ids[:SEED_LIMIT] period=spec.period if spec.period and spec.period.field!='work_year' else None params={'scope':store.SCOPE,'profiles':profiles,'works':works,'ids':ids, + 'meeting_ids':spec.meeting.ids if spec.meeting else [], '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, diff --git a/tools/knowledge/query.py b/tools/knowledge/query.py index d21bf1f..0ef091e 100644 --- a/tools/knowledge/query.py +++ b/tools/knowledge/query.py @@ -48,6 +48,9 @@ def model_json(response): def intent(question: str) -> str | None: from tools.knowledge.academic import academic_intent + text = normalized(question) + if re.search(r'^(?:navrhni|navrhnite)\b|\b(?:co|ako) by sa (?:dalo|dali|mohlo|mohli) (?:spojit|prepojit|vyuzit)\b|\bmoznosti spoluprace\b',text): + return 'proposal' if mode := academic_intent(question): return mode text = normalized(question) @@ -59,7 +62,7 @@ def intent(question: str) -> str | None: r"\b(?:ktore|ake|kto)\b", text ): return "supervisor" - if re.search(r"\b(?:pokroc\w*|pokrok\w*|posun\w*|napred\w*)\b|\bna com\b", text): + if re.search(r"\b(?:pokroc\w*|pokrok\w*|posun\w*|napred\w*)\b|\bna com\b|\bcomu sa venoval\w*\b|\bnad cim pracoval\w*\b|\bco (?:urobil|spravil|riesil)\w*\b", text): return "progress" if re.search(r"\bkedy\b", text) and re.search( r"\b(?:vysled\w*|dosiah\w*|urobil\w*|implement\w*)\b", text @@ -1048,6 +1051,8 @@ def deliver_content(question: str, *, engine: str, limit: int, db_file, complete 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) + if plan and plan.spec.meeting: + units=[row for row in units if plan.spec.matches_evidence(row)] def traced(result): if not include_diagnostics: return result diff --git a/tools/knowledge/text_retrieval.py b/tools/knowledge/text_retrieval.py index bfcdc23..5da14e6 100644 --- a/tools/knowledge/text_retrieval.py +++ b/tools/knowledge/text_retrieval.py @@ -29,7 +29,7 @@ def retrieve_units(question, index, spec, *, limit=5): 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 not spec.matches_evidence(row):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 diff --git a/tools/retrieval/person_names.py b/tools/retrieval/person_names.py index 6f30b87..39f8034 100644 --- a/tools/retrieval/person_names.py +++ b/tools/retrieval/person_names.py @@ -99,7 +99,7 @@ _STOP = set( "uved uvedte vypis vyber najdi zisti porovnaj popis opis čomu komu vymenuj pomenuj " "vsetci vsetky vsetkych plan stav vysledky problemy metody experimenty " "osoba osoby osobu osobe osobach student studenti studentov autora autor " - "aktualne teraz vedie viedol pokrocil pokrocili kedy prislusne o com pod".split() + "aktualne teraz vtedy predtym potom vedie viedol pokrocil pokrocili kedy prislusne o com pod".split() ) _STOP = {normalize(word) for word in _STOP} @@ -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\w*)\b", normalize(query) + r"\b(?:autor\w*|dokument\w*|vedie|viedol|veduc\w*|veden\w*|skolitel\w*|konzult\w*)\b", normalize(query) ) ) people = [