Zrozumiteľnejšie odpovede
This commit is contained in:
parent
4c8e494560
commit
a5dd917e6d
@ -1527,6 +1527,13 @@ aby dlhý prvý profil nevytlačil ostatné. Skrátenie rešpektuje celé odseky
|
||||
zoznamu vrátane ich výhrad; pri stručnom opise má zachovanie podmienky prednosť
|
||||
pred orientačným limitom dĺžky.
|
||||
|
||||
Krátke porovnanie zobrazuje rovnaké oblasti vedľa seba v tabuľke; dlhé výňatky,
|
||||
kód a odkazy zostávajú v samostatných sekciách bez úpravy ich obsahu. Prázdne
|
||||
pomocné sekcie sa nevypisujú a chýbajúce oblasti sa konkrétne pomenujú pri danej
|
||||
osobe. Spoločné vlastnosti sa uvádzajú najprv súhrnne a potom s doloženými
|
||||
osobami a zdrojmi. Stručnejšie formulácie nemenia rok práce na dátum jej
|
||||
začatia alebo dokončenia a ciele neprezentujú ako dosiahnuté výsledky.
|
||||
|
||||
Rozpoznané, ale nepodporované operátory (napríklad interval rokov, vylučovanie
|
||||
alebo minimálny počet) vedú k `ambiguous` a konkrétnemu spresneniu. Rok práce
|
||||
sa nepoužije ako dôkaz dátumu jej začatia či obhajoby. Chybové odpovede odlišujú
|
||||
|
||||
158
test/test_answer_clarity.py
Normal file
158
test/test_answer_clarity.py
Normal file
@ -0,0 +1,158 @@
|
||||
"""Concise presentation must keep factual bindings, qualifications and gaps."""
|
||||
import copy
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
|
||||
from test_quality_block import corpus, prepare, complete
|
||||
from test_evidence_selection import update_eva, selection
|
||||
from tools.answer_evaluation.context_policy import answer_context_issues
|
||||
from tools.answer_evaluation.content_selection import facet_lines
|
||||
from tools.answer_evaluation.graph_context import graph_context
|
||||
from tools.answer_evaluation.presentation import comparison_table
|
||||
from tools.answer_evaluation.rag_evidence import rag_evidence_context
|
||||
from tools.answer_evaluation.render_answer import render_graph_answer
|
||||
from tools.answer_evaluation.verified_rag import generate_verified_rag, rag_answer_issues
|
||||
|
||||
|
||||
def test_missing_aspects_share_one_specific_notice_without_empty_sections(corpus):
|
||||
update_eva(corpus,'Stav:\n- Implementovaný parser.')
|
||||
q='Aké výsledky, problémy a plánované experimenty má diplomová práca Evy Novej?'
|
||||
p=prepare(corpus,q); c=rag_evidence_context(q,p)
|
||||
result=generate_verified_rag(q,p,complete(selection(c)))
|
||||
assert result['accepted_for_delivery'] and result['answer_coverage']['status']=='partial'
|
||||
text=result['answer']
|
||||
assert 'Implementovaný parser.' in text
|
||||
assert text.count('V poskytnutých podkladoch chýbajú údaje')==1
|
||||
assert 'riešených problémoch, plánovaných úlohách a plánovaných experimentoch' in text
|
||||
assert '**Riešené problémy:**' not in text and '**Plánované experimenty' not in text
|
||||
assert 'Táto časť odpovede' not in text and '\n\n\n' not in text
|
||||
|
||||
|
||||
def test_short_comparison_aligns_people_and_different_results(corpus):
|
||||
q='Porovnaj výsledky Evy Novej a Borisa Nového.'
|
||||
p=prepare(corpus,q); c=rag_evidence_context(q,p); a=selection(c)
|
||||
before=copy.deepcopy((a,c))
|
||||
lines=facet_lines(a,c); text='\n'.join(lines)
|
||||
assert text.startswith('| Oblasť | Eva Nová — diplomová práca (2024) | Boris Nový — bakalárska práca (2023) |')
|
||||
row=next(line for line in lines if line.startswith('| Zaznamenané výsledky |'))
|
||||
assert 'BERT' in row.split('|')[2] and 'Python' in row.split('|')[3]
|
||||
assert 'Konzultácia' not in text and 'Pripravte experiment' not in text
|
||||
assert 'lepš' not in text and (a,c)==before
|
||||
r=generate_verified_rag(q,p,complete(a))
|
||||
assert r['accepted_for_delivery'] and len(r['source_urls'])==2
|
||||
|
||||
|
||||
@pytest.mark.parametrize('literal',[
|
||||
'Implementovaný parser. Výsledok platí iba pre malý korpus. '+('Technický opis. '*70),
|
||||
'Implementovaný parser pre `a | b` iba na testovacích dátach.',
|
||||
'Implementovaný parser podľa https://example.test/method iba na testovacích dátach.',
|
||||
])
|
||||
def test_complex_comparisons_fall_back_without_losing_qualifiers_or_literals(corpus,literal):
|
||||
update_eva(corpus,'Výsledky:\n- '+literal)
|
||||
q='Porovnaj výsledky Evy Novej a Borisa Nového.'
|
||||
p=prepare(corpus,q); c=rag_evidence_context(q,p); a=selection(c)
|
||||
text='\n'.join(facet_lines(a,c))
|
||||
assert not text.startswith('| Oblasť |') and literal.strip() in text
|
||||
assert 'Boris Nový' in text and 'Python' in text
|
||||
assert all('Ďalšie doložené' not in line for line in text.splitlines())
|
||||
|
||||
|
||||
def test_shared_excerpt_is_not_repeated_under_each_matching_facet(corpus):
|
||||
update_eva(corpus,'Výsledky:\n- Vyhodnotená presnosť modelu v experimente.')
|
||||
q='Aké experimenty a výsledky má diplomová práca Evy Novej?'
|
||||
p=prepare(corpus,q); c=rag_evidence_context(q,p)
|
||||
result=generate_verified_rag(q,p,complete(selection(c)))
|
||||
assert result['accepted_for_delivery']
|
||||
assert result['answer'].count('Vyhodnotená presnosť modelu v experimente.')==1
|
||||
assert 'chýbajú údaje' not in result['answer'] and 'Doložené vyššie' not in result['answer']
|
||||
assert 'Zaznamenaný výsledok:' in result['answer'] or '**Zaznamenané výsledky:**' in result['answer']
|
||||
|
||||
|
||||
def test_multiline_table_is_bounded_and_does_not_rewrite_source_html():
|
||||
table=comparison_table(['Oblasť','Eva','Boris'],[('Výsledky',['Prvý výsledok.\nDruhý výsledok.','Parser.'])])
|
||||
assert table and 'Prvý výsledok.<br>Druhý výsledok.' in table[-1]
|
||||
assert comparison_table(['Oblasť','Eva'],[('Výsledky',['<b>Prvý výsledok.</b>'])]) is None
|
||||
|
||||
|
||||
def test_shared_result_precedes_people_and_citations_without_repeating_tags():
|
||||
people=['Ján Pták','Ján Malinovský']; urls=['https://example.test/ptak','https://example.test/malinovsky']
|
||||
claims=[{'person':person,'keyword':tag} for person in people for tag in ['NLP','RAG']]
|
||||
c={'status':'ok','complete':True,'truncated':False,'facts':[
|
||||
{**claim,'source_url':urls[people.index(claim['person'])]} for claim in claims],
|
||||
'sources':[{'source_url':u} for u in urls], 'requested_people':people,
|
||||
'shared_properties':[{'field':'keyword','value':tag,'people':people} for tag in ['NLP','RAG']]}
|
||||
a={'status':'ok','claims':claims,'source_urls':urls}
|
||||
r=render_graph_answer(a,c); text=r['answer']
|
||||
assert text.startswith('Spoločné kľúčové slová: **NLP**, **RAG**.')
|
||||
assert text.index('RAG')<text.index('Doložené pri:')<text.index('Zdroje:')
|
||||
assert all(text.count('**'+tag+'**')==1 for tag in ['NLP','RAG'])
|
||||
assert all(person in text for person in people)
|
||||
assert not answer_context_issues(r,c)
|
||||
|
||||
|
||||
def test_negative_names_failed_year_but_does_not_license_positive_year_claims():
|
||||
c={'status':'no_answer','facts':[],'sources':[],'requested_people':['Martin Šarišský'],
|
||||
'requested_years':[1999],'complete':True}
|
||||
a=render_graph_answer({'status':'no_answer','claims':[],'source_urls':[]},c)
|
||||
assert 'Martin Šarišský' in a['answer'] and 'z roku **1999**' in a['answer']
|
||||
assert not answer_context_issues(a,c)
|
||||
for year in [1999,2099]:
|
||||
bad={**a,'answer':a['answer']+f' Práca bola dokončená v roku {year}.'}
|
||||
assert 'unsupported_prose_number' in answer_context_issues(bad,c)
|
||||
|
||||
|
||||
def test_start_year_is_not_rendered_as_a_missing_academic_work_year():
|
||||
c=graph_context({'status':'no_answer','reason':'not_found','facts':[],'sources':[],'aggregation':None,
|
||||
'complete':True,'constraints':{'years':[1999],'mentions_start_year':True},
|
||||
'resolved_entities':[{'kind':'Person','name':'Martin Šarišský'}]})
|
||||
a=render_graph_answer({'status':'no_answer','claims':[],'source_urls':[]},c)
|
||||
assert 'záznam práce' not in a['answer'] and '1999' not in a['answer']
|
||||
|
||||
|
||||
def test_partial_overview_retains_supported_person_and_names_missing_description(corpus):
|
||||
db,raw=corpus
|
||||
with sqlite3.connect(db) as conn:
|
||||
conn.execute("UPDATE chunks SET text='Názov: Ukážkový názov.' WHERE document_path LIKE '%boris_novy%'")
|
||||
q='Porovnaj obsah diplomovej práce Evy Novej a bakalárskej práce Borisa Nového.'
|
||||
p=prepare(corpus,q); c=rag_evidence_context(q,p)
|
||||
assert not c['requirements'].get('facets')
|
||||
passage=next(p for p in c['content_passages'] if p['person']=='Eva Nová')
|
||||
a={'status':'ok','answer':'','claims':[], 'source_urls':[s['source_url'] for s in c['sources']],
|
||||
'evidence_quotes':[{'source_id':passage['source_id'],'passage_id':passage['passage_id'],
|
||||
'quote':'Implementujte systém vyhľadávania.'}]}
|
||||
assert not rag_answer_issues(a,c)
|
||||
result=generate_verified_rag(q,p,complete(a))
|
||||
assert result['accepted_for_delivery'] and result['answer_coverage']['status']=='partial'
|
||||
assert 'Implementácia systému vyhľadávania' in result['answer']
|
||||
assert 'Boris Nový: v poskytnutých podkladoch chýba opis požadovanej práce alebo projektu.' in result['answer']
|
||||
assert 'Výpis je neúplný' not in result['answer']
|
||||
|
||||
|
||||
def test_available_description_cannot_be_silently_omitted(corpus):
|
||||
q='Porovnaj obsah diplomovej práce Evy Novej a bakalárskej práce Borisa Nového.'
|
||||
p=prepare(corpus,q); c=rag_evidence_context(q,p)
|
||||
passage=next(p for p in c['content_passages'] if p['person']=='Eva Nová')
|
||||
a={'status':'ok','answer':'','claims':[], 'source_urls':[s['source_url'] for s in c['sources']],
|
||||
'evidence_quotes':[{'source_id':passage['source_id'],'passage_id':passage['passage_id'],
|
||||
'quote':'Implementujte systém vyhľadávania.'}]}
|
||||
assert 'missing_bound_content_evidence' in rag_answer_issues(a,c)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('aspect',['obsah','zameranie'])
|
||||
def test_overview_comparison_keeps_each_work_in_its_own_column(corpus,aspect):
|
||||
q=f'Porovnaj {aspect} diplomovej práce Evy Novej a bakalárskej práce Borisa Nového.'
|
||||
p=prepare(corpus,q); c=rag_evidence_context(q,p)
|
||||
a={'status':'ok','answer':'','claims':[], 'source_urls':[s['source_url'] for s in c['sources']],
|
||||
'evidence_quotes':[{'source_id':passage['source_id'],'passage_id':passage['passage_id'],'quote':text}
|
||||
for person,text in [('Eva Nová','Implementujte systém vyhľadávania.'),
|
||||
('Boris Nový','Implementujte parser.')]
|
||||
for passage in c['content_passages'] if passage['person']==person and text in passage['text']]}
|
||||
result=generate_verified_rag(q,p,complete(a))
|
||||
assert result['accepted_for_delivery'] and result['answer_coverage']['status']=='supported'
|
||||
assert result['answer'].startswith('| Oblasť | Diplomová práca Evy Novej (2024) | Bakalárska práca Borisa Nového (2023) |')
|
||||
row=next(line for line in result['answer'].splitlines() if line.startswith('| Zameranie podľa podkladov |'))
|
||||
assert 'vyhľadávania' in row.split('|')[2] and 'parser' in row.split('|')[3]
|
||||
assert 'Pripravte experiment' not in row and len(result['source_urls'])==2
|
||||
refused={**a,'status':'no_answer','evidence_quotes':[],'source_urls':[]}
|
||||
assert 'rejected_supported_work_description' in rag_answer_issues(refused,c)
|
||||
@ -17,8 +17,8 @@ def test_identity_is_a_sentence_but_year_is_not_a_start_or_completion_date():
|
||||
'title': 'Systém získavania informácií.', 'year': 2021}
|
||||
before = copy.deepcopy(claim)
|
||||
text = fact_sentences(claim, 'Aký je názov diplomovej práce Jána Holpa a z ktorého roku je?')
|
||||
assert text == ('Diplomová práca Jána Holpa má názov **Systém získavania informácií**. '
|
||||
'Rok práce je **2021**.')
|
||||
assert text == ('Diplomová práca Jána Holpa má názov **Systém získavania informácií** '
|
||||
'(rok práce: **2021**).')
|
||||
assert not any(word in text for word in ('začal', 'dokonč', 'vypracovaná'))
|
||||
assert claim == before
|
||||
|
||||
@ -115,6 +115,6 @@ def test_negative_is_a_bounded_absence_not_a_claim_about_real_existence():
|
||||
'complete': True, 'question': 'Akú prácu mal Ján Holp v roku 1999?'}
|
||||
a = {'status': 'no_answer', 'claims': [], 'source_urls': []}
|
||||
text = render_graph_answer(a, c)['answer']
|
||||
assert 'Ján Holp' in text and 'zodpovedajúce tejto otázke' in text
|
||||
assert 'Ján Holp' in text and 'požadované údaje sa v dostupných podkladoch nenašli' in text
|
||||
assert 'neexistuje' not in text and '2021' not in text
|
||||
assert answer_context_issues(render_graph_answer(a, c), c) == []
|
||||
|
||||
@ -48,7 +48,7 @@ def test_goal_conversion_is_not_all_or_nothing():
|
||||
'Ján Malinovský', 'diploma_project', 'Aké sú ciele diplomového projektu Jána Malinovského?'))
|
||||
assert 'Diplomový projekt Jána Malinovského' in text
|
||||
assert 'Zistiť, čo je to RAG.' in text and 'Použiť databázu Neo4j.' in text
|
||||
assert 'Ďalšie podklady:' in text and 'Práca neimplementuje vlastný model.' in text
|
||||
assert 'Ďalšie podklady:' not in text and 'Práca neimplementuje vlastný model.' in text
|
||||
assert not any(word in text for word in ['Zistite', 'Naučte', 'Zostavte', 'Využite', 'Použite'])
|
||||
|
||||
|
||||
@ -150,7 +150,9 @@ def test_comparison_separates_goals_and_results(comparison_payload):
|
||||
result=generate_verified_rag(COMPARE,payload,lambda _: {'choices':[{'message':{'content':json.dumps(selected)}}]})
|
||||
assert result['accepted_for_delivery']
|
||||
text=result['answer']
|
||||
assert text.count('**Zadanie a ciele:**')==text.count('**Zaznamenané výsledky:**')==2
|
||||
assert text.startswith('| Oblasť |')
|
||||
assert text.count('| Zadanie a ciele |')==text.count('| Zaznamenané výsledky |')==1
|
||||
assert 'Diplomová práca Jána Holpa (2021)' in text and 'Bakalárska práca Ondreja Megelu (2020)' in text
|
||||
assert 'Natrénovať jazykový model metódou BERT.' in text
|
||||
assert 'Natrénovaný model wiki103' in text and 'Implementovaný PageRank' in text
|
||||
assert 'Cassandra' not in text and len(result['source_urls'])==2
|
||||
@ -172,7 +174,7 @@ def test_missing_results_allow_an_explicit_partial_comparison(comparison_payload
|
||||
selected=selected_comparison(c)
|
||||
result=generate_verified_rag(COMPARE,payload,lambda _: {'choices':[{'message':{'content':json.dumps(selected)}}]})
|
||||
assert result['accepted_for_delivery']
|
||||
assert 'Táto časť odpovede nie je v poskytnutých podkladoch doložená.' in result['answer']
|
||||
assert '| Zaznamenané výsledky | V podkladoch chýbajú údaje. |' in result['answer']
|
||||
assert 'Implementácia systému vyhľadávania' in result['answer']
|
||||
|
||||
|
||||
|
||||
@ -162,7 +162,7 @@ def test_partial_answer_names_the_missing_aspect(corpus):
|
||||
q='Aké problémy riešili Eva Nová a Boris Nový a aké experimenty mali pripraviť?'
|
||||
p=prepare(corpus,q); r=generate_verified_rag(q,p,complete(model_selection(q,p)))
|
||||
assert r['accepted_for_delivery'] and r['answer_coverage']['status']=='partial'
|
||||
assert 'Táto časť odpovede nie je v poskytnutých podkladoch doložená.' in r['answer']
|
||||
assert 'V poskytnutých podkladoch chýbajú údaje o plánovaných experimentoch.' in r['answer']
|
||||
assert 'parserom' in r['answer']
|
||||
|
||||
|
||||
|
||||
@ -113,7 +113,8 @@ def test_no_answer_is_bounded_and_only_reports_actual_context_cut(payload, budge
|
||||
c = rag_evidence_context('Aké je zadanie diplomovej práce Evy Novej v roku 1999?', payload)
|
||||
a = {'status': 'no_answer', 'claims': [], 'evidence_quotes': [], 'source_urls': []}
|
||||
text = render_rag_answer(a, c)
|
||||
assert 'nepodarilo spoľahlivo nájsť' in text and 'Výpis je neúplný' not in text
|
||||
assert 'Eva Nová' in text and 'v dostupných podkladoch sa nenašiel záznam diplomovej práce z roku **1999**' in text
|
||||
assert 'Výpis je neúplný' not in text
|
||||
assert ('Časť podkladov bola pri príprave kontextu skrátená.' in text) == budget
|
||||
assert answer_coverage(a, c)['status'] == 'not_found'
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ def response_scope(question: str, *, content: bool = False) -> str:
|
||||
if content:
|
||||
if re.search(r'\b(?:zadani\w*|ciel\w*)\b', query) and not re.search(r'\b(?:strucn\w*|zhrn\w*)\b', query):
|
||||
return 'assignment'
|
||||
if re.search(r'\b(?:comu|o com|opis\w*|popis\w*|obsah\w*|zhrn\w*|strucn\w*)\b', query):
|
||||
if re.search(r'\b(?:comu|o com|opis\w*|popis\w*|obsah\w*|zamer\w*|zhrn\w*|strucn\w*)\b', query):
|
||||
return 'overview'
|
||||
return 'fact'
|
||||
|
||||
@ -55,6 +55,10 @@ def answer_coverage(answer: dict, context: dict) -> dict:
|
||||
from .content_selection import missing_facets
|
||||
if missing_facets(answer,context):
|
||||
coverage='partial'; warnings.append('partial_requested_aspects')
|
||||
elif requirements.get('content'):
|
||||
from .rag_evidence import missing_content_people
|
||||
if missing_content_people(answer,context):
|
||||
coverage='partial'; warnings.append('partial_requested_aspects')
|
||||
if scope == 'list' and (limited or clipped):
|
||||
coverage = 'partial'; warnings.append('list_may_be_incomplete')
|
||||
elif scope == 'count' and (context.get('aggregation') or {}).get('complete') is not True:
|
||||
|
||||
@ -218,34 +218,22 @@ def fit_candidates(sent: dict, context: dict, limit: int, encoding: str) -> None
|
||||
|
||||
|
||||
def facet_lines(answer: dict, context: dict) -> list[str]:
|
||||
from .presentation import goal_text, work_subject, owner_in_question
|
||||
from .rag_evidence import WORK_LABELS
|
||||
from .presentation import goal_text, evidence_subject, missing_aspects, comparison_table
|
||||
req=context['requirements']; units=evidence_units(context,answer.get('evidence_quotes',[]))
|
||||
lines=[]
|
||||
facets=req.get('facets',[]);groups=[];labels={f:FACET_LABELS[f] for f in facets}
|
||||
if 'experiments' in facets and planned_experiments(context.get('question','')):
|
||||
labels['experiments']='Plánované experimenty a vyhodnotenie'
|
||||
for person in req['people']:
|
||||
scopes={(p['work_type'],p.get('year')) for p in context.get('content_passages',[]) if p.get('person')==person}
|
||||
kind=req['work_types'].get(person) or (next(iter(scopes))[0] if len(scopes)==1 else None)
|
||||
subject=work_subject(person,kind,context.get('question',''))
|
||||
if not owner_in_question(person,context.get('question','')):
|
||||
subject=person+(' — '+WORK_LABELS[kind] if kind in WORK_LABELS else '')
|
||||
if len(scopes)==1 and next(iter(scopes))[1] is not None:
|
||||
subject+=' ('+str(next(iter(scopes))[1])+')'
|
||||
labels={p.get('work_label') for p in context.get('content_passages',[])
|
||||
if p.get('person')==person and p.get('work_label')}
|
||||
if req.get('project_number') is not None and len(labels)==1:
|
||||
subject=person+' — '+' '.join(next(iter(labels)).split())
|
||||
lines.extend(['**'+subject+'**', ''])
|
||||
seen=set()
|
||||
for facet in req.get('facets', []):
|
||||
label=FACET_LABELS[facet]
|
||||
if facet=='experiments' and planned_experiments(context.get('question','')):
|
||||
label='Plánované experimenty a vyhodnotenie'
|
||||
group={'subject':evidence_subject(person,context),'items':{},'missing':[]};seen=set()
|
||||
for facet in facets:
|
||||
rows=[u for u in units if u['person']==person and facet in u['facets']]
|
||||
if facet=='progress' and 'results' in facets and any(u['phase']=='progress' for u in rows):
|
||||
rows=[u for u in rows if u['phase']=='progress']
|
||||
rows.sort(key=lambda u:-unit_score(u['text'],u['kind'],facet))
|
||||
lines.append('**'+label+':**')
|
||||
shown=0
|
||||
if not rows:group['missing'].append(facet)
|
||||
rendered=[]
|
||||
for row in rows:
|
||||
key=' '.join(re.sub(r'^\s*(?:\d+[.)]|[-+*])\s+','',row['text']).split())
|
||||
key=(' '.join(re.sub(r'^\s*(?:\d+[.)]|[-+*])\s+','',row['text']).split()),row['phase'])
|
||||
if key in seen:
|
||||
continue
|
||||
text=row['text']
|
||||
@ -253,14 +241,30 @@ def facet_lines(answer: dict, context: dict) -> list[str]:
|
||||
text,_=goal_text(text)
|
||||
text=re.sub(r'^\s*(?:\d+[.)]|[-+*])\s+','',text)
|
||||
phase=' (plán)' if row['kind'] in {'assignment','task'} and facet not in {'assignment','plans'} else ''
|
||||
state=({'result':'Zaznamenaný výsledok: ','progress':'Priebežný stav: '}.get(row['phase'],'')
|
||||
if facet=='results' else '')
|
||||
lines.append('- '+state+text+phase); seen.add(key); shown+=1
|
||||
if shown==2: break
|
||||
if not shown:
|
||||
if rows:
|
||||
lines.append('Je doložené rovnakým výňatkom uvedeným vyššie.')
|
||||
else:
|
||||
lines.append('Táto časť odpovede nie je v poskytnutých podkladoch doložená.')
|
||||
lines.append('')
|
||||
state='Zaznamenaný výsledok: ' if row['phase']=='result' and 'results' in facets and facet!='results' else ''
|
||||
rendered.append(state+text+phase);seen.add(key)
|
||||
if len(rendered)==2:break
|
||||
group['items'][facet]=rendered
|
||||
groups.append(group)
|
||||
if len(groups)>1 and re.search(r'\bporovnaj\w*\b',normalized(context.get('question',''))):
|
||||
rows=[]
|
||||
for facet in facets:
|
||||
if not any(g['items'][facet] for g in groups):
|
||||
if all(facet not in g['missing'] for g in groups):continue
|
||||
values=['\n'.join(g['items'][facet]) or ('V podkladoch chýbajú údaje.' if facet in g['missing'] else 'Doložené vyššie.') for g in groups]
|
||||
rows.append((labels[facet],values))
|
||||
if any(any(g['items'][f] for f in facets) for g in groups):
|
||||
table=comparison_table(['Oblasť',*(g['subject'] for g in groups)],rows)
|
||||
if table is not None:return table
|
||||
lines=[]
|
||||
for group in groups:
|
||||
title=group['subject']
|
||||
if len(facets)==1 and group['items'][facets[0]]:title+=' — '+labels[facets[0]].lower()
|
||||
lines.extend(['**'+title+'**',''])
|
||||
for facet in facets:
|
||||
items=group['items'][facet]
|
||||
if not items:continue
|
||||
if len(facets)>1:lines.append('**'+labels[facet]+':**')
|
||||
lines.extend('- '+text for text in items);lines.append('')
|
||||
if group['missing']:lines.extend([missing_aspects(group['missing'],context.get('question','')),''])
|
||||
return lines
|
||||
|
||||
@ -108,9 +108,15 @@ def coverage_issues(answer: dict, context: dict) -> list[str]:
|
||||
return list(dict.fromkeys(issues))
|
||||
|
||||
|
||||
def prose_issues(answer: dict) -> list[str]:
|
||||
def prose_issues(answer: dict, context: dict | None = None) -> list[str]:
|
||||
"""Conservative numeric consistency check; final prose is rendered from claims."""
|
||||
text = str(answer.get('answer', ''))
|
||||
if context is not None and answer.get('status')=='no_answer':
|
||||
from .presentation import no_answer_text
|
||||
expected=no_answer_text(context)
|
||||
# These years describe the failed query, never a positive factual claim.
|
||||
# Strip only the exact trusted template; appended prose stays checked.
|
||||
if text.startswith(expected):text=text[len(expected):]
|
||||
for url in sorted(text_urls(text), key=len, reverse=True):
|
||||
text = text.replace(url, '')
|
||||
# A bounded ordered-list marker is presentation, not a factual count/year.
|
||||
@ -236,7 +242,7 @@ def answer_context_issues(answer: dict, context: dict, *, check_text: bool = Tru
|
||||
issues.append('split_or_missing_person_join')
|
||||
if context.get('status') in {'no_answer','ambiguous','error'} and answer.get('status')=='ok':
|
||||
issues.append('unsupported_positive_status')
|
||||
issues.extend(prose_issues(answer))
|
||||
issues.extend(prose_issues(answer,context))
|
||||
if check_text or str(answer.get('answer', '')).strip():
|
||||
issues.extend(coverage_issues(answer,context))
|
||||
return list(dict.fromkeys(issues))
|
||||
|
||||
@ -61,6 +61,8 @@ def graph_context(result: dict) -> dict:
|
||||
'index_version':result.get('index_version')}
|
||||
context['answer_candidates']=joined_candidates(facts,intent)
|
||||
context['question'] = result.get('query', '')
|
||||
context['requested_years']=intent.get('years',[]) if not intent.get('mentions_start_year') else []
|
||||
context['requested_work_types']=[*intent.get('thesis_types',[]),*intent.get('work_types',[])]
|
||||
context['clarification'] = result.get('clarification')
|
||||
context['requested_people'] = list(dict.fromkeys(row['name']
|
||||
for row in result.get('resolved_entities', []) if row.get('kind') == 'Person' and row.get('name')))
|
||||
|
||||
@ -53,7 +53,8 @@ def fact_sentences(claim: dict, question: str = '') -> str:
|
||||
if owner:
|
||||
subject = work_subject(person, kind, question)
|
||||
if title is not None:
|
||||
parts.append(f'{subject} má názov **{displayed_title}**.')
|
||||
parts.append(f'{subject} má názov **{displayed_title}**'+
|
||||
(f' (rok práce: **{year}**).' if year is not None else '.'))
|
||||
elif year is not None:
|
||||
parts.append(f'{subject} je v podkladoch evidovaná s rokom **{year}**.'
|
||||
if not (kind or '').endswith('_project') else
|
||||
@ -69,14 +70,17 @@ def fact_sentences(claim: dict, question: str = '') -> str:
|
||||
if year is not None:
|
||||
sentence += f' (rok práce: **{year}**)'
|
||||
parts.append(sentence + '.')
|
||||
if owner and title is not None and year is not None:
|
||||
parts.append(f'Rok práce je **{year}**.')
|
||||
if claim.get('start_year') is not None:
|
||||
parts.append(f'{person} má uvedený začiatok štúdia v roku **{claim["start_year"]}**.')
|
||||
for key, label in [('keyword', 'kľúčové slovo'), ('category', 'kategória'),
|
||||
('document_author', 'autor dokumentu')]:
|
||||
if claim.get(key) is not None:
|
||||
parts.append(f'Pri osobe {person} je uvedené: {label} **{claim[key]}**.')
|
||||
if key=='keyword':
|
||||
if parts and (kind or title is not None or year is not None):
|
||||
parts[-1]=parts[-1].rstrip('.')+f' a tag **{claim[key]}**.'
|
||||
else:parts.append(f'{person} má tag **{claim[key]}**.')
|
||||
else:
|
||||
parts.append(f'{person}: '+('autor dokumentu je' if key=='document_author' else 'uvedená kategória je')+f' **{claim[key]}**.')
|
||||
return ' '.join(parts) or person + '.'
|
||||
|
||||
|
||||
@ -158,7 +162,7 @@ def content_lines(units: list[str], person: str | None, kind: str | None,
|
||||
lines = [subject + ' má podľa dostupných podkladov tieto ciele:', '',
|
||||
*[f'{index}. {text}' for index, text in enumerate(goals, 1)]]
|
||||
if other:
|
||||
lines.extend(['', 'Ďalšie podklady:', *other])
|
||||
lines.extend(['', *other])
|
||||
return lines
|
||||
return [subject + ' je v dostupných podkladoch opísaná takto:' if not (kind or '').endswith('_project')
|
||||
else subject + ' je v dostupných podkladoch opísaný takto:', '', *units]
|
||||
@ -167,6 +171,62 @@ def content_lines(units: list[str], person: str | None, kind: str | None,
|
||||
def no_answer_text(context: dict) -> str:
|
||||
people = context.get('requested_people') or context.get('requirements', {}).get('people', [])
|
||||
if len(people) == 1:
|
||||
return (f'Pre osobu {people[0]} sa v dostupných podkladoch nepodarilo spoľahlivo nájsť '
|
||||
'údaje zodpovedajúce tejto otázke.')
|
||||
return 'K tejto otázke sa v dostupných podkladoch nepodarilo spoľahlivo nájsť zodpovedajúce údaje.'
|
||||
req=context.get('requirements',{})
|
||||
years=context.get('requested_years') or req.get('years') or []
|
||||
kinds=context.get('requested_work_types') or [req.get('work_types',{}).get(people[0])]
|
||||
genitive={'bachelor_thesis':'bakalárskej práce','diploma_thesis':'diplomovej práce',
|
||||
'doctoral_thesis':'dizertačnej práce','bachelor_project':'bakalárskeho projektu',
|
||||
'diploma_project':'diplomového projektu','team_project':'tímového projektu',
|
||||
'research_project':'výskumného projektu'}
|
||||
kind=genitive.get(kinds[0],'práce alebo projektu') if len(kinds)==1 else 'práce alebo projektu'
|
||||
if years:
|
||||
date=(' z roku **'+str(years[0])+'**' if len(years)==1 else
|
||||
' z požadovaných rokov **'+', '.join(map(str,years))+'**')
|
||||
return f'{people[0]}: v dostupných podkladoch sa nenašiel záznam {kind}{date}.'
|
||||
return f'{people[0]}: požadované údaje sa v dostupných podkladoch nenašli.'
|
||||
return 'Požadované údaje sa v dostupných podkladoch nenašli.'
|
||||
|
||||
|
||||
def missing_aspects(facets: list[str], question: str = '') -> str:
|
||||
"""Name only the missing requested fields, without empty helper sections."""
|
||||
from .question_intent import planned_experiments
|
||||
labels={'assignment':'zadaní a cieľoch','plans':'plánovaných úlohách','progress':'priebežnom stave',
|
||||
'results':'výsledkoch','problems':'riešených problémoch','methods':'metódach a technológiách',
|
||||
'experiments':('plánovaných experimentoch' if planned_experiments(question) else 'experimentoch a ich vyhodnotení')}
|
||||
values=list(dict.fromkeys(labels.get(f,f) for f in facets))
|
||||
if not values:return ''
|
||||
joined=values[0] if len(values)==1 else ', '.join(values[:-1])+' a '+values[-1]
|
||||
return 'V poskytnutých podkladoch chýbajú údaje o '+joined+'.'
|
||||
|
||||
|
||||
def clean_answer(lines: list[str]) -> str:
|
||||
text='\n'.join(lines).strip()
|
||||
return re.sub(r'\n{3,}','\n\n',text)
|
||||
|
||||
|
||||
def evidence_subject(person: str, context: dict) -> str:
|
||||
"""Use only work identity actually present in the supplied passages."""
|
||||
req=context.get('requirements',{})
|
||||
passages=[p for p in context.get('content_passages',[]) if p.get('person')==person]
|
||||
scopes={(p.get('work_type'),p.get('year')) for p in passages}
|
||||
if not scopes:return person
|
||||
kind=req.get('work_types',{}).get(person) or (next(iter(scopes))[0] if len(scopes)==1 else None)
|
||||
subject=work_subject(person,kind,context.get('question',''))
|
||||
if not owner_in_question(person,context.get('question','')):
|
||||
subject=person+(' — '+WORK_LABELS[kind] if kind in WORK_LABELS else '')
|
||||
if len(scopes)==1 and next(iter(scopes))[1] is not None:
|
||||
subject+=' ('+str(next(iter(scopes))[1])+')'
|
||||
labels={p.get('work_label') for p in passages if p.get('work_label')}
|
||||
if req.get('project_number') is not None and len(labels)==1:
|
||||
subject=person+' — '+' '.join(next(iter(labels)).split())
|
||||
return subject
|
||||
|
||||
|
||||
def comparison_table(headers: list[str], rows: list[tuple[str,list[str]]]) -> list[str] | None:
|
||||
"""Lay out short literal evidence; keep complex text outside Markdown tables."""
|
||||
cells=[*headers,*(cell for label,values in rows for cell in [label,*values])]
|
||||
if len(headers)>4 or any(len(cell)>900 or '|' in cell or '`' in cell or
|
||||
re.search(r'<[^>]+>',cell) or text_urls(cell) for cell in cells):
|
||||
return None
|
||||
def row(values):return '| '+' | '.join(v.replace('\n','<br>') for v in values)+' |'
|
||||
return [row(headers),row(['---']*len(headers)),*(row([label,*values]) for label,values in rows)]
|
||||
|
||||
@ -280,6 +280,14 @@ def quote_matches_passage(quote: dict, passage: dict) -> bool:
|
||||
' '.join(quote['quote'].split()) in ' '.join(passage['text'].split()))
|
||||
|
||||
|
||||
def missing_content_people(answer: dict, context: dict) -> list[str]:
|
||||
"""Requested people without a selected, work-bound descriptive excerpt."""
|
||||
return [person for person in context.get('requirements',{}).get('people',[])
|
||||
if not any(p.get('person')==person and quote_matches_passage(q,p)
|
||||
for p in context.get('content_passages',[])
|
||||
for q in answer.get('evidence_quotes',[]))]
|
||||
|
||||
|
||||
def quote_sections(quote: dict, context: dict) -> list[dict]:
|
||||
"""Classify only literal excerpts bound to the delivered academic passage."""
|
||||
full = [section for passage in context.get('content_passages', [])
|
||||
|
||||
@ -3,7 +3,7 @@ from copy import deepcopy
|
||||
import re
|
||||
|
||||
from tools.graphrag.graphrag import WORK_TYPE_LABELS
|
||||
from .presentation import fact_sentences, no_answer_text
|
||||
from .presentation import fact_sentences, no_answer_text, clean_answer
|
||||
|
||||
|
||||
def render_graph_answer(answer: dict, context: dict) -> dict:
|
||||
@ -11,6 +11,7 @@ def render_graph_answer(answer: dict, context: dict) -> dict:
|
||||
lines = []
|
||||
if out['status'] == 'ok':
|
||||
shared_fields=set()
|
||||
shared_people=[]
|
||||
labels={'keyword':'kľúčové slová','category':'kategórie','document_author':'autori dokumentov'}
|
||||
for field,label in labels.items():
|
||||
values=sorted({row['value'] for row in context.get('shared_properties',[]) if row['field']==field
|
||||
@ -20,6 +21,11 @@ def render_graph_answer(answer: dict, context: dict) -> dict:
|
||||
shared_fields.add(field)
|
||||
prefix='Spoločné ' if context.get('complete') is True and not context.get('truncated') else 'Doložené spoločné '
|
||||
lines.append(prefix+label+': '+', '.join('**'+v+'**' for v in values)+'.')
|
||||
for row in context.get('shared_properties',[]):
|
||||
if row['field']==field and row['value'] in values:
|
||||
shared_people.extend(row['people'])
|
||||
if shared_people:
|
||||
lines.extend(['Doložené pri: '+', '.join('**'+p+'**' for p in dict.fromkeys(shared_people))+'.',''])
|
||||
comparison = context.get('academic_year_comparison')
|
||||
if comparison:
|
||||
people = comparison.get('people', [])
|
||||
@ -81,5 +87,5 @@ def render_graph_answer(answer: dict, context: dict) -> dict:
|
||||
lines.append(no_answer_text(context))
|
||||
if out['source_urls']:
|
||||
lines.extend(['', ('Zdroj: ' if len(out['source_urls']) == 1 else 'Zdroje: ') + ', '.join(out['source_urls'])])
|
||||
out['answer'] = '\n'.join(lines)
|
||||
out['answer'] = clean_answer(lines)
|
||||
return out
|
||||
|
||||
@ -12,7 +12,8 @@ from .answer_format import Claim
|
||||
from .context_policy import answer_context_issues, citation_check, allowed_urls, normalized, text_urls
|
||||
from .rag_evidence import descriptive_text, rag_evidence_context, quote_sections, quote_matches_passage
|
||||
from .answer_coverage import answer_coverage, coverage_notices
|
||||
from .presentation import content_lines, fact_sentences, no_answer_text, work_subject
|
||||
from .presentation import (content_lines, fact_sentences, no_answer_text, work_subject,
|
||||
clean_answer, evidence_subject, comparison_table)
|
||||
from .content_selection import facet_lines, evidence_units, missing_facets, fit_candidates, has_qualification
|
||||
|
||||
|
||||
@ -200,7 +201,7 @@ def rag_answer_issues(answer: dict, context: dict) -> list[str]:
|
||||
issues.append('rejected_supported_partial_fact')
|
||||
if answer['status'] == 'no_answer' and requirements.get('content'):
|
||||
query = normalized(context['question'])
|
||||
overview = re.search(r'\b(?:comu(?:\s+sa)?\s+ven(?:uj|ov)\w*|o\s+com|opis\w*|popis\w*|obsah\w*|zhrn\w*)\b', query)
|
||||
overview = re.search(r'\b(?:comu(?:\s+sa)?\s+ven(?:uj|ov)\w*|o\s+com|opis\w*|popis\w*|obsah\w*|zamer\w*|zhrn\w*)\b', query)
|
||||
# Only a broad overview is directly licensed by a documented assignment.
|
||||
# Do not reject a refusal about specific results/budgets merely because
|
||||
# some unrelated narrative was retrieved from the requested work.
|
||||
@ -260,7 +261,8 @@ def rag_answer_issues(answer: dict, context: dict) -> list[str]:
|
||||
all(claim.get(key) is None or claim[key] == p.get(key) for key in ['work_type', 'year'])
|
||||
for claim in claims)]
|
||||
eligible = any(c.get('person')==person for c in context.get('content_candidates', []))
|
||||
if (not requirements.get('facets') or eligible) and not any(quote_matches_passage(quote,p)
|
||||
required = eligible if requirements.get('facets') else bool(passages)
|
||||
if required and not any(quote_matches_passage(quote,p)
|
||||
for p in passages for quote in quotes):
|
||||
issues.append('missing_bound_content_evidence')
|
||||
if requirements.get('distinguish_progress') and not requirements.get('facets'):
|
||||
@ -347,8 +349,8 @@ def comparison_lines(answer: dict, context: dict) -> list[str]:
|
||||
key = section['kind'] if section['kind'] in roles else 'other'
|
||||
roles[key].append({'source_id': quote['source_id'], 'quote': section['text']})
|
||||
for role, label in [('assignment', 'Zadanie a ciele'), ('progress', 'Zaznamenaný stav a výsledky')]:
|
||||
lines.append('**' + label + ':**')
|
||||
if roles[role]:
|
||||
lines.append('**' + label + ':**')
|
||||
units = overview_excerpts({'evidence_quotes': roles[role]}, context)
|
||||
if role == 'assignment':
|
||||
lines.extend(content_lines(units, person, kind, context['question'])[1:])
|
||||
@ -359,13 +361,12 @@ def comparison_lines(answer: dict, context: dict) -> list[str]:
|
||||
('nie je zadanie doložené.' if role == 'assignment' else 'nie sú dosiahnuté výsledky doložené.'))
|
||||
lines.append('')
|
||||
if roles['task']:
|
||||
lines.append('**Ďalšie doložené úlohy (plán):**')
|
||||
lines.append('**Plánované úlohy:**')
|
||||
units = overview_excerpts({'evidence_quotes': roles['task']}, context)
|
||||
lines.extend(content_lines(units, person, kind, context['question'])[1:])
|
||||
lines.append('')
|
||||
if roles['other']:
|
||||
lines.extend(['**Ďalšie doložené podklady:**',
|
||||
*overview_excerpts({'evidence_quotes': roles['other']}, context), ''])
|
||||
lines.extend([*overview_excerpts({'evidence_quotes': roles['other']}, context), ''])
|
||||
return lines
|
||||
|
||||
|
||||
@ -408,7 +409,15 @@ def render_rag_answer(answer: dict, context: dict) -> str:
|
||||
if quote_matches_passage(quote,p)), {})
|
||||
identity = (passage.get('person'), passage.get('work_type'), passage.get('year'))
|
||||
groups.setdefault(identity, []).append(selection['text'])
|
||||
for (person, kind, _year), selections in groups.items():
|
||||
table = None
|
||||
if style == 'overview' and len(groups)>1 and re.search(r'\bporovnaj\w*\b',normalized(context.get('question',''))):
|
||||
from .presentation import goal_text
|
||||
table=comparison_table(['Oblasť',*(evidence_subject(person,context) for person,_,_ in groups)],
|
||||
[('Zameranie podľa podkladov',['\n'.join(goal_text(text)[0] for text in units)
|
||||
for units in groups.values()])])
|
||||
if table is not None:
|
||||
lines.extend(table)
|
||||
for (person, kind, _year), selections in ([] if table is not None else groups.items()):
|
||||
if style == 'overview':
|
||||
units = selections
|
||||
else:
|
||||
@ -426,6 +435,10 @@ def render_rag_answer(answer: dict, context: dict) -> str:
|
||||
': „' + quote['quote'] + '“' for quote in quotes]
|
||||
if answer.get('count') is not None:
|
||||
lines.append(f'Počet: {answer["count"]}.')
|
||||
if req.get('content') and not req.get('facets'):
|
||||
from .rag_evidence import missing_content_people
|
||||
for person in missing_content_people(answer,context):
|
||||
lines.extend(['',f'{person}: v poskytnutých podkladoch chýba opis požadovanej práce alebo projektu.'])
|
||||
elif answer['status'] == 'no_answer':
|
||||
if context.get('requirements', {}).get('facets') and context['requirements']['people']:
|
||||
lines.extend(facet_lines(answer,context))
|
||||
@ -438,7 +451,7 @@ def render_rag_answer(answer: dict, context: dict) -> str:
|
||||
lines.extend(coverage_notices(answer_coverage(answer, context)))
|
||||
if answer.get('source_urls'):
|
||||
lines.extend(['', ('Zdroj: ' if len(answer['source_urls']) == 1 else 'Zdroje: ') + ', '.join(answer['source_urls'])])
|
||||
return '\n'.join(lines)
|
||||
return clean_answer(lines)
|
||||
|
||||
|
||||
def generate_verified_rag(question: str, payload: dict, complete) -> dict:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user