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

160 lines
6.5 KiB
Python

"""Production graph-to-answer context adapter; no benchmark or oracle dependencies."""
from tools.answers.context_policy import joined_candidates
def delivery_facts(context, result):
"""Preserve source work identity independently of the model's field subset.
A question can ask for type/year without asking to display a record ID or
heading. The final scope check still needs those trusted source fields.
These extra bindings are not added to the language model's payload.
"""
facts = list(context.get('facts', []))
for row in [*result.get('facts', []), *result.get('supporting_facts', [])]:
if not row.get('person') or row.get('fact_type') not in {'person_thesis', 'person_work'}:
continue
fact = {k: row[k] for k in ['person', 'title', 'year', 'start_year', 'source_url', 'document_path'] if k in row}
fact.update(work_type=row.get('thesis_type') or row.get('work_type'),
work_label=row.get('heading'), record_id=row.get('thesis_id') or row.get('work_id'))
facts.append(fact)
return facts
def graph_context(result: dict) -> dict:
"""Remove storage/profile metadata that cannot answer the actual question.
Uses only actual retrieval output and query intent, never gold annotations.
Values/subjects/source paths are copied without inventing or rewriting facts.
"""
facts = []
intent = result["constraints"]
shared_year_query = intent.get("mentions_shared") and intent.get("mentions_academic_year")
work_listing = (
intent.get("mentions_work_listing", False)
or shared_year_query
or (
intent.get("mentions_title")
and any(
row.get("fact_type") in {"person_thesis", "person_work"} and not row.get("title")
for row in result["facts"]
)
)
)
academic_records = []
for row in [*result["facts"], *result.get("supporting_facts", [])]:
kind = row.get("fact_type", "")
if kind.startswith("shared_"):
field = {
"shared_keyword": "keyword",
"shared_category": "category",
"shared_document_author": "document_author",
}.get(kind)
if field:
for side in ["left", "right"]:
facts.append(
{
"person": row[f"{side}_person"],
field: row[field],
"document_path": row.get(f"{side}_document_path"),
"source_url": row.get(f"{side}_source_url"),
}
)
continue
if not row.get("person"):
continue
allowed = {
"person",
"document_path",
"source_url",
"keyword",
"category",
"document_author",
}
if intent["mentions_title"] or work_listing:
allowed.add("title")
if (
intent["mentions_title"]
or intent["mentions_academic_year"]
or intent["years"]
or work_listing
):
allowed.add("year")
if intent["mentions_start_year"]:
allowed.add("start_year")
fact = {k: v for k, v in row.items() if k in allowed and v is not None}
if (intent.get("thesis_types") or intent.get("work_types")) and kind in {
"person_thesis",
"person_work",
}:
work_type = row.get("thesis_type") or row.get("work_type")
if work_type:
fact["work_type"] = work_type
if work_listing and kind in {"person_thesis", "person_work"}:
# Stable source record identity distinguishes two untitled projects
# by the same person in the same year. Never use Neo4j element IDs.
fact.update(
record_id=row.get("thesis_id") or row.get("work_id"),
work_type=row.get("thesis_type") or row.get("work_type"),
work_label=row.get("heading"),
title=row.get("title"),
)
fact = {k: v for k, v in fact.items() if v is not None or k == "title"}
if fact not in academic_records:
academic_records.append(fact)
if fact not in facts:
facts.append(fact)
sources = result.get("sources")
if sources is None:
sources = [
{k: d[k] for k in ["document_path", "source_url"] if d.get(k)}
for d in result.get("documents", [])
]
context = {
"status": result["status"],
"reason": result["reason"],
"facts": facts,
"sources": sources,
"aggregation": result["aggregation"],
"complete": result.get("complete"),
"truncated": result.get("truncated", False),
"truncation_reasons": result.get("truncation_reasons", []),
"index_version": result.get("index_version"),
}
context["answer_candidates"] = joined_candidates(facts, intent)
context["question"] = result.get("resolved_query") or result.get("query", "")
context["requested_years"] = (
intent.get("years", []) if not intent.get("mentions_start_year") else []
)
context["requested_work_types"] = [
*intent.get("thesis_types", []),
*intent.get("work_types", []),
]
context["clarification"] = result.get("clarification")
context["requested_people"] = list(
dict.fromkeys(
row["name"]
for row in result.get("resolved_entities", [])
if row.get("kind") == "Person" and row.get("name")
)
)
shared_values = {}
for fact in facts:
for field in ["keyword", "category", "document_author"]:
if intent.get("mentions_shared") and fact.get(field) is not None:
shared_values.setdefault((field, fact[field]), set()).add(fact["person"])
people = context["requested_people"]
context["shared_properties"] = [
{"field": field, "value": value, "people": people}
for (field, value), witnesses in shared_values.items()
if len(people) >= 2 and set(people) <= witnesses
]
comparison = result.get("academic_year_comparison") or next(
(row for row in result["facts"] if row.get("fact_type") == "academic_year_comparison"), None
)
if comparison is not None:
context["academic_year_comparison"] = comparison
if work_listing:
context["required_academic_records"] = academic_records
return context