"""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 ilimit;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') proposal=(facet=='assignment' and row['phase']=='planned' and row.get('section_scope')=='work' and any(ntake: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') if facet=='assignment' and row['phase']=='planned':phase='navrhovaný cieľ' lines.append('- '+quote+' ('+phase+')') cells[(key,facet)].append(quote+' ('+phase+')') if row['id'] not in used:evidence.append(row);used.add(row['id']) if facet=='assignment' and candidates and key in tentative_goals: note=person+' — '+label+': zdroj tieto body označuje ako možné/navrhované ciele, nie ako potvrdené splnené výsledky.' notes.append(note);lines.append(note) 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 []}}