diff --git a/backend/pipeline/constructability.py b/backend/pipeline/constructability.py index ed1c735..e8265e1 100644 --- a/backend/pipeline/constructability.py +++ b/backend/pipeline/constructability.py @@ -27,6 +27,10 @@ def constructability_review(sheets: List[Dict], clusters: List[Dict], "assertions": dumps(slim_sheets(sheets)), "clusters": dumps(slim_clusters(clusters)), "conflicts": dumps(conflicts), + "disputes": dumps([ + d for cluster in clusters + for d in (cluster.get("disputed_attributes") or []) + ]), }, max_tokens=config.CONSTRUCT_MAX_TOKENS, ) diff --git a/backend/pipeline/runner.py b/backend/pipeline/runner.py index 519733d..47d2164 100644 --- a/backend/pipeline/runner.py +++ b/backend/pipeline/runner.py @@ -33,6 +33,7 @@ from backend.pipeline.normalizer import normalize_assertions, build_project_inte from backend.pipeline.clusterer import cluster_by_location from backend.pipeline.llm_clusterer import cluster_by_location_llm from backend import config +from backend.agents.disputes import annotate_clusters from backend.pipeline.conflict_checker import check_conflicts from backend.pipeline.qaqc_review import senior_review from backend.pipeline.code_review import code_review @@ -129,6 +130,10 @@ def _run_stages( else: clusters = cluster_by_location(sheets) + disputed_count = annotate_clusters(clusters) + if disputed_count: + print(f"[Cluster] {disputed_count} cluster(s) carry disputed extracted values") + stage("Reason over clusters (conflicts)") conflicts = check_conflicts(clusters, pages) diff --git a/tests/agents/test_classic_constructability_disputes.py b/tests/agents/test_classic_constructability_disputes.py new file mode 100644 index 0000000..6181e1c --- /dev/null +++ b/tests/agents/test_classic_constructability_disputes.py @@ -0,0 +1,51 @@ +"""Classic pipeline path must also satisfy the {disputes} placeholder added to +CONSTRUCTABILITY_USER_INSTRUCTION (agent path substitutes it in construct_agent.py; +the classic stage builds its own subs dict).""" + +from unittest.mock import patch + +from backend.pipeline._stage import render +from backend.pipeline.constructability import constructability_review +from backend.prompts import CONSTRUCTABILITY_USER_INSTRUCTION + + +def _cluster_with_dispute(): + return { + "key": "c1", + "assertions": [], + "disputed_attributes": [{ + "attribute": "stud_pack_size", + "values": ["(2) 2x6 STUD PACK", "(5) 2x6 STUD PACK"], + "assertion_ids": ["a1", "a2"], + }], + } + + +def test_classic_constructability_supplies_disputes_sub(): + captured = {} + + def fake_call_stage(system_prompt, user_instruction, subs=None, **kwargs): + captured["subs"] = subs or {} + return {"issues": []} + + with patch("backend.pipeline.constructability.call_stage", fake_call_stage): + constructability_review([], [_cluster_with_dispute()], []) + + assert "disputes" in captured["subs"], "classic path must substitute {disputes}" + rendered = render(CONSTRUCTABILITY_USER_INSTRUCTION, captured["subs"]) + assert "{disputes}" not in rendered + assert "(5) 2x6 STUD PACK" in rendered + + +def test_classic_constructability_disputes_defaults_empty(): + captured = {} + + def fake_call_stage(system_prompt, user_instruction, subs=None, **kwargs): + captured["subs"] = subs or {} + return {"issues": []} + + with patch("backend.pipeline.constructability.call_stage", fake_call_stage): + constructability_review([], [{"key": "c2", "assertions": []}], []) + + rendered = render(CONSTRUCTABILITY_USER_INSTRUCTION, captured["subs"]) + assert "{disputes}" not in rendered