129 lines
8.3 KiB
Python
129 lines
8.3 KiB
Python
"""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}
|
|
# 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':[],
|
|
'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)
|