From 16d1eb72acfc73754daaccfef859a9fffbdca5b4 Mon Sep 17 00:00:00 2001 From: jp170na Date: Tue, 10 Nov 2026 08:58:38 +0100 Subject: [PATCH] Preserve trusted metadata scope through final answer validation --- .../delivery_contract_v1/post_test_dev.json | 477 ++++++++++++++++++ test/test_delivery_bindings.py | 71 +++ test/test_person_names.py | 2 +- tools/answers/answer_delivery.py | 8 +- tools/answers/delivery_contract.py | 12 +- tools/answers/graph_context.py | 18 + tools/answers/verified_rag.py | 10 +- 7 files changed, 591 insertions(+), 7 deletions(-) create mode 100644 experiments/delivery_contract_v1/post_test_dev.json create mode 100644 test/test_delivery_bindings.py diff --git a/experiments/delivery_contract_v1/post_test_dev.json b/experiments/delivery_contract_v1/post_test_dev.json new file mode 100644 index 0000000..e7e5685 --- /dev/null +++ b/experiments/delivery_contract_v1/post_test_dev.json @@ -0,0 +1,477 @@ +{ + "scope": "Frozen TEST cases reused as known DEV after a source-binding repair. NOT new independent TEST accuracy.", + "frozen_test_remains_unchanged": true, + "frozen_test_score_does_not_describe_changed_runtime": true, + "repair": "Preserve trusted work identity through RAG preparation and graph presentation field selection; require source-backed work witnesses for person-metadata conjunctions.", + "checks": [ + "HTTP status ok and accepted", + "source URL equals the gold profile", + "nonempty claims", + "every claim has the literal work heading year and the expected work type" + ], + "passed": 20, + "total": 20, + "runtime_files_sha256": "f2e53317b1e0371b08bb2c12084d674cf532cedbb5b53de87f322de2369933d2", + "results": [ + { + "id": "C001", + "query": "U osoby Kamil Tomčufčík uveď typ a evidovaný rok záznamu „Diplomová práca 2025“.", + "engine": "rag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Kamil Tomčufčík", + "work_type": "diploma_thesis", + "year": 2025 + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2020/kamil_tomcufcik" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "40b1ea380c6cfeb870b0a8f309cf6895df6b2e1cec80d2ae22230bad0d3a90a7" + } + }, + { + "id": "C001", + "query": "U osoby Kamil Tomčufčík uveď typ a evidovaný rok záznamu „Diplomová práca 2025“.", + "engine": "graphrag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Kamil Tomčufčík", + "year": 2025, + "work_type": "diploma_thesis" + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2020/kamil_tomcufcik" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "8d70bbd70f4c117117fc2b013920d7e37c76c14f38359f7b2cb010c14174c33d" + } + }, + { + "id": "C009", + "query": "U osoby Vladimír Ferko uveď typ a evidovaný rok záznamu „Bakalárska práca 2024“.", + "engine": "rag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Vladimír Ferko", + "work_type": "bachelor_thesis", + "year": 2024 + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2021/vladimir_ferko" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "af44359ab30c1eb16eaa4e4ac972cde0cb7fe36f0b8fbc786f910f75dcef9ebe" + } + }, + { + "id": "C009", + "query": "U osoby Vladimír Ferko uveď typ a evidovaný rok záznamu „Bakalárska práca 2024“.", + "engine": "graphrag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Vladimír Ferko", + "year": 2024, + "work_type": "bachelor_thesis" + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2021/vladimir_ferko" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "a362e5e6a86dd6e8653e1c379a0bdbf6f06bb1a1922eadee332ce41ffd1ef6bb" + } + }, + { + "id": "C017", + "query": "U osoby Dávid Omasta uveď typ a evidovaný rok záznamu „Diplomová práca 2024“.", + "engine": "rag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Dávid Omasta", + "work_type": "diploma_thesis", + "year": 2024 + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2018/david_omasta" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "df566cba179452b6ec761abf6e11ce5d46c1b622a35eb4f704f510cc6579a05a" + } + }, + { + "id": "C017", + "query": "U osoby Dávid Omasta uveď typ a evidovaný rok záznamu „Diplomová práca 2024“.", + "engine": "graphrag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Dávid Omasta", + "year": 2024, + "work_type": "diploma_thesis" + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2018/david_omasta" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "3e70a1633a926ed3e1c6c1b86601a587b6cd9f48b80c7b002aea56ee154548bd" + } + }, + { + "id": "C025", + "query": "U osoby Matej Ščišľak uveď typ a evidovaný rok záznamu „Bakalárska práca 2024“.", + "engine": "rag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Matej Ščišľak", + "work_type": "bachelor_thesis", + "year": 2024 + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2021/matej_scislak" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "9817ffbd759ab19997a4ebc5c6ee86e8482bf9d687a91851002cb45e29f7e703" + } + }, + { + "id": "C025", + "query": "U osoby Matej Ščišľak uveď typ a evidovaný rok záznamu „Bakalárska práca 2024“.", + "engine": "graphrag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Matej Ščišľak", + "year": 2024, + "work_type": "bachelor_thesis" + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2021/matej_scislak" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "21abb9ffd01fab89d9b8cd5d198348f35d67b6f50869cdb529d2f0f459057273" + } + }, + { + "id": "C033", + "query": "U osoby Yevhenii Leonov uveď typ a evidovaný rok záznamu „Bakalárska práca 2025“.", + "engine": "rag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Yevhenii Leonov", + "work_type": "bachelor_thesis", + "year": 2025 + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2022/yevhenii_leonov" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "20998da90b31479cbb52deed9c295f506ff7f0ad2ced6e67c99fbb040f108431" + } + }, + { + "id": "C033", + "query": "U osoby Yevhenii Leonov uveď typ a evidovaný rok záznamu „Bakalárska práca 2025“.", + "engine": "graphrag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Yevhenii Leonov", + "year": 2025, + "work_type": "bachelor_thesis" + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2022/yevhenii_leonov" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "c03cd06974d8a3e12ed0585aac192112b2a435ae0d19d324e504c896e3fb37a5" + } + }, + { + "id": "C041", + "query": "U osoby Serhii Yemets uveď typ a evidovaný rok záznamu „Bakalárska práca 2025“.", + "engine": "rag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Serhii Yemets", + "work_type": "bachelor_thesis", + "year": 2025 + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2022/serhii_yemets" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "7e4abcd74aceee1b9970bdea9af2078f91d863ed9e3752df7ca2c97fd29bdab7" + } + }, + { + "id": "C041", + "query": "U osoby Serhii Yemets uveď typ a evidovaný rok záznamu „Bakalárska práca 2025“.", + "engine": "graphrag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Serhii Yemets", + "year": 2025, + "work_type": "bachelor_thesis" + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2022/serhii_yemets" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "22ddb6a34f509f28734bc307ef84aea5f22eb47ab39d4b184492b3b1e5b6f8ce" + } + }, + { + "id": "C049", + "query": "U osoby Artur Hyrenko uveď typ a evidovaný rok záznamu „Diplomová práca 2026“.", + "engine": "rag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Artur Hyrenko", + "work_type": "diploma_thesis", + "year": 2026 + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2021/artur_hyrenko" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "f308ccc97c0bb5a93d109477f3e977eb67b887480ea5f952801ef4fe4f23b59b" + } + }, + { + "id": "C049", + "query": "U osoby Artur Hyrenko uveď typ a evidovaný rok záznamu „Diplomová práca 2026“.", + "engine": "graphrag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Artur Hyrenko", + "year": 2026, + "work_type": "diploma_thesis" + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2021/artur_hyrenko" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "db6a19db9eca2a43c0483c239d8184b0fd1163f48bd5cd201a0b0a778df60af5" + } + }, + { + "id": "C057", + "query": "U osoby Nikita Bodnar uveď typ a evidovaný rok záznamu „Bakalárska práca 2024“.", + "engine": "rag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Nikita Bodnar", + "work_type": "bachelor_thesis", + "year": 2024 + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2021/nikita_bodnar" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "563cd03a1b203ffe0bbbac07d9e8d202a1d02f0fdce017a2506165c2b4638300" + } + }, + { + "id": "C057", + "query": "U osoby Nikita Bodnar uveď typ a evidovaný rok záznamu „Bakalárska práca 2024“.", + "engine": "graphrag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Nikita Bodnar", + "year": 2024, + "work_type": "bachelor_thesis" + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2021/nikita_bodnar" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "a3da35c1976f1117d52a48f2ffdd280b7ebbccd2020d3eda1be16f059084f261" + } + }, + { + "id": "C065", + "query": "U osoby Dávid Kostilník uveď typ a evidovaný rok záznamu „Diplomová práca 2025“.", + "engine": "rag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Dávid Kostilník", + "work_type": "diploma_thesis", + "year": 2025 + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2020/david_kostilnik" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "644f469d2be50b94d6458cb53cbb2e781f2ce0ef34d94e05966139db9754dd94" + } + }, + { + "id": "C065", + "query": "U osoby Dávid Kostilník uveď typ a evidovaný rok záznamu „Diplomová práca 2025“.", + "engine": "graphrag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Dávid Kostilník", + "year": 2025, + "work_type": "diploma_thesis" + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2020/david_kostilnik" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "bc9ecfd809e4acaad3a468d538637f651b3e94b6737d8858664bb9db2ec9ada2" + } + }, + { + "id": "C073", + "query": "U osoby Valerii Kutsenko uveď typ a evidovaný rok záznamu „Bakalárska práca 2025“.", + "engine": "rag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Valerii Kutsenko", + "work_type": "bachelor_thesis", + "year": 2025 + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2022/valerii_kutsenko" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "48b8745ac97968457129947083dd7a7230b2ff4e7c3c1884b9d3fd726f244085" + } + }, + { + "id": "C073", + "query": "U osoby Valerii Kutsenko uveď typ a evidovaný rok záznamu „Bakalárska práca 2025“.", + "engine": "graphrag", + "passed": true, + "status": "ok", + "reason": null, + "claims": [ + { + "person": "Valerii Kutsenko", + "year": 2025, + "work_type": "bachelor_thesis" + } + ], + "source_urls": [ + "https://zp.kemt.fei.tuke.sk/students/2022/valerii_kutsenko" + ], + "delivery_check": { + "layer": "delivery", + "code": "complete", + "request_fingerprint": "e97f121dc3d87415b2eeaeff02a089e65a612727b84fb96d0b20775dae865566" + } + } + ] +} diff --git a/test/test_delivery_bindings.py b/test/test_delivery_bindings.py new file mode 100644 index 0000000..3ca4d17 --- /dev/null +++ b/test/test_delivery_bindings.py @@ -0,0 +1,71 @@ +"""Source identity survives presentation field selection in both retrievers.""" +import copy +import pytest +from tools.answers import delivery_contract as contract +from tools.answers.graph_context import delivery_facts +from tools.answers.question_plan import QuestionPlan, PersonRequest, WorkRequest +from tools.answers.verified_rag import generate_verified_rag + + +URL='https://example.test/eva' + + +def plan(): + return QuestionPlan(question='Aký typ a rok má záznam „Diplomová práca 2027“ osoby Eva Nová?', + people=[PersonRequest(name='Eva Nová',source_document='pages/students/2020/eva/README.md')], + works=[WorkRequest(person='Eva Nová',work_type='diploma_thesis',years=[2027],labels=['diplomova praca 2027'])], + fields=['work_type','year'],scope='single_work') + + +def output(claims): + return {'status':'ok','accepted_for_delivery':True,'answer':'Eva Nová: diplomová práca, rok 2027.\nZdroj: '+URL, + 'claims':claims,'source_urls':[URL],'evidence':[]} + + +def test_graph_source_scope_is_preserved_without_adding_unrequested_fields_to_prompt(): + fact={'person':'Eva Nová','work_type':'diploma_thesis','year':2027,'source_url':URL} + context={'facts':[fact]};before=copy.deepcopy(context) + raw={'facts':[{'person':'Eva Nová','thesis_type':'diploma_thesis','year':2027, + 'heading':'Diplomová práca 2027','thesis_id':'work-1','source_url':URL,'fact_type':'person_thesis'}]} + p=plan();result=output([{'person':'Eva Nová','work_type':'diploma_thesis','year':2027}]) + assert not contract.enforce(p.question,'graphrag',p,result,facts=context['facts'])['accepted_for_delivery'] + bindings=delivery_facts(context,raw) + assert contract.enforce(p.question,'graphrag',p,result,facts=bindings)['accepted_for_delivery'] + assert context==before and bindings[-1]['record_id']=='work-1' + + +def test_person_tag_requires_separate_source_backed_work_for_a_conjunction(): + p=plan();p.question='Ktorí študenti majú tag NLP a diplomovú prácu z roku 2027?' + p.fields=['keyword'];p.presentation='list' + fact={'person':'Eva Nová','keyword':'NLP','source_url':URL} + result=output([{'person':'Eva Nová','keyword':'NLP'}]) + result['answer']='Eva Nová má tag NLP.\nZdroj: '+URL + assert not contract.enforce(p.question,'graphrag',p,result,facts=[fact])['accepted_for_delivery'] + work={'person':'Eva Nová','work_type':'diploma_thesis','year':2027,'work_label':'Diplomová práca 2027','source_url':URL} + assert contract.enforce(p.question,'graphrag',p,result,facts=[fact,work])['accepted_for_delivery'] + for change in [{'year':2024},{'source_url':'https://example.test/other'},{'person':'Dana Iná'}]: + assert not contract.enforce(p.question,'graphrag',p,result,facts=[fact,{**work,**change}])['accepted_for_delivery'] + + +def test_rag_binds_actual_prepared_facts_instead_of_incomplete_model_claims(): + p=plan() + payload={'status':'ok','reason':None,'index_version':{'version':'fixture'},'complete':True,'truncated':False, + 'truncation_reasons':[],'named_scope_complete':True,'question_plan':p.model_dump(), + 'sources':[{'source_id':'S1','title':'Eva Nová','document_path':'pages/students/2020/eva/README.md', + 'source_url':URL,'categories':['dp2027'],'text':'# Diplomová práca 2027\nNázov: Pokusný systém.\n'}]} + from tools.rag.rag_utils import build_context_text + payload['context']=build_context_text(payload['sources']) + def forbidden(_):pytest.fail('Exact metadata needs no language model') + plain=generate_verified_rag(p.question,payload,forbidden) + assert '_delivery_facts' not in plain + result=generate_verified_rag(p.question,payload,forbidden,include_bindings=True) + facts=result.pop('_delivery_facts') + assert any(f.get('work_label')=='Diplomová práca 2027' for f in facts) + assert contract.enforce(p.question,'rag',p,result,facts=facts)['accepted_for_delivery'] + + +def test_rejected_verification_is_not_diagnosed_as_provider_unavailability(): + rejected=contract.diagnose({'status':'error','reason':'answer_rejected'},'q','rag') + unavailable=contract.diagnose({'status':'error','reason':'verification_unavailable'},'q','rag') + assert rejected['delivery_check']['code']=='request_not_fulfilled' + assert unavailable['delivery_check']['code']=='unavailable' diff --git a/test/test_person_names.py b/test/test_person_names.py index 1870b2a..8e142b6 100644 --- a/test/test_person_names.py +++ b/test/test_person_names.py @@ -357,7 +357,7 @@ def test_both_engines_use_same_resolution_and_notice(names_db, people, monkeypat monkeypatch.setattr(delivery, "prepare_named_content", lambda db, q, p: p) monkeypatch.setattr(delivery, "compact_rag_payload", lambda p: p) - def rag(q, p, model): + def rag(q, p, model, **kwargs): assert q == resolution["resolved_query"] return dict(checked) diff --git a/tools/answers/answer_delivery.py b/tools/answers/answer_delivery.py index 8660aea..de04aa6 100644 --- a/tools/answers/answer_delivery.py +++ b/tools/answers/answer_delivery.py @@ -9,7 +9,7 @@ from tools.rag.common import DB_FILE from tools.rag.rag_utils import build_rag_context from tools.rag.llm_context import compact_rag_payload from tools.graphrag.graph_search import search_graph -from tools.answers.graph_context import graph_context +from tools.answers.graph_context import graph_context, delivery_facts from tools.answers.checked_generation import generate_checked_answer from tools.answers.verified_rag import generate_verified_rag from tools.answers.rag_evidence import expand_comparison_sources @@ -204,7 +204,8 @@ def _deliver_answer( retrieved = compact_rag_payload(retrieved) except Exception: return retrieval_failure(engine) - result = generate_verified_rag(question, retrieved, model) + result = generate_verified_rag(question, retrieved, model, include_bindings=True) + facts = result.pop('_delivery_facts', []) context = retrieved elif engine == "graphrag": if plan.spec.intent != "metadata": @@ -224,6 +225,7 @@ def _deliver_answer( return retrieval_failure(engine) retrieved["name_resolution"] = plan.resolution context = graph_context(retrieved) + facts = delivery_facts(context, retrieved) except Exception: return retrieval_failure(engine) if context.get("clarification"): @@ -269,6 +271,6 @@ def _deliver_answer( **result, "engine": engine, **({"retrieval_context": context, "question_plan": plan.model_dump()} if include_diagnostics else {}), - },context.get('facts',[])) + },facts) finally: _capacity.release() diff --git a/tools/answers/delivery_contract.py b/tools/answers/delivery_contract.py index f79202b..9ebff7c 100644 --- a/tools/answers/delivery_contract.py +++ b/tools/answers/delivery_contract.py @@ -75,7 +75,8 @@ def diagnose(result,query,engine): 'planning' if result.get('status')=='ambiguous' else 'retrieval' if result.get('status')=='no_answer' or reason in {'retrieval_unavailable','content_index_unavailable'} else 'verification' if reason in {'answer_rejected','verification_unavailable'} else 'service') - code={'ambiguous':'clarification_required','no_answer':'evidence_not_found'}.get(result.get('status'),'unavailable') + code=('request_not_fulfilled' if reason=='answer_rejected' else + {'ambiguous':'clarification_required','no_answer':'evidence_not_found'}.get(result.get('status'),'unavailable')) return {**result,'delivery_check':{'layer':layer,'code':code,'request_fingerprint':fingerprint(query,engine)}} @@ -138,7 +139,14 @@ def mechanical_check(plan,result,index=None,*,facts=()): k not in claim or claim[k] is None or r.get(k) is not None and _same(claim[k],r[k]) for k in ['record_id','work_label','work_type','title','year','start_year','keyword','category','document_author'])] if candidates: - if not any(plan.matches_work(person,r.get('work_label'),r.get('work_type'),r.get('year')) for r in candidates): + # Person-level metadata (e.g. a tag) can be conjoined with a work + # condition, but cannot itself prove that work exists. Require a + # separate scoped fact from the same person and source document. + scoped = candidates + if not any(claim.get(k) is not None for k in ['record_id','work_label','work_type','title','year']): + urls={r.get('source_url') for r in candidates if r.get('source_url')} + scoped=[r for r in facts if r.get('person')==person and (not urls or r.get('source_url') in urls)] + if not any(plan.matches_work(person,r.get('work_label'),r.get('work_type'),r.get('year')) for r in scoped): raise ContractError('scope','wrong_work_or_meeting') elif facts: raise ContractError('evidence','altered_evidence') diff --git a/tools/answers/graph_context.py b/tools/answers/graph_context.py index d84958e..5f5e21e 100644 --- a/tools/answers/graph_context.py +++ b/tools/answers/graph_context.py @@ -3,6 +3,24 @@ from tools.answers.context_policy import joined_candidates +def delivery_facts(context, result): + """Preserve source work identity independently of the model's field subset. + + A question can ask for type/year without asking to display a record ID or + heading. The final scope check still needs those trusted source fields. + These extra bindings are not added to the language model's payload. + """ + facts = list(context.get('facts', [])) + for row in [*result.get('facts', []), *result.get('supporting_facts', [])]: + if not row.get('person') or row.get('fact_type') not in {'person_thesis', 'person_work'}: + continue + fact = {k: row[k] for k in ['person', 'title', 'year', 'start_year', 'source_url', 'document_path'] if k in row} + fact.update(work_type=row.get('thesis_type') or row.get('work_type'), + work_label=row.get('heading'), record_id=row.get('thesis_id') or row.get('work_id')) + facts.append(fact) + return facts + + def graph_context(result: dict) -> dict: """Remove storage/profile metadata that cannot answer the actual question. diff --git a/tools/answers/verified_rag.py b/tools/answers/verified_rag.py index 899b22c..164b195 100644 --- a/tools/answers/verified_rag.py +++ b/tools/answers/verified_rag.py @@ -792,8 +792,16 @@ def render_rag_answer(answer: dict, context: dict) -> str: return clean_answer(lines) -def generate_verified_rag(question: str, payload: dict, complete) -> dict: +def generate_verified_rag(question: str, payload: dict, complete, *, include_bindings=False) -> dict: context = rag_evidence_context(question, payload) + result = _generate_from_context(question, payload, complete, context) + if include_bindings: + # Server-only provenance used by the final contract, never model output. + result = {**result, '_delivery_facts': context['facts']} + return result + + +def _generate_from_context(question, payload, complete, context): if payload.get("status") == "ambiguous" or context.get("clarification"): return { "status": "ambiguous",