121 lines
8.2 KiB
Python
121 lines
8.2 KiB
Python
"""Execute facet requests against scoped assertions, never infer missing facts."""
|
|
from collections import defaultdict
|
|
import re
|
|
|
|
from tools.answers.answer_errors import clarification_result
|
|
from tools.answers.context_policy import normalized
|
|
from tools.answers.question_intent import FACET_LABELS
|
|
from tools.answers.content_selection import facets_for, unit_score
|
|
from tools.knowledge.evidence import claim_eligible
|
|
|
|
|
|
def answer_facets(bound, index, units, *, engine, limit):
|
|
plan=bound.spec
|
|
people=[p for p in plan.people if p.role=='student']
|
|
base={'engine':engine,'index_version':index['index_version'],'knowledge_version':index['version'],
|
|
'accepted_for_delivery':True,'claims':[],'complete':False,'truncated':False,'truncation_reasons':[],
|
|
'name_resolution':bound.resolution}
|
|
def clarify(reason,text):
|
|
return {**base,**clarification_result(text,reason,engine=engine,index_version=index['index_version'])}
|
|
if not people:return clarify('missing_subject','Uveď celé meno študenta, ktorého práce mám prehľadať.')
|
|
paths={p.source_document for p in people if p.source_document}
|
|
groups=defaultdict(list)
|
|
for row in units:
|
|
if not claim_eligible(row) or row['person'] not in {p.name for p in people}:continue
|
|
if paths and row['source_path'] not in paths:continue
|
|
if not row.get('work_label') or not plan.matches_work(row['person'],row['work_label'],row.get('work_type'),row.get('work_year')):continue
|
|
period=plan.period
|
|
if period and period.field!='work_year':
|
|
actual=row.get(period.field)
|
|
if not actual or not period.start<=actual<=period.end:continue
|
|
groups[(row['person'],row['work_label'])].append(row)
|
|
from tools.answers.question_plan import ambiguous_work_choice
|
|
message=ambiguous_work_choice(plan,[{'person':p,'work_label':label} for p,label in groups])
|
|
if message:return clarify('ambiguous_work',message)
|
|
for person in people:
|
|
labels=[label for name,label in groups if name==person.name]
|
|
if plan.scope=='selected_works':
|
|
from tools.retrieval.work_selector import label_key
|
|
for work in plan.works:
|
|
if work.person in {None,person.name} and any(v not in {label_key(label) for label in labels} for v in work.labels):
|
|
return clarify('unresolved_work_selection','Niektoré požadované práce osoby '+person.name+' sa v indexe nenašli. Uveď presné označenia prác.')
|
|
order={p.name:i for i,p in enumerate(people)}
|
|
keys=sorted(groups,key=lambda k:(order[k[0]],-(max(r.get('work_year') or 0 for r in groups[k])),k[1]))
|
|
if len(people)>1:
|
|
by_person={p.name:[key for key in keys if key[0]==p.name] for p in people}
|
|
keys=[part[i] for i in range(max((len(v) for v in by_person.values()),default=0))
|
|
for part in by_person.values() if i<len(part)]
|
|
requested=plan.facets or ['assignment','results']
|
|
lines=[];evidence=[];missing=False;truncated=len(keys)>limit;size=0;notes=[]
|
|
cells=defaultdict(list)
|
|
for key in keys[:limit]:
|
|
person,label=key
|
|
lines.append('**'+person+' — '+label+'**')
|
|
used=set();work_size=0
|
|
work_budget=18000//max(1,min(len(keys),limit))
|
|
for facet in requested:
|
|
candidates=[]
|
|
for row in groups[key]:
|
|
if row['kind'] in {'reference','work_record','meeting','supervisor','consultant','lifecycle','topic'}:continue
|
|
role={'assignment':'assignment','planned':'task','result':'progress','progress':'progress'}.get(row['phase'],'other')
|
|
if facet in facets_for(row['text'],role,plan.question,row['phase']):candidates.append(row)
|
|
candidates.sort(key=lambda r:(-unit_score(r['text'], {'result':'progress','progress':'progress','planned':'task'}.get(r['phase'],r['phase']),facet),r['line']))
|
|
if facet=="assignment":candidates.sort(key=lambda r:r["line"])
|
|
distinct={}
|
|
for row in candidates:distinct.setdefault(normalized(row['text']),row)
|
|
candidates=list(distinct.values())
|
|
lines.append(FACET_LABELS[facet]+':')
|
|
if not candidates:
|
|
notes.append(person+' — '+label+': '+FACET_LABELS[facet].lower()+': v podkladoch chýbajú doložené údaje.')
|
|
lines.append('V podkladoch k tejto práci chýbajú doložené údaje.');cells[(key,facet)].append('V podkladoch chýbajú doložené údaje.');missing=True;continue
|
|
take=16 if facet=='assignment' else 2 if plan.presentation=='brief' else 5
|
|
if len(candidates)>take:truncated=True
|
|
for row in candidates[:take]:
|
|
if size+len(row['text'])>18000 or work_size+len(row['text'])>work_budget:
|
|
truncated=True;continue
|
|
size+=len(row['text']);work_size+=len(row['text'])
|
|
quote=re.sub(r'^\s*(?:[-*+]\s+|\d+[.)]\s+)','',row['text'])
|
|
phase={'assignment':'zadanie','planned':'plán','progress':'priebežný stav','result':'zaznamenaný výsledok'}.get(row['phase'],'podklad')
|
|
lines.append('- '+quote+' ('+phase+')')
|
|
cells[(key,facet)].append(quote+' ('+phase+')')
|
|
if row['id'] not in used:evidence.append(row);used.add(row['id'])
|
|
if not cells[(key,facet)]:lines.append('Doložené výňatky sa nezmestili do tohto výpisu.')
|
|
if plan.presentation in {'comparison','table'} and keys:
|
|
from tools.answers.presentation import comparison_table
|
|
table=comparison_table(['Oblasť',*(name+' — '+label for name,label in keys[:limit])],
|
|
[(FACET_LABELS[facet],['\n'.join(cells[(key,facet)]) for key in keys[:limit]]) for facet in requested])
|
|
if table:lines=['\n'.join(table)]
|
|
elif plan.presentation=='table':lines.append('Podklady sú príliš dlhé alebo obsahujú zložité citácie; prehľad uvádzam v samostatných sekciách.')
|
|
if groups and not evidence and truncated:
|
|
return clarify('content_budget_exceeded','Doložené podklady sa nezmestili do výpisu. Zúž otázku na jednu prácu alebo konkrétnu oblasť; nejde o potvrdenie, že údaje chýbajú.')
|
|
if not groups:
|
|
lines=['Pre požadované osoby a práce sa v dostupných podkladoch nenašli zodpovedajúce údaje.']
|
|
for p in people:
|
|
if not any(key[0]==p.name for key in groups):
|
|
notes.append(p.name+': požadované podklady k práci sa nenašli.')
|
|
lines.append(p.name+': požadované podklady k práci sa nenašli.');missing=True
|
|
elif not any(key[0]==p.name for key in keys[:limit]):
|
|
notes.append(p.name+': podklady sa nezmestili do limitu výpisu. Zvýš limit alebo zúž otázku.')
|
|
lines.append(p.name+': podklady sa nezmestili do limitu výpisu. Zvýš limit alebo zúž otázku.');missing=True
|
|
if any(c.kind=='current_status' for c in plan.conditions):
|
|
note='Ide o zaznamenané podklady; samy nepotvrdzujú aktuálny stav k dnešnému dňu.'
|
|
lines.append(note);notes.append(note)
|
|
if truncated:
|
|
note='Zobrazený je obmedzený výber. Zúž otázku na osobu, prácu, obdobie alebo konkrétnu oblasť.'
|
|
lines.append(note);notes.append(note)
|
|
urls=list(dict.fromkeys(r['source_url'] for r in evidence))
|
|
if urls:lines.append('Zdroje: '+', '.join(urls))
|
|
text='\n\n'.join([*bound.resolution.get('notices',[]),*lines])
|
|
from tools.answers.context_policy import citation_check,text_urls
|
|
allowed=set(urls)
|
|
for row in evidence:allowed|=text_urls(row['text'])
|
|
if not citation_check({'answer':text,'source_urls':urls},allowed)['citations_ok']:
|
|
from tools.knowledge.query import ContentAnswerRejected
|
|
raise ContentAnswerRejected('Planned answer citations rejected')
|
|
return {**base,'status':'ok' if evidence else 'no_answer','reason':None if evidence else 'missing_evidence',
|
|
'answer':text,'source_urls':urls,'evidence':evidence,'truncated':truncated,'presentation_notes':notes,
|
|
'truncation_reasons':['content_result_limit'] if truncated else [],
|
|
'answer_coverage':{'scope':'assignment' if requested==['assignment'] else 'overview',
|
|
'status':'partial' if missing or truncated else 'supported' if evidence else 'not_found','warnings':[],
|
|
'limitations':['content_result_limit'] if truncated else []}}
|