dp-zp-agent/tools/answer_evaluation/check_context_scenarios.py

100 lines
6.2 KiB
Python

"""Live synthetic functional checks; separate from corpus accuracy metrics."""
import argparse
import concurrent.futures
import copy
import json
import os
from pathlib import Path
from tools.rag.evaluation.rag_runner import request_json, OPENWEBUI_URL, DEFAULT_MODEL
from tools.graphrag.evaluation.reproducibility import sha256_file
from .checked_generation import generate_checked_answer
from .context_policy import joined_candidates
HERE = Path(__file__).parent
def scenarios():
base = {'status': 'ok', 'reason': None, 'complete': True, 'truncated': False,
'truncation_reasons': [], 'index_version': {'version': 'synthetic-functional-fixture'},
'sources': [{'source_id': 'S1', 'document_path': 'a.md', 'source_url': 'https://example.test/a'}],
'facts': [{'person': 'Eva Vzorová', 'title': 'Vyhľadávanie informácií', 'year': 2026,
'document_path': 'a.md', 'source_url': 'https://example.test/a'}],
'aggregation': None, 'answer_candidates': []}
query = 'Uveď názov práce osoby Eva Vzorová a rok práce.'
result = []
for name, fields in [
('complete', {}), ('incomplete_without_truncated', {'complete': False}),
('truncated', {'complete': False, 'truncated': True, 'truncation_reasons': ['limit']}),
('unknown_completeness', {'complete': None})]:
context = copy.deepcopy(base); context.update(fields)
result.append({'id': name, 'question': query, 'context': context,
'expected': {'status': 'ok', 'claims': [{'person': 'Eva Vzorová',
'title': 'Vyhľadávanie informácií', 'year': 2026}]}})
context = copy.deepcopy(base)
context.update(complete=False, truncated=True, truncation_reasons=['evidence_limit'],
aggregation={'count': 2, 'complete': True})
result.append({'id': 'exact_count_partial_evidence', 'question': 'Aký je počet vyhovujúcich osôb?',
'context': context, 'expected': {'status': 'ok', 'count': 2}})
context = copy.deepcopy(base)
context['sources'].append({'source_id': 'S2', 'document_path': 'child.md', 'source_url': 'https://example.test/child'})
context['facts'].append({'person': 'Eva Vzorová', 'keyword': 'python',
'document_path': 'child.md', 'source_url': 'https://example.test/child'})
context['answer_candidates'] = joined_candidates(context['facts'], {'mentions_title': True, 'mentions_keyword': True})
result.append({'id': 'same_person_child_tag',
'question': 'Uveď názov práce osoby Eva Vzorová v roku 2026 s kľúčovým slovom python.',
'context': context, 'expected': {'status': 'ok', 'claims': [
{'person': 'Eva Vzorová', 'title': 'Vyhľadávanie informácií', 'year': 2026, 'keyword': 'python'}]}})
context = copy.deepcopy(context)
context['facts'][0].pop('title')
context['answer_candidates'] = joined_candidates(context['facts'], {'years': [2026], 'mentions_keyword': True})
result.append({'id': 'same_person_year_child_tag',
'question': 'Ktorá osoba spĺňa kľúčové slovo python a rok práce 2026?',
'context': context, 'expected': {'status': 'ok', 'claims': [
{'person': 'Eva Vzorová', 'year': 2026, 'keyword': 'python'}]}})
result.append({'id': 'type_is_not_keyword', 'question': 'Uveď názov diplomového projektu osoby Eva Vzorová a rok.',
'context': copy.deepcopy(base), 'expected': {'status': 'ok', 'no_keyword': True}})
context = copy.deepcopy(base)
context.update(status='no_answer', reason='unknown_person', facts=[], sources=[])
result.append({'id': 'no_answer', 'question': 'Akú prácu má Neexistujúca Osoba?',
'context': context, 'expected': {'status': 'no_answer'}})
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
if args.output.exists():
parser.error('Refusing to overwrite results.')
files = [Path(__file__), HERE/'graph_answer_prompt.txt', HERE/'context_policy.py', HERE/'checked_generation.py', HERE/'answer_format.py', HERE/'render_answer.py']
hashes = {str(p): sha256_file(p) for p in files}
declared = scenarios()
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.with_suffix('.declaration.json').write_text(json.dumps(
{'method': __doc__, 'model': DEFAULT_MODEL, 'hashes': hashes, 'scenarios': declared}, ensure_ascii=False, indent=2)+'\n')
prompt = (HERE/'graph_answer_prompt.txt').read_text()
def run(case):
def complete(messages):
return request_json(OPENWEBUI_URL, method='POST', timeout=90, max_attempts=2,
headers={'Authorization': 'Bearer '+os.environ['OPENWEBUI_API_KEY'], 'Content-Type': 'application/json'},
payload={'model': DEFAULT_MODEL, 'temperature': 0, 'stream': False,
'response_format': {'type': 'json_object'}, 'messages': messages})
out = generate_checked_answer(case['question'], case['context'], prompt, complete)
answer = out.get('answer', {}); expected = case['expected']
ok = answer.get('status') == expected['status']
for binding in expected.get('claims', []):
ok &= any(all(claim.get(k) == v for k, v in binding.items()) for claim in answer.get('claims', []))
if 'count' in expected: ok &= answer.get('count') == expected['count']
if expected.get('no_keyword'): ok &= not any(c.get('keyword') for c in answer.get('claims', []))
return {'id': case['id'], **out, 'passed': bool(ok and out['accepted_for_delivery'])}
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
rows = list(executor.map(run, declared))
assert hashes == {str(p): sha256_file(p) for p in files}
output = {'method': __doc__, 'questions': rows, 'summary': {'total': len(rows), 'passed': sum(r['passed'] for r in rows)}}
args.output.write_text(json.dumps(output, ensure_ascii=False, indent=2)+'\n')
print(json.dumps(output['summary']))
if __name__ == '__main__': main()