feat: deterministic disputed-value detection for cluster assertions
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
"""Deterministic detection of contradictory extracted values within a cluster.
|
||||
|
||||
Extraction is a vision pass: quantities and sizes can be misread ("(2) 2x6" vs
|
||||
"(5) 2x6"). Cluster members are supposed to describe the same real-world
|
||||
element, so two members asserting different values for the same attribute are
|
||||
a probable misread. Flag these so downstream text-only stages treat the value
|
||||
as unverified instead of reasoning from one reading.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
def _norm(value) -> str:
|
||||
return re.sub(r"\s+", " ", str(value or "").strip().lower())
|
||||
|
||||
|
||||
def find_disputes(assertions: List[Dict]) -> List[Dict]:
|
||||
"""Same attribute with >= 2 distinct normalized values = disputed."""
|
||||
groups: Dict[str, Dict[str, Dict]] = {}
|
||||
for assertion in assertions:
|
||||
attribute = _norm(assertion.get("attribute"))
|
||||
value = _norm(assertion.get("value"))
|
||||
if not attribute or not value:
|
||||
continue
|
||||
# Group on the normalized value, but keep the original (whitespace-
|
||||
# collapsed) text so disputes read like the sheet, not a lowercase munge.
|
||||
original = re.sub(r"\s+", " ", str(assertion.get("value")).strip())
|
||||
bucket = groups.setdefault(attribute, {}).setdefault(
|
||||
value, {"original": original, "ids": set()}
|
||||
)
|
||||
bucket["ids"].add(assertion.get("id"))
|
||||
disputes = []
|
||||
for attribute, values in sorted(groups.items()):
|
||||
if len(values) < 2:
|
||||
continue
|
||||
disputes.append({
|
||||
"attribute": attribute,
|
||||
"values": sorted(v["original"] for v in values.values()),
|
||||
"assertion_ids": sorted(
|
||||
aid for v in values.values() for aid in v["ids"] if aid
|
||||
),
|
||||
})
|
||||
return disputes
|
||||
|
||||
|
||||
def annotate_clusters(clusters: List[Dict]) -> int:
|
||||
"""Attach disputed_attributes to each cluster that has any. Returns count."""
|
||||
annotated = 0
|
||||
for cluster in clusters:
|
||||
disputes = find_disputes(cluster.get("assertions") or [])
|
||||
if disputes:
|
||||
cluster["disputed_attributes"] = disputes
|
||||
annotated += 1
|
||||
return annotated
|
||||
@@ -0,0 +1,42 @@
|
||||
from backend.agents.disputes import annotate_clusters, find_disputes
|
||||
|
||||
|
||||
def _a(id_, attribute, value):
|
||||
return {"id": id_, "attribute": attribute, "value": value,
|
||||
"source_text": value}
|
||||
|
||||
|
||||
def test_find_disputes_flags_same_attribute_different_values():
|
||||
assertions = [
|
||||
_a("a1", "stud_pack_size", "(2) 2x6 STUD PACK"),
|
||||
_a("a2", "stud_pack_size", "(5) 2x6 STUD PACK"),
|
||||
_a("a3", "beam_size", "HSS16X4X5/8"),
|
||||
]
|
||||
disputes = find_disputes(assertions)
|
||||
assert len(disputes) == 1
|
||||
assert disputes[0]["attribute"] == "stud_pack_size"
|
||||
assert disputes[0]["values"] == ["(2) 2x6 STUD PACK", "(5) 2x6 STUD PACK"]
|
||||
assert disputes[0]["assertion_ids"] == ["a1", "a2"]
|
||||
|
||||
|
||||
def test_find_disputes_ignores_agreeing_values_and_blanks():
|
||||
assertions = [
|
||||
_a("a1", "beam_size", "HSS16X4X5/8"),
|
||||
_a("a2", "beam_size", " hss16x4x5/8 "), # same after normalize
|
||||
_a("a3", "", "orphan"), # no attribute -> skipped
|
||||
_a("a4", "beam_size", ""), # no value -> skipped
|
||||
]
|
||||
assert find_disputes(assertions) == []
|
||||
|
||||
|
||||
def test_annotate_clusters_writes_disputed_attributes():
|
||||
clusters = [
|
||||
{"key": "c1", "assertions": [
|
||||
_a("a1", "stud_pack_size", "(2) 2x6"),
|
||||
_a("a2", "stud_pack_size", "(5) 2x6"),
|
||||
]},
|
||||
{"key": "c2", "assertions": [_a("a3", "x", "1"), _a("a4", "x", "1")]},
|
||||
]
|
||||
assert annotate_clusters(clusters) == 1
|
||||
assert clusters[0]["disputed_attributes"][0]["attribute"] == "stud_pack_size"
|
||||
assert "disputed_attributes" not in clusters[1]
|
||||
Reference in New Issue
Block a user