dp-zp-agent/tools/answers/evidence_selection.py

145 lines
5.1 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 tools.answers.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 tools.answers.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