"""ZP Wiki + CRZP in one answer: order, labels, clarification, follow-ups, CRZP timeout.""" import time import pytest from tools.answers import combined as C WIKI = {"status": "ok", "reason": None, "accepted_for_delivery": True, "claims": [], "index_version": {"version": "v"}, "complete": True, "truncated": False, "truncation_reasons": [], "answer": "**Ján Pták — Diplomová práca 2027**\n\nVedúci práce: Daniel Hladek.\n\nZdroje: https://zp.test/jan", "source_urls": ["https://zp.test/jan"], "delivery_check": {"layer": "delivery", "code": "complete", "request_fingerprint": "x"}} @pytest.fixture def combo(monkeypatch): monkeypatch.setenv("SEARCH_API_KEY", "combined-test-key") monkeypatch.setenv("ANSWER_WITH_CRZP", "1") state = {"part": None, "match": {"name": "Ján Pták", "student": True}} monkeypatch.setattr("tools.crzp.answering.wiki_bridge.wiki_match", lambda q, db_file=None: state["match"]) monkeypatch.setattr(C, "crzp_part", lambda q, person, complete=None, student=True: state["part"]) state["deliver"] = lambda q, **kw: dict(WIKI) return state def ask(combo, question, token=None): return C.answer(question, engine="rag", context_token=token, deliver=lambda q, **kw: combo["deliver"](q, **kw)) def test_wiki_first_then_the_marked_crzp_part(combo): combo["part"] = {"mode": "person", "text": "- bakalárska práca 2025 „IoT“ — vedúci Pavol Galajda; oponent Daniel Hládek", "rows": [], "urls": ["https://opac.test/1"], "people": [], "works": []} result = ask(combo, "Kto vedie Jána Ptáka?") text = result["answer"] assert text.index("Vedúci práce: Daniel Hladek") < text.index("**V CRZP**") < text.index("Pavol Galajda") assert result["source_urls"] == ["https://zp.test/jan", "https://opac.test/1"] and result["status"] == "ok" import packages.claude.client as client client.validate_metadata(result) assert client.validated_delivery_check(result, "Kto vedie Jána Ptáka?", "rag") def test_without_crzp_the_wiki_answer_is_unchanged_but_for_wording(combo): result = ask(combo, "Kedy mal Ján Pták posledné stretnutie?") assert "V CRZP" not in result["answer"] and result["answer"].startswith("**Ján Pták") def test_a_work_only_crzp_has_is_asked_for_and_the_choice_continues(combo, monkeypatch): combo["part"] = {"mode": "person", "text": "x", "rows": [], "urls": [], "people": [], "works": [{"source": "crzp", "label": "bakalárska práca 2025 „IoT“", "type": "bachelor_thesis", "key": "k"}]} monkeypatch.setattr(C, "wiki_works", lambda person: {"Diplomová práca 2027": {"type": "diploma_thesis", "topic": "Agent"}}) asked = ask(combo, "Aké technológie využíva Ján Pták vo svojej práci?") assert asked["status"] == "ambiguous" and "1. Diplomová práca 2027 „Agent“ (ZP Wiki)" in asked["answer"] assert "2. bakalárska práca 2025 „IoT“ (CRZP)" in asked["answer"] seen = [] combo["deliver"] = lambda q, **kw: (seen.append(q), dict(WIKI))[1] ask(combo, "1", asked["conversation_context"]["token"]) assert seen == ["Aké technológie využíva Ján Pták vo svojej diplomovej práci?"] chosen = [] monkeypatch.setattr(C, "_answer_choice", lambda q, c, **kw: chosen.append(c) or dict(WIKI)) ask(combo, "tú bakalársku", asked["conversation_context"]["token"]) assert chosen[0]["source"] == "crzp" def test_schools_of_the_people_of_the_previous_answer(combo): combo["part"] = {"mode": "similar", "text": "- Maksym Zatirka — Technická univerzita v Košiciach: „X“", "rows": [], "urls": [], "people": [("Eva Malá", "Slovenská technická univerzita v Bratislave")]} first = ask(combo, "Kto pracoval na podobnej téme ako Ján Pták?") combo["match"] = None schools = ask(combo, "a z akých škôl boli tí študenti?", first["conversation_context"]["token"]) assert "- Ján Pták — Technická univerzita v Košiciach (KEMT FEI)" in schools["answer"] assert "- Eva Malá — Slovenská technická univerzita v Bratislave" in schools["answer"] def test_a_slow_crzp_does_not_hold_the_wiki_answer(combo, monkeypatch): monkeypatch.setattr(C, "CRZP_SECONDS", 0.5) monkeypatch.setattr(C, "crzp_part", lambda *a, **k: (time.sleep(4), None)[1]) started = time.monotonic() result = ask(combo, "Kto vedie Jána Ptáka?") assert result["status"] == "ok" and time.monotonic() - started < 2.0 def test_a_forged_combined_token_is_refused(combo): first = ask(combo, "Kto vedie Jána Ptáka?") token = first["conversation_context"]["token"] assert ask(combo, "a z akých škôl?", token[:-3] + "abc")["reason"] == "invalid_context" def test_crzp_people_and_keywords_link_to_the_wiki_graph_by_code(): from tools.crzp.storage.graph import link_pairs rows = [{"school": "Technická univerzita v Košiciach", "authors": ["Pták, Ján"], "supervisors": ["Galajda, Pavol"], "opponents": [], "keywords": ["Speech Recognition", "iot"]}, {"school": "Iná univerzita", "authors": ["Pták, Jan"], "supervisors": [], "opponents": [], "keywords": []}] people, keywords = link_pairs(rows, ["Ján Pták", "Daniel Hladek"], ["rozpoznávanie reči", "Dialógový systém"]) assert people == [{"crzp": "Pták, Ján", "wiki": "Ján Pták"}] # only at TUKE, the namesake elsewhere is not linked assert keywords == [{"key": "speech recognition", "wiki": "rozpoznávanie reči"}] # through the reviewed synonyms def test_the_wiki_question_which_person_wins_over_crzp(combo): combo["match"] = None combo["part"] = {"mode": "explicit", "status": "ambiguous", "text": "Menu „Ján“ zodpovedá v CRZP…", "rows": [], "urls": [], "people": []} combo["deliver"] = lambda q, **kw: {**WIKI, "status": "ambiguous", "reason": "ambiguous_person", "answer": "Ktorého Jána myslíš? Ján Holp, Ján Pták."} result = ask(combo, "Na čom pracoval Ján?") assert result["answer"] == "Ktorého Jána myslíš? Ján Holp, Ján Pták." combo["deliver"] = lambda q, **kw: {**WIKI, "status": "ambiguous", "reason": "missing_subject", "answer": "Uveď meno študenta."} combo["part"] = {**combo["part"], "status": "ok", "text": "Školitelia prác …"} assert "Školitelia prác" in ask(combo, "Kto viedol najviac prác o strojovom učení?")["answer"] def test_an_unknown_person_goes_to_crzp_only_for_a_thesis_question(): assert C.THESIS.search(C.normalized("O čom je bakalárska práca Jána Nováka?")) assert not C.THESIS.search(C.normalized("Na čom pracuje Peter Novák?")) assert not C.ABOUT.search(C.normalized("Čomu sa venuje Jakub Schwarc?"))