Укрепить валидацию агентного лупа по сценарным якорям
This commit is contained in:
parent
bbb3620956
commit
e2b928ac19
|
|
@ -1695,6 +1695,47 @@ def resolve_question_template(question_template: str, scenario_state: dict[str,
|
|||
return pattern.sub(replace, question_template).strip()
|
||||
|
||||
|
||||
def resolve_scenario_contract_placeholders(value: Any, scenario_state: dict[str, Any]) -> Any:
|
||||
if isinstance(value, str):
|
||||
runtime_resolved = str(resolve_runtime_placeholders(value))
|
||||
if "{{" not in runtime_resolved:
|
||||
return runtime_resolved
|
||||
pattern = re.compile(r"{{\s*([^{}]+?)\s*}}")
|
||||
|
||||
def replace(match: re.Match[str]) -> str:
|
||||
value = lookup_placeholder_value(match.group(1), scenario_state)
|
||||
if isinstance(value, (dict, list)):
|
||||
return dump_json(value)
|
||||
return str(value)
|
||||
|
||||
return pattern.sub(replace, runtime_resolved)
|
||||
if isinstance(value, list):
|
||||
return [resolve_scenario_contract_placeholders(item, scenario_state) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: resolve_scenario_contract_placeholders(item, scenario_state)
|
||||
for key, item in value.items()
|
||||
}
|
||||
return value
|
||||
|
||||
|
||||
def resolve_step_contract_placeholders(step: dict[str, Any], scenario_state: dict[str, Any]) -> dict[str, Any]:
|
||||
resolved_step = dict(step)
|
||||
resolved_step["required_filters"] = normalize_validation_filters(
|
||||
resolve_scenario_contract_placeholders(step.get("required_filters") or {}, scenario_state)
|
||||
)
|
||||
for pattern_field in (
|
||||
"forbidden_answer_patterns",
|
||||
"required_answer_patterns_any",
|
||||
"required_answer_patterns_all",
|
||||
):
|
||||
if pattern_field in step:
|
||||
resolved_step[pattern_field] = normalize_pattern_list(
|
||||
resolve_scenario_contract_placeholders(step.get(pattern_field) or [], scenario_state)
|
||||
)
|
||||
return resolved_step
|
||||
|
||||
|
||||
def build_failed_step_state(
|
||||
*,
|
||||
scenario_id: str,
|
||||
|
|
@ -2313,6 +2354,9 @@ def step_intent_matches_expected(state: dict[str, Any], expected_intents: list[s
|
|||
observed_intents = [
|
||||
str(state.get("detected_intent") or "").strip(),
|
||||
*normalize_string_list(state.get("mcp_discovery_effective_intents")),
|
||||
str(state.get("mcp_discovery_selected_chain_id") or "").strip(),
|
||||
str(state.get("mcp_discovery_route_candidate_fact_family") or "").strip(),
|
||||
str(state.get("mcp_discovery_route_candidate_action_family") or "").strip(),
|
||||
]
|
||||
return any(identifier_in_list(intent, expected_intents) for intent in observed_intents if intent)
|
||||
|
||||
|
|
@ -2551,6 +2595,8 @@ def is_validated_memory_checkpoint_answer(
|
|||
) -> bool:
|
||||
if is_validated_comparison_boundary_memory_checkpoint(state, business_review, violations):
|
||||
return True
|
||||
if is_validated_bare_organization_scope_checkpoint(state, business_review, violations):
|
||||
return True
|
||||
tags = set(normalize_string_list(state.get("semantic_tags")))
|
||||
if "memory" not in tags:
|
||||
return False
|
||||
|
|
@ -2566,6 +2612,39 @@ def is_validated_memory_checkpoint_answer(
|
|||
)
|
||||
|
||||
|
||||
def is_validated_bare_organization_scope_checkpoint(
|
||||
state: dict[str, Any],
|
||||
business_review: dict[str, Any],
|
||||
violations: list[str],
|
||||
) -> bool:
|
||||
if violations:
|
||||
return False
|
||||
tags = set(normalize_string_list(state.get("semantic_tags")))
|
||||
if not tags.intersection({"bare_org_scope", "company_scope", "company_selected"}):
|
||||
return False
|
||||
if str(state.get("living_chat_response_source") or "").strip() != "deterministic_data_scope_selection_contract":
|
||||
return False
|
||||
if str(state.get("living_chat_data_scope_probe_status") or "").strip() != "resolved":
|
||||
return False
|
||||
selected_organization = first_non_empty_string(
|
||||
state.get("living_chat_selected_organization"),
|
||||
state.get("assistant_active_organization"),
|
||||
)
|
||||
if not selected_organization:
|
||||
return False
|
||||
assistant_text = str(state.get("assistant_text") or "")
|
||||
normalized_answer = normalize_entity_filter_value_for_comparison(assistant_text)
|
||||
normalized_organization = normalize_entity_filter_value_for_comparison(selected_organization)
|
||||
if not normalized_organization or normalized_organization not in normalized_answer:
|
||||
return False
|
||||
return (
|
||||
business_review.get("business_usefulness_ok") is True
|
||||
and business_review.get("direct_answer_first_ok") is True
|
||||
and business_review.get("answer_layering_ok") is True
|
||||
and business_review.get("technical_garbage_present") is False
|
||||
)
|
||||
|
||||
|
||||
def is_validated_comparison_boundary_memory_checkpoint(
|
||||
state: dict[str, Any],
|
||||
business_review: dict[str, Any],
|
||||
|
|
@ -3235,6 +3314,10 @@ def build_scenario_step_state(
|
|||
"detected_intent": debug.get("detected_intent"),
|
||||
"selected_recipe": debug.get("selected_recipe"),
|
||||
"capability_id": debug.get("capability_id"),
|
||||
"living_chat_response_source": debug.get("living_chat_response_source"),
|
||||
"living_chat_data_scope_probe_status": debug.get("living_chat_data_scope_probe_status"),
|
||||
"living_chat_selected_organization": debug.get("living_chat_selected_organization"),
|
||||
"assistant_active_organization": debug.get("assistant_active_organization"),
|
||||
"capability_contract_id": first_non_empty_string(
|
||||
debug.get("capability_contract_id"),
|
||||
runtime_contract.get("capability_contract_id"),
|
||||
|
|
@ -3600,6 +3683,7 @@ def execute_scenario_manifest(
|
|||
step_dir = steps_dir / step["step_id"]
|
||||
try:
|
||||
resolved_question = resolve_question_template(step["question_template"], scenario_state)
|
||||
resolved_step = resolve_step_contract_placeholders(step, scenario_state)
|
||||
step_analysis_context = merge_analysis_context(manifest.get("analysis_context"), step.get("analysis_context"))
|
||||
step_analysis_context = carry_forward_analysis_context(
|
||||
scenario_state,
|
||||
|
|
@ -3611,7 +3695,7 @@ def execute_scenario_manifest(
|
|||
args=args,
|
||||
domain=manifest["domain"],
|
||||
scenario_id=manifest["scenario_id"],
|
||||
step=step,
|
||||
step=resolved_step,
|
||||
step_index=step_index,
|
||||
session_id=scenario_state.get("session_id"),
|
||||
question_resolved=resolved_question,
|
||||
|
|
|
|||
|
|
@ -190,6 +190,109 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
|
|||
self.assertIn("required_answer_patterns_any_missing", step_state["violated_invariants"])
|
||||
self.assertEqual(step_state["acceptance_status"], "rejected")
|
||||
|
||||
def test_binding_placeholder_required_filter_is_resolved_before_step_validation(self) -> None:
|
||||
step = dcl.normalize_step_definition(
|
||||
1,
|
||||
{
|
||||
"step_id": "counterparty_binding_filter",
|
||||
"title": "Counterparty binding filter",
|
||||
"question": "Отдельно по {{bindings.counterparty}}: какой это поток?",
|
||||
"required_filters": {"counterparty": "{{bindings.counterparty}}"},
|
||||
"required_answer_patterns_all": ["(?i){{bindings.counterparty}}|банк"],
|
||||
},
|
||||
)
|
||||
scenario_state = {
|
||||
"bindings": {"counterparty": "СБЕРБАНК"},
|
||||
"step_outputs": {},
|
||||
"semantic_memory": {},
|
||||
}
|
||||
|
||||
resolved_step = dcl.resolve_step_contract_placeholders(step, scenario_state)
|
||||
|
||||
self.assertEqual(resolved_step["required_filters"]["counterparty"], "СБЕРБАНК")
|
||||
self.assertEqual(resolved_step["required_answer_patterns_all"], ["(?i)СБЕРБАНК|банк"])
|
||||
|
||||
step_state = dcl.build_scenario_step_state(
|
||||
scenario_id="binding_filter_demo",
|
||||
domain="agentic_loop",
|
||||
step=resolved_step,
|
||||
step_index=1,
|
||||
question_resolved="Отдельно по СБЕРБАНК: какой это поток?",
|
||||
analysis_context={},
|
||||
turn_artifact={
|
||||
"assistant_message": {
|
||||
"reply_type": "factual",
|
||||
"text": "СБЕРБАНК подтвержден как банковский/финансовый поток.",
|
||||
"message_id": "msg-1",
|
||||
"trace_id": "trace-1",
|
||||
},
|
||||
"technical_debug_payload": {
|
||||
"detected_mode": "address_query",
|
||||
"detected_intent": "bank_operations_by_counterparty",
|
||||
"selected_recipe": "address_bank_operations_by_counterparty_v1",
|
||||
"capability_id": "bank_operations_drilldown",
|
||||
"truth_gate_contract_status": "full_confirmed",
|
||||
"mcp_call_status": "matched_non_empty",
|
||||
"extracted_filters": {"counterparty": "СБЕРБАНК"},
|
||||
},
|
||||
"session_summary": {},
|
||||
},
|
||||
entries=[],
|
||||
)
|
||||
|
||||
self.assertNotIn("missing_required_filter", step_state["violated_invariants"])
|
||||
|
||||
def test_expected_intent_accepts_discovery_chain_id(self) -> None:
|
||||
self.assertTrue(
|
||||
dcl.step_intent_matches_expected(
|
||||
{
|
||||
"detected_intent": "supplier_payouts_profile",
|
||||
"mcp_discovery_selected_chain_id": "value_flow",
|
||||
"mcp_discovery_route_candidate_fact_family": "value_flow",
|
||||
"mcp_discovery_route_candidate_action_family": "payout",
|
||||
},
|
||||
["value_flow"],
|
||||
)
|
||||
)
|
||||
|
||||
def test_bare_organization_scope_selection_is_validated_memory_checkpoint(self) -> None:
|
||||
step_state = dcl.build_scenario_step_state(
|
||||
scenario_id="org_scope_demo",
|
||||
domain="agentic_loop",
|
||||
step={
|
||||
"step_id": "step_01",
|
||||
"title": "Choose organization",
|
||||
"depends_on": [],
|
||||
"question_template": "ООО Альтернатива Плюс",
|
||||
"semantic_tags": ["bare_org_scope", "company_scope"],
|
||||
"required_answer_patterns_all": ["(?i)фиксир|рабоч.*организац|контур"],
|
||||
},
|
||||
step_index=1,
|
||||
question_resolved="ООО Альтернатива Плюс",
|
||||
analysis_context={},
|
||||
turn_artifact={
|
||||
"assistant_message": {
|
||||
"reply_type": "factual_with_explanation",
|
||||
"text": 'Отлично, фиксирую рабочую организацию: ООО "Альтернатива Плюс".',
|
||||
"message_id": "msg-1",
|
||||
"trace_id": "trace-1",
|
||||
},
|
||||
"technical_debug_payload": {
|
||||
"detected_mode": "chat",
|
||||
"fallback_type": "none",
|
||||
"living_chat_response_source": "deterministic_data_scope_selection_contract",
|
||||
"living_chat_data_scope_probe_status": "resolved",
|
||||
"living_chat_selected_organization": 'ООО "Альтернатива Плюс"',
|
||||
"assistant_active_organization": 'ООО "Альтернатива Плюс"',
|
||||
},
|
||||
"session_summary": {},
|
||||
},
|
||||
entries=[],
|
||||
)
|
||||
|
||||
self.assertEqual(step_state["status"], "validated")
|
||||
self.assertTrue(step_state["memory_checkpoint_validated"])
|
||||
|
||||
def test_repair_targets_promote_route_candidate_enablement_gaps(self) -> None:
|
||||
repair_targets = dcl.build_deterministic_repair_targets(
|
||||
{"pack_id": "route_candidate_pack", "domain": "open_world", "final_status": "accepted"},
|
||||
|
|
|
|||
Loading…
Reference in New Issue