From b34466677cf48215df51228e8f0e035541d797c6 Mon Sep 17 00:00:00 2001 From: jp170na Date: Wed, 4 Nov 2026 13:21:53 +0100 Subject: [PATCH] =?UTF-8?q?Hl=C3=A1senia=20a=20limity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 +- packages/claude/client.py | 5 +- packages/claude/dist/packages.json | 4 +- packages/claude/dist/zpwiki-graphrag.zip | Bin 7672 -> 7781 bytes packages/claude/dist/zpwiki-rag.zip | Bin 7748 -> 7842 bytes packages/claude/graphrag/SKILL.md | 3 + packages/claude/mcp_server.py | 3 + packages/claude/rag/SKILL.md | 3 + scripts/build_claude_packages.py | 2 +- test/test_answer_failures_limits.py | 161 ++++++++++++++++++ test/test_graph_query_regressions.py | 37 ++++ test/test_graph_work_listing.py | 2 +- test/test_verified_rag.py | 2 +- tools/answer_evaluation/checked_generation.py | 18 +- tools/answer_evaluation/graph_context.py | 2 +- tools/answer_evaluation/render_answer.py | 9 +- tools/answers/answer_delivery.py | 18 +- tools/answers/answer_errors.py | 2 +- tools/graphrag/graph_search.py | 16 +- .../graphrag/tests/test_graph_publication.py | 47 +++++ 20 files changed, 326 insertions(+), 23 deletions(-) create mode 100644 test/test_answer_failures_limits.py diff --git a/README.md b/README.md index a1c776d..39acbce 100644 --- a/README.md +++ b/README.md @@ -1160,7 +1160,7 @@ upozornenie na neúplnosť, nezamieňať autora dokumentu s autorom práce a pri sa nevysvetľujú odhadom; verzia dát neoznačuje verziu programu. Tieto pokyny ovplyvňujú správanie Claude, jeho finálny text však náš backend nemôže programovo vynútiť. Po aktualizácii už nahratých Skills nahraď ich -novými ZIPmi z `packages/claude/dist/` (verzia 1.5.1) a over správanie v novom chate. +novými ZIPmi z `packages/claude/dist/` (verzia 1.5.2) a over správanie v novom chate. Predvolený výstup Claude nástrojov obsahuje overenú odpoveď, citácie, identitu otázky/nástroja, stav doručenia a verziu dát. Technické údaje o výbere kandidátov, @@ -1171,6 +1171,19 @@ Samotný text odpovede sa nemení: konkrétne chýbajúce údaje aj upozornenia skutočne skrátenom výpise zostávajú zachované. Neprítomnosť technických polí neznamená úplnosť vyhľadania. +Zamietnutie `answer_rejected` oznamuje neúspešné overenie odpovede, nie chybu +otázky alebo neprítomnosť údajov. Spresnenie sa žiada pri konkrétnej nejednoznačnosti; +nedostupnosť modelu alebo indexu má vlastné hlásenie. Neoverený text sa nezobrazí. +Pri `/answer` s `engine=graphrag` sa `limit` (predvolene 5, rozsah 1–20) +odovzdáva až do grafového vyhľadávania. Obmedzuje samostatne počet vrátených +faktových riadkov a ciest, nie počet študentov. Každý akademický fakt zachová +identitu práce, osobu, typ, rok a zdroj. Potrebné tagové dôkazy pre zobrazené +práce zostávajú v `supporting_facts`; nepridávajú ďalšie osoby ani práce do výpisu. +Samotný presun dôkazu do tohto poľa sa neoznačuje za neúplný výsledok. +Agregácia sa počíta pred prezentačným +orezaním; presný celkový počet je dovolený iba pri úplnej agregácii. Skutočne +orezaný grafový výpis obsahuje upozornenie a ponuku zúženia otázky. + ### Nahratie Skills do webového Claude 1. Zapni Code execution and file creation (v novom rozložení Create and edit files) diff --git a/packages/claude/client.py b/packages/claude/client.py index 1f6984a..3ebd289 100644 --- a/packages/claude/client.py +++ b/packages/claude/client.py @@ -90,7 +90,8 @@ def error_result(reason): 'engine_mismatch':'Príkaz a zvolený nástroj sa nezhodujú. Použi nástroj podľa /rag alebo /graphrag.', 'model_unavailable':'Jazykový model momentálne nie je dostupný. Skús otázku neskôr.', 'answer_rejected':'Odpoveď sa nepodarilo spoľahlivo overiť podľa podkladov. ' - 'Spresni osobu, typ práce alebo rok; neoverený text sa nezobrazí.', + 'Neoverené údaje sa nezobrazia.', + 'answer_unavailable':'Overenú odpoveď sa momentálne nepodarilo pripraviť. Skús to neskôr.', 'answer_capacity_exceeded':'Agent práve spracúva iné otázky. Skús to o chvíľu.', 'backend_busy':'Agent práve spracúva iné otázky. Skús to o chvíľu.', 'backend_timeout':'Vyhľadávanie neodpovedalo včas. Skús otázku zopakovať neskôr.', @@ -206,7 +207,7 @@ def query_backend(query: str, engine: str, *, mode='answer', limit=5, config=Non if engine == 'graphrag' and (not isinstance(value.get('facts'), list) or not isinstance(value.get('paths'), list)): raise ClientError('invalid_backend_response') - fields |= {'sources', 'context', 'facts', 'paths', 'aggregation', 'constraints', + fields |= {'sources', 'context', 'facts', 'supporting_facts', 'paths', 'aggregation', 'constraints', 'answer_candidates', 'instructions'} result = {key: item for key, item in value.items() if key in fields} return {**result, 'engine': engine, 'output_kind': 'verified_answer' if mode == 'answer' else 'retrieval_context', diff --git a/packages/claude/dist/packages.json b/packages/claude/dist/packages.json index bcca32c..f017a86 100644 --- a/packages/claude/dist/packages.json +++ b/packages/claude/dist/packages.json @@ -3,12 +3,12 @@ { "archive": "zpwiki-rag.zip", "name": "rag", - "sha256": "f439237795a770473bf5efd0760667f9ef75fa4882086133996d4a928a9d0252" + "sha256": "01b9d32e246c992f529b264f5c6e9ab487e0357dfe74db3094fb77aca18b0a01" }, { "archive": "zpwiki-graphrag.zip", "name": "graphrag", - "sha256": "cd748dc0035bf932a72f6d178f986b18609728a3478fdf9a49ab6247a37a431d" + "sha256": "81ce8d4ad9540671bc6c0d4f191e2874cf543d2d39f646de8cc79d722144c207" } ], "schema_version": 1 diff --git a/packages/claude/dist/zpwiki-graphrag.zip b/packages/claude/dist/zpwiki-graphrag.zip index 609eafb093a1acc476fddffed50cb5576b4a4649..09bf972a621157366ba0c2ad4566bf490dee80f8 100644 GIT binary patch delta 7116 zcmY*;RZyG_ucXto&!TtW{R^7UF zW~v{ix@I2wX{!5Pa94=niyS-xE(iod`45z!66uFGBUBK`88?=I7X8m!gA93_^Tbk1 zbWE%)2~Z_t$L6>{opqY<}+i-5FQQw$LoyyWo{OUbN&p`7G<+R6ghM zO!kBI;xC4BwM9#%jBpzc=GU_QCRZ|mBm+>p@dRsmqvyMfmz6o# z`dA8nXqdxCUMdauQ~|S;hI!fX5cd~lSnkJK0p(@fxy3ftd~v{D#v_;N=RKG8s2>(= z$lJa)o?dBGS`3D}Yb1kjEXA-DpLyZAIb{3rHzA1CSUh;jJFsKs@_Q zz-b_9e1qGIx05mJt8e+kxszlE&q$7zu$#CYO2&(DhF|5}6RD1_$S0NHZc_f(EoF|s zOum~=eeJ0H!3V3^ZT;;h$qh$EE3QXPMVyi0nT6eTv=BN;&M(Bh1q=K>7@zvhv)yii zpieUlQe_`9X&fT}J0Jofh7eCFPmqlo7~iH1(B_cFV~W|FjYCsnLr7w;U;ltoP@vrw-P<2*~#5g4ZB z&JW_C#q?A?m|%<1o|&+LK))DMArvFgnQNh_=Cbd6&HzI%ko+2`X*MNuKoprxI-U$v z`wF(@WTk6D==)5Zl^#E?#PMQ*L?=w-x3{Am=~UfPFI=kJvF`Fx6*AiSqp17{j$8X% ze1FYJk$ojcuWk(FHt#)RLslr0R5>M(ZenH*C1@UMB_o%Bn%gyCmPgXJ{7Ud}(OHRK zKDv!9$jeeF%2CRdEk@pTxxf0!5a sVUoq(nBx_lxQa&ha^UEXWIcb1z*ld$!95s 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 diff --git a/packages/claude/dist/zpwiki-rag.zip b/packages/claude/dist/zpwiki-rag.zip index 3c5d3afa81ba1502303ceb72bb84609acc50d2c2..074106ecd1314f0c7e7c75f31fb9a3c1fd390570 100644 GIT binary patch delta 7164 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 delta 7067 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: + assert r['complete'] is False and r['truncated'] is True + assert 'fact_limit' in r['truncation_reasons'] + assert 'Výpis je neúplný.' in r['answer'] and 'Výpis môžeš zúžiť' in r['answer'] + else: + assert r['complete'] is True and not r['truncated'] + assert 'Výpis je neúplný.' not in r['answer'] + + +def test_api_passes_graph_limit_and_hides_diagnostics(monkeypatch): + session=graph_session();calls=[] + def search(question,**kwargs): + calls.append(kwargs['limit']) + return gs.search_graph_with_session(session,question,**kwargs) + monkeypatch.setattr(delivery,'search_graph',search) + monkeypatch.setattr(delivery,'complete_with_configured_model',select_graph) + monkeypatch.setattr(main,'embed_query',lambda _:None) + with TestClient(main.app) as api: + response=api.post('/answer',json={'query':QUESTION,'engine':'graphrag','limit':1}, + headers={'X-API-Key':KEY}) + assert response.status_code==200 + r=response.json() + assert calls==[1] and len(r['claims'])==1 + assert r['truncated'] and 'Výpis môžeš zúžiť' in r['answer'] + assert 'attempts' not in r and 'retrieval_context' not in r + assert api.post('/answer',json={'query':QUESTION,'engine':'graphrag','limit':0}, + headers={'X-API-Key':KEY}).status_code==422 + + +@pytest.mark.parametrize('engine',['rag','graphrag']) +@pytest.mark.parametrize('returned_error',[False,True]) +def test_failed_retrieval_is_not_no_answer_and_never_calls_model(monkeypatch,engine,returned_error): + def search(*args,**kwargs): + if returned_error:return {'status':'error','reason':'PRIVATE_RETRIEVAL_FAILURE'} + raise RuntimeError('PRIVATE_RETRIEVAL_FAILURE') + monkeypatch.setattr(delivery,'search_graph',search) + monkeypatch.setattr(delivery,'build_rag_context',search) + monkeypatch.setattr(delivery,'index_snapshot',lambda *a,**kw:nullcontext()) + r=delivery.deliver_answer(QUESTION,engine=engine, + complete=lambda _:pytest.fail('No model after retrieval failure')) + assert r['status']=='error' and r['reason']=='retrieval_unavailable' + assert not r['accepted_for_delivery'] and r['answer']==MESSAGES['retrieval_unavailable'] + assert 'PRIVATE' not in json.dumps(r) + + +@pytest.mark.parametrize('engine',['rag','graphrag']) +@pytest.mark.parametrize('mode',['transport','invalid_format','unsupported_claim']) +def test_model_failure_is_distinct_from_missing_data(monkeypatch,mode,engine): + if engine=='graphrag': + monkeypatch.setattr(delivery,'search_graph',lambda q,**kw:gs.search_graph_with_session(graph_session(),q,**kw)) + else: + from tools.rag.rag_utils import build_context_text + source={'title':'Eva Nová','source_url':URL,'document_path':'pages/students/2022/eva_nova/README.md', + 'source_id':'S1','text':'Dokument: Eva Nová\nSekcia: Diplomová práca 2024\n\nNázov: Ukážkový systém.'} + payload={'status':'ok','complete':True,'truncated':False,'truncation_reasons':[], + 'index_version':None,'sources':[source],'context':build_context_text([source])} + monkeypatch.setattr(delivery,'build_rag_context',lambda *a,**kw:payload) + monkeypatch.setattr(delivery,'index_snapshot',lambda *a,**kw:nullcontext()) + monkeypatch.setattr(delivery,'expand_comparison_sources',lambda db,q,p:p) + monkeypatch.setattr(delivery,'prepare_named_content',lambda db,q,p:p) + calls=[] + def fail(messages): + calls.append(messages) + if mode=='transport':raise RuntimeError('PRIVATE_MODEL_FAILURE') + if mode=='invalid_format':text='PRIVATE NOT JSON' + else:text=json.dumps({'status':'ok','claims':[{'person':'PRIVATE WRONG PERSON','year':2099}], + 'source_urls':[URL],'answer':''}) + return {'choices':[{'message':{'content':text}}]} + r=delivery.deliver_answer(QUESTION,engine=engine,complete=fail) + reason='model_unavailable' if mode=='transport' else 'answer_rejected' + assert r['reason']==reason and r['status']=='error' and not r['accepted_for_delivery'] + assert r['answer']==MESSAGES[reason] and 'spresni' not in r['answer'].casefold() + assert 'PRIVATE' not in r['answer'] and not r['claims'] and not r['source_urls'] + assert len(calls)==(1 if mode=='transport' else 2) + + +@pytest.mark.parametrize('engine',['rag','graphrag']) +def test_busy_agent_is_not_an_ambiguous_question(monkeypatch,engine): + import threading + monkeypatch.setattr(delivery,'_capacity',threading.BoundedSemaphore(0)) + r=delivery.deliver_answer(QUESTION,engine=engine,complete=lambda _:pytest.fail('Agent is busy')) + assert r['status']=='error' and r['reason']=='answer_capacity_exceeded' + assert r['answer']==MESSAGES['answer_capacity_exceeded'] and not r['accepted_for_delivery'] + + +@pytest.mark.parametrize('reason',list(MESSAGES)) +def test_client_and_api_have_consistent_safe_failure_wording(backend,reason): + backend['checked'].update(status='error',reason=reason,answer='PRIVATE REJECTED PROSE') + r=client.query_backend(QUESTION,'rag') + assert r['status']=='error' and r['reason']==reason + assert r['answer']==MESSAGES[reason] and not r['accepted_for_delivery'] + assert 'PRIVATE' not in json.dumps(r) + + +@pytest.mark.parametrize('status,reason,text',[ + ('ambiguous','ambiguous_work','Ktorý projekt myslíš? Vyber projekt 1 alebo projekt 2.'), + ('no_answer','requested_aspects_not_found','V podkladoch chýbajú údaje o riešených problémoch.'), + ('ok',None,'Doložený výsledok. V podkladoch chýbajú údaje o experimentoch.'), +]) +def test_clarification_missing_evidence_and_partial_answer_stay_distinct(backend,status,reason,text): + backend['checked'].update(status=status,reason=reason,answer=text) + r=client.answer_for_presentation(client.query_backend(QUESTION,'rag')) + assert r['status']==status and r['reason']==reason and r['accepted_for_delivery'] + assert r['answer']==text diff --git a/test/test_graph_query_regressions.py b/test/test_graph_query_regressions.py index 1fe52fc..c05702c 100644 --- a/test/test_graph_query_regressions.py +++ b/test/test_graph_query_regressions.py @@ -75,6 +75,43 @@ def test_diploma_thesis_cannot_be_replaced_by_a_project(tagged_projects): assert not r['facts'] and not r['constraints']['unresolved_keyword'] +@pytest.mark.parametrize('limit',[1,2,3,4,5,6]) +def test_tag_predicate_stays_bound_to_visible_records_after_truncation(tagged_projects,limit): + q='Ktorí študenti majú tag RAG a zároveň diplomový projekt z roku 2026?' + raw=gs.search_graph_with_session(tagged_projects,q,limit=limit) + assert len(raw['facts'])<=limit and raw['truncated']==(limit<3) + context=graph_context(raw) + visible={r['person'] for r in raw['facts'] if r['fact_type']=='person_work'} + assert {c['person'] for c in context['answer_candidates']}==visible + assert all(r['person'] in visible for r in raw['supporting_facts']) + assert all(r['fact_type']=='person_keyword' for r in raw['supporting_facts']) + claims=[{k:c[k] for k in ['person','year','keyword','work_type']} for c in context['answer_candidates']] + answer={'status':'ok','claims':claims,'source_urls':[s['source_url'] for s in context['sources']]} + rendered=render_graph_answer(answer,context) + assert not answer_context_issues(rendered,context) + assert 'Jakub Schwarc' not in rendered['answer'] and 'Iná Osoba' not in rendered['answer'] + assert ('Výpis je neúplný.' in rendered['answer'])==(limit<3) + + +def test_count_model_input_keeps_actual_total_and_only_claim_fields(tagged_projects): + import json + from tools.answer_evaluation.answer_format import Claim + q='Koľko študentov má tag RAG a zároveň diplomový projekt z roku 2026?' + context=graph_context(gs.search_graph_with_session(tagged_projects,q,limit=1)) + before=copy.deepcopy(context) + def select(messages): + sent=json.loads(messages[1]['content'])['graph_context'] + assert sent['aggregation']['count']==3 and 'candidate_count' not in sent['aggregation'] + assert 'count=3' in messages[0]['content'] + assert len(sent['answer_candidates'])==1 + assert set(sent['answer_candidates'][0])<=set(Claim.model_fields) + a={'status':'ok','answer':'','count':sent['aggregation']['count'], + 'claims':sent['answer_candidates'],'source_urls':context['answer_candidates'][0]['source_urls']} + return {'choices':[{'message':{'content':json.dumps(a)}}]} + r=generate_checked_answer(q,context,PROMPT,select) + assert r['accepted_for_delivery'] and r['answer']['count']==3 and context==before + + def test_unknown_tag_cannot_trigger_a_global_project_listing(tagged_projects): r=gs.search_graph_with_session(tagged_projects, 'Ktorí študenti majú tag XNEZNAMY a zároveň diplomový projekt z roku 2026?') diff --git a/test/test_graph_work_listing.py b/test/test_graph_work_listing.py index fe5df84..b79a678 100644 --- a/test/test_graph_work_listing.py +++ b/test/test_graph_work_listing.py @@ -199,7 +199,7 @@ def test_year_only_question_still_returns_only_years(retrieved): def test_production_delivery_repairs_year_only_answer_and_renders_all_records(monkeypatch, retrieved): from tools.answers import answer_delivery as delivery - monkeypatch.setattr(delivery, 'search_graph', lambda query: retrieved) + monkeypatch.setattr(delivery, 'search_graph', lambda query, **kwargs: retrieved) good = listing_answer(graph_context(retrieved)) bad = {**good, 'claims': [{'person': 'Ján Holp', 'year': year} for year in (2019, 2020, 2021)]} replies = iter([bad, good]); messages = [] diff --git a/test/test_verified_rag.py b/test/test_verified_rag.py index 621798f..03aa123 100644 --- a/test/test_verified_rag.py +++ b/test/test_verified_rag.py @@ -200,7 +200,7 @@ def test_graph_production_delivery_checks_renders_and_propagates_metadata(monkey 'index_version': {'version': 'fixture'}, 'answer_candidates': [], 'facts': [{'person': 'Eva Nová', 'year': 2026, 'source_url': 'https://example.test/eva'}], 'sources': [{'source_url': 'https://example.test/eva'}]} - monkeypatch.setattr(delivery, 'search_graph', lambda query: {}) + monkeypatch.setattr(delivery, 'search_graph', lambda query, **kwargs: {}) monkeypatch.setattr(delivery, 'graph_context', lambda result: context) answer = {'status': 'ok', 'answer': '', 'claims': [{'person': 'Eva Nová', 'year': 2026}], 'source_urls': ['https://example.test/eva']} diff --git a/tools/answer_evaluation/checked_generation.py b/tools/answer_evaluation/checked_generation.py index 305e535..886fd59 100644 --- a/tools/answer_evaluation/checked_generation.py +++ b/tools/answer_evaluation/checked_generation.py @@ -1,13 +1,25 @@ """One bounded corrective request based solely on retrieval evidence.""" import json -from .answer_format import parse_answer, Answer +from .answer_format import parse_answer, Answer, Claim from .context_policy import answer_context_issues from .render_answer import render_graph_answer def generate_checked_answer(question: str, context: dict, prompt: str, complete) -> dict: - messages=[{'role':'system','content':prompt},{'role':'user','content':json.dumps( - {'question':question,'graph_context':context},ensure_ascii=False)}] + # Candidate provenance is used by the guard, but is not part of a Claim. + # Keep the model's selection shape identical to the actual output schema. + sent={**context,'answer_candidates':[ + {k:v for k,v in row.items() if k in Claim.model_fields} + for row in context.get('answer_candidates',[])]} + aggregation=context.get('aggregation') + if aggregation is not None: + sent['aggregation']={k:v for k,v in aggregation.items() if k in {'count','people','complete'}} + policy='\nDo claims nevkladaj source_urls ani evidence_documents; source_urls patrí iba na vrchnú úroveň odpovede.' + if aggregation and aggregation.get('complete') is True and type(aggregation.get('count')) is int: + policy+=('\nÚplná overená agregácia tohto dotazu má count='+str(aggregation['count'])+ + '. Pri status=ok zachovaj tento count; počet dodaných facts, claims ani answer_candidates ho nemení.') + messages=[{'role':'system','content':prompt+policy},{'role':'user','content':json.dumps( + {'question':question,'graph_context':sent},ensure_ascii=False)}] result={'attempts':[],'accepted_for_delivery':False} for number in range(2): record={};text='' diff --git a/tools/answer_evaluation/graph_context.py b/tools/answer_evaluation/graph_context.py index b4b3e3a..ae29c78 100644 --- a/tools/answer_evaluation/graph_context.py +++ b/tools/answer_evaluation/graph_context.py @@ -15,7 +15,7 @@ def graph_context(result: dict) -> dict: row.get('fact_type') in {'person_thesis','person_work'} and not row.get('title') for row in result['facts']))) academic_records = [] - for row in result['facts']: + for row in [*result['facts'],*result.get('supporting_facts',[])]: kind = row.get('fact_type', '') if kind.startswith('shared_'): field = {'shared_keyword': 'keyword', 'shared_category': 'category', diff --git a/tools/answer_evaluation/render_answer.py b/tools/answer_evaluation/render_answer.py index c4c34da..67e8449 100644 --- a/tools/answer_evaluation/render_answer.py +++ b/tools/answer_evaluation/render_answer.py @@ -72,10 +72,15 @@ def render_graph_answer(answer: dict, context: dict) -> dict: lines.append('') if out.get('count') is not None: lines.append(f'Počet vyhovujúcich osôb: {out["count"]}.') + aggregation=context.get('aggregation') or {} + if aggregation.get('complete') is True and out['count']==aggregation.get('count'): + # A full total is backed by the inspected candidate set, not + # just the one person left visible after a presentation cap. + out['source_urls']=list(dict.fromkeys([*out['source_urls'], + *(s['source_url'] for s in context.get('sources',[]) if s.get('source_url'))])) if context.get('complete') is False or context.get('truncated'): lines.append('Výpis je neúplný.') - if records: - lines.append('Výpis môžeš zúžiť uvedením osoby, typu práce alebo konkrétneho roku.') + lines.append('Výpis môžeš zúžiť uvedením osoby, typu práce alebo konkrétneho roku.') elif context.get('complete') is None: lines.append('Úplnosť vyhľadania nemožno zaručiť.') elif out['status'] == 'ambiguous': diff --git a/tools/answers/answer_delivery.py b/tools/answers/answer_delivery.py index e596947..a524976 100644 --- a/tools/answers/answer_delivery.py +++ b/tools/answers/answer_delivery.py @@ -29,8 +29,7 @@ def retrieval_failure(engine: str) -> dict: def deliver_answer(question: str, *, engine: str = 'rag', limit: int = 5, complete=None, db_file: Path = DB_FILE, include_diagnostics: bool = False) -> dict: if not _capacity.acquire(blocking=False): - return {'status': 'error', 'reason': 'answer_capacity_exceeded', - 'accepted_for_delivery': False, 'answer': 'Agent práve spracúva iné odpovede. Skúste to o chvíľu.'} + return public_failure({'reason':'answer_capacity_exceeded','engine':engine}) try: model = complete or complete_with_configured_model from tools.answer_evaluation.question_intent import unresolved_followup @@ -47,6 +46,8 @@ def deliver_answer(question: str, *, engine: str = 'rag', limit: int = 5, try: with index_snapshot(db_file.parent, db_file=db_file): retrieved = build_rag_context(db_file, question, limit=limit) + if retrieved.get('status')=='error': + return retrieval_failure(engine) retrieved = expand_comparison_sources(db_file, question, retrieved) retrieved = prepare_named_content(db_file, question, retrieved) retrieved = compact_rag_payload(retrieved) @@ -64,7 +65,10 @@ def deliver_answer(question: str, *, engine: str = 'rag', limit: int = 5, 'claims':[],'source_urls':[],'engine':engine,'index_version':None, 'complete':False,'truncated':False,'truncation_reasons':[]} try: - context = graph_context(search_graph(question)) + retrieved=search_graph(question, limit=limit) + if retrieved.get('status')=='error': + return retrieval_failure(engine) + context = graph_context(retrieved) except Exception: return retrieval_failure(engine) if context.get('clarification'): @@ -81,12 +85,12 @@ def deliver_answer(question: str, *, engine: str = 'rag', limit: int = 5, 'attempts': generated['attempts']} else: reason=generated.get('reason','answer_rejected') - result = {'status': 'error', 'reason': reason, 'accepted_for_delivery': False, - 'answer': ('Jazykový model nie je dostupný. Skús otázku neskôr.' if reason=='model_unavailable' else - 'Odpoveď neprešla kontrolou podkladov; neoverené údaje sa nezobrazia.'), - 'index_version': context.get('index_version'), 'attempts': generated['attempts']} + result = public_failure({'reason':reason, + 'index_version':context.get('index_version'),'attempts':generated['attempts']}) else: raise ValueError('Unsupported answer engine') + if result.get('status')=='error' or result.get('accepted_for_delivery') is not True: + result=public_failure(result) return {**result, 'engine': engine, **({'retrieval_context': context} if include_diagnostics else {})} finally: diff --git a/tools/answers/answer_errors.py b/tools/answers/answer_errors.py index e7afcf3..fe14d00 100644 --- a/tools/answers/answer_errors.py +++ b/tools/answers/answer_errors.py @@ -4,7 +4,7 @@ MESSAGES={ 'retrieval_unavailable':'Vyhľadávací index momentálne nie je dostupný. Skús otázku neskôr.', 'answer_capacity_exceeded':'Agent práve spracúva iné otázky. Skús to o chvíľu.', 'answer_rejected':'Odpoveď sa nepodarilo spoľahlivo overiť podľa podkladov. ' - 'Skús spresniť osobu, typ práce alebo rok; neoverené údaje sa nezobrazia.', + 'Neoverené údaje sa nezobrazia.', 'answer_unavailable':'Overenú odpoveď sa momentálne nepodarilo pripraviť. Skús to neskôr.', } diff --git a/tools/graphrag/graph_search.py b/tools/graphrag/graph_search.py index 2298c0f..61d4e86 100644 --- a/tools/graphrag/graph_search.py +++ b/tools/graphrag/graph_search.py @@ -2651,12 +2651,26 @@ def search_graph_with_session( truncation_reasons.append(name) titles = {row['document_path']: row.get('title') for row in sources} citations = source_references([{**doc, 'title': titles.get(doc['document_path'])} for doc in documents]) + visible_facts=facts[:limit] + # A display cap must not separate an academic row from the tag proof that + # licensed its selection. These are supporting predicates for visible + # records, never additional people/academic records in the visible list. + visible_academic_people={row.get('person_element_id') for row in visible_facts + if row.get('fact_type') in {'person_thesis','person_work'} and row.get('person_element_id')} + supporting_facts=[row for row in facts[limit:] + if keyword_specific and (thesis_specific or work_specific) + and row.get('fact_type')=='person_keyword' + and row.get('person_element_id') in visible_academic_people] + if len(visible_facts)+len(supporting_facts)==len(facts): + # Moving predicate evidence to its support field loses no result. + truncation_reasons=[reason for reason in truncation_reasons if reason!='fact_limit'] return { **result_fields(status=status, sources=citations, reason=reason, complete=True, truncation_reasons=truncation_reasons), "reasons": blocked_reasons, "query": clean_query, "strategies": strategies, "constraints": constraints, - "resolved_entities": resolved_entities, "paths": paths[:limit], "facts": facts[:limit], + "resolved_entities": resolved_entities, "paths": paths[:limit], "facts": visible_facts, + "supporting_facts": supporting_facts, "documents": documents, "aggregation": aggregation, "academic_year_comparison": ({ "shared_years": shared_years, diff --git a/tools/graphrag/tests/test_graph_publication.py b/tools/graphrag/tests/test_graph_publication.py index c3c6ed9..ad4007d 100644 --- a/tools/graphrag/tests/test_graph_publication.py +++ b/tools/graphrag/tests/test_graph_publication.py @@ -115,6 +115,53 @@ def test_graph_contract_distinguishes_exact_count_from_trimmed_evidence(graph): assert result['index_version'] is None # Raw fixture session has no published snapshot. +@pytest.mark.parametrize('year,limit,expected',[(2024,1,2),(2024,2,2),(2024,3,2),(1999,1,0)]) +def test_delivery_limit_uses_actual_cypher_and_keeps_verified_total(graph,monkeypatch,year,limit,expected): + from tools.answers import answer_delivery as delivery + tx,_=graph;calls=[] + def search(question,**kwargs): + calls.append(kwargs['limit']) + return gs.search_graph_with_session(tx,question,**kwargs) + monkeypatch.setattr(delivery,'search_graph',search) + def select(messages): + context=json.loads(messages[1]['content'])['graph_context'] + answer={'status':'ok','answer':'','count':context['aggregation']['count'], + 'claims':[{k:f[k] for k in ['person','title','year','work_type'] if f.get(k) is not None} + for f in context['facts']], + 'source_urls':list({f['source_url'] for f in context['facts'] if f.get('source_url')})} + return {'choices':[{'message':{'content':json.dumps(answer)}}]} + r=delivery.deliver_answer(f'Koľko osôb má prácu v roku {year}?',engine='graphrag',limit=limit,complete=select) + assert calls==[limit] and r['accepted_for_delivery'] + assert r['count']==expected and len(r['claims'])==min(limit,expected) + if expected: + assert set(r['source_urls'])=={'https://example.test/a','https://example.test/b'} + assert r['truncated']==(expected>limit) + if expected>limit: + assert 'Výpis je neúplný.' in r['answer'] and 'Výpis môžeš zúžiť' in r['answer'] + else: + assert 'Výpis je neúplný.' not in r['answer'] + + +def test_small_limit_keeps_actual_tag_proof_with_the_same_person(graph,monkeypatch): + from tools.answers import answer_delivery as delivery + tx,_=graph + q='Koľko osôb má kľúčové slovo sharedcheck a zároveň prácu v roku 2024?' + monkeypatch.setattr(delivery,'search_graph',lambda question,**kw:gs.search_graph_with_session(tx,question,**kw)) + def select(messages): + c=json.loads(messages[1]['content'])['graph_context'] + assert len(c['answer_candidates'])==1 + candidate=c['answer_candidates'][0] + answer={'status':'ok','answer':'','count':c['aggregation']['count'], + 'claims':[{k:candidate[k] for k in ['person','year','keyword']}], + 'source_urls':list({f['source_url'] for f in c['facts'] if f.get('person')==candidate['person'] + and f.get('source_url')})} + return {'choices':[{'message':{'content':json.dumps(answer)}}]} + r=delivery.deliver_answer(q,engine='graphrag',limit=1,complete=select) + assert r['accepted_for_delivery'] and r['count']==2 and len(r['claims'])==1 + assert r['truncated'] and 'Výpis je neúplný.' in r['answer'] + assert 'Cyril Malý' not in r['answer'] + + def test_graph_traversal_reports_database_limit_not_just_python_slicing(graph): tx, _ = graph result = gs.search_graph_with_session(tx, 'Alena Nová', limit=1)