Закрепить Y9 selected-object agent loop
This commit is contained in:
+124
-6
@@ -2937,15 +2937,27 @@ def build_scenario_step_state(
|
||||
current_date_scope = derive_mcp_discovery_business_overview_date_scope(debug)
|
||||
if current_date_scope is None:
|
||||
current_date_scope = context.get("date_scope")
|
||||
runtime_contract = (
|
||||
debug.get("assistant_runtime_contract_v1")
|
||||
if isinstance(debug.get("assistant_runtime_contract_v1"), dict)
|
||||
else {}
|
||||
)
|
||||
assistant_truth_policy = (
|
||||
debug.get("assistant_truth_answer_policy_v1")
|
||||
if isinstance(debug.get("assistant_truth_answer_policy_v1"), dict)
|
||||
else {}
|
||||
)
|
||||
truth_policy = debug.get("truth_answer_policy_v1") if isinstance(debug.get("truth_answer_policy_v1"), dict) else {}
|
||||
if not truth_policy:
|
||||
truth_policy = assistant_truth_policy
|
||||
truth_gate = truth_policy.get("truth_gate") if isinstance(truth_policy.get("truth_gate"), dict) else {}
|
||||
truth_answer_shape = truth_policy.get("answer_shape") if isinstance(truth_policy.get("answer_shape"), dict) else {}
|
||||
coverage_gate_contract = (
|
||||
debug.get("coverage_gate_contract") if isinstance(debug.get("coverage_gate_contract"), dict) else {}
|
||||
)
|
||||
answer_shape_contract = (
|
||||
debug.get("answer_shape_contract") if isinstance(debug.get("answer_shape_contract"), dict) else {}
|
||||
)
|
||||
coverage_gate_contract = debug.get("coverage_gate_contract") if isinstance(debug.get("coverage_gate_contract"), dict) else {}
|
||||
if not coverage_gate_contract:
|
||||
coverage_gate_contract = truth_gate
|
||||
answer_shape_contract = debug.get("answer_shape_contract") if isinstance(debug.get("answer_shape_contract"), dict) else {}
|
||||
if not answer_shape_contract:
|
||||
answer_shape_contract = truth_answer_shape
|
||||
raw_answer_shape = debug.get("answer_shape")
|
||||
debug_answer_shape = raw_answer_shape.get("answer_shape") if isinstance(raw_answer_shape, dict) else raw_answer_shape
|
||||
|
||||
@@ -2982,7 +2994,22 @@ def build_scenario_step_state(
|
||||
"detected_intent": debug.get("detected_intent"),
|
||||
"selected_recipe": debug.get("selected_recipe"),
|
||||
"capability_id": debug.get("capability_id"),
|
||||
"capability_contract_id": first_non_empty_string(
|
||||
debug.get("capability_contract_id"),
|
||||
runtime_contract.get("capability_contract_id"),
|
||||
answer_shape_contract.get("capability_contract_id"),
|
||||
),
|
||||
"capability_route_mode": debug.get("capability_route_mode"),
|
||||
"truth_gate_contract_status": first_non_empty_string(
|
||||
debug.get("truth_gate_contract_status"),
|
||||
runtime_contract.get("truth_gate_contract_status"),
|
||||
truth_gate.get("source_truth_gate_status"),
|
||||
),
|
||||
"carryover_eligibility": first_non_empty_string(
|
||||
debug.get("carryover_eligibility"),
|
||||
runtime_contract.get("carryover_eligibility"),
|
||||
truth_gate.get("carryover_eligibility"),
|
||||
),
|
||||
"mcp_discovery_catalog_chain_alignment_status": debug.get("mcp_discovery_catalog_chain_alignment_status"),
|
||||
"mcp_discovery_catalog_chain_top_match": debug.get("mcp_discovery_catalog_chain_top_match"),
|
||||
"mcp_discovery_catalog_chain_selected_matches_top": debug.get("mcp_discovery_catalog_chain_selected_matches_top"),
|
||||
@@ -4081,6 +4108,9 @@ def compact_step_output_for_review(step_output: Any) -> dict[str, Any]:
|
||||
"detected_intent": step_output.get("detected_intent"),
|
||||
"selected_recipe": step_output.get("selected_recipe"),
|
||||
"capability_id": step_output.get("capability_id"),
|
||||
"capability_contract_id": step_output.get("capability_contract_id"),
|
||||
"truth_gate_contract_status": step_output.get("truth_gate_contract_status"),
|
||||
"carryover_eligibility": step_output.get("carryover_eligibility"),
|
||||
"depends_on": step_output.get("depends_on"),
|
||||
"semantic_tags": step_output.get("semantic_tags"),
|
||||
"required_state_objects": step_output.get("required_state_objects"),
|
||||
@@ -4713,6 +4743,84 @@ def normalize_analyst_priority_repair_target(raw_target: dict[str, Any], index:
|
||||
}
|
||||
|
||||
|
||||
def iso_date_ordinal(value: Any) -> int | None:
|
||||
normalized = normalize_iso_date(value)
|
||||
if not normalized:
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(normalized, "%Y-%m-%d").date().toordinal()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def extract_date_ordinals_from_text(value: Any) -> list[int]:
|
||||
text = str(value or "")
|
||||
dates: list[int] = []
|
||||
for match in re.finditer(r"\b(\d{4})-(\d{2})-(\d{2})\b", text):
|
||||
ordinal = iso_date_ordinal(match.group(0))
|
||||
if ordinal is not None:
|
||||
dates.append(ordinal)
|
||||
for match in re.finditer(r"\b(\d{2})\.(\d{2})\.(\d{4})\b", text):
|
||||
day, month, year = match.groups()
|
||||
ordinal = iso_date_ordinal(f"{year}-{month}-{day}")
|
||||
if ordinal is not None:
|
||||
dates.append(ordinal)
|
||||
return dates
|
||||
|
||||
|
||||
def validated_temporal_cutoff_target_is_false_positive(
|
||||
target: dict[str, Any],
|
||||
step_snapshot: dict[str, Any],
|
||||
target_text: str,
|
||||
) -> bool:
|
||||
temporal_markers = (
|
||||
"temporal",
|
||||
"date",
|
||||
"cutoff",
|
||||
"historical",
|
||||
"later",
|
||||
"post-",
|
||||
"out-of-window",
|
||||
"window",
|
||||
"scope",
|
||||
"дата",
|
||||
"дат",
|
||||
"позже",
|
||||
"после",
|
||||
"границ",
|
||||
"временн",
|
||||
"окн",
|
||||
"срез",
|
||||
)
|
||||
if not any(marker in target_text for marker in temporal_markers):
|
||||
return False
|
||||
|
||||
extracted_filters = step_snapshot.get("extracted_filters") if isinstance(step_snapshot.get("extracted_filters"), dict) else {}
|
||||
date_scope = step_snapshot.get("date_scope") if isinstance(step_snapshot.get("date_scope"), dict) else {}
|
||||
analysis_context = (
|
||||
step_snapshot.get("analysis_context") if isinstance(step_snapshot.get("analysis_context"), dict) else {}
|
||||
)
|
||||
cutoff_ordinal = (
|
||||
iso_date_ordinal(extracted_filters.get("as_of_date"))
|
||||
or iso_date_ordinal(date_scope.get("as_of_date"))
|
||||
or iso_date_ordinal(analysis_context.get("as_of_date"))
|
||||
)
|
||||
if cutoff_ordinal is None:
|
||||
return False
|
||||
|
||||
assistant_dates = extract_date_ordinals_from_text(step_snapshot.get("assistant_text_excerpt"))
|
||||
if not assistant_dates:
|
||||
return False
|
||||
if any(date_ordinal > cutoff_ordinal for date_ordinal in assistant_dates):
|
||||
return False
|
||||
|
||||
target_dates = extract_date_ordinals_from_text(target_text)
|
||||
if target_dates and any(date_ordinal > cutoff_ordinal for date_ordinal in target_dates):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def analyst_target_contradicts_validated_step(target: dict[str, Any], step_snapshot: dict[str, Any] | None) -> bool:
|
||||
if not isinstance(step_snapshot, dict):
|
||||
return False
|
||||
@@ -4742,6 +4850,13 @@ def analyst_target_contradicts_validated_step(target: dict[str, Any], step_snaps
|
||||
"guarded_insufficiency_validated",
|
||||
"runtime_factual_answer_validated",
|
||||
"bounded_mcp_answer_validated",
|
||||
"truth gate",
|
||||
"runtime truth",
|
||||
"truth confirmation",
|
||||
"validated factual status",
|
||||
"runtime truth confirmation",
|
||||
"shape-only",
|
||||
"candidate-only",
|
||||
"heuristic_candidates",
|
||||
"silent heuristic",
|
||||
"unvalidated",
|
||||
@@ -4751,6 +4866,9 @@ def analyst_target_contradicts_validated_step(target: dict[str, Any], step_snaps
|
||||
if any(marker in target_text for marker in stale_validation_markers):
|
||||
return True
|
||||
|
||||
if validated_temporal_cutoff_target_is_false_positive(target, step_snapshot, target_text):
|
||||
return True
|
||||
|
||||
assistant_text = str(step_snapshot.get("assistant_text_excerpt") or "").casefold()
|
||||
problem_type = str(target.get("problem_type") or "").strip()
|
||||
root_cause_layers = set(normalize_string_list(target.get("root_cause_layers")))
|
||||
|
||||
@@ -677,6 +677,179 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
|
||||
self.assertEqual(step_state["business_first_review"]["issue_codes"], [])
|
||||
self.assertEqual(step_state["acceptance_status"], "validated")
|
||||
|
||||
def test_assistant_truth_policy_runtime_contract_validates_exact_factual_step(self) -> None:
|
||||
step_state = dcl.build_scenario_step_state(
|
||||
scenario_id="truth_contract_demo",
|
||||
domain="inventory_selected_object_reliability",
|
||||
step={
|
||||
"step_id": "step_01",
|
||||
"title": "VAT by selected purchase date",
|
||||
"depends_on": [],
|
||||
"question_template": "calculate VAT by selected purchase date",
|
||||
"required_answer_shape": "direct_answer_first",
|
||||
"required_answer_patterns_all": ["VAT", "Q1 2015"],
|
||||
},
|
||||
step_index=1,
|
||||
question_resolved="calculate VAT by selected purchase date",
|
||||
analysis_context={},
|
||||
turn_artifact={
|
||||
"assistant_message": {
|
||||
"reply_type": "factual",
|
||||
"text": "VAT for Q1 2015 is confirmed: 1,556,449.06. Basis: sales and purchase ledgers.",
|
||||
"message_id": "msg-1",
|
||||
"trace_id": "trace-1",
|
||||
},
|
||||
"technical_debug_payload": {
|
||||
"detected_mode": "address_query",
|
||||
"detected_intent": "vat_liability_confirmed_for_tax_period",
|
||||
"selected_recipe": "address_vat_liability_confirmed_tax_period_v1",
|
||||
"capability_id": "confirmed_vat_liability_for_tax_period",
|
||||
"capability_route_mode": "exact",
|
||||
"fallback_type": "none",
|
||||
"mcp_call_status": "matched_non_empty",
|
||||
"response_type": "FACTUAL_SUMMARY",
|
||||
"result_mode": "confirmed_balance",
|
||||
"balance_confirmed": True,
|
||||
"mcp_discovery_route_candidate_status": "ready_for_reviewed_execution",
|
||||
"mcp_discovery_response_applied": False,
|
||||
"assistant_runtime_contract_v1": {
|
||||
"capability_contract_id": "confirmed_vat_liability_for_tax_period",
|
||||
"truth_gate_contract_status": "full_confirmed",
|
||||
"carryover_eligibility": "root_only",
|
||||
},
|
||||
"assistant_truth_answer_policy_v1": {
|
||||
"truth_gate": {
|
||||
"coverage_status": "full",
|
||||
"evidence_grade": "strong",
|
||||
"truth_mode": "confirmed",
|
||||
"source_truth_gate_status": "full_confirmed",
|
||||
},
|
||||
"answer_shape": {
|
||||
"answer_shape": "confirmed_factual",
|
||||
"capability_contract_id": "confirmed_vat_liability_for_tax_period",
|
||||
},
|
||||
},
|
||||
},
|
||||
"session_summary": {},
|
||||
},
|
||||
entries=[],
|
||||
)
|
||||
|
||||
self.assertEqual(step_state["capability_contract_id"], "confirmed_vat_liability_for_tax_period")
|
||||
self.assertEqual(step_state["truth_gate_contract_status"], "full_confirmed")
|
||||
self.assertEqual(step_state["truth_mode"], "confirmed")
|
||||
self.assertEqual(step_state["coverage_status"], "full")
|
||||
self.assertEqual(step_state["answer_shape"], "confirmed_factual")
|
||||
self.assertTrue(step_state["runtime_factual_answer_validated"])
|
||||
self.assertEqual(step_state["acceptance_status"], "validated")
|
||||
|
||||
def test_analyst_shape_only_target_is_suppressed_for_truth_confirmed_runtime_step(self) -> None:
|
||||
repair_targets = {
|
||||
"targets": [],
|
||||
"step_validation_index": {
|
||||
"scenario_a:step_01": {
|
||||
"acceptance_status": "validated",
|
||||
"violated_invariants": [],
|
||||
"warnings": [],
|
||||
"runtime_factual_answer_validated": True,
|
||||
"truth_gate_contract_status": "full_confirmed",
|
||||
"assistant_text_excerpt": "Confirmed business answer for the selected item.",
|
||||
}
|
||||
},
|
||||
}
|
||||
analyst_verdict = {
|
||||
"priority_targets": [
|
||||
{
|
||||
"scenario_id": "scenario_a",
|
||||
"step_id": "step_01",
|
||||
"severity": "P1",
|
||||
"problem_type": "capability_gap",
|
||||
"root_cause_layers": ["runtime_capability_gap"],
|
||||
"fix_goal": "Remove shape-only acceptance and add runtime truth confirmation.",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
merged = dcl.merge_analyst_priority_repair_targets(repair_targets, analyst_verdict)
|
||||
|
||||
self.assertEqual(merged["target_count"], 0)
|
||||
self.assertEqual(merged["suppressed_analyst_priority_target_count"], 1)
|
||||
self.assertEqual(
|
||||
merged["suppressed_analyst_priority_targets"][0]["reason"],
|
||||
"analyst_target_contradicts_validated_step",
|
||||
)
|
||||
|
||||
def test_analyst_temporal_cutoff_false_positive_is_suppressed_for_validated_step(self) -> None:
|
||||
repair_targets = {
|
||||
"targets": [],
|
||||
"step_validation_index": {
|
||||
"inventory_chain:s03": {
|
||||
"acceptance_status": "validated",
|
||||
"violated_invariants": [],
|
||||
"warnings": [],
|
||||
"runtime_factual_answer_validated": True,
|
||||
"extracted_filters": {"as_of_date": "2016-03-31"},
|
||||
"assistant_text_excerpt": (
|
||||
"По позиции до 31.03.2016 поставщик не подтвержден. "
|
||||
"Первая закупка: 05.02.2015. Последняя закупка: 22.07.2015. "
|
||||
"Опорный документ от 22.07.2015."
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
analyst_verdict = {
|
||||
"priority_targets": [
|
||||
{
|
||||
"scenario_id": "inventory_chain",
|
||||
"step_id": "s03",
|
||||
"severity": "P0",
|
||||
"problem_type": "other",
|
||||
"root_cause_layers": ["temporal_honesty_gap"],
|
||||
"fix_goal": "The answer declares a historical cutoff but cites proof rows with later dates.",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
merged = dcl.merge_analyst_priority_repair_targets(repair_targets, analyst_verdict)
|
||||
|
||||
self.assertEqual(merged["target_count"], 0)
|
||||
self.assertEqual(merged["suppressed_analyst_priority_target_count"], 1)
|
||||
|
||||
def test_analyst_temporal_cutoff_real_out_of_window_target_is_kept(self) -> None:
|
||||
repair_targets = {
|
||||
"targets": [],
|
||||
"step_validation_index": {
|
||||
"inventory_chain:s03": {
|
||||
"acceptance_status": "validated",
|
||||
"violated_invariants": [],
|
||||
"warnings": [],
|
||||
"runtime_factual_answer_validated": True,
|
||||
"extracted_filters": {"as_of_date": "2016-03-31"},
|
||||
"assistant_text_excerpt": (
|
||||
"По позиции до 31.03.2016 поставщик не подтвержден. "
|
||||
"Опорный документ от 25.05.2026."
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
analyst_verdict = {
|
||||
"priority_targets": [
|
||||
{
|
||||
"scenario_id": "inventory_chain",
|
||||
"step_id": "s03",
|
||||
"severity": "P0",
|
||||
"problem_type": "other",
|
||||
"root_cause_layers": ["temporal_honesty_gap"],
|
||||
"fix_goal": "The answer declares a historical cutoff but cites proof rows with later dates.",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
merged = dcl.merge_analyst_priority_repair_targets(repair_targets, analyst_verdict)
|
||||
|
||||
self.assertEqual(merged["target_count"], 1)
|
||||
self.assertEqual(merged["suppressed_analyst_priority_target_count"], 0)
|
||||
|
||||
def test_literal_result_mode_contract_still_rejects_mismatch(self) -> None:
|
||||
step_state = dcl.build_scenario_step_state(
|
||||
scenario_id="literal_result_mode_demo",
|
||||
|
||||
Reference in New Issue
Block a user