222 lines
12 KiB
Python
222 lines
12 KiB
Python
"""Content questions must get source passages, not a metadata-only substitute."""
|
|
import copy
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from tools.answer_evaluation.rag_evidence import rag_evidence_context
|
|
from tools.answer_evaluation.verified_rag import generate_verified_rag, rag_answer_issues
|
|
from tools.rag.llm_context import compact_rag_payload
|
|
from tools.rag.rag_utils import build_context_text
|
|
|
|
QUESTION = 'Čomu sa venuje diplomová práca Jána Holpa? Odpovedz podľa dostupných podkladov a uveď zdroj.'
|
|
ASSIGNMENT = ('1. Vypracujte prehľad metód ohodnotenia dokumentov v systémoch získavania informácií.\n'
|
|
'2. Implementujte systém pre získavanie informácií v slovenskom jazyku.')
|
|
URL = 'https://example.test/jan'
|
|
|
|
|
|
@pytest.fixture
|
|
def payload():
|
|
sources = [
|
|
{'source_id': 'S1', 'title': 'Ján Holp', 'author': 'Daniel Hladek',
|
|
'document_path': 'pages/students/2016/jan_holp/README.md', 'source_url': URL,
|
|
'text': 'Dokument: Ján Holp\nSekcia: Diplomová práca 2021\n'
|
|
'Názov: Systém získavania informácií v slovenskom jazyku.\n'
|
|
'Meno vedúceho: Ing. Daniel Hladek, PhD.\nZadanie diplomovej práce:\n' + ASSIGNMENT +
|
|
'\n## Metódy\nVyhodnotenie využíva PageRank.\n'
|
|
'## Bakalárska práca 2019\nNázov: Vyhľadávač na slovenskom internete.\n'
|
|
'Práca implementuje prehľadávanie slovenského webu.\n'
|
|
'## Diplomový projekt 2020\nNázov: Pokusy s databázou.\n'
|
|
'Projekt využíva databázu Cassandra.'},
|
|
{'source_id': 'S2', 'title': 'Boris Nový',
|
|
'document_path': 'pages/students/2020/boris_novy/README.md',
|
|
'source_url': 'https://example.test/boris',
|
|
'text': 'Sekcia: Diplomová práca 2021\nNázov: Spracovanie obrázkov.\n'
|
|
'Práca skúma klasifikáciu medicínskych obrázkov.'}]
|
|
return {'status': 'ok', 'reason': None, 'sources': sources, 'context': build_context_text(sources),
|
|
'complete': False, 'truncated': True, 'truncation_reasons': ['result_limit'],
|
|
'index_version': {'version': 'fixture'}}
|
|
|
|
|
|
def description():
|
|
return {'answer': '', 'status': 'ok', 'claims': [], 'count': None,
|
|
'source_urls': [URL], 'evidence_quotes': [{'source_id': 'S1', 'quote': ASSIGNMENT}]}
|
|
|
|
|
|
def metadata():
|
|
return {'answer': '', 'status': 'ok', 'claims': [{'person': 'Ján Holp', 'work_type': 'diploma_thesis',
|
|
'title': 'Systém získavania informácií v slovenskom jazyku', 'year': 2021}],
|
|
'evidence_quotes': [], 'source_urls': [URL]}
|
|
|
|
|
|
def completion(answer):
|
|
return {'choices': [{'message': {'content': json.dumps(answer, ensure_ascii=False)}}]}
|
|
|
|
|
|
@pytest.mark.parametrize('question', [QUESTION,
|
|
'O čom je diplomová práca Jána Holpa?',
|
|
'Čomu sa venovala diplomová práca Jána Holpa?',
|
|
'Čo rieši diplomová práca Jána Holpa?',
|
|
'Stručne opíš diplomovú prácu Jána Holpa.',
|
|
'Zhrň obsah diplomovej práce Jána Holpa.',
|
|
'Aké sú ciele diplomovej práce Jána Holpa?',
|
|
'Aké je zadanie diplomovej práce Jána Holpa?',
|
|
'Aké metódy používa diplomová práca Jána Holpa?'])
|
|
def test_content_intent_is_not_a_title_or_year_lookup(payload, question):
|
|
c = rag_evidence_context(question, payload)
|
|
assert c['requirements']['content']
|
|
assert c['requirements']['response_mode'] == 'quotes'
|
|
assert not c['requirements']['title'] and not c['requirements']['year']
|
|
assert any(ASSIGNMENT in p['text'] for p in c['content_passages'])
|
|
|
|
|
|
def test_named_work_content_without_explicit_work_type_still_needs_text(payload):
|
|
c = rag_evidence_context('Opíš obsah práce Jána Holpa.', payload)
|
|
assert c['requirements']['content'] and c['requirements']['response_mode'] == 'quotes'
|
|
assert 'missing_content_evidence' in rag_answer_issues(metadata(), c)
|
|
|
|
|
|
@pytest.mark.parametrize('question', [
|
|
'Aký je názov diplomovej práce Jána Holpa?',
|
|
'Aký je názov diplomovej práce Jána Holpa a z ktorého roku je?',
|
|
'Uveď názov a rok diplomovej práce Jána Holpa.',
|
|
'V ktorom roku je diplomová práca Jána Holpa?'])
|
|
def test_identity_questions_keep_the_existing_typed_binding(payload, question):
|
|
c = rag_evidence_context(question, payload)
|
|
assert c['requirements']['response_mode'] == 'claims'
|
|
assert not c['requirements']['content']
|
|
assert rag_answer_issues(metadata(), c) == []
|
|
bad = metadata(); bad['claims'][0]['year'] = 2019
|
|
assert 'unsupported_person_work_type_year_binding' in rag_answer_issues(bad, c)
|
|
|
|
|
|
@pytest.mark.parametrize('question', [
|
|
'Aký je názov diplomovej práce Jána Holpa, ktorá používa PageRank?',
|
|
'Aký je názov diplomovej práce Jána Holpa o metódach vyhľadávania?',
|
|
'Uveď názov diplomovej práce Jána Holpa „Metódy a obsah implementácie“.'])
|
|
def test_topic_words_in_identity_question_do_not_request_a_description(payload, question):
|
|
c = rag_evidence_context(question, payload)
|
|
assert not c['requirements']['content'] and c['requirements']['response_mode'] == 'claims'
|
|
|
|
|
|
def test_passages_exclude_other_people_and_other_work_types(payload):
|
|
passages = rag_evidence_context(QUESTION, payload)['content_passages']
|
|
assert passages and all(p['source_id'] == 'S1' and p['work_type'] == 'diploma_thesis' for p in passages)
|
|
assert all('Cassandra' not in p['text'] and 'medicínskych' not in p['text'] for p in passages)
|
|
|
|
|
|
@pytest.mark.parametrize('year,expected', [(2021, True), (1999, False)])
|
|
def test_year_constraint_also_applies_to_content(payload, year, expected):
|
|
c = rag_evidence_context(f'Opíš obsah diplomovej práce Jána Holpa v roku {year}.', payload)
|
|
assert bool(c['content_passages']) == expected
|
|
assert all(p['year'] == year for p in c['content_passages'])
|
|
if not expected:
|
|
assert 'quote_outside_requested_work' in rag_answer_issues(description(), c)
|
|
refusal = {'status': 'no_answer', 'claims': [], 'evidence_quotes': [], 'source_urls': []}
|
|
assert rag_answer_issues(refusal, c) == []
|
|
|
|
|
|
@pytest.mark.parametrize('quote', ['Názov: Systém získavania informácií v slovenskom jazyku.',
|
|
'Systém získavania informácií v slovenskom jazyku.',
|
|
'Meno vedúceho: Ing. Daniel Hladek, PhD.', 'Zadanie diplomovej práce:'])
|
|
def test_metadata_or_an_assignment_heading_does_not_describe_work(payload, quote):
|
|
c = rag_evidence_context(QUESTION, payload)
|
|
answer = description(); answer['evidence_quotes'][0]['quote'] = quote
|
|
assert 'metadata_quote_does_not_describe_work' in rag_answer_issues(answer, c)
|
|
|
|
|
|
def test_metadata_claims_alone_are_rejected(payload):
|
|
issues = rag_answer_issues(metadata(), rag_evidence_context(QUESTION, payload))
|
|
assert 'missing_content_evidence' in issues and 'unrequested_structured_claims' in issues
|
|
|
|
|
|
@pytest.mark.parametrize('source_id,quote', [
|
|
('S1', 'Práca implementuje prehľadávanie slovenského webu.'),
|
|
('S1', 'Projekt využíva databázu Cassandra.'),
|
|
('S2', 'Práca skúma klasifikáciu medicínskych obrázkov.')])
|
|
def test_literal_quote_from_wrong_work_or_person_is_rejected(payload, source_id, quote):
|
|
answer = description(); answer['source_urls'] += ['https://example.test/boris']
|
|
answer['evidence_quotes'] = [{'source_id': source_id, 'quote': quote}]
|
|
assert 'quote_outside_requested_work' in rag_answer_issues(answer, rag_evidence_context(QUESTION, payload))
|
|
|
|
|
|
def test_correction_sends_real_description_but_no_unrelated_sections(payload):
|
|
payload = compact_rag_payload(payload)
|
|
outputs = iter([metadata(), description()]); calls = []
|
|
def complete(messages):
|
|
calls.append(copy.deepcopy(messages))
|
|
return completion(next(outputs))
|
|
result = generate_verified_rag(QUESTION, payload, complete)
|
|
assert result['accepted_for_delivery'] and len(calls) == 2
|
|
sent = json.loads(calls[0][1]['content'])['context']
|
|
assert 'facts' not in sent and 'context' not in sent
|
|
text = json.dumps(sent['content_passages'], ensure_ascii=False)
|
|
assert 'ohodnotenia dokumentov' in text and 'Cassandra' not in text and 'medicínskych' not in text
|
|
assert 'missing_content_evidence' in result['attempts'][0]['issues']
|
|
assert ASSIGNMENT in result['answer'] and 'Výpis je neúplný.' not in result['answer']
|
|
assert result['complete'] is False and result['answer_coverage']['status'] == 'supported'
|
|
assert 'expected_bindings' not in json.dumps(calls)
|
|
|
|
|
|
def test_two_metadata_only_attempts_never_reach_user(payload):
|
|
result = generate_verified_rag(QUESTION, payload, lambda _: completion(metadata()))
|
|
assert not result['accepted_for_delivery'] and len(result['attempts']) == 2
|
|
|
|
|
|
def test_name_year_and_description_can_be_requested_together(payload):
|
|
question = 'Uveď názov, rok a stručne opíš obsah diplomovej práce Jána Holpa.'
|
|
c = rag_evidence_context(question, payload)
|
|
assert c['requirements']['response_mode'] == 'mixed'
|
|
good = {**metadata(), 'evidence_quotes': description()['evidence_quotes']}
|
|
assert rag_answer_issues(good, c) == []
|
|
assert 'missing_content_evidence' in rag_answer_issues(metadata(), c)
|
|
assert 'missing_requested_title' in rag_answer_issues(description(), c)
|
|
|
|
|
|
def test_overview_with_documented_assignment_cannot_be_falsely_refused(payload):
|
|
answer = {'status': 'no_answer', 'claims': [], 'evidence_quotes': [], 'source_urls': []}
|
|
assert 'rejected_supported_work_description' in rag_answer_issues(answer, rag_evidence_context(QUESTION, payload))
|
|
|
|
|
|
def test_no_description_in_delivered_text_does_not_license_an_invented_one(payload):
|
|
payload['sources'] = [payload['sources'][0]]
|
|
payload['sources'][0]['text'] = 'Sekcia: Diplomová práca 2021\nNázov: Systém získavania informácií v slovenskom jazyku.'
|
|
payload['context'] = build_context_text(payload['sources'])
|
|
c = rag_evidence_context(QUESTION, payload)
|
|
assert c['content_passages'] == []
|
|
answer = {'status': 'no_answer', 'claims': [], 'evidence_quotes': [], 'source_urls': []}
|
|
assert rag_answer_issues(answer, c) == []
|
|
|
|
|
|
def test_a_request_for_results_is_not_answered_merely_by_a_proposed_assignment(payload):
|
|
c = rag_evidence_context('Zhrň dosiahnuté výsledky diplomovej práce Jána Holpa.', payload)
|
|
answer = {'status': 'no_answer', 'claims': [], 'evidence_quotes': [], 'source_urls': []}
|
|
assert rag_answer_issues(answer, c) == []
|
|
|
|
|
|
def test_same_person_other_diploma_year_does_not_satisfy_content_constraint(payload):
|
|
payload['sources'][0]['text'] += '\n## Diplomová práca 2022\nNázov: Iná diplomová téma.\nPráca skúma automatické preklady.'
|
|
payload['context'] = build_context_text(payload['sources'])
|
|
question = 'Uveď názov a rok a opíš diplomovú prácu Jána Holpa v roku 2021.'
|
|
c = rag_evidence_context(question, payload)
|
|
answer = metadata(); answer['evidence_quotes'] = [{'source_id': 'S1', 'quote': 'Práca skúma automatické preklady.'}]
|
|
assert 'quote_outside_requested_work' in rag_answer_issues(answer, c)
|
|
assert 'missing_bound_content_evidence' in rag_answer_issues(answer, c)
|
|
|
|
|
|
def test_mixed_answer_cannot_attach_another_years_description_to_its_claim(payload):
|
|
payload['sources'][0]['text'] += '\n## Diplomová práca 2022\nNázov: Iná diplomová téma.\nPráca skúma automatické preklady.'
|
|
payload['context'] = build_context_text(payload['sources'])
|
|
c = rag_evidence_context('Uveď názov, rok a opíš diplomovú prácu Jána Holpa.', payload)
|
|
answer = metadata(); answer['evidence_quotes'] = [{'source_id': 'S1', 'quote': 'Práca skúma automatické preklady.'}]
|
|
assert 'missing_bound_content_evidence' in rag_answer_issues(answer, c)
|
|
|
|
|
|
def test_comparing_contents_requires_evidence_for_each_requested_person(payload):
|
|
c = rag_evidence_context('Porovnaj obsah diplomových prác osôb Ján Holp a Boris Nový.', payload)
|
|
answer = description()
|
|
assert 'missing_bound_content_evidence' in rag_answer_issues(answer, c)
|
|
answer['evidence_quotes'].append({'source_id': 'S2', 'quote': 'Práca skúma klasifikáciu medicínskych obrázkov.'})
|
|
answer['source_urls'].append('https://example.test/boris')
|
|
assert rag_answer_issues(answer, c) == []
|