Oprava grafových podmienok
This commit is contained in:
parent
16af0f8b4f
commit
05f96ac9c0
10
README.md
10
README.md
@ -1523,6 +1523,16 @@ sa nepoužije ako dôkaz dátumu jej začatia či obhajoby. Chybové odpovede od
|
||||
nedostupné vyhľadávanie, nedostupný model, vyťaženie a zamietnutý neoverený text;
|
||||
raw výstupy odmietnutého modelu sa štandardnému klientovi neposkytujú.
|
||||
|
||||
Grafový filter tagu sa uplatňuje aj pri otázkach na neurčených študentov a
|
||||
konkrétny typ/rok práce: všetky podmienky musí spĺňať tá istá osoba. Spojenie
|
||||
„tag RAG a zároveň diplomový projekt“ nepatrí do názvu tagu. Úvodzovky umožňujú
|
||||
zachovať viacslovný tag aj so spojkou „a“. Pri názve práce konkrétnej osoby sa
|
||||
zachovajú aj záznamy bez názvu; odpoveď uvedie známy typ a rok a chýbajúci názov.
|
||||
Spoločné tagy sa zobrazujú súhrnne, s dôkazmi pre každú požadovanú osobu.
|
||||
Rozpoznané podmienky na počet vrátane slovných čísloviek („aspoň tri“) vyžadujú
|
||||
spresnenie; prahové filtrovanie zatiaľ nie je implementované. Predikcia dokončenia
|
||||
práce sa výslovne odlišuje od vyhľadania práce evidovanej v určitom roku.
|
||||
|
||||
Otázky na obsah, zadanie, ciele alebo metódy práce používajú textové výňatky
|
||||
z príslušnej akademickej sekcie, nie iba metadáta o názve a roku. Výňatok musí
|
||||
patriť požadovanej osobe, typu práce a prípadne roku. Názov, rok ani samotný
|
||||
|
||||
191
test/test_graph_query_regressions.py
Normal file
191
test/test_graph_query_regressions.py
Normal file
@ -0,0 +1,191 @@
|
||||
"""Regressions for actual Claude questions and adversarial same-person joins."""
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.answer_evaluation.checked_generation import generate_checked_answer
|
||||
from tools.answer_evaluation.context_policy import answer_context_issues
|
||||
from tools.answer_evaluation.graph_context import graph_context
|
||||
from tools.answer_evaluation.render_answer import render_graph_answer
|
||||
from tools.answers.answer_delivery import deliver_answer
|
||||
from tools.graphrag import graph_search as gs
|
||||
from tools.graphrag.tests.test_graph_search import FakeSession
|
||||
from tools.retrieval.question_constraints import unsupported_constraint
|
||||
|
||||
|
||||
PEOPLE=['Ján Pták','Ján Malinovský','Valerii Kutsenko','Jakub Schwarc']
|
||||
PROMPT=Path('tools/answer_evaluation/graph_answer_prompt.txt').read_text()
|
||||
|
||||
|
||||
def person(index,name):
|
||||
return {'kind':'Person','name':name,'element_id':f'p{index}',
|
||||
'source_document':f'pages/students/2022/p{index}/README.md'}
|
||||
|
||||
|
||||
def source(index):
|
||||
return {'document_path':f'pages/students/2022/p{index}/README.md',
|
||||
'source_url':f'https://example.test/p{index}'}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tagged_projects():
|
||||
catalog=[person(i,name) for i,name in enumerate(PEOPLE,1)]
|
||||
catalog.append({'kind':'Keyword','name':'RAG','id':'rag','element_id':'krag'})
|
||||
projects=[{'person':name,'person_element_id':f'p{i}','year':2026,
|
||||
'work_type':'diploma_project','work_id':f'work:p{i}',
|
||||
'heading':'Diplomový projekt 2026','title':f'Téma projektu {name}',**source(i)}
|
||||
for i,name in enumerate(PEOPLE,1)]
|
||||
tags=[{'person':name,'person_element_id':f'p{i}','keyword':'RAG',
|
||||
'keyword_id':'rag','keyword_element_id':'krag',**source(i)}
|
||||
for i,name in enumerate(PEOPLE[:3],1)]
|
||||
# A fifth person has the tag, but no work in the requested year/type.
|
||||
tags.append({'person':'Iná Osoba','person_element_id':'p5','keyword':'RAG',
|
||||
'keyword_id':'rag','keyword_element_id':'krag',**source(5)})
|
||||
return FakeSession({gs.CATALOG_QUERY:catalog,gs.WORK_FACTS_QUERY:projects,
|
||||
gs.PERSON_KEYWORD_FACTS_QUERY:tags})
|
||||
|
||||
|
||||
@pytest.mark.parametrize('question',[
|
||||
'Ktorí študenti majú tag RAG a zároveň diplomový projekt z roku 2026?',
|
||||
'Ktorí študenti s tagom RAG majú diplomový projekt v roku 2026?',
|
||||
'Ktorí študenti majú tag RAG a diplomový projekt z roku 2026?',
|
||||
'Ktorí študenti majú tag „RAG“ a zároveň diplomový projekt z roku 2026?',
|
||||
'Ktorí študenti s tagom RAG a zároveň diplomovým projektom z roku 2026?',
|
||||
'Ktorí študenti s tagom RAG a diplomovým projektom z roku 2026?',
|
||||
])
|
||||
def test_tag_and_project_are_witnessed_by_the_same_person(tagged_projects,question):
|
||||
r=gs.search_graph_with_session(tagged_projects,question,limit=50)
|
||||
assert r['status']=='ok' and r['constraints']['keyword_phrase']=='rag'
|
||||
assert {f['person'] for f in r['facts']}==set(PEOPLE[:3])
|
||||
work=[params for q,params in tagged_projects.calls if q==gs.WORK_FACTS_QUERY][0]
|
||||
tags=[params for q,params in tagged_projects.calls if q==gs.PERSON_KEYWORD_FACTS_QUERY][0]
|
||||
assert work['years']==[2026] and work['work_types']==['diploma_project']
|
||||
assert tags['keyword_ids']==['krag']
|
||||
context=graph_context(r)
|
||||
assert {c['person'] for c in context['answer_candidates']}==set(PEOPLE[:3])
|
||||
assert all(c['work_type']=='diploma_project' for c in context['answer_candidates'])
|
||||
|
||||
|
||||
def test_diploma_thesis_cannot_be_replaced_by_a_project(tagged_projects):
|
||||
r=gs.search_graph_with_session(tagged_projects,
|
||||
'Ktorí študenti majú tag RAG a zároveň diplomovú prácu z roku 2026?',limit=50)
|
||||
assert r['status']=='no_answer' and r['reason']=='no_matching_facts'
|
||||
assert not r['facts'] and not r['constraints']['unresolved_keyword']
|
||||
|
||||
|
||||
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?')
|
||||
assert r['status']=='no_answer' and r['reason']=='unknown_keyword' and not r['facts']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('question,expected',[
|
||||
('Ľudia s tagom strojové učenie a zároveň diplomovou prácou.','strojove ucenie'),
|
||||
('Ľudia s tagom „výskum a aplikácie“ a zároveň diplomovým projektom.','vyskum a aplikacie'),
|
||||
('Ľudia s tagom RAG z roku 2026.','rag'),
|
||||
])
|
||||
def test_multiword_tag_and_condition_boundary(question,expected):
|
||||
assert gs.explicit_keyword_phrase(question)==expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('question,code',[
|
||||
('Ktorí študenti majú aspoň tri práce?','quantity_condition'),
|
||||
('Ktorí študenti majú aspoň 3 práce?','quantity_condition'),
|
||||
('Kto má najmenej tri práce?','quantity_condition'),
|
||||
('Študenti s najviac dvoma projektmi?','quantity_condition'),
|
||||
('Študenti s minimálne piatimi prácami?','quantity_condition'),
|
||||
('ktorý študenti týmto tempom ukončia svoje diplomové práce v roku 2027?','completion_prediction'),
|
||||
('Ktorí študenti ukončia diplomové práce v roku 2027?','completion_prediction'),
|
||||
('Odhadni termín dokončenia diplomovej práce Jána Holpa.','completion_prediction'),
|
||||
('Kedy Ján Holp ukončil diplomovú prácu?','work_event_date'),
|
||||
('Aké metódy predikcie počasia používa diplomová práca?',None),
|
||||
('Aký je názov práce „Kto dokončí prácu v roku 2027“?',None),
|
||||
])
|
||||
def test_unsupported_condition_wording(question,code):
|
||||
assert unsupported_constraint(question)==code
|
||||
|
||||
|
||||
@pytest.mark.parametrize('engine',['rag','graphrag'])
|
||||
@pytest.mark.parametrize('question,word',[
|
||||
('Ktorí študenti majú aspoň tri práce?','počet'),
|
||||
('Ktorí študenti týmto tempom ukončia diplomové práce v roku 2027?','predpovedať'),
|
||||
])
|
||||
def test_unsupported_condition_never_calls_a_model(engine,question,word):
|
||||
r=deliver_answer(question,engine=engine,complete=lambda _:pytest.fail('No model for unsupported condition'))
|
||||
assert r['status']=='ambiguous' and r['accepted_for_delivery'] and word in r['answer']
|
||||
assert not r['claims'] and not r['source_urls']
|
||||
|
||||
|
||||
def selected(context):
|
||||
fields=['person','record_id','title','year','work_type','work_label','keyword','category','document_author']
|
||||
rows=context.get('required_academic_records') or context['facts']
|
||||
claims=[{k:r[k] for k in fields if r.get(k) is not None} for r in rows]
|
||||
return {'status':'ok','answer':'','claims':claims,
|
||||
'source_urls':[s['source_url'] for s in context['sources']]}
|
||||
|
||||
|
||||
def generated(answer,context):
|
||||
return generate_checked_answer(context.get('question',''),context,PROMPT,
|
||||
lambda _: {'choices':[{'message':{'content':json.dumps(answer)}}]})
|
||||
|
||||
|
||||
def test_titleless_records_survive_and_are_delivered_without_invented_titles():
|
||||
catalog=[person(1,'Ján Holp')]
|
||||
records=[{'person':'Ján Holp','person_element_id':'p1','work_id':f'project-{i}',
|
||||
'title':None,'heading':label,'work_type':'diploma_project','year':2020,**source(1)}
|
||||
for i,label in enumerate(['Diplomový projekt 2020','Diplomový projekt 2 2020'])]
|
||||
s=FakeSession({gs.CATALOG_QUERY:catalog,gs.WORK_FACTS_QUERY:records})
|
||||
raw=gs.search_graph_with_session(s,'Aký názov má diplomový projekt Jána Holpa z roku 2020?')
|
||||
assert raw['status']=='ok' and len(raw['facts'])==2
|
||||
context=graph_context(raw);a=selected(context)
|
||||
result=generated(a,context)
|
||||
assert result['accepted_for_delivery']
|
||||
assert result['answer']['answer'].count('názov v zdroji nie je uvedený')==2
|
||||
assert len(result['answer']['claims'])==2 and not any(c.get('title') for c in result['answer']['claims'])
|
||||
bad=copy.deepcopy(a);bad['claims'][0]['title']='Vymyslený názov'
|
||||
assert 'unsupported_academic_binding' in answer_context_issues(bad,context,check_text=False)
|
||||
bad=copy.deepcopy(a);bad['claims'].pop()
|
||||
assert 'missing_academic_record_details' in answer_context_issues(bad,context,check_text=False)
|
||||
|
||||
|
||||
def shared_context(three=False):
|
||||
people=PEOPLE[:3] if three else PEOPLE[:2]
|
||||
raw={'status':'ok','reason':None,'complete':True,'truncated':False,'aggregation':None,
|
||||
'query':'Aké spoločné kľúčové slová majú '+' a '.join(people)+'?',
|
||||
'constraints':gs.extract_query_constraints('spoločné kľúčové slová'),
|
||||
'resolved_entities':[person(i,p) for i,p in enumerate(people,1)],
|
||||
'sources':[source(i) for i in range(1,len(people)+1)],'facts':[]}
|
||||
for keyword,left,right in [('NLP',1,2),('RAG',1,2)]+([('NLP',1,3)] if three else []):
|
||||
raw['facts'].append({'fact_type':'shared_keyword','keyword':keyword,
|
||||
'left_person':people[left-1],'right_person':people[right-1],
|
||||
'left_document_path':source(left)['document_path'],'right_document_path':source(right)['document_path'],
|
||||
'left_source_url':source(left)['source_url'],'right_source_url':source(right)['source_url']})
|
||||
return graph_context(raw)
|
||||
|
||||
|
||||
def test_shared_tags_are_summarized_once_and_require_all_peoples_evidence():
|
||||
context=shared_context();a=selected(context)
|
||||
result=generated(a,context)
|
||||
assert result['accepted_for_delivery']
|
||||
text=result['answer']['answer']
|
||||
assert 'Spoločné kľúčové slová: **NLP**, **RAG**.' in text
|
||||
assert text.count('NLP')==1 and text.count('RAG')==1 and 'Pri osobe' not in text
|
||||
bad=copy.deepcopy(a);bad['claims'].pop()
|
||||
assert 'missing_shared_property_evidence' in answer_context_issues(bad,context,check_text=False)
|
||||
|
||||
|
||||
def test_pairwise_shared_tag_does_not_mean_shared_by_everyone():
|
||||
context=shared_context(three=True)
|
||||
assert {r['value'] for r in context['shared_properties']}=={'NLP'}
|
||||
text=render_graph_answer(selected(context),context)['answer']
|
||||
assert 'Spoločné kľúčové slová: **NLP**.' in text and '**RAG**' not in text
|
||||
|
||||
|
||||
def test_incomplete_shared_tags_cannot_claim_exhaustive_results():
|
||||
context=shared_context();context.update(complete=False,truncated=True,truncation_reasons=['fact_limit'])
|
||||
result=generated(selected(context),context)
|
||||
assert result['accepted_for_delivery']
|
||||
assert 'Doložené spoločné kľúčové slová' in result['answer']['answer']
|
||||
assert 'Výpis je neúplný.' in result['answer']['answer']
|
||||
@ -76,6 +76,8 @@ def joined_candidates(facts: list[dict], intent: dict) -> list[dict]:
|
||||
continue
|
||||
fields = ['person', 'title', 'year'] if needs_title else ['person', 'year']
|
||||
candidate={k:record[k] for k in fields if record.get(k) is not None}
|
||||
if record.get('work_type'):
|
||||
candidate['work_type']=record['work_type']
|
||||
candidate['keyword']=tag['keyword']
|
||||
candidate['evidence_documents']=sorted({p for p in [record.get('document_path'),tag.get('document_path')] if p})
|
||||
candidate['source_urls']=sorted({u for u in [record.get('source_url'),tag.get('source_url')] if u})
|
||||
@ -220,11 +222,16 @@ def answer_context_issues(answer: dict, context: dict, *, check_text: bool = Tru
|
||||
for claim in claims):
|
||||
issues.append('missing_academic_record_details')
|
||||
break
|
||||
for shared in context.get('shared_properties',[]):
|
||||
for person in shared['people']:
|
||||
if not any(normalized(c.get('person'))==normalized(person) and
|
||||
normalized(c.get(shared['field']))==normalized(shared['value']) for c in claims):
|
||||
issues.append('missing_shared_property_evidence')
|
||||
candidates=context.get('answer_candidates',[])
|
||||
if context.get('status')=='ok' and candidates:
|
||||
if answer.get('status')=='no_answer':
|
||||
issues.append('rejected_supported_person_join')
|
||||
elif not all(any(all(normalized(claim.get(k))==normalized(candidate[k]) for k in ['person','title','year','keyword'] if k in candidate)
|
||||
elif not all(any(all(normalized(claim.get(k))==normalized(candidate[k]) for k in ['person','title','year','keyword','work_type'] if k in candidate)
|
||||
for claim in answer.get('claims',[])) for candidate in candidates):
|
||||
issues.append('split_or_missing_person_join')
|
||||
if context.get('status') in {'no_answer','ambiguous','error'} and answer.get('status')=='ok':
|
||||
|
||||
@ -6,6 +6,7 @@ Title je názov akademickej práce/projektu, nikdy názov profilovej stránky al
|
||||
Všetky podmienky jednej claim patria tej istej osobe. Title a year patria rovnakému akademickému záznamu. Category a document_author musia byť doložené tým istým document_path.
|
||||
Tag patrí osobe cez jej profil aj dcérske dokumenty. Pre kombináciu názvu/roku a tagu nevyžaduj rovnaký dokument. Answer_candidates sú už doložené spojenia cez tú istú osobu; zachovaj ich person, year, keyword a prípadne title spolu v jednej claim. Neodmietaj toto spojenie len pre odlišné document_path.
|
||||
Claims musia pokryť všetky relevantné výsledky dotazu a požadované väzby. Pri porovnaní ľudí uveď každého v samostatnej claim. Pri výbere kandidátov vynechaj nevyhovujúce osoby. Pri názve práce uveď aj rok, ak je vo faktoch známy.
|
||||
Ak sú dodané shared_properties, každú spoločnú hodnotu dolož claim pre každú uvedenú osobu. Text spoločného výsledku zostaví aplikácia. Pri answer_candidates zachovaj aj work_type, ak je dodaný: diplomový projekt a diplomová práca sú odlišné typy. Required_academic_records môžu zahŕňať aj záznamy bez názvu pri otázke na názov; zachovaj ich známy typ, rok a identitu, ale názov nevymýšľaj.
|
||||
Academic_year_comparison opisuje spoločné roky všetkých požadovaných osôb, nie zjednotenie ich rokov. Pri dodaných required_academic_records zachovaj aj typy, názvy a identity prác z týchto rokov. Nepridávaj práce z iných rokov.
|
||||
Source_urls musí obsahovať konkrétne relevantné source_url zo sources pre všetky tvrdenia. Nevymýšľaj ani neupravuj URL. Ak názov/rok dokladá profil a tag dcérsky dokument, cituj oba potrebné zdroje. Nepoužívaj URL iných osôb ako náhradnú citáciu.
|
||||
Count môže byť číslo iba pri aggregation.complete=true a musí sa rovnať aggregation.count. Inak count=null, aj pri no_answer. Počet nie je počet zobrazených zdrojov. Presný agregovaný počet môže byť úplný aj pri orezanom výpise dôkazov.
|
||||
|
||||
@ -10,7 +10,10 @@ def graph_context(result: dict) -> dict:
|
||||
"""
|
||||
facts = []; intent = result['constraints']
|
||||
shared_year_query = intent.get('mentions_shared') and intent.get('mentions_academic_year')
|
||||
work_listing = intent.get('mentions_work_listing', False) or shared_year_query
|
||||
work_listing = (intent.get('mentions_work_listing', False) or shared_year_query or
|
||||
(intent.get('mentions_title') and any(
|
||||
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']:
|
||||
kind = row.get('fact_type', '')
|
||||
@ -33,6 +36,10 @@ def graph_context(result: dict) -> dict:
|
||||
if intent['mentions_start_year']:
|
||||
allowed.add('start_year')
|
||||
fact = {k: v for k, v in row.items() if k in allowed and v is not None}
|
||||
if (intent.get('thesis_types') or intent.get('work_types')) and kind in {'person_thesis','person_work'}:
|
||||
work_type=row.get('thesis_type') or row.get('work_type')
|
||||
if work_type:
|
||||
fact['work_type']=work_type
|
||||
if work_listing and kind in {'person_thesis', 'person_work'}:
|
||||
# Stable source record identity distinguishes two untitled projects
|
||||
# by the same person in the same year. Never use Neo4j element IDs.
|
||||
@ -57,6 +64,14 @@ def graph_context(result: dict) -> dict:
|
||||
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')))
|
||||
shared_values={}
|
||||
for fact in facts:
|
||||
for field in ['keyword','category','document_author']:
|
||||
if intent.get('mentions_shared') and fact.get(field) is not None:
|
||||
shared_values.setdefault((field,fact[field]),set()).add(fact['person'])
|
||||
people=context['requested_people']
|
||||
context['shared_properties']=[{'field':field,'value':value,'people':people}
|
||||
for (field,value),witnesses in shared_values.items() if len(people)>=2 and set(people)<=witnesses]
|
||||
comparison = result.get('academic_year_comparison') or next((row for row in result['facts']
|
||||
if row.get('fact_type') == 'academic_year_comparison'), None)
|
||||
if comparison is not None:
|
||||
|
||||
@ -10,6 +10,16 @@ def render_graph_answer(answer: dict, context: dict) -> dict:
|
||||
out = deepcopy(answer)
|
||||
lines = []
|
||||
if out['status'] == 'ok':
|
||||
shared_fields=set()
|
||||
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
|
||||
and all(any(c.get('person')==person and c.get(field)==row['value'] for c in out['claims'])
|
||||
for person in row['people'])})
|
||||
if values:
|
||||
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)+'.')
|
||||
comparison = context.get('academic_year_comparison')
|
||||
if comparison:
|
||||
people = comparison.get('people', [])
|
||||
@ -27,7 +37,9 @@ def render_graph_answer(answer: dict, context: dict) -> dict:
|
||||
if claim.get('record_id'):
|
||||
records.setdefault(claim['person'], []).append(claim)
|
||||
else:
|
||||
lines.append(fact_sentences(claim, context.get('question', '')))
|
||||
shown={key:value for key,value in claim.items() if key not in shared_fields}
|
||||
if any(key!='person' and value is not None for key,value in shown.items()):
|
||||
lines.append(fact_sentences(shown, context.get('question', '')))
|
||||
index = 0
|
||||
for person, claims in records.items():
|
||||
lines.extend([f'{person} má v dostupných podkladoch tieto práce a projekty:', ''])
|
||||
|
||||
@ -725,6 +725,13 @@ def has_stem(
|
||||
|
||||
def explicit_keyword_phrase(query: str) -> str | None:
|
||||
"""Identify a requested tag without treating 'ZP Wiki' as the tag ZP."""
|
||||
quoted = re.search(
|
||||
r'\b(?:kľúčové\s+slovo|kľúčovým\s+slovom|kľúčového\s+slova|'
|
||||
r'klucove\s+slovo|klucovym\s+slovom|klucoveho\s+slova|tagom|tagu|tag)'
|
||||
r'\s+[„“"«]([^„“”"»]+)[“”"»]', query, flags=re.I,
|
||||
)
|
||||
if quoted:
|
||||
return normalize_for_match(quoted[1])
|
||||
match = re.search(
|
||||
r"\b(?:klucove\s+slovo|klucovym\s+slovom|klucoveho\s+slova|"
|
||||
r"tagom|tagu|tag)\s+(.+)",
|
||||
@ -733,7 +740,9 @@ def explicit_keyword_phrase(query: str) -> str | None:
|
||||
if not match:
|
||||
return None
|
||||
phrase = re.split(
|
||||
r"\s+(?:pri\s+osob\w*|u\s+osob\w*|a\s+(?:jej|jeho|ich|rok|pomenuj|uved\w*)|ak\s+|uved\w*\s+|"
|
||||
r"\s+(?:pri\s+osob\w*|u\s+osob\w*|a\s+(?:zaroven|sucasne|diplomov\w*|bakalarsk\w*|"
|
||||
r"dizertacn\w*|timov\w*|vyskumn\w*|projekt\w*|prac\w*|jej|jeho|ich|rok|pomenuj|uved\w*)|"
|
||||
r"zaroven\b|sucasne\b|(?:v|z|pre)\s+rok\w*\b|ak\s+|uved\w*\s+|"
|
||||
r"pomenuj\s+|maju\s+|ma\s+)",
|
||||
match.group(1),
|
||||
maxsplit=1,
|
||||
@ -1241,7 +1250,7 @@ def requested_name_phrases(query: str) -> list[str]:
|
||||
)
|
||||
if role:
|
||||
tail = re.split(
|
||||
r'\s+(?:s(?:o)?\s+(?:nazv\w*|nazov\w*|rok\w*)|splna\w*|ma|maju|mal|mali|je|su|'
|
||||
r'(?:^|\s+)(?:s(?:o)?\s+(?:nazv\w*|nazov\w*|rok\w*)|splna\w*|ma|maju|mal|mali|je|su|'
|
||||
r'ktory\w*|ktora\w*|pri\s+ktorych|v\s+roku|uved\w*|prepojen\w*|spojen\w*|'
|
||||
r'vyber|najdi|zisti|cez|pomenuj|uved\w*|vypis|ak)\b', role.group(1), maxsplit=1,
|
||||
)[0]
|
||||
@ -1263,6 +1272,7 @@ def requested_name_phrases(query: str) -> list[str]:
|
||||
return list(dict.fromkeys(
|
||||
candidate.strip() for candidate in candidates
|
||||
if 1 <= len(candidate.split()) <= 5 and candidate.split()[0] not in generic
|
||||
and not re.match(r'^(?:diplomov|bakalarsk|dizertacn|timov|vyskumn|zaverecn)\w*\s+(?:prac\w*|projekt\w*)\b',candidate)
|
||||
))
|
||||
|
||||
|
||||
@ -2448,7 +2458,7 @@ def search_graph_with_session(
|
||||
)
|
||||
keyword_specific = bool(
|
||||
not shared_keyword and (
|
||||
(keyword_ids and not (thesis_specific or work_specific))
|
||||
keyword_ids
|
||||
or (person_ids and constraints["mentions_keyword"])
|
||||
)
|
||||
)
|
||||
@ -2488,7 +2498,9 @@ def search_graph_with_session(
|
||||
if constraints["mentions_count"] and not (thesis_specific or work_specific):
|
||||
blocked_reasons.append("unsupported_count_domain")
|
||||
# A quoted unknown work title is a failed exact lookup, not a global listing.
|
||||
quoted_titles = re.findall(r'[„“"«]([^„“”"»]+)[“”"»]', clean_query)
|
||||
quoted_titles = [match[1] for match in re.finditer(r'[„“"«]([^„“”"»]+)[“”"»]', clean_query)
|
||||
if not re.search(r'\b(?:tag|tagom|tagu|klucov\w*\s+slov\w*)$',
|
||||
normalize_for_match(clean_query[:match.start()]))]
|
||||
if quoted_titles and (thesis_specific or work_specific) and not thesis_ids and not work_ids:
|
||||
blocked_reasons.append("unknown_title")
|
||||
constraints["unresolved_title"] = True
|
||||
@ -2509,7 +2521,7 @@ def search_graph_with_session(
|
||||
work_rows = fetch(work_specific, WORK_FACTS_QUERY,
|
||||
work_ids=work_ids, years=constraints["years"],
|
||||
work_types=constraints["work_types"])
|
||||
if title_specific and not constraints["mentions_work_listing"]:
|
||||
if title_specific and not constraints["mentions_work_listing"] and not person_ids:
|
||||
thesis_rows = [row for row in thesis_rows if row.get("title")]
|
||||
work_rows = [row for row in work_rows if row.get("title")]
|
||||
start_rows = fetch(constraints["mentions_start_year"], START_YEAR_FACTS_QUERY,
|
||||
|
||||
@ -80,13 +80,17 @@ def test_last_leading_title_wins_over_introductory_description():
|
||||
assert extract_academics(doc)[0]['title'] == 'Generovanie jazyka s podporou vyhľadávania'
|
||||
|
||||
|
||||
def test_title_lookup_does_not_claim_untitled_work_as_an_answer():
|
||||
def test_title_lookup_preserves_known_record_without_claiming_a_title():
|
||||
fact = {'person_element_id': 'p2', 'person': 'Jakub Maruniak', 'thesis_element_id': 'th1',
|
||||
'year': 2024, 'title': None, 'thesis_type': 'diploma_thesis'}
|
||||
session = FakeSession({gs.CATALOG_QUERY: sample_catalog(), gs.THESIS_FACTS_QUERY: [fact]})
|
||||
result = gs.search_graph_with_session(session, 'Aký názov práce má osoba Jakub Maruniak v roku 2024?')
|
||||
assert result['status'] == 'no_answer'
|
||||
assert result['facts'] == []
|
||||
# A missing title must not erase the independently known work/year. The
|
||||
# delivery regression checks the explicit "title missing" presentation.
|
||||
assert result['status'] == 'ok'
|
||||
assert len(result['facts']) == 1
|
||||
assert result['facts'][0]['title'] is None
|
||||
assert result['facts'][0]['year'] == 2024
|
||||
|
||||
|
||||
def test_explicit_name_precedes_a_different_topic_and_link_title_is_preserved():
|
||||
|
||||
@ -14,13 +14,23 @@ def unsupported_constraint(question: str) -> str | None:
|
||||
if not unicodedata.combining(c))
|
||||
# Do not treat words inside quoted academic titles as query operators.
|
||||
text = re.sub(r'[„“"«][^„“”"»]+[“”"»]', ' ', text)
|
||||
academic = re.search(r'\b(?:prac\w*|projekt\w*)\b',text)
|
||||
completion = re.search(r'\b(?:ukonc\w*|dokonc\w*|obhaj\w*|odovzd\w*|stihn\w*)\b',text)
|
||||
if academic and completion and (re.search(r'\b(?:tymto tempom|predpoved\w*|predik\w*|stihne|stihnu)\b',text) or
|
||||
re.search(r'\b(?:ukonci|ukoncia|dokonci|dokoncia|obhaji|obhaja)\b',text) or
|
||||
(re.search(r'\bodhad\w*\b',text) and
|
||||
re.search(r'\b(?:ukonc\w*|dokonc\w*|obhaj\w*)\b',text))):
|
||||
return 'completion_prediction'
|
||||
if (re.search(r'\b(?:rok\w*|datum\w*|kedy)\b',text) and
|
||||
re.search(r'\b(?:obhajob\w*|obhajil\w*|dokonc\w*|ukoncil\w*|zacal\w* pracovat)\b',text) and
|
||||
re.search(r'\b(?:prac\w*|projekt\w*)\b',text)):
|
||||
re.search(r'\b(?:obhajob\w*|obhajil\w*|dokonc\w*|ukonc\w*|zacal\w* pracovat)\b',text) and academic):
|
||||
return 'work_event_date'
|
||||
if re.search(r'\b(?:od|do|pred|po|medzi)\s+(?:rok(?:om|mi|u)?\s+)?\d{4}\b|\b\d{4}\s*[-–]\s*\d{4}\b', text):
|
||||
return 'year_range'
|
||||
if re.search(r'\b(?:aspon|najviac|viac ako|menej ako|minimalne|maximalne)\s+\d+', text):
|
||||
number = (r'(?:\d+|jeden|jedna|jedno|jednu|jednej|jedneho|dva|dve|dvoch|dvom|dvoma|'
|
||||
r'tri|troch|trom|troma|styri|styroch|styrom|pat\w*|piat\w*|sest\w*|siest\w*|'
|
||||
r'sedem\w*|siedm\w*|osem\w*|osm\w*|devat\w*|deviat\w*|desat\w*|desiat\w*|'
|
||||
r'jedenast\w*|dvanast\w*|trinast\w*|strnast\w*|dvadsat\w*)')
|
||||
if re.search(r'\b(?:aspon|najmenej|najviac|viac ako|menej ako|minimalne|maximalne)\s+'+number+r'\b', text):
|
||||
return 'quantity_condition'
|
||||
if re.search(r'\b(?:okrem|s vynimkou)\b|\b(?:nema|nemaju)\s+(?:tag|klucov|kategori|prac|projekt)|'
|
||||
r'\bbez\s+(?:tagu|klucoveho slova|kategorie)\b', text):
|
||||
@ -32,6 +42,8 @@ def unsupported_constraint(question: str) -> str | None:
|
||||
|
||||
def constraint_message(code: str) -> str:
|
||||
return {
|
||||
'completion_prediction':'Z dostupných poznámok neviem spoľahlivo predpovedať, kto a kedy dokončí prácu. '
|
||||
'Môžem vyhľadať práce evidované v konkrétnom roku; to nie je odhad ich dokončenia.',
|
||||
'work_event_date':'Rok uvedený pri práci nepreukazuje dátum jej začatia, dokončenia alebo obhajoby. '
|
||||
'Chceš zistiť rok evidovaný pri práci?',
|
||||
'year_range': 'Časovú podmienku neviem spoľahlivo vyhodnotiť ako interval. Uveď konkrétny rok práce.',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user