145 lines
5.1 KiB
Python
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
|