Усилить агентные проверки selected-object сценариев
This commit is contained in:
parent
908d011743
commit
c17495d1bb
|
|
@ -166,6 +166,60 @@ def read_text_or_empty(path: Path) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def assistant_text_from_turn_path(path: Path | None) -> str:
|
||||||
|
if path is None or not path.exists():
|
||||||
|
return ""
|
||||||
|
payload = read_json_object(path)
|
||||||
|
assistant_message = payload.get("assistant_message") if isinstance(payload.get("assistant_message"), dict) else {}
|
||||||
|
text = str(assistant_message.get("text") or "").strip()
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def current_answer_text_for_output(output: dict[str, Any]) -> str:
|
||||||
|
turn_path = output.get("turn_path")
|
||||||
|
if isinstance(turn_path, Path):
|
||||||
|
turn_answer = assistant_text_from_turn_path(turn_path)
|
||||||
|
if turn_answer:
|
||||||
|
return turn_answer
|
||||||
|
return str(output.get("text") or "")
|
||||||
|
|
||||||
|
|
||||||
|
CURRENT_TRACE_FIELD_NAMES = {
|
||||||
|
"detected_mode",
|
||||||
|
"query_shape",
|
||||||
|
"detected_intent",
|
||||||
|
"selected_recipe",
|
||||||
|
"requested_result_mode",
|
||||||
|
"result_mode",
|
||||||
|
"response_type",
|
||||||
|
"capability_id",
|
||||||
|
"capability_layer",
|
||||||
|
"capability_route_mode",
|
||||||
|
"capability_route_reason",
|
||||||
|
"execution_lane",
|
||||||
|
"fallback_type",
|
||||||
|
"match_failure_stage",
|
||||||
|
"match_failure_reason",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def current_trace_text_from_turn_path(path: Path) -> str:
|
||||||
|
payload = read_json_object(path)
|
||||||
|
debug = payload.get("technical_debug_payload") if isinstance(payload.get("technical_debug_payload"), dict) else {}
|
||||||
|
scoped: dict[str, Any] = {}
|
||||||
|
for field_name in CURRENT_TRACE_FIELD_NAMES:
|
||||||
|
if field_name in debug:
|
||||||
|
scoped[field_name] = debug.get(field_name)
|
||||||
|
for prefix in ("route_expectation_", "capability_route_"):
|
||||||
|
for field_name, value in debug.items():
|
||||||
|
if isinstance(field_name, str) and field_name.startswith(prefix):
|
||||||
|
scoped[field_name] = value
|
||||||
|
assistant_message = payload.get("assistant_message") if isinstance(payload.get("assistant_message"), dict) else {}
|
||||||
|
if assistant_message.get("reply_type") is not None:
|
||||||
|
scoped["assistant_reply_type"] = assistant_message.get("reply_type")
|
||||||
|
return json.dumps(scoped, ensure_ascii=False, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
def output_turn_path(output_path: Path) -> Path | None:
|
def output_turn_path(output_path: Path) -> Path | None:
|
||||||
name = output_path.name
|
name = output_path.name
|
||||||
if name == "output.md":
|
if name == "output.md":
|
||||||
|
|
@ -305,7 +359,7 @@ def evaluate_forbidden_regex(
|
||||||
return build_result(detector_name, detector, "skipped", "no output.md-style artifacts matched detector scope")
|
return build_result(detector_name, detector, "skipped", "no output.md-style artifacts matched detector scope")
|
||||||
evidence: list[dict[str, Any]] = []
|
evidence: list[dict[str, Any]] = []
|
||||||
for output in outputs:
|
for output in outputs:
|
||||||
text = str(output.get("text") or "")
|
text = current_answer_text_for_output(output)
|
||||||
if prefix_line_count is not None:
|
if prefix_line_count is not None:
|
||||||
lines = [line for line in text.splitlines() if line.strip()]
|
lines = [line for line in text.splitlines() if line.strip()]
|
||||||
text = "\n".join(lines[:prefix_line_count])
|
text = "\n".join(lines[:prefix_line_count])
|
||||||
|
|
@ -336,7 +390,7 @@ def evaluate_required_any(
|
||||||
missing: list[dict[str, Any]] = []
|
missing: list[dict[str, Any]] = []
|
||||||
matched: list[dict[str, Any]] = []
|
matched: list[dict[str, Any]] = []
|
||||||
for output in outputs:
|
for output in outputs:
|
||||||
text = str(output.get("text") or "")
|
text = current_answer_text_for_output(output)
|
||||||
match = next((pattern.search(text) for pattern in patterns if pattern.search(text)), None)
|
match = next((pattern.search(text) for pattern in patterns if pattern.search(text)), None)
|
||||||
if match:
|
if match:
|
||||||
matched.append({"path": output["repo_path"], "match": match.group(0)[:240]})
|
matched.append({"path": output["repo_path"], "match": match.group(0)[:240]})
|
||||||
|
|
@ -360,7 +414,7 @@ def evaluate_limited_next_action(
|
||||||
failures: list[dict[str, Any]] = []
|
failures: list[dict[str, Any]] = []
|
||||||
limited_outputs = 0
|
limited_outputs = 0
|
||||||
for output in outputs:
|
for output in outputs:
|
||||||
text = str(output.get("text") or "")
|
text = current_answer_text_for_output(output)
|
||||||
limited_match = next((pattern.search(text) for pattern in limited_patterns if pattern.search(text)), None)
|
limited_match = next((pattern.search(text) for pattern in limited_patterns if pattern.search(text)), None)
|
||||||
if not limited_match:
|
if not limited_match:
|
||||||
continue
|
continue
|
||||||
|
|
@ -388,7 +442,11 @@ def evaluate_trace_guard(
|
||||||
return build_result(detector_name, detector, "skipped", "no turn.json-style artifacts matched detector scope")
|
return build_result(detector_name, detector, "skipped", "no turn.json-style artifacts matched detector scope")
|
||||||
evidence: list[dict[str, Any]] = []
|
evidence: list[dict[str, Any]] = []
|
||||||
for turn in turns:
|
for turn in turns:
|
||||||
text = str(turn.get("text") or "").lower()
|
path = turn.get("path")
|
||||||
|
if isinstance(path, Path):
|
||||||
|
text = current_trace_text_from_turn_path(path).lower()
|
||||||
|
else:
|
||||||
|
text = str(turn.get("text") or "").lower()
|
||||||
for marker in markers:
|
for marker in markers:
|
||||||
if marker in text:
|
if marker in text:
|
||||||
evidence.append({"path": turn["repo_path"], "marker": marker})
|
evidence.append({"path": turn["repo_path"], "marker": marker})
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,9 @@ BUSINESS_EXPECTED_RESULT_MODES = {
|
||||||
"evidence_or_honest_boundary",
|
"evidence_or_honest_boundary",
|
||||||
"ranking_or_limited_accounting_answer",
|
"ranking_or_limited_accounting_answer",
|
||||||
"same_inventory_margin_context_or_clarification",
|
"same_inventory_margin_context_or_clarification",
|
||||||
|
"selected_object_purchase_provenance_or_honest_boundary",
|
||||||
|
"selected_object_purchase_date_or_honest_boundary",
|
||||||
|
"selected_object_purchase_documents_or_honest_boundary",
|
||||||
}
|
}
|
||||||
|
|
||||||
MCP_DISCOVERY_CHAIN_INTENT_ALIASES: dict[str, tuple[str, ...]] = {
|
MCP_DISCOVERY_CHAIN_INTENT_ALIASES: dict[str, tuple[str, ...]] = {
|
||||||
|
|
@ -2607,6 +2610,20 @@ def business_expected_result_mode_matches(expected_result_mode: str, step_state:
|
||||||
detected_intent == "inventory_margin_ranking_for_nomenclature"
|
detected_intent == "inventory_margin_ranking_for_nomenclature"
|
||||||
or capability_id == "inventory_inventory_margin_ranking_for_nomenclature"
|
or capability_id == "inventory_inventory_margin_ranking_for_nomenclature"
|
||||||
)
|
)
|
||||||
|
selected_object_expected_routes = {
|
||||||
|
"selected_object_purchase_provenance_or_honest_boundary": (
|
||||||
|
"inventory_purchase_provenance_for_item",
|
||||||
|
"inventory_inventory_purchase_provenance_for_item",
|
||||||
|
),
|
||||||
|
"selected_object_purchase_date_or_honest_boundary": (
|
||||||
|
"inventory_purchase_provenance_for_item",
|
||||||
|
"inventory_inventory_purchase_provenance_for_item",
|
||||||
|
),
|
||||||
|
"selected_object_purchase_documents_or_honest_boundary": (
|
||||||
|
"inventory_purchase_documents_for_item",
|
||||||
|
"inventory_inventory_purchase_documents_for_item",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
if expected_result_mode == "clarification_required":
|
if expected_result_mode == "clarification_required":
|
||||||
return (
|
return (
|
||||||
|
|
@ -2668,6 +2685,25 @@ def business_expected_result_mode_matches(expected_result_mode: str, step_state:
|
||||||
and reply_type in {"partial_coverage", "factual", "factual_with_explanation"}
|
and reply_type in {"partial_coverage", "factual", "factual_with_explanation"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if expected_result_mode in selected_object_expected_routes:
|
||||||
|
expected_intent, expected_capability = selected_object_expected_routes[expected_result_mode]
|
||||||
|
route_matches = detected_intent == expected_intent or capability_id == expected_capability
|
||||||
|
factual_or_honest_boundary = (
|
||||||
|
truth_mode == "confirmed"
|
||||||
|
or answer_shape == "confirmed_factual"
|
||||||
|
or step_state.get("balance_confirmed") is True
|
||||||
|
or truth_mode in GUARDED_INSUFFICIENCY_TRUTH_MODES
|
||||||
|
or answer_shape in GUARDED_INSUFFICIENCY_ANSWER_SHAPES
|
||||||
|
or step_state.get("balance_confirmed") is False
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
clean_business_review
|
||||||
|
and route_matches
|
||||||
|
and factual_or_honest_boundary
|
||||||
|
and bool(assistant_text)
|
||||||
|
and reply_type in {"partial_coverage", "factual", "factual_with_explanation"}
|
||||||
|
)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -428,6 +428,9 @@ BUSINESS_EXPECTED_RESULT_MODES = {
|
||||||
"evidence_or_honest_boundary",
|
"evidence_or_honest_boundary",
|
||||||
"ranking_or_limited_accounting_answer",
|
"ranking_or_limited_accounting_answer",
|
||||||
"same_inventory_margin_context_or_clarification",
|
"same_inventory_margin_context_or_clarification",
|
||||||
|
"selected_object_purchase_provenance_or_honest_boundary",
|
||||||
|
"selected_object_purchase_date_or_honest_boundary",
|
||||||
|
"selected_object_purchase_documents_or_honest_boundary",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -451,6 +454,20 @@ def business_expected_result_mode_matches(expected_result_mode: str, step_state:
|
||||||
detected_intent == "inventory_margin_ranking_for_nomenclature"
|
detected_intent == "inventory_margin_ranking_for_nomenclature"
|
||||||
or capability_id == "inventory_inventory_margin_ranking_for_nomenclature"
|
or capability_id == "inventory_inventory_margin_ranking_for_nomenclature"
|
||||||
)
|
)
|
||||||
|
selected_object_expected_routes = {
|
||||||
|
"selected_object_purchase_provenance_or_honest_boundary": (
|
||||||
|
"inventory_purchase_provenance_for_item",
|
||||||
|
"inventory_inventory_purchase_provenance_for_item",
|
||||||
|
),
|
||||||
|
"selected_object_purchase_date_or_honest_boundary": (
|
||||||
|
"inventory_purchase_provenance_for_item",
|
||||||
|
"inventory_inventory_purchase_provenance_for_item",
|
||||||
|
),
|
||||||
|
"selected_object_purchase_documents_or_honest_boundary": (
|
||||||
|
"inventory_purchase_documents_for_item",
|
||||||
|
"inventory_inventory_purchase_documents_for_item",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
if expected_result_mode == "clarification_required":
|
if expected_result_mode == "clarification_required":
|
||||||
return (
|
return (
|
||||||
|
|
@ -497,6 +514,25 @@ def business_expected_result_mode_matches(expected_result_mode: str, step_state:
|
||||||
and reply_type in {"partial_coverage", "factual", "factual_with_explanation"}
|
and reply_type in {"partial_coverage", "factual", "factual_with_explanation"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if expected_result_mode in selected_object_expected_routes:
|
||||||
|
expected_intent, expected_capability = selected_object_expected_routes[expected_result_mode]
|
||||||
|
route_matches = detected_intent == expected_intent or capability_id == expected_capability
|
||||||
|
factual_or_honest_boundary = (
|
||||||
|
truth_mode == "confirmed"
|
||||||
|
or answer_shape == "confirmed_factual"
|
||||||
|
or step_state.get("balance_confirmed") is True
|
||||||
|
or truth_mode in dcl.GUARDED_INSUFFICIENCY_TRUTH_MODES
|
||||||
|
or answer_shape in dcl.GUARDED_INSUFFICIENCY_ANSWER_SHAPES
|
||||||
|
or step_state.get("balance_confirmed") is False
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
clean_business_review
|
||||||
|
and route_matches
|
||||||
|
and factual_or_honest_boundary
|
||||||
|
and bool(assistant_text)
|
||||||
|
and reply_type in {"partial_coverage", "factual", "factual_with_explanation"}
|
||||||
|
)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -262,10 +262,57 @@ def validate_domain_pack_loop_dir(loop_dir: Path) -> dict[str, Any]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_domain_case_loop_pack_dir(run_dir: Path) -> dict[str, Any]:
|
||||||
|
run_dir = run_dir.resolve()
|
||||||
|
effective_runtime = require_effective_runtime_manifest(run_dir)
|
||||||
|
pack_state = load_json_object(run_dir / "pack_state.json", "Validated domain-case-loop pack_state.json")
|
||||||
|
repair_targets = load_json_object(run_dir / "repair_targets.json", "Validated domain-case-loop repair_targets.json")
|
||||||
|
severity_counts = repair_targets.get("severity_counts") if isinstance(repair_targets.get("severity_counts"), dict) else {}
|
||||||
|
|
||||||
|
problems: list[str] = []
|
||||||
|
assert_status(pack_state.get("execution_status"), "exact", "pack_state.execution_status", problems)
|
||||||
|
assert_status(pack_state.get("final_status"), "accepted", "pack_state.final_status", problems)
|
||||||
|
assert_status(pack_state.get("acceptance_status"), "accepted", "pack_state.acceptance_status", problems)
|
||||||
|
assert_status(repair_targets.get("final_status"), "accepted", "repair_targets.final_status", problems)
|
||||||
|
assert_status(repair_targets.get("acceptance_status"), "accepted", "repair_targets.acceptance_status", problems)
|
||||||
|
if int(repair_targets.get("target_count") or 0) != 0:
|
||||||
|
problems.append(f"repair_targets.target_count={repair_targets.get('target_count')}")
|
||||||
|
if int(severity_counts.get("P0") or 0) != 0 or int(severity_counts.get("P1") or 0) != 0:
|
||||||
|
problems.append(
|
||||||
|
f"repair_targets.severity_counts=P0:{severity_counts.get('P0') or 0},P1:{severity_counts.get('P1') or 0}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if problems:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Refusing to save AGENT autorun because the validated domain-case-loop pack is not clean: "
|
||||||
|
+ ", ".join(problems)
|
||||||
|
)
|
||||||
|
|
||||||
|
scenario_results = pack_state.get("scenario_results") if isinstance(pack_state.get("scenario_results"), list) else []
|
||||||
|
return {
|
||||||
|
"schema_version": VALIDATED_AGENT_SAVE_SCHEMA_VERSION,
|
||||||
|
"validation_status": "accepted_domain_case_loop_pack",
|
||||||
|
"validated_run_dir": repo_relative(run_dir),
|
||||||
|
"final_status": pack_state.get("final_status"),
|
||||||
|
"acceptance_status": pack_state.get("acceptance_status"),
|
||||||
|
"execution_status": pack_state.get("execution_status"),
|
||||||
|
"pack_id": pack_state.get("pack_id"),
|
||||||
|
"scenario_count": len(scenario_results),
|
||||||
|
"repair_target_count": repair_targets.get("target_count"),
|
||||||
|
"repair_target_severity_counts": repair_targets.get("severity_counts"),
|
||||||
|
"effective_runtime": build_effective_runtime_save_summary(effective_runtime, run_dir),
|
||||||
|
"saved_after_validated_replay": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def validate_accepted_run_dir(run_dir: Path) -> dict[str, Any]:
|
def validate_accepted_run_dir(run_dir: Path) -> dict[str, Any]:
|
||||||
run_dir = run_dir.resolve()
|
run_dir = run_dir.resolve()
|
||||||
if (run_dir / "loop_state.json").exists():
|
if (run_dir / "loop_state.json").exists():
|
||||||
return validate_domain_pack_loop_dir(run_dir)
|
return validate_domain_pack_loop_dir(run_dir)
|
||||||
|
if (run_dir / "truth_review.json").exists():
|
||||||
|
return validate_truth_harness_run_dir(run_dir)
|
||||||
|
if (run_dir / "pack_state.json").exists() and (run_dir / "repair_targets.json").exists():
|
||||||
|
return validate_domain_case_loop_pack_dir(run_dir)
|
||||||
return validate_truth_harness_run_dir(run_dir)
|
return validate_truth_harness_run_dir(run_dir)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,152 @@ class AgentDetectorRunnerTests(unittest.TestCase):
|
||||||
self.assertEqual(statuses["forbidden_margin_terms"], "fail")
|
self.assertEqual(statuses["forbidden_margin_terms"], "fail")
|
||||||
self.assertEqual(statuses["margin_domain_leak_accounting_route"], "fail")
|
self.assertEqual(statuses["margin_domain_leak_accounting_route"], "fail")
|
||||||
|
|
||||||
|
def test_forbidden_regex_uses_current_assistant_answer_from_turn_json(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
artifact_dir = root / "run"
|
||||||
|
step_dir = artifact_dir / "steps" / "s01"
|
||||||
|
write_text(
|
||||||
|
step_dir / "output.md",
|
||||||
|
"# Assistant conversation export\n\n"
|
||||||
|
"## 1. user\n"
|
||||||
|
"Can you compute margin from payment_document evidence?\n\n"
|
||||||
|
"## 2. assistant\n"
|
||||||
|
"Clean current answer.\n",
|
||||||
|
)
|
||||||
|
write_json(
|
||||||
|
step_dir / "turn.json",
|
||||||
|
{
|
||||||
|
"user_message": {"text": "Can you compute margin from payment_document evidence?"},
|
||||||
|
"assistant_message": {"text": "Clean current answer.", "reply_type": "factual"},
|
||||||
|
"technical_debug_payload": {"detected_intent": "inventory_margin_ranking_for_nomenclature"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
registry_path = root / "detector_registry.json"
|
||||||
|
issue_catalog_path = root / "issue_catalog.json"
|
||||||
|
write_json(
|
||||||
|
registry_path,
|
||||||
|
{
|
||||||
|
"schema_version": "agent_detector_registry_v1",
|
||||||
|
"detectors": {
|
||||||
|
"forbidden_margin_terms": {
|
||||||
|
"kind": "answer_text_regex_forbidden",
|
||||||
|
"automation_level": "automatic",
|
||||||
|
"description": "No wrong-domain words in the current answer.",
|
||||||
|
"issue_codes": ["margin_domain_leak_accounting_route"],
|
||||||
|
"inputs": ["output.md"],
|
||||||
|
"check": {"forbidden_patterns": ["payment_document"]},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
write_json(issue_catalog_path, {"schema_version": "agent_issue_catalog_v1", "issues": {}})
|
||||||
|
|
||||||
|
results = runner.build_detector_results(
|
||||||
|
artifact_dir,
|
||||||
|
detector_names=["forbidden_margin_terms"],
|
||||||
|
registry_path=registry_path,
|
||||||
|
issue_catalog_path=issue_catalog_path,
|
||||||
|
include_default_global=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(results["summary"]["status"], "pass")
|
||||||
|
self.assertEqual(results["results"][0]["status"], "pass")
|
||||||
|
|
||||||
|
def test_trace_guard_uses_current_route_fields_not_full_turn_history(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
artifact_dir = root / "run"
|
||||||
|
step_dir = artifact_dir / "steps" / "s01"
|
||||||
|
write_json(
|
||||||
|
step_dir / "turn.json",
|
||||||
|
{
|
||||||
|
"user_message": {"text": "Ignore fixed_asset and answer selected item document."},
|
||||||
|
"assistant_message": {"text": "Inventory document answer.", "reply_type": "factual"},
|
||||||
|
"technical_debug_payload": {
|
||||||
|
"detected_intent": "inventory_purchase_documents_for_item",
|
||||||
|
"selected_recipe": "address_inventory_purchase_documents_for_item_v1",
|
||||||
|
"capability_id": "inventory_inventory_purchase_documents_for_item",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
registry_path = root / "detector_registry.json"
|
||||||
|
issue_catalog_path = root / "issue_catalog.json"
|
||||||
|
write_json(
|
||||||
|
registry_path,
|
||||||
|
{
|
||||||
|
"schema_version": "agent_detector_registry_v1",
|
||||||
|
"detectors": {
|
||||||
|
"wrong_capability_family": {
|
||||||
|
"kind": "trace_value_guard",
|
||||||
|
"automation_level": "automatic",
|
||||||
|
"description": "No wrong current route family.",
|
||||||
|
"issue_codes": ["margin_domain_leak_accounting_route"],
|
||||||
|
"inputs": ["turn.json"],
|
||||||
|
"check": {"forbidden_trace_markers": ["fixed_asset"]},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
write_json(issue_catalog_path, {"schema_version": "agent_issue_catalog_v1", "issues": {}})
|
||||||
|
|
||||||
|
results = runner.build_detector_results(
|
||||||
|
artifact_dir,
|
||||||
|
detector_names=["wrong_capability_family"],
|
||||||
|
registry_path=registry_path,
|
||||||
|
issue_catalog_path=issue_catalog_path,
|
||||||
|
include_default_global=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(results["summary"]["status"], "pass")
|
||||||
|
self.assertEqual(results["results"][0]["status"], "pass")
|
||||||
|
|
||||||
|
def test_trace_guard_fails_when_current_route_field_has_forbidden_marker(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
artifact_dir = root / "run"
|
||||||
|
step_dir = artifact_dir / "steps" / "s01"
|
||||||
|
write_json(
|
||||||
|
step_dir / "turn.json",
|
||||||
|
{
|
||||||
|
"assistant_message": {"text": "Wrong route answer.", "reply_type": "factual"},
|
||||||
|
"technical_debug_payload": {
|
||||||
|
"detected_intent": "fixed_asset_amortization",
|
||||||
|
"selected_recipe": "address_fixed_asset_amortization_v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
registry_path = root / "detector_registry.json"
|
||||||
|
issue_catalog_path = root / "issue_catalog.json"
|
||||||
|
write_json(
|
||||||
|
registry_path,
|
||||||
|
{
|
||||||
|
"schema_version": "agent_detector_registry_v1",
|
||||||
|
"detectors": {
|
||||||
|
"wrong_capability_family": {
|
||||||
|
"kind": "trace_value_guard",
|
||||||
|
"automation_level": "automatic",
|
||||||
|
"description": "No wrong current route family.",
|
||||||
|
"issue_codes": ["margin_domain_leak_accounting_route"],
|
||||||
|
"inputs": ["turn.json"],
|
||||||
|
"check": {"forbidden_trace_markers": ["fixed_asset"]},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
write_json(issue_catalog_path, {"schema_version": "agent_issue_catalog_v1", "issues": {}})
|
||||||
|
|
||||||
|
results = runner.build_detector_results(
|
||||||
|
artifact_dir,
|
||||||
|
detector_names=["wrong_capability_family"],
|
||||||
|
registry_path=registry_path,
|
||||||
|
issue_catalog_path=issue_catalog_path,
|
||||||
|
include_default_global=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(results["summary"]["status"], "fail")
|
||||||
|
self.assertEqual(results["results"][0]["status"], "fail")
|
||||||
|
|
||||||
def test_stage_review_signal_detector_fails_when_issue_code_is_present(self) -> None:
|
def test_stage_review_signal_detector_fails_when_issue_code_is_present(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp)
|
root = Path(tmp)
|
||||||
|
|
|
||||||
|
|
@ -715,6 +715,55 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
|
||||||
|
|
||||||
self.assertIn("wrong_result_mode", step_state["violated_invariants"])
|
self.assertIn("wrong_result_mode", step_state["violated_invariants"])
|
||||||
|
|
||||||
|
def test_selected_object_purchase_document_mode_accepts_confirmed_factual_route(self) -> None:
|
||||||
|
step_state = dcl.build_scenario_step_state(
|
||||||
|
scenario_id="margin_selected_object_demo",
|
||||||
|
domain="margin_profitability",
|
||||||
|
step={
|
||||||
|
"step_id": "step_05_selected_item_documents",
|
||||||
|
"title": "Selected item document",
|
||||||
|
"depends_on": ["step_04_selected_item_purchase_date"],
|
||||||
|
"question_template": "Which document confirms this selected item?",
|
||||||
|
"expected_intents": ["inventory_purchase_documents_for_item"],
|
||||||
|
"expected_capability": "inventory_inventory_purchase_documents_for_item",
|
||||||
|
"expected_recipe": "address_inventory_purchase_documents_for_item_v1",
|
||||||
|
"expected_result_mode": "selected_object_purchase_documents_or_honest_boundary",
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
},
|
||||||
|
step_index=5,
|
||||||
|
question_resolved="Which document confirms this selected item?",
|
||||||
|
analysis_context={},
|
||||||
|
turn_artifact={
|
||||||
|
"assistant_message": {
|
||||||
|
"reply_type": "factual",
|
||||||
|
"text": "Confirmation document for the selected item: Purchase document 0001.",
|
||||||
|
"message_id": "msg-1",
|
||||||
|
"trace_id": "trace-1",
|
||||||
|
},
|
||||||
|
"technical_debug_payload": {
|
||||||
|
"detected_mode": "address_query",
|
||||||
|
"detected_intent": "inventory_purchase_documents_for_item",
|
||||||
|
"selected_recipe": "address_inventory_purchase_documents_for_item_v1",
|
||||||
|
"capability_id": "inventory_inventory_purchase_documents_for_item",
|
||||||
|
"capability_route_mode": "exact",
|
||||||
|
"fallback_type": "none",
|
||||||
|
"mcp_call_status": "matched_non_empty",
|
||||||
|
"response_type": "FACTUAL_SUMMARY",
|
||||||
|
"result_mode": "confirmed_balance",
|
||||||
|
"truth_mode": "confirmed",
|
||||||
|
"answer_shape": "confirmed_factual",
|
||||||
|
"balance_confirmed": True,
|
||||||
|
"extracted_filters": {"item": "Selected item"},
|
||||||
|
},
|
||||||
|
"session_summary": {},
|
||||||
|
},
|
||||||
|
entries=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(step_state["execution_status"], "exact")
|
||||||
|
self.assertNotIn("wrong_result_mode", step_state["violated_invariants"])
|
||||||
|
self.assertEqual(step_state["acceptance_status"], "validated")
|
||||||
|
|
||||||
def test_exact_confirmed_document_followup_sets_runtime_factual_validation(self) -> None:
|
def test_exact_confirmed_document_followup_sets_runtime_factual_validation(self) -> None:
|
||||||
step_state = dcl.build_scenario_step_state(
|
step_state = dcl.build_scenario_step_state(
|
||||||
scenario_id="svk_pivot",
|
scenario_id="svk_pivot",
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,62 @@ class SaveAgentSemanticRunTests(unittest.TestCase):
|
||||||
self.assertEqual(metadata["effective_runtime"]["runner"], "domain_truth_harness.run-live")
|
self.assertEqual(metadata["effective_runtime"]["runner"], "domain_truth_harness.run-live")
|
||||||
self.assertEqual(metadata["effective_runtime"]["llm_model"], "test-model")
|
self.assertEqual(metadata["effective_runtime"]["llm_model"], "test-model")
|
||||||
|
|
||||||
|
def write_domain_case_loop_pack(self, run_dir: Path, *, target_count: int = 0, p1_count: int = 0) -> None:
|
||||||
|
write_json(
|
||||||
|
run_dir / "pack_state.json",
|
||||||
|
{
|
||||||
|
"pack_id": "agent_pack",
|
||||||
|
"execution_status": "exact",
|
||||||
|
"acceptance_status": "accepted",
|
||||||
|
"final_status": "accepted",
|
||||||
|
"scenario_results": [{"scenario_id": "s01", "final_status": "accepted"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
write_json(
|
||||||
|
run_dir / "repair_targets.json",
|
||||||
|
{
|
||||||
|
"target_count": target_count,
|
||||||
|
"acceptance_status": "accepted",
|
||||||
|
"final_status": "accepted",
|
||||||
|
"severity_counts": {"P0": 0, "P1": p1_count, "P2": 0},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
write_json(
|
||||||
|
run_dir / runtime_manifest.EFFECTIVE_RUNTIME_FILE_NAME,
|
||||||
|
{
|
||||||
|
"schema_version": runtime_manifest.EFFECTIVE_RUNTIME_SCHEMA_VERSION,
|
||||||
|
"runner": "domain_case_loop.run-pack",
|
||||||
|
"git_sha": "test-sha",
|
||||||
|
"llm_provider": "local",
|
||||||
|
"llm_model": "test-model",
|
||||||
|
"temperature": 0.0,
|
||||||
|
"max_output_tokens": 2048,
|
||||||
|
"prompt_version": "normalizer_v2_0_2",
|
||||||
|
"prompt_source": "file",
|
||||||
|
"prompt_hash": "abc123",
|
||||||
|
"prompt_registry_status": "pass",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_validate_domain_case_loop_pack_accepts_clean_pack(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
run_dir = Path(tmp)
|
||||||
|
self.write_domain_case_loop_pack(run_dir)
|
||||||
|
|
||||||
|
metadata = saver.validate_accepted_run_dir(run_dir)
|
||||||
|
|
||||||
|
self.assertEqual(metadata["validation_status"], "accepted_domain_case_loop_pack")
|
||||||
|
self.assertEqual(metadata["effective_runtime"]["runner"], "domain_case_loop.run-pack")
|
||||||
|
self.assertEqual(metadata["repair_target_count"], 0)
|
||||||
|
|
||||||
|
def test_validate_domain_case_loop_pack_refuses_open_repair_targets(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
run_dir = Path(tmp)
|
||||||
|
self.write_domain_case_loop_pack(run_dir, target_count=1, p1_count=1)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "not clean"):
|
||||||
|
saver.validate_accepted_run_dir(run_dir)
|
||||||
|
|
||||||
def test_extract_questions_resolves_scenario_pack_bindings(self) -> None:
|
def test_extract_questions_resolves_scenario_pack_bindings(self) -> None:
|
||||||
spec = {
|
spec = {
|
||||||
"schema_version": "domain_scenario_pack_v1",
|
"schema_version": "domain_scenario_pack_v1",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue