69 lines
4.4 KiB
Python
69 lines
4.4 KiB
Python
"""Exact metadata answers from delivered text, never from Neo4j or a gold set."""
|
|
import re
|
|
from tools.answers.context_policy import normalized,citation_check
|
|
from tools.answers.question_plan import QuestionPlan
|
|
from tools.answers.render_answer import render_graph_answer
|
|
|
|
|
|
def exact_metadata_answer(payload,context):
|
|
if not payload.get('question_plan'):return None
|
|
spec=QuestionPlan.model_validate(payload['question_plan'])
|
|
people=[p.name for p in spec.people if p.role=='student']
|
|
if spec.intent!='metadata' or not people or any(p.role!='student' for p in spec.people):return None
|
|
if set(spec.fields)&{'supervisor','consultant','status','date'}:return None
|
|
q=normalized(spec.question)
|
|
shared=bool(re.search(r'\b(?:spoloc\w*|prienik|obaja|spajaj\w*)\b',q) and len(people)>1)
|
|
keyword=shared and bool(set(spec.fields)&{'keyword','category','document_author'})
|
|
if spec.conditions and not keyword:return None
|
|
sources={s.get('title'):s for s in context['sources'] if s.get('document_path') in {p.source_document for p in spec.people}}
|
|
if not set(people)<=set(sources):return None
|
|
if not keyword and (not payload.get('named_scope_complete') or 'llm_context_budget' in payload.get('truncation_reasons',[])):
|
|
return None
|
|
facts=context['facts']
|
|
record_map={}
|
|
for row in facts:
|
|
if row.get('record_id') and row['person'] in people and spec.matches_work(
|
|
row['person'],row.get('work_label'),row.get('work_type'),row.get('year')):
|
|
old=record_map.get(row['record_id'],{})
|
|
if old.get('title') and row.get('title') and normalized(old['title'])!=normalized(row['title']):return None
|
|
record_map[row['record_id']]={**row,**{k:v for k,v in old.items() if v is not None}}
|
|
records=list(record_map.values())
|
|
claims=[];extra={};status='ok'
|
|
if keyword:
|
|
fields=set(spec.fields)&{'keyword','category','document_author'}
|
|
for field in fields:
|
|
sets=[{r[field] for r in facts if r['person']==p and r.get(field) is not None} for p in people]
|
|
for value in sorted(set.intersection(*sets)):
|
|
extra.setdefault('shared_properties',[]).append({'field':field,'value':value,'people':people})
|
|
claims.extend({'person':p,field:value} for p in people)
|
|
if not claims:status='no_answer'
|
|
elif shared and 'year' in spec.fields:
|
|
years=set.intersection(*[{r['year'] for r in records if r['person']==p and r.get('year') is not None} for p in people])
|
|
records=[r for r in records if r.get('year') in years]
|
|
extra['academic_year_comparison']={'people':people,'shared_years':sorted(years)}
|
|
if not years:status='no_answer'
|
|
if not keyword:
|
|
listing=spec.presentation in {'list','count'} or shared or 'work_type' in spec.fields and spec.scope=='across_works' or 'title' in spec.fields and any(not r.get('title') for r in records)
|
|
for row in records:
|
|
keys=['person','record_id','work_label','work_type','title','year'] if listing else ['person','work_type',*(k for k in ['title','year'] if k in spec.fields)]
|
|
claims.append({k:row[k] for k in keys if k in row})
|
|
if not claims:status='no_answer'
|
|
urls=[sources[p]['source_url'] for p in people]
|
|
out={'status':status,'claims':claims,'source_urls':list(dict.fromkeys(urls))}
|
|
rendered_context={**context,**extra,'complete':True,'truncated':False,'requested_people':people}
|
|
if spec.presentation=='count':
|
|
# Every record has an explicit heading in the fully delivered named
|
|
# profile scope. Never count top-k hits, meetings or taxonomy years.
|
|
count=len({r['record_id'] for r in records})
|
|
status='ok'
|
|
out.update(status='ok',count=count)
|
|
out['answer']='Počet evidovaných prác a projektov: **'+str(count)+'**.\n\nZdroje: '+', '.join(out['source_urls'])
|
|
else:out=render_graph_answer(out,rendered_context)
|
|
if not citation_check(out,set(urls))['citations_ok']:raise ValueError('Exact metadata citations rejected')
|
|
return {**out,'accepted_for_delivery':True,'attempts':[],'complete':True,'truncated':False,
|
|
'truncation_reasons':[],'index_version':context['index_version'],
|
|
'completeness_scope':'explicit_named_profiles',
|
|
'answer_coverage':{'status':'supported' if status=='ok' else 'not_found',
|
|
'scope':'count' if spec.presentation=='count' else 'list' if keyword or listing else 'fact',
|
|
'warnings':[],'limitations':[]}}
|