"""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':[]}}