86 lines
4.4 KiB
Python
86 lines
4.4 KiB
Python
"""Select literal evidence by opaque IDs; never repair a model's factual prose."""
|
|
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
import hashlib
|
|
import json
|
|
import re
|
|
|
|
|
|
class EvidenceSelectionError(ValueError):
|
|
pass
|
|
|
|
|
|
def identify(row: dict, *, required: bool = False) -> dict:
|
|
payload={k:row.get(k) for k in ['source_id','passage_id','person','work_type','year',
|
|
'work_label','text','kind','phase']}
|
|
digest=hashlib.sha256(json.dumps(payload,sort_keys=True,ensure_ascii=False).encode()).hexdigest()[:24]
|
|
return {**row,'evidence_id':'E'+digest,'required':required}
|
|
|
|
|
|
def overview_candidates(context: dict) -> list[dict]:
|
|
"""Keep every main assignment item; do not confuse a backlog with goals."""
|
|
from .content_selection import evidence_units, administrative_only, unit_score, scope_groups, matches_scope
|
|
rows=evidence_units(context); selected=[]
|
|
for person,label in scope_groups(context):
|
|
own=[row for row in rows if matches_scope(row,person,label)]
|
|
main=[row for row in own if row['kind']=='assignment']
|
|
# Without an explicit assignment, retain a bounded descriptive sample.
|
|
# A plan or meeting history cannot become the main assignment.
|
|
fallback=[row for row in own if row['kind'] in {'other','progress'} and not administrative_only(row['text'])]
|
|
fallback.sort(key=lambda row:-unit_score(row['text'],row['kind']))
|
|
for row in main or fallback[:2]:
|
|
if len(row['text'])>2500:
|
|
context['complete']=False;context['truncated']=True
|
|
context['truncation_reasons']=list(dict.fromkeys([*(context.get('truncation_reasons') or []),'evidence_unit_limit']))
|
|
continue
|
|
selected.append(identify(row,required=bool(main)))
|
|
return selected
|
|
|
|
|
|
def resolve_selection(answer: dict, context: dict) -> dict:
|
|
"""Expand only IDs from the actual budgeted model input, keeping raw data intact."""
|
|
out=deepcopy(answer); ids=out.pop('evidence_ids',[])
|
|
if not ids:return out
|
|
if out.get('evidence_quotes'):
|
|
raise EvidenceSelectionError('mixed_evidence_selection')
|
|
choices={row['evidence_id']:row for row in context.get('content_candidates',[]) if row.get('evidence_id')}
|
|
if any(identifier not in choices for identifier in ids):
|
|
raise EvidenceSelectionError('unknown_evidence_id')
|
|
wanted=set(ids)
|
|
chosen=[row for row in context.get('content_candidates',[]) if row.get('evidence_id') in wanted]
|
|
out['evidence_quotes']=[{k:row[k] for k in ['source_id','passage_id']}|{'quote':row['text']} for row in chosen]
|
|
sources={row['source_id']:row for row in context.get('sources',[])}
|
|
urls=[sources[row['source_id']]['source_url'] for row in chosen
|
|
if sources.get(row['source_id'],{}).get('source_url')]
|
|
# Extra model-supplied URLs remain visible to the citation guard.
|
|
out['source_urls']=list(dict.fromkeys([*out.get('source_urls',[]),*urls]))
|
|
return out
|
|
|
|
|
|
def missing_required_evidence(answer: dict, context: dict) -> list[str]:
|
|
"""A short overview must still cover the supplied main assignment items."""
|
|
def plain(text):
|
|
return ' '.join(re.sub(r'^[ \t]*(?:\d+[.)]|[-+*])\s+','',text,flags=re.M).split())
|
|
quotes=answer.get('evidence_quotes',[])
|
|
return [row['evidence_id'] for row in context.get('content_candidates',[]) if row.get('required')
|
|
and not any(q['source_id']==row['source_id'] and
|
|
(not q.get('passage_id') or q['passage_id']==row['passage_id']) and
|
|
plain(row['text']) in plain(q['quote']) for q in quotes)]
|
|
|
|
|
|
def overview_units(answer: dict, context: dict) -> list[dict]:
|
|
"""Render all checked assignment items and at most two fallback units per person."""
|
|
from .content_selection import evidence_units,scope_groups,matches_scope
|
|
rows=evidence_units(context,answer.get('evidence_quotes',[]))
|
|
selected=[]
|
|
for person,label in scope_groups(context):
|
|
own=[row for row in rows if matches_scope(row,person,label)]
|
|
main=[row for row in own if row['kind']=='assignment']
|
|
for row in main or own[:2]:
|
|
quote=next(q for q in answer['evidence_quotes'] if q['source_id']==row['source_id'] and
|
|
' '.join(row['text'].split()) in ' '.join(q['quote'].split()) and
|
|
(not q.get('passage_id') or q['passage_id']==row['passage_id']))
|
|
selected.append({'evidence':quote,'text':row['text']})
|
|
return selected
|