Доказать полезность margin-agent через честный boundary replay

This commit is contained in:
2026-05-24 17:52:57 +03:00
parent 98bdff31dc
commit f69393a887
14 changed files with 302 additions and 8 deletions
+103 -2
View File
@@ -256,6 +256,9 @@ GUARDED_INSUFFICIENCY_PRIMARY_MARKERS = (
"\u0442\u043e\u0447\u043d\u044b\u0435",
"\u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d",
"\u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043d",
"\u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430",
"\u043d\u0435\u043b\u044c\u0437\u044f \u0447\u0435\u0441\u0442\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434",
"\u043d\u0435\u043b\u044c\u0437\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b",
)
GUARDED_INSUFFICIENCY_LIMITATION_MARKERS = (
"\u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d",
@@ -265,12 +268,16 @@ GUARDED_INSUFFICIENCY_LIMITATION_MARKERS = (
"\u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043d\u043d\u043e\u0435 \u0441\u0430\u043b\u044c\u0434\u043e",
"\u043d\u0435 \u0434\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043e\u0441\u0442\u0430\u0442\u043e\u043a",
"\u043d\u0435 \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0435\u0441\u0442\u0440",
"\u0433\u0440\u0430\u043d\u0438\u0446\u0430 \u043e\u0442\u0432\u0435\u0442\u0430",
"\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u0443\u044e \u0431\u0430\u0437\u0443",
"\u0440\u0430\u0441\u0447\u0451\u0442\u043d\u0443\u044e \u0431\u0430\u0437\u0443",
)
GUARDED_INSUFFICIENCY_RESULT_MODES = {"heuristic_candidates"}
GUARDED_INSUFFICIENCY_TRUTH_MODES = {"limited"}
GUARDED_INSUFFICIENCY_ANSWER_SHAPES = {"limited_with_reason"}
BUSINESS_EXPECTED_RESULT_MODES = {
"clarification_required",
"honest_boundary_with_next_action",
"limited_accounting_answer",
"evidence_or_honest_boundary",
"ranking_or_limited_accounting_answer",
@@ -966,6 +973,23 @@ def is_margin_profitability_step(step_output: dict[str, Any]) -> bool:
question = str(step_output.get("question_resolved") or step_output.get("question_template") or "")
if is_nomenclature_margin_context(step_output, question):
return True
margin_context_values = [
str(step_output.get("scenario_id") or ""),
str(step_output.get("target_id") or ""),
str(step_output.get("fix_goal") or ""),
str(step_output.get("business_mismatch") or ""),
str(step_output.get("minimal_patch_direction") or ""),
*normalize_string_list(step_output.get("signals")),
]
margin_context = " ".join(margin_context_values).casefold()
if (
"inventory_margin_ranking_for_nomenclature" in margin_context
or "inventory_inventory_margin_ranking_for_nomenclature" in margin_context
or "margin_false_source" in margin_context
or "payment_false_source" in margin_context
or ("margin" in margin_context and ("оплат" in margin_context or "банк" in margin_context))
):
return True
tokens = [
str(step_output.get("expected_business_answer_contract") or ""),
str(step_output.get("required_answer_contract") or ""),
@@ -978,6 +1002,8 @@ def derive_repair_issue_code(step_output: dict[str, Any], problem_type: str) ->
violated = normalize_string_list(step_output.get("violated_invariants"))
if "domain_leak_accounting_route" in violated and is_margin_profitability_step(step_output):
return "margin_domain_leak_accounting_route"
if is_margin_profitability_step(step_output) and problem_type in {"route_gap", "capability_gap", "evidence_gap"}:
return "margin_domain_leak_accounting_route"
for issue_code in (
"technical_garbage_in_answer",
"business_direct_answer_missing",
@@ -2141,6 +2167,36 @@ def is_nomenclature_margin_context(step_state: dict[str, Any], question: str) ->
return has_subject and has_margin_signal and has_rank_signal
def is_margin_false_source_boundary_answer(step_state: dict[str, Any], question: str, assistant_text: str) -> bool:
tags = set(normalize_string_list(step_state.get("semantic_tags")))
question_text = _review_text(question)
answer_text = _review_text(assistant_text)
has_false_source_question = (
"payment_false_source" in tags
or (
("марж" in question_text or "прибыл" in question_text)
and ("товар" in question_text or "номенклатур" in question_text)
and ("оплат" in question_text or "банк" in question_text)
)
)
if not has_false_source_question:
return False
rejects_source = any(
marker in answer_text
for marker in (
"нельзя",
"не подтвержд",
"не подтвержда",
"не использовать",
"не является",
"не расчет",
"не расчёт",
)
)
names_correct_basis = "выруч" in answer_text and "себестоим" in answer_text
return rejects_source and names_correct_basis
def build_business_first_review(step_state: dict[str, Any]) -> dict[str, Any]:
question = str(step_state.get("question_resolved") or step_state.get("question_template") or "").strip()
assistant_text = str(step_state.get("assistant_text") or "")
@@ -2171,11 +2227,18 @@ def build_business_first_review(step_state: dict[str, Any]) -> dict[str, Any]:
limited_answer = _has_any_marker(assistant_text, BUSINESS_LIMITED_ANSWER_MARKERS)
has_next_action = _has_any_marker(assistant_text, BUSINESS_NEXT_ACTION_MARKERS)
nomenclature_margin_context = is_nomenclature_margin_context(step_state, question)
wrong_margin_domain_hits = (
raw_wrong_margin_domain_hits = (
_marker_hits(assistant_text, NOMENCLATURE_MARGIN_WRONG_DOMAIN_ANSWER_MARKERS)
if nomenclature_margin_context
else []
)
if raw_wrong_margin_domain_hits and is_margin_false_source_boundary_answer(step_state, question, assistant_text):
allowed_false_source_boundary_hits = {"банковск", "списание с расчетного", "списание с расчётного"}
wrong_margin_domain_hits = [
hit for hit in raw_wrong_margin_domain_hits if hit not in allowed_false_source_boundary_hits
]
else:
wrong_margin_domain_hits = raw_wrong_margin_domain_hits
margin_contract_hits = (
_marker_hits(assistant_text, NOMENCLATURE_MARGIN_EXPECTED_ANSWER_MARKERS)
if nomenclature_margin_context
@@ -2440,6 +2503,21 @@ def business_expected_result_mode_matches(expected_result_mode: str, step_state:
and reply_type in {"partial_coverage", "factual", "factual_with_explanation"}
)
if expected_result_mode == "honest_boundary_with_next_action":
business_review = step_state.get("business_first_review") if isinstance(step_state.get("business_first_review"), dict) else {}
return (
clean_business_review
and bool(assistant_text)
and bool(business_review.get("next_action_present"))
and (
truth_mode in GUARDED_INSUFFICIENCY_TRUTH_MODES
or answer_shape in GUARDED_INSUFFICIENCY_ANSWER_SHAPES
or step_state.get("balance_confirmed") is False
or is_margin_false_source_boundary_answer(step_state, str(step_state.get("question_resolved") or ""), assistant_text)
)
and reply_type in {"partial_coverage", "factual", "factual_with_explanation"}
)
if expected_result_mode == "ranking_or_limited_accounting_answer":
return (
clean_business_review
@@ -4357,6 +4435,16 @@ def normalize_analyst_priority_repair_target(raw_target: dict[str, Any], index:
if not root_cause_layers:
root_cause_layers = [problem_type]
issue_code = str(raw_target.get("issue_code") or problem_type or "other").strip()
issue_probe = {
**raw_target,
"scenario_id": scenario_id,
"target_id": f"{scenario_id}:{step_id}",
"problem_type": problem_type,
"root_cause_layers": root_cause_layers,
"fix_goal": fix_goal,
}
if issue_code in {"route_gap", "capability_gap", "evidence_gap"} and is_margin_profitability_step(issue_probe):
issue_code = "margin_domain_leak_accounting_route"
catalog_entry = issue_catalog_entry(issue_code)
return {
"issue_code": issue_code,
@@ -5212,6 +5300,18 @@ def build_issue_catalog_snapshot(repair_targets: dict[str, Any], catalog: dict[s
}
def detector_evidence_paths_for_target(target: dict[str, Any]) -> list[str]:
explicit = normalize_string_list(target.get("evidence_paths"))
if explicit:
return explicit
refs = target.get("artifact_refs") if isinstance(target.get("artifact_refs"), dict) else {}
step_state_path = str(refs.get("step_state_json") or "").strip()
if not step_state_path:
return []
step_state = Path(step_state_path)
return [str(step_state.with_name("output.md")), str(step_state.with_name("turn.json"))]
def build_detector_candidates(repair_targets: dict[str, Any], catalog: dict[str, Any] | None = None) -> dict[str, Any]:
source = catalog if isinstance(catalog, dict) else load_issue_catalog()
issues = source.get("issues") if isinstance(source.get("issues"), dict) else {}
@@ -5225,6 +5325,7 @@ def build_detector_candidates(repair_targets: dict[str, Any], catalog: dict[str,
detectors = normalize_string_list(entry.get("detectors"))
if not detectors and issue_code:
detectors = [f"{issue_code}_detector"]
evidence_paths = detector_evidence_paths_for_target(target)
for detector in detectors:
key = (issue_code, detector)
if key in seen:
@@ -5236,7 +5337,7 @@ def build_detector_candidates(repair_targets: dict[str, Any], catalog: dict[str,
"detector": detector,
"severity": target.get("severity"),
"sample_target_id": target.get("target_id"),
"evidence_paths": target.get("evidence_paths") or [],
"evidence_paths": evidence_paths,
}
)
return {