"""Render witnessed graph paths; an ABOUT edge never means 'used' or 'finished'.""" from collections import defaultdict from tools.answers.answer_errors import clarification_result from tools.answers.context_policy import citation_check, text_urls from tools.knowledge.concepts import concept_key PHASES={'result':'zaznamenaný výsledok','progress':'priebežný stav', 'assignment':'zadanie','planned':'plán','unknown':'podklad'} PREDICATES={'uses':'používa','evaluates':'vyhodnocuje','produces':'vytvára','about':'týka sa'} def graph_details(answer, local): """Keep witnesses for delivered evidence only; never append unseen claims.""" ids={r['id'] for r in answer.get('evidence',[])} paths=[p for p in local.paths if p['anchor_id'] in ids and p['peer_id'] in ids] relations=[r for r in local.relations if r['evidence_id'] in ids] reasons=list(dict.fromkeys([*answer.get('truncation_reasons',[]),*local.truncation_reasons])) if local.truncation_reasons and answer.get('accepted_for_delivery'): notice='Prechod grafom dosiahol limit výberu. Zúž otázku na prácu, tému alebo obdobie.' if notice not in answer['answer']:answer={**answer,'answer':answer['answer']+'\n\n'+notice} if 'presentation_notes' in answer: answer={**answer,'presentation_notes':list(dict.fromkeys([*answer['presentation_notes'],notice]))} # Predicate claims are shown only with their own exact, phase-bound witness. if relations and answer.get('status')=='ok': by_id={r['id']:r for r in answer['evidence']} lines=[] for relation in relations[:3]: row=by_id[relation['evidence_id']] lines.append(f"- {row['person']} — {row['work_label']} ({PHASES.get(row['phase'],'podklad')}): " f"{relation['subject']} → {PREDICATES[relation['predicate']]} → {relation['object']}. " f"Dôkaz: „{relation['quote']}“") text=answer['answer']; marker='\n\nZdroje:' before,sep,after=text.rpartition(marker) block='\n\nOverené významové vzťahy:\n'+'\n'.join(lines) text=before+block+sep+after if sep else text+block answer={**answer,'answer':text} return {**answer,'complete':False,'truncated':bool(reasons), 'truncation_reasons':reasons, 'graph_paths':paths,'graph_relations':relations, 'graph_retrieval':{'strategy':'bounded_local_graph','seeds':len(local.seeds), 'evidence':len(local.units),'paths':len(local.paths)}} def render_similarity(question,index,bound,local,*,limit=5,complete=None): spec=bound.spec if spec.period or any(w.work_type or w.years or w.labels or w.numbers or w.excluded_labels or w.excluded_numbers or w.unnumbered for w in spec.works): return clarification_result('Má rok alebo typ práce obmedziť porovnávanú prácu, alebo aj hľadaných študentov? Uveď rozsah tejto podmienky.', 'ambiguous_similarity_scope',engine='graphrag',index_version=index['index_version']) by_id={u['id']:u for u in local.units} valid=[p for p in local.paths if p['anchor_id'] in by_id and p['peer_id'] in by_id and by_id[p['anchor_id']]['source_path']!=by_id[p['peer_id']]['source_path']] candidates=list({p['peer_id']:by_id[p['peer_id']] for p in valid}.values()) references=list({p['anchor_id']:by_id[p['anchor_id']] for p in valid}.values()) if complete and candidates: from tools.knowledge.query import select_topic_evidence if len(candidates)>40:local.truncation_reasons.append('graph_selection_limit') candidates=select_topic_evidence(question,candidates[:40],complete,references=references[:8]) chosen={r['id'] for r in candidates} groups=defaultdict(list) for path in valid: if path['peer_id'] in chosen:groups[by_id[path['peer_id']]['source_path']].append(path) def priority(key): rows=[by_id[p['peer_id']] for p in groups[key]] return (-sum(r['phase'] in {'result','progress'} for r in rows),key) paths=[];evidence={};lines=[];size=0 if len(groups)>limit:local.truncation_reasons.append('graph_result_limit') for source in sorted(groups,key=priority)[:limit]: matches=groups[source] peer=by_id[matches[0]['peer_id']] selected=[];topics_seen=set() for path in sorted(matches,key=lambda p:(by_id[p['peer_id']]['phase'] not in {'result','progress'},p['concept'])): if path['concept'] in topics_seen:continue selected.append(path);topics_seen.add(path['concept']) if len(selected)==2:break fits=[];used=set() for path in selected: ids={path['anchor_id'],path['peer_id']} - used cost=sum(len(by_id[key]['text'])+len(by_id[key]['work_label'])+150 for key in ids) if size+cost>18000: local.truncation_reasons.append('graph_answer_budget');continue fits.append(path);used|=ids;size+=cost selected=fits if not selected:continue lines.append('**'+peer['person']+'**') topics=sorted({p['concept'] for p in selected}) lines.append('Prepája ich doložená téma: '+', '.join(topics)+'.') shown=set() for path in selected: for endpoint in ('anchor_id','peer_id'): row=by_id[path[endpoint]] if row['id'] in shown:continue shown.add(row['id']);evidence[row['id']]=row stamp='; záznam '+row['record_date'] if row.get('record_date') else '' lines.append(f"- {row['person']} — {row['work_label']} ({PHASES.get(row['phase'],'podklad')}{stamp}): {row['text']}") paths.append(path) # An explicit common predicate requires two verified relations, never # just both students mentioning its endpoints. left=[r for r in local.relations if r['evidence_id'] in shown and by_id[r['evidence_id']]['source_path']!=source] right=[r for r in local.relations if r['evidence_id'] in shown and by_id[r['evidence_id']]['source_path']==source] for a in left: for b in right: actual=lambda r:by_id[r['evidence_id']]['phase'] in {'result','progress'} planned=lambda r:by_id[r['evidence_id']]['phase'] in {'assignment','planned'} if a['predicate']==b['predicate'] and concept_key(a['object'])==concept_key(b['object']) and ( actual(a) and actual(b) or planned(a) and planned(b)): lines.append('Spoločný overený vzťah'+('' if actual(a) else ' v zadaní alebo pláne')+ ': '+PREDICATES[a['predicate']]+' → '+a['object']+'.') status='ok' if evidence else 'no_answer' if not evidence and 'graph_answer_budget' in local.truncation_reasons: return graph_details(clarification_result('Doložené prepojenia sa nezmestili do odpovede. Zúž otázku na konkrétnu prácu alebo tému.', 'graph_answer_budget',engine='graphrag',index_version=index['index_version']),local) if evidence: lines.append('Prepojenie dokladá uvedenú tému alebo činnosť; nepreukazuje spoluprácu ani rovnaký stupeň dokončenia.') else: lines=['V ohraničenom lokálnom grafe sa nenašlo dostatočne doložené prepojenie k porovnávanej osobe. Nejde o potvrdenie, že podobná práca neexistuje.'] urls=list(dict.fromkeys(r['source_url'] for r in evidence.values())) if urls:lines.append('Zdroje: '+', '.join(urls)) text='\n\n'.join([*bound.resolution.get('notices',[]),*lines]) allowed=set(urls) for row in evidence.values():allowed|=text_urls(row['text'])|text_urls(row['work_label']) if not citation_check({'answer':text,'source_urls':urls},allowed)['citations_ok']: from tools.knowledge.query import ContentAnswerRejected raise ContentAnswerRejected('Graph path citations rejected') return graph_details({'status':status,'reason':None if evidence else 'no_local_graph_connection', 'engine':'graphrag','accepted_for_delivery':True,'claims':[],'answer':text,'source_urls':urls, 'index_version':index['index_version'],'knowledge_version':index['version'], 'evidence':list(evidence.values()),'complete':False,'truncated':False,'truncation_reasons':[], 'presentation_notes':['Prepojenie dokladá uvedenú tému alebo činnosť; nepreukazuje spoluprácu ani rovnaký stupeň dokončenia.'] if evidence else [], 'answer_coverage':{'scope':'overview','status':'partial' if local.truncation_reasons else 'supported' if evidence else 'not_found', 'warnings':[],'limitations':list(dict.fromkeys(local.truncation_reasons))}},local)