Закрепить широкий phase12 agent replay

This commit is contained in:
dctouch 2026-06-02 10:02:12 +03:00
parent b52279d19e
commit 7995980215
8 changed files with 435 additions and 10 deletions

View File

@ -407,6 +407,17 @@ exports.ROOT_EXACT_CAPABILITY_CONTRACTS = [
resultShape: "open_contracts_snapshot",
answerObjectShape: "open_contracts_snapshot"
}),
rootExactCapability({
capability_id: "address_open_items_by_counterparty_or_contract",
domainId: "contracts",
intent_ids: ["open_items_by_counterparty_or_contract"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
optionalAnchors: ["organization", "date_scope", "account", "counterparty", "contract"],
resultShape: "open_items_by_account_counterparty_or_contract",
answerObjectShape: "open_items_snapshot",
scenarioFamilies: ["canonical", "colloquial", "account_scope", "exact_negative_answer"]
}),
rootExactCapability({
capability_id: "confirmed_vat_payable_as_of_date",
domainId: "vat",

View File

@ -33,6 +33,28 @@ function createAssistantTransitionPolicy(deps) {
.some((normalized) => normalized &&
/(?:\b(?:same|this|that)\s+period\b|(?:за|на)\s+(?:этот|тот|такой\s+же|тот\s+же)\s+период|(?:Р·Р°|РЅР°)\s+(?:этот|СРѕС|такой\s+Р¶Рµ|СРѕС\s+Р¶Рµ)\s+период)/iu.test(normalized));
}
function hasExplicitDateScopeSignal(...values) {
return values
.map((value) => normalizeFollowupText(value))
.some((normalized) => {
if (!normalized) {
return false;
}
return /(?:\b(?:19|20)\d{2}\b|\b\d{1,2}[./-]\d{1,2}[./-]\d{2,4}\b|\b\d{4}[./-]\d{1,2}[./-]\d{1,2}\b|за\s+(?:период|месяц|год|квартал)|на\s+(?:дат|конец|сегодня)|январ|феврал|март|апрел|ма(?:й|я|е)|июн|июл|август|сентябр|октябр|ноябр|декабр|same\s+period|this\s+period|that\s+period)/iu.test(normalized);
});
}
function shouldClearStaleDateScopeForCounterpartyDocsRoot(targetIntent, sourceIntentHint, userMessage, alternateMessage) {
if (targetIntent !== "list_documents_by_counterparty") {
return false;
}
if (sourceIntentHint === "list_documents_by_counterparty" || sourceIntentHint === "list_documents_by_contract") {
return false;
}
if (hasSamePeriodReferenceCue(userMessage, alternateMessage) || hasExplicitDateScopeSignal(userMessage, alternateMessage)) {
return false;
}
return true;
}
function hasBankOperationsPivotCue(text) {
const normalized = normalizeFollowupText(text);
if (!normalized) {
@ -1224,6 +1246,18 @@ function createAssistantTransitionPolicy(deps) {
if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) {
return null;
}
const predecomposeIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const carryoverTargetIntentPreview = explicitIntent ?? predecomposeIntent ?? previousIntent;
if (shouldClearStaleDateScopeForCounterpartyDocsRoot(carryoverTargetIntentPreview, sourceIntentHint, userMessage, alternateMessage)) {
delete previousFilters.as_of_date;
delete previousFilters.period_from;
delete previousFilters.period_to;
delete previousFilters.purchase_date_bridge_selected;
delete previousFilters.purchase_date_bridge_first;
delete previousFilters.purchase_date_bridge_last;
delete previousFilters.purchase_date_bridge_basis;
delete previousFilters.purchase_date_bridge_has_multiple;
}
const shouldAttachInventoryRootFrame = Boolean(inventoryRootFrame &&
(rootScopedPivot ||
deps.isInventoryRootFrameIntent(sourceIntentHint) ||
@ -1238,7 +1272,6 @@ function createAssistantTransitionPolicy(deps) {
hasSelectedObjectInventorySignalPrimary ||
hasSelectedObjectInventorySignalAlternate));
const previousHasPeriodWindow = Boolean(deps.toNonEmptyString(previousFilters.period_from) || deps.toNonEmptyString(previousFilters.period_to));
const predecomposeIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const shouldRetargetVatSamePeriod = previousHasPeriodWindow &&
hasSamePeriodReferenceCue(userMessage, alternateMessage) &&
(explicitIntent === "vat_payable_confirmed_as_of_date" ||

View File

@ -435,6 +435,17 @@ export const ROOT_EXACT_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContra
resultShape: "open_contracts_snapshot",
answerObjectShape: "open_contracts_snapshot"
}),
rootExactCapability({
capability_id: "address_open_items_by_counterparty_or_contract",
domainId: "contracts",
intent_ids: ["open_items_by_counterparty_or_contract"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
optionalAnchors: ["organization", "date_scope", "account", "counterparty", "contract"],
resultShape: "open_items_by_account_counterparty_or_contract",
answerObjectShape: "open_items_snapshot",
scenarioFamilies: ["canonical", "colloquial", "account_scope", "exact_negative_answer"]
}),
rootExactCapability({
capability_id: "confirmed_vat_payable_as_of_date",
domainId: "vat",

View File

@ -84,6 +84,37 @@ export function createAssistantTransitionPolicy(deps) {
);
}
function hasExplicitDateScopeSignal(...values) {
return values
.map((value) => normalizeFollowupText(value))
.some((normalized) => {
if (!normalized) {
return false;
}
return /(?:\b(?:19|20)\d{2}\b|\b\d{1,2}[./-]\d{1,2}[./-]\d{2,4}\b|\b\d{4}[./-]\d{1,2}[./-]\d{1,2}\b|за\s+(?:период|месяц|год|квартал)|на\s+(?:дат|конец|сегодня)|январ|феврал|март|апрел|ма(?:й|я|е)|июн|июл|август|сентябр|октябр|ноябр|декабр|same\s+period|this\s+period|that\s+period)/iu.test(
normalized
);
});
}
function shouldClearStaleDateScopeForCounterpartyDocsRoot(
targetIntent,
sourceIntentHint,
userMessage,
alternateMessage
) {
if (targetIntent !== "list_documents_by_counterparty") {
return false;
}
if (sourceIntentHint === "list_documents_by_counterparty" || sourceIntentHint === "list_documents_by_contract") {
return false;
}
if (hasSamePeriodReferenceCue(userMessage, alternateMessage) || hasExplicitDateScopeSignal(userMessage, alternateMessage)) {
return false;
}
return true;
}
function hasBankOperationsPivotCue(text) {
const normalized = normalizeFollowupText(text);
if (!normalized) {
@ -1663,6 +1694,25 @@ export function createAssistantTransitionPolicy(deps) {
if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) {
return null;
}
const predecomposeIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const carryoverTargetIntentPreview = explicitIntent ?? predecomposeIntent ?? previousIntent;
if (
shouldClearStaleDateScopeForCounterpartyDocsRoot(
carryoverTargetIntentPreview,
sourceIntentHint,
userMessage,
alternateMessage
)
) {
delete previousFilters.as_of_date;
delete previousFilters.period_from;
delete previousFilters.period_to;
delete previousFilters.purchase_date_bridge_selected;
delete previousFilters.purchase_date_bridge_first;
delete previousFilters.purchase_date_bridge_last;
delete previousFilters.purchase_date_bridge_basis;
delete previousFilters.purchase_date_bridge_has_multiple;
}
const shouldAttachInventoryRootFrame = Boolean(
inventoryRootFrame &&
(rootScopedPivot ||
@ -1681,7 +1731,6 @@ export function createAssistantTransitionPolicy(deps) {
const previousHasPeriodWindow = Boolean(
deps.toNonEmptyString(previousFilters.period_from) || deps.toNonEmptyString(previousFilters.period_to)
);
const predecomposeIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const shouldRetargetVatSamePeriod =
previousHasPeriodWindow &&
hasSamePeriodReferenceCue(userMessage, alternateMessage) &&

View File

@ -4944,6 +4944,27 @@ describe("address decompose stage follow-up carryover", () => {
expect(result?.baseReasons).toContain("organization_from_followup_context");
});
it("does not inherit stale debt date when a new counterparty documents root starts", () => {
const result = runAddressDecomposeStage("покажи документы по свк", {
previous_intent: "receivables_confirmed_as_of_date",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
period_from: "2017-05-01",
period_to: "2017-05-31",
as_of_date: "2017-05-31"
},
previous_anchor_type: "unknown",
previous_anchor_value: null
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("list_documents_by_counterparty");
expect(result?.filters.extracted_filters.counterparty).toBe("свк");
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
expect(result?.filters.extracted_filters.as_of_date).toBeUndefined();
expect(result?.filters.extracted_filters.period_from).toBeUndefined();
expect(result?.filters.extracted_filters.period_to).toBeUndefined();
});
it("inherits as_of_date from previous period for same-date balance follow-up", () => {
const result = runAddressDecomposeStage("а по счету 60.01 на ту же дату", {
previous_intent: "list_documents_by_counterparty",

View File

@ -107,6 +107,16 @@ describe("assistant runtime contract registry", () => {
expect(contract?.execution_adapter).toBe("AddressQueryService");
});
it("declares open-items account/counterparty capability as a contracted runtime route", () => {
const contract = getAssistantCapabilityContractByIntent("open_items_by_counterparty_or_contract");
expect(contract?.capability_id).toBe("address_open_items_by_counterparty_or_contract");
expect(contract?.runtime_lane).toBe("address_exact");
expect(contract?.supported_transition_classes).toEqual(["T1", "T2", "T6", "T7"]);
expect(contract?.result_shape).toBe("open_items_by_account_counterparty_or_contract");
expect(contract?.required_scenario_families).toContain("exact_negative_answer");
});
it("declares counterparty document and value-flow contracts used by semantic dialog authority replay", () => {
const documents = getAssistantCapabilityContractByIntent("list_documents_by_counterparty");
expect(documents?.capability_id).toBe("documents_drilldown");

View File

@ -712,6 +712,21 @@ def merge_scenario_date_scope(
def question_resets_temporal_scope(value: Any) -> bool:
normalized = str(value or "").casefold().replace("ё", "е")
same_scope_markers = (
"за тот же период",
"на тот же период",
"за этот же период",
"на этот же период",
"за этот период",
"на этот период",
"на ту же дату",
"на эту же дату",
"на эту дату",
"same period",
"same date",
)
if any(marker in normalized for marker in same_scope_markers):
return False
markers = (
"за все доступное время",
"за все время",
@ -725,7 +740,14 @@ def question_resets_temporal_scope(value: Any) -> bool:
"не тащи период",
"не тащи старый период",
)
return any(marker in normalized for marker in markers)
if any(marker in normalized for marker in markers):
return True
has_document_signal = bool(re.search(r"\b(?:док(?:и|умент\w*)?|docs?|documents?)\b", normalized, flags=re.IGNORECASE))
has_anchor_signal = bool(re.search(r"(?:^|\s)по\s+[\w\"'«».-]{2,}", normalized, flags=re.IGNORECASE))
has_root_action_signal = bool(
re.search(r"\b(?:покажи|выведи|найди|дай|посмотри|show|list|find)\b", normalized, flags=re.IGNORECASE)
)
return has_document_signal and has_anchor_signal and has_root_action_signal
def repair_text_mojibake(value: str) -> str:
@ -809,6 +831,21 @@ def normalize_validation_filters(raw_filters: Any) -> dict[str, str]:
return normalized
def normalize_entity_filter_value_for_comparison(value: Any) -> str:
text = repair_text_mojibake(str(value or "")).casefold().replace("ё", "е")
text = re.sub(r"[\"'«»“”„]", "", text)
text = re.sub(r"[\s\-–—]+", " ", text)
return text.strip()
def validation_filter_values_match(filter_key: str, actual_value: str, expected_value: str) -> bool:
if actual_value == expected_value:
return True
if filter_key in {"organization", "counterparty", "contract", "warehouse"}:
return normalize_entity_filter_value_for_comparison(actual_value) == normalize_entity_filter_value_for_comparison(expected_value)
return False
def normalize_invariant_severity(raw_mapping: Any) -> dict[str, str]:
if not isinstance(raw_mapping, dict):
return {}
@ -2461,15 +2498,34 @@ def is_validated_confirmed_runtime_answer(
and business_review.get("answer_layering_ok") is True
and business_review.get("technical_garbage_present") is False
)
if str(state.get("reply_type") or "").strip() not in {"factual", "factual_with_explanation", "empty_but_valid"}:
reply_type = str(state.get("reply_type") or "").strip()
if reply_type not in {"factual", "factual_with_explanation", "partial_coverage", "empty_but_valid"}:
return False
if str(state.get("fallback_type") or "").strip() not in {"", "none"}:
return False
if str(state.get("mcp_call_status") or "").strip() != "matched_non_empty":
return False
response_type = str(state.get("response_type") or "").strip()
if response_type not in {"FACTUAL_LIST", "FACTUAL_SUMMARY"}:
return False
mcp_call_status = str(state.get("mcp_call_status") or "").strip()
selected_recipe = str(state.get("selected_recipe") or "").strip()
truth_gate_contract_status = str(state.get("truth_gate_contract_status") or "").strip()
exact_negative_open_items = (
mcp_call_status == "no_raw_rows"
and state.get("balance_confirmed") is False
and selected_recipe == "address_open_items_by_party_or_contract_v1"
and truth_gate_contract_status == "full_confirmed"
)
if exact_negative_open_items:
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
)
if reply_type not in {"factual", "factual_with_explanation", "empty_but_valid"}:
return False
if mcp_call_status != "matched_non_empty":
return False
result_mode = str(state.get("result_mode") or "").strip()
truth_mode = str(state.get("truth_mode") or "").strip()
answer_shape = str(state.get("answer_shape") or "").strip()
@ -2590,6 +2646,42 @@ def is_validated_missing_axis_clarification_answer(
)
def is_validated_clean_meta_chat_answer(
state: dict[str, Any],
execution_status: str,
business_review: dict[str, Any],
violations: list[str],
) -> bool:
if execution_status != "needs_exact_capability":
return False
if violations:
return False
semantic_tags = set(normalize_string_list(state.get("semantic_tags")))
allowed_tags = {
"meta_smalltalk",
"company_selected",
"organization_authority",
"meta_capability",
"human_answer_quality",
"meta_historical_capability",
"capability_over_followup",
}
if not semantic_tags.intersection(allowed_tags):
return False
if str(state.get("detected_mode") or "").strip() not in {"chat", "unsupported", ""}:
return False
if str(state.get("reply_type") or "").strip() not in {"factual", "factual_with_explanation", "partial_coverage"}:
return False
if not str(state.get("assistant_text") or "").strip():
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 _business_review_is_clean(step_state: dict[str, Any]) -> bool:
business_review = step_state.get("business_first_review")
if not isinstance(business_review, dict):
@ -2710,12 +2802,12 @@ def business_expected_result_mode_matches(expected_result_mode: str, step_state:
def acceptance_status_from_execution(execution_status: str, hard_fail: bool, semantic_validated: bool = False) -> str:
if execution_status == "blocked":
return "blocked"
if execution_status == "needs_exact_capability":
return "needs_exact_capability"
if hard_fail:
return "rejected"
if semantic_validated:
return "validated"
if execution_status == "needs_exact_capability":
return "needs_exact_capability"
if execution_status == "exact":
return "validated"
return "rejected"
@ -2784,7 +2876,7 @@ def validate_step_contract(step_state: dict[str, Any]) -> dict[str, Any]:
if not actual_value:
violated_invariants.append("missing_required_filter")
continue
if actual_value != expected_value:
if not validation_filter_values_match(filter_key, actual_value, expected_value):
if filter_key == "as_of_date":
violated_invariants.append("wrong_as_of_date")
elif filter_key == "period_from":
@ -2876,6 +2968,12 @@ def validate_step_contract(step_state: dict[str, Any]) -> dict[str, Any]:
business_review,
unique_violations,
)
clean_meta_chat_validated = is_validated_clean_meta_chat_answer(
state,
execution_status,
business_review,
unique_violations,
)
state["violated_invariants"] = unique_violations
state["warnings"] = list(dict.fromkeys(warnings))
state["hard_fail"] = hard_fail
@ -2885,6 +2983,7 @@ def validate_step_contract(step_state: dict[str, Any]) -> dict[str, Any]:
state["guarded_insufficiency_validated"] = guarded_insufficiency_validated
state["clarification_answer_validated"] = clarification_validated
state["missing_axis_clarification_validated"] = missing_axis_clarification_validated
state["clean_meta_chat_answer_validated"] = clean_meta_chat_validated
state["acceptance_status"] = acceptance_status_from_execution(
execution_status,
hard_fail,
@ -2895,6 +2994,7 @@ def validate_step_contract(step_state: dict[str, Any]) -> dict[str, Any]:
or guarded_insufficiency_validated
or clarification_validated
or missing_axis_clarification_validated
or clean_meta_chat_validated
),
)
state["status"] = state["acceptance_status"]
@ -2935,8 +3035,19 @@ def build_scenario_step_state(
if not effective_analysis_context:
effective_analysis_context = step.get("analysis_context") if isinstance(step.get("analysis_context"), dict) else {}
current_date_scope = derive_mcp_discovery_business_overview_date_scope(debug)
if current_date_scope is None:
current_date_scope_from_session_context = current_date_scope is None
if current_date_scope_from_session_context:
current_date_scope = context.get("date_scope")
debug_extracted_filters = debug.get("extracted_filters") if isinstance(debug.get("extracted_filters"), dict) else {}
if current_date_scope_from_session_context and question_resets_temporal_scope(question_resolved) and not any(
normalize_iso_date(debug_extracted_filters.get(key)) for key in ("as_of_date", "period_from", "period_to")
):
current_date_scope = {
"as_of_date": None,
"period_from": None,
"period_to": None,
"source": "question_temporal_scope_reset",
}
runtime_contract = (
debug.get("assistant_runtime_contract_v1")
if isinstance(debug.get("assistant_runtime_contract_v1"), dict)

View File

@ -251,6 +251,9 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
def test_temporal_reset_question_skips_carried_date_scope(self) -> None:
self.assertTrue(dcl.question_resets_temporal_scope("show money za all time"))
self.assertTrue(dcl.question_resets_temporal_scope("сколько всего денег за все доступное время"))
self.assertTrue(dcl.question_resets_temporal_scope("по чепурнову покажи все доки"))
self.assertTrue(dcl.question_resets_temporal_scope("покажи документы по свк"))
self.assertFalse(dcl.question_resets_temporal_scope("а документы по этому же договору за тот же период"))
carried = dcl.carry_forward_analysis_context(
{
@ -271,6 +274,182 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
self.assertNotIn("as_of_date", carried)
self.assertEqual(carried["organization_scope"], {"label": "ООО Альтернатива Плюс"})
def test_temporal_reset_question_clears_stale_navigation_date_scope(self) -> None:
step_state = dcl.build_scenario_step_state(
scenario_id="docs_reset_demo",
domain="agentic_loop",
step={
"step_id": "step_01",
"title": "Docs root",
"depends_on": [],
"question_template": "по чепурнову покажи все доки",
"expected_intents": ["list_documents_by_counterparty"],
"expected_recipe": "address_documents_by_counterparty_v1",
},
step_index=1,
question_resolved="по чепурнову покажи все доки",
analysis_context={},
turn_artifact={
"assistant_message": {
"reply_type": "factual",
"text": "Контрагент: Чепурнов П.Д. Найдено документов: 1.",
"message_id": "msg-1",
"trace_id": "trace-1",
},
"technical_debug_payload": {
"detected_mode": "address_query",
"detected_intent": "list_documents_by_counterparty",
"selected_recipe": "address_documents_by_counterparty_v1",
"capability_id": "documents_drilldown",
"capability_contract_id": "documents_drilldown",
"truth_gate_contract_status": "full_confirmed",
"extracted_filters": {"counterparty": "Чепурнов", "organization": "ООО Альтернатива Плюс"},
},
"session_summary": {
"address_navigation_state": {
"session_context": {
"date_scope": {
"as_of_date": "2017-05-31",
"period_from": None,
"period_to": None,
}
}
}
},
},
entries=[],
)
self.assertIsNone(step_state["date_scope"]["as_of_date"])
self.assertEqual(step_state["date_scope"]["source"], "question_temporal_scope_reset")
def test_open_items_exact_negative_answer_validates_without_rows(self) -> None:
step_state = dcl.build_scenario_step_state(
scenario_id="open_items_negative_demo",
domain="agentic_loop",
step={
"step_id": "step_01",
"title": "Open items account 60",
"depends_on": [],
"question_template": "хвосты покажи по счету 60 на август 2022",
"expected_intents": ["open_items_by_counterparty_or_contract"],
},
step_index=1,
question_resolved="хвосты покажи по счету 60 на август 2022",
analysis_context={},
turn_artifact={
"assistant_message": {
"reply_type": "partial_coverage",
"text": "По счету 60 за период 2022-08-01..2022-08-31 открытых остатков не найдено.",
"message_id": "msg-1",
"trace_id": "trace-1",
},
"technical_debug_payload": {
"detected_mode": "address_query",
"detected_intent": "open_items_by_counterparty_or_contract",
"selected_recipe": "address_open_items_by_party_or_contract_v1",
"capability_id": "address_open_items_by_counterparty_or_contract",
"capability_contract_id": "address_open_items_by_counterparty_or_contract",
"truth_gate_contract_status": "full_confirmed",
"response_type": "FACTUAL_SUMMARY",
"result_mode": "confirmed_balance",
"truth_mode": "limited",
"answer_shape": "limited_with_reason",
"mcp_call_status": "no_raw_rows",
"balance_confirmed": False,
"extracted_filters": {
"account": "60",
"period_from": "2022-08-01",
"period_to": "2022-08-31",
"as_of_date": "2022-08-31",
},
},
"session_summary": {},
},
entries=[],
)
self.assertEqual(step_state["status"], "validated")
self.assertTrue(step_state["runtime_factual_answer_validated"])
def test_required_organization_filter_accepts_quoted_legal_name(self) -> None:
step_state = dcl.build_scenario_step_state(
scenario_id="quoted_org_filter_demo",
domain="agentic_loop",
step={
"step_id": "step_01",
"title": "Inventory root",
"depends_on": [],
"question_template": "что там на складе по остаткам",
"expected_intents": ["inventory_on_hand_as_of_date"],
"required_filters": {"organization": "ООО Альтернатива Плюс"},
},
step_index=1,
question_resolved="что там на складе по остаткам",
analysis_context={},
turn_artifact={
"assistant_message": {
"reply_type": "factual",
"text": "На складе подтверждено 11 позиций.",
"message_id": "msg-1",
"trace_id": "trace-1",
},
"technical_debug_payload": {
"detected_mode": "address_query",
"detected_intent": "inventory_on_hand_as_of_date",
"selected_recipe": "address_inventory_on_hand_as_of_date_v1",
"capability_id": "confirmed_inventory_on_hand_as_of_date",
"capability_contract_id": "confirmed_inventory_on_hand_as_of_date",
"truth_gate_contract_status": "full_confirmed",
"response_type": "FACTUAL_LIST",
"truth_mode": "confirmed",
"answer_shape": "confirmed_factual",
"mcp_call_status": "matched_non_empty",
"balance_confirmed": True,
"extracted_filters": {"organization": 'ООО "Альтернатива Плюс"', "as_of_date": "2026-06-02"},
},
"session_summary": {},
},
entries=[],
)
self.assertEqual(step_state["status"], "validated")
self.assertNotIn("missing_required_filter", step_state["violated_invariants"])
def test_clean_meta_chat_answer_validates_without_exact_capability(self) -> None:
step_state = dcl.build_scenario_step_state(
scenario_id="meta_chat_demo",
domain="agentic_loop",
step={
"step_id": "step_01",
"title": "Capability meta",
"depends_on": [],
"question_template": "расскажи что можешь интересного",
"semantic_tags": ["meta_capability", "human_answer_quality"],
"required_answer_patterns_all": ["(?i)1с", "(?i)могу"],
},
step_index=1,
question_resolved="расскажи что можешь интересного",
analysis_context={},
turn_artifact={
"assistant_message": {
"reply_type": "factual_with_explanation",
"text": "Могу помочь с быстрым анализом данных 1С: долги, остатки, НДС и документы.",
"message_id": "msg-1",
"trace_id": "trace-1",
},
"technical_debug_payload": {
"detected_mode": "chat",
"detected_intent": None,
},
"session_summary": {},
},
entries=[],
)
self.assertEqual(step_state["status"], "validated")
self.assertTrue(step_state["clean_meta_chat_answer_validated"])
def test_merge_scenario_date_scope_keeps_current_scope_over_stale_previous(self) -> None:
merged = dcl.merge_scenario_date_scope(
{