Compare commits
13 Commits
9e2bc8cb6c
...
e9f0ef6b41
| Author | SHA1 | Date |
|---|---|---|
|
|
e9f0ef6b41 | |
|
|
41463c9965 | |
|
|
b3f2e405bb | |
|
|
7e70ec1d46 | |
|
|
631391f47a | |
|
|
e4eac8e465 | |
|
|
d0d7962b65 | |
|
|
10cc779de8 | |
|
|
13c48ea96e | |
|
|
e2b928ac19 | |
|
|
bbb3620956 | |
|
|
1507ca2bb9 | |
|
|
77c88a018a |
File diff suppressed because it is too large
Load Diff
|
|
@ -66,6 +66,9 @@
|
||||||
"required_patterns_any": [
|
"required_patterns_any": [
|
||||||
"(?i)(выруч|себестоим|валов|марж|не хватает|не могу подтвердить|не подтвержден|unknown)"
|
"(?i)(выруч|себестоим|валов|марж|не хватает|не могу подтвердить|не подтвержден|unknown)"
|
||||||
],
|
],
|
||||||
|
"artifact_path_include_patterns": [
|
||||||
|
"(?i)steps[\\\\/][^\\\\/]*(?:margin|profitability|марж)"
|
||||||
|
],
|
||||||
"artifact_path_exclude_patterns": [
|
"artifact_path_exclude_patterns": [
|
||||||
"(?i)selected_item",
|
"(?i)selected_item",
|
||||||
"(?i)supplier",
|
"(?i)supplier",
|
||||||
|
|
@ -208,6 +211,45 @@
|
||||||
"forbidden_patterns": ["(?im)^\\s*(Что проверили|Опорные документы):"]
|
"forbidden_patterns": ["(?im)^\\s*(Что проверили|Опорные документы):"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"counterparty_value_flow_required_surface": {
|
||||||
|
"kind": "answer_text_required_any",
|
||||||
|
"automation_level": "automatic",
|
||||||
|
"description": "Counterparty value-flow answers must surface counterparty scope, incoming amount, outgoing amount, and net/saldo wording.",
|
||||||
|
"issue_codes": ["counterparty_value_flow_misrouted_to_company_profit"],
|
||||||
|
"inputs": ["steps/<step_id>/output.md"],
|
||||||
|
"check": {
|
||||||
|
"artifact_path_include_patterns": [
|
||||||
|
"(?i)(s01_svk_money_documents|s01_select_svk_counterparty_money|step_05_net_after_payout)"
|
||||||
|
],
|
||||||
|
"required_patterns_any": [
|
||||||
|
"(?is)(?=.*(СВК|Группа\\s+СВК))(?=.*(входящ|получил|получено|получили))(?=.*(исходящ|заплатил|заплачено|заплатили|ушло))(?=.*(нетто|сальдо|разниц|чистый\\s+денежный))"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"counterparty_value_flow_profit_accounts_forbidden": {
|
||||||
|
"kind": "answer_text_regex_forbidden",
|
||||||
|
"automation_level": "automatic",
|
||||||
|
"description": "Counterparty received/paid/net-flow answers must not use company profit or 90/91/99 accounts as the direct answer.",
|
||||||
|
"issue_codes": ["counterparty_value_flow_misrouted_to_company_profit"],
|
||||||
|
"inputs": ["steps/<step_id>/output.md"],
|
||||||
|
"check": {
|
||||||
|
"artifact_path_include_patterns": [
|
||||||
|
"(?i)(s01_svk_money_documents|s01_select_svk_counterparty_money|step_03_incoming_by_resolved_entity|step_04_payout_switch_by_resolved_entity|step_05_net_after_payout)"
|
||||||
|
],
|
||||||
|
"forbidden_patterns": ["(?i)(сч[её]т\\s*(90|91|99)|90[\\./](01|02|09)|91[\\./]|99\\b|company[- ]level\\s+profit)"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bank_counterparty_boundary_required": {
|
||||||
|
"kind": "answer_text_required_any",
|
||||||
|
"automation_level": "semi_automatic",
|
||||||
|
"description": "Money-leader answers that explicitly mention bank role must include a boundary instead of presenting a bank as an ordinary customer/supplier.",
|
||||||
|
"issue_codes": ["bank_counterparty_misclassified_as_business_partner"],
|
||||||
|
"inputs": ["steps/<step_id>/output.md"],
|
||||||
|
"check": {
|
||||||
|
"artifact_path_include_patterns": ["(?i)(s02_colloquial_money_leaders|step_03_sberbank_role_after_role_tail_overview)"],
|
||||||
|
"required_patterns_any": ["(?is)(банк|банковск|финансов|не\\s+обычн|роль|назначени[ея]|плат[её]ж)"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"route_candidate_needs_enablement": {
|
"route_candidate_needs_enablement": {
|
||||||
"kind": "stage_review_signal",
|
"kind": "stage_review_signal",
|
||||||
"automation_level": "semi_automatic",
|
"automation_level": "semi_automatic",
|
||||||
|
|
|
||||||
|
|
@ -411,7 +411,11 @@
|
||||||
"severity": "P0",
|
"severity": "P0",
|
||||||
"business_meaning": "Контрагентский вопрос про получили/заплатили/нетто отвечен company-level прибылью или 90/91/99 вместо контрагентского денежного потока.",
|
"business_meaning": "Контрагентский вопрос про получили/заплатили/нетто отвечен company-level прибылью или 90/91/99 вместо контрагентского денежного потока.",
|
||||||
"root_layers": ["followup_action_resolution_gap", "answer_shape_mismatch", "domain_scope_leak"],
|
"root_layers": ["followup_action_resolution_gap", "answer_shape_mismatch", "domain_scope_leak"],
|
||||||
"detectors": ["counterparty_value_flow_profit_mismatch"],
|
"detectors": [
|
||||||
|
"counterparty_value_flow_required_surface",
|
||||||
|
"counterparty_value_flow_profit_accounts_forbidden",
|
||||||
|
"counterparty_value_flow_profit_mismatch"
|
||||||
|
],
|
||||||
"allowed_patch_targets": [
|
"allowed_patch_targets": [
|
||||||
"llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts",
|
"llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts",
|
||||||
"llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts",
|
"llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts",
|
||||||
|
|
@ -489,7 +493,10 @@
|
||||||
"severity": "P1",
|
"severity": "P1",
|
||||||
"business_meaning": "Банк/финансовая организация ошибочно подана как обычный клиент/поставщик без роли, назначения платежа и договорного контекста.",
|
"business_meaning": "Банк/финансовая организация ошибочно подана как обычный клиент/поставщик без роли, назначения платежа и договорного контекста.",
|
||||||
"root_layers": ["business_semantic_role_gap", "domain_scope"],
|
"root_layers": ["business_semantic_role_gap", "domain_scope"],
|
||||||
"detectors": ["bank_counterparty_role_boundary_missing"],
|
"detectors": [
|
||||||
|
"bank_counterparty_boundary_required",
|
||||||
|
"bank_counterparty_role_boundary_missing"
|
||||||
|
],
|
||||||
"allowed_patch_targets": [
|
"allowed_patch_targets": [
|
||||||
"llm_normalizer/backend/src/services/address_runtime/composeStage.ts",
|
"llm_normalizer/backend/src/services/address_runtime/composeStage.ts",
|
||||||
"llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts",
|
"llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts",
|
||||||
|
|
|
||||||
|
|
@ -1694,7 +1694,8 @@ function hasNomenclatureMarginRankingSignal(text) {
|
||||||
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
|
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
|
||||||
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized);
|
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized);
|
||||||
const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized);
|
const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized);
|
||||||
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue);
|
const hasInspectionCue = /(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(normalized);
|
||||||
|
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
|
||||||
}
|
}
|
||||||
function hasVatPeriodInspectionBridgeSignal(text) {
|
function hasVatPeriodInspectionBridgeSignal(text) {
|
||||||
const normalized = String(text ?? "").trim().toLowerCase();
|
const normalized = String(text ?? "").trim().toLowerCase();
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ function hasInventoryMarginRankingSignal(text) {
|
||||||
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
|
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
|
||||||
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized);
|
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized);
|
||||||
const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized);
|
const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized);
|
||||||
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue);
|
const hasInspectionCue = /(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(normalized);
|
||||||
|
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
|
||||||
}
|
}
|
||||||
function hasInventoryOnHandSignal(text) {
|
function hasInventoryOnHandSignal(text) {
|
||||||
const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(text);
|
const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(text);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ exports.cloneAddressNavigationState = cloneAddressNavigationState;
|
||||||
exports.normalizeAddressNavigationState = normalizeAddressNavigationState;
|
exports.normalizeAddressNavigationState = normalizeAddressNavigationState;
|
||||||
exports.evolveAddressNavigationStateWithAssistantItem = evolveAddressNavigationStateWithAssistantItem;
|
exports.evolveAddressNavigationStateWithAssistantItem = evolveAddressNavigationStateWithAssistantItem;
|
||||||
const nanoid_1 = require("nanoid");
|
const nanoid_1 = require("nanoid");
|
||||||
|
const addressTextRepair_1 = require("./addressTextRepair");
|
||||||
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
|
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
|
||||||
const addressNavigation_1 = require("../types/addressNavigation");
|
const addressNavigation_1 = require("../types/addressNavigation");
|
||||||
const MAX_RESULT_SETS = 40;
|
const MAX_RESULT_SETS = 40;
|
||||||
|
|
@ -126,6 +127,48 @@ function readNavigationDiscoveryCounterparty(debug) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function readDiscoveryPilot(debug) {
|
||||||
|
const entry = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
||||||
|
const bridge = toObject(entry?.bridge);
|
||||||
|
return toObject(bridge?.pilot);
|
||||||
|
}
|
||||||
|
function readCurrentDiscoveryCounterparty(debug) {
|
||||||
|
const pilot = readDiscoveryPilot(debug);
|
||||||
|
const entityResolution = toObject(pilot?.derived_entity_resolution);
|
||||||
|
const bidirectionalValueFlow = toObject(pilot?.derived_bidirectional_value_flow);
|
||||||
|
const valueFlow = toObject(pilot?.derived_value_flow);
|
||||||
|
return (toNonEmptyString(entityResolution?.resolved_entity) ??
|
||||||
|
toNonEmptyString(bidirectionalValueFlow?.counterparty) ??
|
||||||
|
toNonEmptyString(valueFlow?.counterparty) ??
|
||||||
|
(0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ??
|
||||||
|
readNavigationDiscoveryCounterparty(debug));
|
||||||
|
}
|
||||||
|
function isCounterpartyDiscoveryChain(value) {
|
||||||
|
return (value === "entity_resolution" ||
|
||||||
|
value === "value_flow" ||
|
||||||
|
value === "value_flow_comparison" ||
|
||||||
|
value === "document_evidence" ||
|
||||||
|
value === "movement_evidence" ||
|
||||||
|
value === "lifecycle");
|
||||||
|
}
|
||||||
|
function intentFromDiscoveryChain(value) {
|
||||||
|
if (value === "business_overview") {
|
||||||
|
return "business_overview";
|
||||||
|
}
|
||||||
|
if (value === "value_flow" || value === "value_flow_comparison") {
|
||||||
|
return "customer_revenue_and_payments";
|
||||||
|
}
|
||||||
|
if (value === "document_evidence") {
|
||||||
|
return "list_documents_by_counterparty";
|
||||||
|
}
|
||||||
|
if (value === "movement_evidence") {
|
||||||
|
return "bank_operations_by_counterparty";
|
||||||
|
}
|
||||||
|
if (value === "lifecycle") {
|
||||||
|
return "counterparty_activity_lifecycle";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
function toAddressFocusObjectType(value) {
|
function toAddressFocusObjectType(value) {
|
||||||
const normalized = toNonEmptyString(value);
|
const normalized = toNonEmptyString(value);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -295,6 +338,32 @@ function normalizeFilters(value) {
|
||||||
}
|
}
|
||||||
return { ...record };
|
return { ...record };
|
||||||
}
|
}
|
||||||
|
function isBusinessOverviewRoleCounterpartyFragment(value) {
|
||||||
|
const text = (0, addressTextRepair_1.normalizeRussianComparableText)(value);
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasRoleWord = /(?:^|\s)(?:клиент|поставщик|контрагент|получатель|источник|покупатель|заказчик)(?:\s|$)/u.test(text);
|
||||||
|
if (!hasRoleWord) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasRankingWord = /(?:^|\s)(?:главн[а-яёa-z0-9_-]*|крупнейш[а-яёa-z0-9_-]*|основн[а-яёa-z0-9_-]*|топ(?:-\d+)?)(?:\s|$)/u.test(text);
|
||||||
|
const hasRoleOnlyExplanation = /(?:^|\s)(?:клиент|поставщик)(?:\s|$).{0,40}(?:^|\s)как(?:\s|$).{0,30}(?:^|\s)рол/u.test(text);
|
||||||
|
const endsWithDanglingPreposition = /(?:^|\s)(?:в|за|по|на|от|для|к)$/u.test(text);
|
||||||
|
return hasRoleOnlyExplanation || (hasRankingWord && (endsWithDanglingPreposition || text.length <= 48));
|
||||||
|
}
|
||||||
|
function shouldSuppressBusinessOverviewCounterpartyFocus(debug, counterparty) {
|
||||||
|
return (toNonEmptyString(debug.mcp_discovery_selected_chain_id) === "business_overview" &&
|
||||||
|
debug.mcp_discovery_response_applied === true &&
|
||||||
|
isBusinessOverviewRoleCounterpartyFragment(counterparty));
|
||||||
|
}
|
||||||
|
function sanitizeBusinessOverviewNavigationFilters(filters, debug) {
|
||||||
|
if (!shouldSuppressBusinessOverviewCounterpartyFocus(debug, filters.counterparty)) {
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
const { counterparty: _counterparty, ...rest } = filters;
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
function resolveNavigationAction(debug, hasFocusObject) {
|
function resolveNavigationAction(debug, hasFocusObject) {
|
||||||
const continuationContract = toObject(debug.dialog_continuation_contract_v2);
|
const continuationContract = toObject(debug.dialog_continuation_contract_v2);
|
||||||
const decision = toNonEmptyString(continuationContract?.decision);
|
const decision = toNonEmptyString(continuationContract?.decision);
|
||||||
|
|
@ -337,8 +406,7 @@ function readBusinessOverviewComparisonProofBundles(debug) {
|
||||||
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
||||||
const turnInput = toObject(entryPoint?.turn_input);
|
const turnInput = toObject(entryPoint?.turn_input);
|
||||||
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
|
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
|
||||||
const bridge = toObject(entryPoint?.bridge);
|
const pilot = readDiscoveryPilot(debug);
|
||||||
const pilot = toObject(bridge?.pilot);
|
|
||||||
const counterpartyValueFlowBundle = cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
|
const counterpartyValueFlowBundle = cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
|
||||||
cloneRecord(pilot?.derived_bidirectional_value_flow);
|
cloneRecord(pilot?.derived_bidirectional_value_flow);
|
||||||
const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle);
|
const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle);
|
||||||
|
|
@ -356,12 +424,24 @@ function readBusinessOverviewComparisonProofBundles(debug) {
|
||||||
counterparty_document_bundle: counterpartyDocumentBundle
|
counterparty_document_bundle: counterpartyDocumentBundle
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function readCounterpartyValueFlowProofBundles(debug) {
|
||||||
|
const pilot = readDiscoveryPilot(debug);
|
||||||
|
const counterpartyValueFlowBundle = cloneRecord(pilot?.derived_bidirectional_value_flow);
|
||||||
|
const counterparty = toNonEmptyString(counterpartyValueFlowBundle?.counterparty);
|
||||||
|
if (!counterparty || !counterpartyValueFlowBundle) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
|
||||||
|
counterparty_document_bundle: null
|
||||||
|
};
|
||||||
|
}
|
||||||
function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
|
function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
|
||||||
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
||||||
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
||||||
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
|
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
|
||||||
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
||||||
if (counterparty) {
|
if (counterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, counterparty)) {
|
||||||
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
||||||
}
|
}
|
||||||
const organization = (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
|
const organization = (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
|
||||||
|
|
@ -369,8 +449,8 @@ function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
|
||||||
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
|
if (isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true) {
|
||||||
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
const counterparty = readCurrentDiscoveryCounterparty(debug);
|
||||||
if (counterparty) {
|
if (counterparty) {
|
||||||
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
||||||
}
|
}
|
||||||
|
|
@ -560,24 +640,23 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
|
||||||
}
|
}
|
||||||
const debug = item.debug;
|
const debug = item.debug;
|
||||||
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
||||||
const discoveryIntent = selectedDiscoveryChain === "business_overview"
|
|
||||||
? "business_overview"
|
|
||||||
: selectedDiscoveryChain === "value_flow_comparison"
|
|
||||||
? "customer_revenue_and_payments"
|
|
||||||
: "unknown";
|
|
||||||
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
||||||
const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
|
const discoveryIntent = intentFromDiscoveryChain(selectedDiscoveryChain);
|
||||||
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
|
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
|
||||||
|
const shouldPreferDiscoveryIntent = trackableDiscoveryTurn &&
|
||||||
|
discoveryIntent !== "unknown" &&
|
||||||
|
selectedDiscoveryChain !== "business_overview";
|
||||||
|
const intent = toAddressIntent(shouldPreferDiscoveryIntent ? discoveryIntent : detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
|
||||||
if (intent === "unknown" && !trackableDiscoveryTurn) {
|
if (intent === "unknown" && !trackableDiscoveryTurn) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
|
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
|
||||||
const resultSetId = `rs-${item.message_id}`;
|
const resultSetId = `rs-${item.message_id}`;
|
||||||
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
|
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
|
||||||
const filters = normalizeFilters(debug.extracted_filters);
|
const filters = sanitizeBusinessOverviewNavigationFilters(normalizeFilters(debug.extracted_filters), debug);
|
||||||
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text) ?? (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
|
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text) ?? (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
|
||||||
const derivedCounterpartyScope = selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
const derivedCounterpartyScope = isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true
|
||||||
? (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
|
? readCurrentDiscoveryCounterparty(debug)
|
||||||
: null;
|
: null;
|
||||||
const filtersWithDerivedScope = derivedOrganizationScope && !toNonEmptyString(filters.organization)
|
const filtersWithDerivedScope = derivedOrganizationScope && !toNonEmptyString(filters.organization)
|
||||||
? {
|
? {
|
||||||
|
|
@ -610,14 +689,23 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
|
||||||
const debugFocusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
|
const debugFocusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
|
||||||
const primaryEntityFocusObject = buildFocusObjectFromPrimaryEntityRef(resultSet, createdAt);
|
const primaryEntityFocusObject = buildFocusObjectFromPrimaryEntityRef(resultSet, createdAt);
|
||||||
const focusObject = resolveFocusObjectForNavigation(state, intent, resultSet, debugFocusObject, primaryEntityFocusObject);
|
const focusObject = resolveFocusObjectForNavigation(state, intent, resultSet, debugFocusObject, primaryEntityFocusObject);
|
||||||
const comparisonCounterparty = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
const rawComparisonCounterparty = debug.mcp_discovery_response_applied === true
|
||||||
|
? selectedDiscoveryChain === "business_overview"
|
||||||
? readNavigationDiscoveryCounterparty(debug)
|
? readNavigationDiscoveryCounterparty(debug)
|
||||||
|
: isCounterpartyDiscoveryChain(selectedDiscoveryChain)
|
||||||
|
? readCurrentDiscoveryCounterparty(debug)
|
||||||
|
: null
|
||||||
|
: null;
|
||||||
|
const comparisonCounterparty = rawComparisonCounterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, rawComparisonCounterparty)
|
||||||
|
? rawComparisonCounterparty
|
||||||
: null;
|
: null;
|
||||||
const comparisonOrganization = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
const comparisonOrganization = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||||
? derivedOrganizationScope ?? toNonEmptyString(filtersWithDerivedScope.organization)
|
? derivedOrganizationScope ?? toNonEmptyString(filtersWithDerivedScope.organization)
|
||||||
: null;
|
: null;
|
||||||
const currentComparisonProofBundles = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
const currentComparisonProofBundles = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||||
? readBusinessOverviewComparisonProofBundles(debug)
|
? readBusinessOverviewComparisonProofBundles(debug)
|
||||||
|
: selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
||||||
|
? readCounterpartyValueFlowProofBundles(debug)
|
||||||
: null;
|
: null;
|
||||||
const inheritedComparisonScope = state.session_context.comparison_scope;
|
const inheritedComparisonScope = state.session_context.comparison_scope;
|
||||||
const inheritedComparisonProofBundles = comparisonCounterparty &&
|
const inheritedComparisonProofBundles = comparisonCounterparty &&
|
||||||
|
|
|
||||||
|
|
@ -1313,6 +1313,15 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
intent === "vat_payable_confirmed_as_of_date";
|
intent === "vat_payable_confirmed_as_of_date";
|
||||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||||
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
||||||
|
const currentPeriodFromForCarryover = toNonEmptyString(merged.period_from);
|
||||||
|
const currentPeriodToForCarryover = toNonEmptyString(merged.period_to);
|
||||||
|
const todayIsoForPeriodCarryover = new Date().toISOString().slice(0, 10);
|
||||||
|
const currentValueFlowPeriodDefaultsToToday = isValueCounterpartyIntent(intent) &&
|
||||||
|
hasFollowupSignal &&
|
||||||
|
!hasExplicitPeriodInMessage &&
|
||||||
|
!hasExplicitCurrentDateInMessage &&
|
||||||
|
!currentPeriodFromForCarryover &&
|
||||||
|
currentPeriodToForCarryover === todayIsoForPeriodCarryover;
|
||||||
const currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
|
const currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
|
||||||
const currentContractExplicit = toNonEmptyString(merged.contract);
|
const currentContractExplicit = toNonEmptyString(merged.contract);
|
||||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||||
|
|
@ -1387,7 +1396,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
}
|
}
|
||||||
reasons.push("period_from_followup_context");
|
reasons.push("period_from_followup_context");
|
||||||
}
|
}
|
||||||
if (!currentHasPeriod &&
|
if ((!currentHasPeriod || currentValueFlowPeriodDefaultsToToday) &&
|
||||||
previousHasPeriod &&
|
previousHasPeriod &&
|
||||||
hasFollowupSignal &&
|
hasFollowupSignal &&
|
||||||
!hasExplicitPeriodInMessage &&
|
!hasExplicitPeriodInMessage &&
|
||||||
|
|
|
||||||
|
|
@ -411,11 +411,25 @@ function isReferentialCounterpartyPlaceholder(value) {
|
||||||
}
|
}
|
||||||
function normalizeCounterpartyCandidate(value, toNonEmptyString) {
|
function normalizeCounterpartyCandidate(value, toNonEmptyString) {
|
||||||
const text = toNonEmptyString(value);
|
const text = toNonEmptyString(value);
|
||||||
if (!text || isReferentialCounterpartyPlaceholder(text)) {
|
if (!text || isReferentialCounterpartyPlaceholder(text) || isCounterpartyRoleFragment(text)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
function isCounterpartyRoleFragment(value) {
|
||||||
|
const text = (0, addressTextRepair_1.normalizeRussianComparableText)(value);
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasRoleWord = /(?:^|\s)(?:клиент|поставщик|контрагент|получатель|источник|покупатель|заказчик)(?:\s|$)/u.test(text);
|
||||||
|
if (!hasRoleWord) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasRankingWord = /(?:^|\s)(?:главн[а-яёa-z0-9_-]*|крупнейш[а-яёa-z0-9_-]*|основн[а-яёa-z0-9_-]*|топ(?:-\d+)?)(?:\s|$)/u.test(text);
|
||||||
|
const hasRoleOnlyExplanation = /(?:^|\s)(?:клиент|поставщик)(?:\s|$).{0,40}(?:^|\s)как(?:\s|$).{0,30}(?:^|\s)рол/u.test(text);
|
||||||
|
const endsWithDanglingPreposition = /(?:^|\s)(?:в|за|по|на|от|для|к)$/u.test(text);
|
||||||
|
return hasRoleOnlyExplanation || (hasRankingWord && (endsWithDanglingPreposition || text.length <= 48));
|
||||||
|
}
|
||||||
function sameCounterpartyCandidate(left, right) {
|
function sameCounterpartyCandidate(left, right) {
|
||||||
return Boolean(left &&
|
return Boolean(left &&
|
||||||
right &&
|
right &&
|
||||||
|
|
@ -590,6 +604,9 @@ function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackT
|
||||||
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
|
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
|
||||||
const preferGroundedDiscoveryCounterparty = hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty);
|
const preferGroundedDiscoveryCounterparty = hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty);
|
||||||
const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString);
|
const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString);
|
||||||
|
if (!existingCounterparty) {
|
||||||
|
delete nextFilters.counterparty;
|
||||||
|
}
|
||||||
if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) {
|
if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) {
|
||||||
nextFilters.counterparty = counterparty;
|
nextFilters.counterparty = counterparty;
|
||||||
}
|
}
|
||||||
|
|
@ -761,7 +778,11 @@ function resolveFollowupTargetIntent(inventoryPurchaseDateVatBridge, selectedObj
|
||||||
}
|
}
|
||||||
function shouldUseNavigationTemporalCarryover(sourceIntentHint) {
|
function shouldUseNavigationTemporalCarryover(sourceIntentHint) {
|
||||||
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
|
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
|
||||||
return (normalizedIntent === "inventory_on_hand_as_of_date" ||
|
return (normalizedIntent === "customer_revenue_and_payments" ||
|
||||||
|
normalizedIntent === "supplier_payouts_profile" ||
|
||||||
|
normalizedIntent === "contract_usage_and_value" ||
|
||||||
|
normalizedIntent === "counterparty_activity_lifecycle" ||
|
||||||
|
normalizedIntent === "inventory_on_hand_as_of_date" ||
|
||||||
normalizedIntent === "inventory_supplier_stock_overlap_as_of_date" ||
|
normalizedIntent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
||||||
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ function valueFlowDirectionLabelRu(pilot) {
|
||||||
? "исходящих платежей/списаний"
|
? "исходящих платежей/списаний"
|
||||||
: "входящих денежных поступлений";
|
: "входящих денежных поступлений";
|
||||||
}
|
}
|
||||||
function valueFlowZeroResultConfirmedLine(pilot) {
|
function valueFlowZeroResultInsufficiencyLine(pilot) {
|
||||||
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +224,22 @@ function valueFlowZeroResultConfirmedLine(pilot) {
|
||||||
const period = explicitDateScope(pilot);
|
const period = explicitDateScope(pilot);
|
||||||
const organizationPart = organization ? ` по организации ${organization}` : "";
|
const organizationPart = organization ? ` по организации ${organization}` : "";
|
||||||
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
|
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
|
||||||
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${valueFlowDirectionLabelRu(pilot)} не найдено.`;
|
const direction = valueFlowDirectionLabelRu(pilot);
|
||||||
|
return `Точную сумму ${direction} по контрагенту ${counterparty}${organizationPart}${periodPart} не подтверждаю: в выполненной проверке 1С строки ${direction} не найдены; это не доказывает отсутствие операций вне доступного банковского контура.`;
|
||||||
|
}
|
||||||
|
function valueFlowZeroResultCheckedLine(pilot) {
|
||||||
|
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const counterparty = firstEntityCandidate(pilot);
|
||||||
|
if (!counterparty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const organization = explicitOrganizationScope(pilot);
|
||||||
|
const period = explicitDateScope(pilot);
|
||||||
|
const organizationPart = organization ? ` по организации ${organization}` : "";
|
||||||
|
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
|
||||||
|
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart} строки ${valueFlowDirectionLabelRu(pilot)} не найдено.`;
|
||||||
}
|
}
|
||||||
function valueFlowZeroResultUnknownLine(pilot) {
|
function valueFlowZeroResultUnknownLine(pilot) {
|
||||||
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
||||||
|
|
@ -239,11 +254,11 @@ function valueFlowZeroResultUnknownLine(pilot) {
|
||||||
return `Это не доказывает отсутствие операций с контрагентом ${counterparty}${periodPart} или вне доступного банковского контура.`;
|
return `Это не доказывает отсутствие операций с контрагентом ${counterparty}${periodPart} или вне доступного банковского контура.`;
|
||||||
}
|
}
|
||||||
function valueFlowZeroResultHeadline(pilot) {
|
function valueFlowZeroResultHeadline(pilot) {
|
||||||
const confirmedLine = valueFlowZeroResultConfirmedLine(pilot);
|
const insufficiencyLine = valueFlowZeroResultInsufficiencyLine(pilot);
|
||||||
if (!confirmedLine) {
|
if (!insufficiencyLine) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return confirmedLine;
|
return insufficiencyLine;
|
||||||
}
|
}
|
||||||
function hasAllTimeScope(pilot) {
|
function hasAllTimeScope(pilot) {
|
||||||
return (dryRunHasAxis(pilot, "all_time_scope") ||
|
return (dryRunHasAxis(pilot, "all_time_scope") ||
|
||||||
|
|
@ -938,6 +953,12 @@ function nextStepFor(mode, pilot) {
|
||||||
}
|
}
|
||||||
return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном.";
|
return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном.";
|
||||||
}
|
}
|
||||||
|
if (mode === "checked_sources_only" && hasExecutedZeroValueFlowRows(pilot)) {
|
||||||
|
const flowLabel = pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1"
|
||||||
|
? "исходящие платежи/списания"
|
||||||
|
: "входящие денежные поступления";
|
||||||
|
return `Можно следующим шагом проверить ${flowLabel} по этому контрагенту в соседних периодах, по договорам или документам, либо собрать двусторонний денежный поток, чтобы отделить отсутствие строк в текущем срезе от операций вне проверенного окна.`;
|
||||||
|
}
|
||||||
if (mode === "confirmed_with_bounded_inference" && isBusinessOverviewPilot(pilot)) {
|
if (mode === "confirmed_with_bounded_inference" && isBusinessOverviewPilot(pilot)) {
|
||||||
return pilot.derived_business_overview
|
return pilot.derived_business_overview
|
||||||
? businessOverviewNextStepLine(pilot.derived_business_overview)
|
? businessOverviewNextStepLine(pilot.derived_business_overview)
|
||||||
|
|
@ -1840,8 +1861,6 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||||
? [derivedValueLine]
|
? [derivedValueLine]
|
||||||
: derivedValueLine
|
: derivedValueLine
|
||||||
? [derivedValueLine, ...monthlyConfirmedLines]
|
? [derivedValueLine, ...monthlyConfirmedLines]
|
||||||
: valueFlowZeroResultConfirmedLine(pilot)
|
|
||||||
? [valueFlowZeroResultConfirmedLine(pilot)]
|
|
||||||
: derivedEntityResolutionLine
|
: derivedEntityResolutionLine
|
||||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||||
: derivedMetadataLine
|
: derivedMetadataLine
|
||||||
|
|
@ -1853,7 +1872,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||||
? pilot.derived_metadata_surface.available_fields.length > 0
|
? pilot.derived_metadata_surface.available_fields.length > 0
|
||||||
? userFacingUnknowns(pilot.evidence.unknown_facts)
|
? userFacingUnknowns(pilot.evidence.unknown_facts)
|
||||||
: ["Детальный список полей этих объектов этим шагом не получен."]
|
: ["Детальный список полей этих объектов этим шагом не получен."]
|
||||||
: appendValueFlowZeroResultUnknown(rankedValueFlowUnknownLines(pilot), pilot);
|
: appendValueFlowZeroResultUnknown(valueFlowZeroResultCheckedLine(pilot)
|
||||||
|
? [valueFlowZeroResultCheckedLine(pilot), ...rankedValueFlowUnknownLines(pilot)]
|
||||||
|
: rankedValueFlowUnknownLines(pilot), pilot);
|
||||||
return {
|
return {
|
||||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = void 0;
|
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = void 0;
|
||||||
exports.buildAssistantMcpDiscoveryDataNeedGraph = buildAssistantMcpDiscoveryDataNeedGraph;
|
exports.buildAssistantMcpDiscoveryDataNeedGraph = buildAssistantMcpDiscoveryDataNeedGraph;
|
||||||
|
const addressTextRepair_1 = require("./addressTextRepair");
|
||||||
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = "assistant_data_need_graph_v1";
|
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = "assistant_data_need_graph_v1";
|
||||||
function toNonEmptyString(value) {
|
function toNonEmptyString(value) {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
|
|
@ -203,6 +204,28 @@ function hasDetailedBusinessOverviewMoneyBreakdownHint(rawUtterance) {
|
||||||
}
|
}
|
||||||
return /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*|\u043f\u043e\u0434\u0440\u043e\u0431\p{L}*|\u0440\u0430\u0437\u0431\u0435\p{L}*|\u0440\u0430\u0437\u0432\u0435\u0440\u043d\p{L}*|\u043a\u0442\u043e\s+\u0433\u043b\u0430\u0432\p{L}*|\u0433\u043b\u0430\u0432\p{L}*\s+\u043a\u043b\u0438\u0435\u043d\p{L}*|\u0433\u043b\u0430\u0432\p{L}*\s+\u043f\u043e\u0441\u0442\u0430\u0432\p{L}*|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\p{L}*\s+\u043f\u043e\u0442\u043e\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0432\u0445\u043e\u0434\p{L}*[\s\S]{0,80}\u0438\u0441\u0445\u043e\u0434\p{L}*|detail|breakdown|drill\s*down|main\s+customer|main\s+supplier)/iu.test(rawUtterance);
|
return /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*|\u043f\u043e\u0434\u0440\u043e\u0431\p{L}*|\u0440\u0430\u0437\u0431\u0435\p{L}*|\u0440\u0430\u0437\u0432\u0435\u0440\u043d\p{L}*|\u043a\u0442\u043e\s+\u0433\u043b\u0430\u0432\p{L}*|\u0433\u043b\u0430\u0432\p{L}*\s+\u043a\u043b\u0438\u0435\u043d\p{L}*|\u0433\u043b\u0430\u0432\p{L}*\s+\u043f\u043e\u0441\u0442\u0430\u0432\p{L}*|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\p{L}*\s+\u043f\u043e\u0442\u043e\p{L}*|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0432\u0445\u043e\u0434\p{L}*[\s\S]{0,80}\u0438\u0441\u0445\u043e\u0434\p{L}*|detail|breakdown|drill\s*down|main\s+customer|main\s+supplier)/iu.test(rawUtterance);
|
||||||
}
|
}
|
||||||
|
function hasWideBusinessOverviewSurfaceHint(rawUtterance) {
|
||||||
|
if (!rawUtterance) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const textVariants = Array.from(new Set([rawUtterance, (0, addressTextRepair_1.normalizeRussianComparableText)(rawUtterance)]));
|
||||||
|
if (textVariants.some((text) => /(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0434\u0435\u043d\p{L}*)/iu.test(text))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasBroadCue = textVariants.some((text) => /(?:\u0431\u0438\u0437\u043d\u0435\u0441[-\s]*\u043e\u0431\u0437\u043e\u0440|\u043e\u0431\u0437\u043e\u0440\s+\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\u0437\u0440\u043e\u0441\u043b\p{L}*\s+\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\p{L}*\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0446\u0435\u043d\p{L}*\s+\u0431\u0438\u0437\u043d\u0435\u0441|business\s+overview|business\s+review|company\s+overview)/iu.test(text));
|
||||||
|
if (!hasBroadCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const surfaceMarkers = [
|
||||||
|
/(?:\u043d\u0434\u0441|vat|\u043d\u0430\u043b\u043e\u0433\p{L}*)/iu,
|
||||||
|
/(?:\u0434\u043e\u043b\u0433\p{L}*|\u0437\u0430\u0434\u043e\u043b\u0436\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440)/iu,
|
||||||
|
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442\u043a|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440\p{L}*)/iu,
|
||||||
|
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b)/iu,
|
||||||
|
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b)/iu,
|
||||||
|
/(?:\u0447\u0442\u043e\s+(?:\u043f\u043e\u043a\u0430\s+)?\u043d\u0435\u043b\u044c\u0437\u044f\s+\u0443\u0442\u0432\u0435\u0440\u0436\u0434|\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d|\u0433\u0440\u0430\u043d\u0438\u0446\p{L}*)/iu
|
||||||
|
];
|
||||||
|
return textVariants.some((text) => surfaceMarkers.filter((marker) => marker.test(text)).length >= 3);
|
||||||
|
}
|
||||||
function supportsOrganizationScopedOpenTotal(action) {
|
function supportsOrganizationScopedOpenTotal(action) {
|
||||||
return action === "turnover" || action === "payout";
|
return action === "turnover" || action === "payout";
|
||||||
}
|
}
|
||||||
|
|
@ -421,7 +444,8 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
||||||
const rawAction = lower(turnMeaning?.asked_action_family);
|
const rawAction = lower(turnMeaning?.asked_action_family);
|
||||||
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
||||||
const rawUtterance = lower(input.rawUtterance);
|
const rawUtterance = lower(input.rawUtterance);
|
||||||
const rawQuestionSignal = lower([input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" "));
|
const rawQuestionSource = [input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" ");
|
||||||
|
const rawQuestionSignal = lower(rawQuestionSource);
|
||||||
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
|
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
|
||||||
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
|
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
|
||||||
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
||||||
|
|
@ -439,10 +463,13 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
||||||
});
|
});
|
||||||
const oneSidedValueAction = inferredOpenScopeOneSidedValueAction(rawQuestionSignal);
|
const oneSidedValueAction = inferredOpenScopeOneSidedValueAction(rawQuestionSignal);
|
||||||
const detailedBusinessOverviewMoneyBreakdownHint = hasDetailedBusinessOverviewMoneyBreakdownHint(rawQuestionSignal);
|
const detailedBusinessOverviewMoneyBreakdownHint = hasDetailedBusinessOverviewMoneyBreakdownHint(rawQuestionSignal);
|
||||||
|
const wideBusinessOverviewSurfaceHint = hasWideBusinessOverviewSurfaceHint(rawQuestionSource);
|
||||||
const oneSidedBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" &&
|
const oneSidedBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" &&
|
||||||
Boolean(oneSidedValueAction) &&
|
Boolean(oneSidedValueAction) &&
|
||||||
!detailedBusinessOverviewMoneyBreakdownHint;
|
!detailedBusinessOverviewMoneyBreakdownHint;
|
||||||
const bidirectionalBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" && hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
|
const bidirectionalBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" &&
|
||||||
|
hasIncomingVsOutgoingComparisonHint(rawQuestionSignal) &&
|
||||||
|
!wideBusinessOverviewSurfaceHint;
|
||||||
const oneSidedValueFlowActionOverride = initialBusinessFactFamily === "value_flow" &&
|
const oneSidedValueFlowActionOverride = initialBusinessFactFamily === "value_flow" &&
|
||||||
Boolean(oneSidedValueAction) &&
|
Boolean(oneSidedValueAction) &&
|
||||||
(rawAction === "counterparty_value_or_turnover" || rawAction === "counterparty_value");
|
(rawAction === "counterparty_value_or_turnover" || rawAction === "counterparty_value");
|
||||||
|
|
@ -574,6 +601,9 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
||||||
if (businessFactFamily === "business_overview" && !explicitDateScope) {
|
if (businessFactFamily === "business_overview" && !explicitDateScope) {
|
||||||
pushReason(reasonCodes, "data_need_graph_business_overview_defaults_to_all_time_scope");
|
pushReason(reasonCodes, "data_need_graph_business_overview_defaults_to_all_time_scope");
|
||||||
}
|
}
|
||||||
|
if (initialBusinessFactFamily === "business_overview" && wideBusinessOverviewSurfaceHint) {
|
||||||
|
pushReason(reasonCodes, "data_need_graph_business_overview_wide_surface_preserved");
|
||||||
|
}
|
||||||
if (directBusinessOverviewMoneyAnswerHint) {
|
if (directBusinessOverviewMoneyAnswerHint) {
|
||||||
pushReason(reasonCodes, "data_need_graph_business_overview_direct_money_answer");
|
pushReason(reasonCodes, "data_need_graph_business_overview_direct_money_answer");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2209,9 +2209,28 @@ function deriveBusinessOverviewContractUsageProfile(result, periodScope) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function deriveValueFlow(result, counterparty, periodScope, direction, aggregationAxis) {
|
function deriveValueFlow(result, counterparty, periodScope, direction, aggregationAxis) {
|
||||||
if (!result || result.error || result.matched_rows <= 0) {
|
if (!result || result.error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (result.matched_rows <= 0) {
|
||||||
|
return {
|
||||||
|
value_flow_direction: direction,
|
||||||
|
counterparty,
|
||||||
|
period_scope: periodScope,
|
||||||
|
aggregation_axis: aggregationAxis,
|
||||||
|
rows_matched: 0,
|
||||||
|
rows_with_amount: 0,
|
||||||
|
total_amount: 0,
|
||||||
|
total_amount_human_ru: formatAmountHumanRu(0),
|
||||||
|
first_movement_date: null,
|
||||||
|
latest_movement_date: null,
|
||||||
|
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
|
||||||
|
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
|
||||||
|
period_chunking_granularity: result.period_chunking_granularity,
|
||||||
|
monthly_breakdown: [],
|
||||||
|
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
|
||||||
|
};
|
||||||
|
}
|
||||||
let totalAmount = 0;
|
let totalAmount = 0;
|
||||||
let rowsWithAmount = 0;
|
let rowsWithAmount = 0;
|
||||||
for (const row of result.rows) {
|
for (const row of result.rows) {
|
||||||
|
|
@ -3926,16 +3945,30 @@ function buildMovementConfirmedFacts(result, counterparty, periodScope) {
|
||||||
return [`В 1С найдены строки движений${checkedCounterpartySuffixRu(counterparty)}${checkedPeriodSuffixRu(periodScope)}.`];
|
return [`В 1С найдены строки движений${checkedCounterpartySuffixRu(counterparty)}${checkedPeriodSuffixRu(periodScope)}.`];
|
||||||
}
|
}
|
||||||
function buildValueFlowConfirmedFacts(result, counterparty, direction) {
|
function buildValueFlowConfirmedFacts(result, counterparty, direction) {
|
||||||
if (result.error || result.matched_rows <= 0) {
|
if (result.error) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (direction === "outgoing_supplier_payout") {
|
if (direction === "outgoing_supplier_payout") {
|
||||||
|
if (result.matched_rows <= 0) {
|
||||||
|
return [
|
||||||
|
counterparty
|
||||||
|
? `1C supplier-payout rows were checked for counterparty ${counterparty}: not_found`
|
||||||
|
: "1C supplier-payout rows were checked for the requested counterparty scope: not_found"
|
||||||
|
];
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
counterparty
|
counterparty
|
||||||
? `1C supplier-payout rows were found for counterparty ${counterparty}`
|
? `1C supplier-payout rows were found for counterparty ${counterparty}`
|
||||||
: "1C supplier-payout rows were found for the requested counterparty scope"
|
: "1C supplier-payout rows were found for the requested counterparty scope"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if (result.matched_rows <= 0) {
|
||||||
|
return [
|
||||||
|
counterparty
|
||||||
|
? `1C value-flow rows were checked for counterparty ${counterparty}: not_found`
|
||||||
|
: "1C value-flow rows were checked for the requested counterparty scope: not_found"
|
||||||
|
];
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
counterparty
|
counterparty
|
||||||
? `1C value-flow rows were found for counterparty ${counterparty}`
|
? `1C value-flow rows were found for counterparty ${counterparty}`
|
||||||
|
|
@ -4115,6 +4148,12 @@ function buildValueFlowInferredFacts(derived) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const facts = [];
|
const facts = [];
|
||||||
|
if (derived.rows_matched <= 0) {
|
||||||
|
facts.push(derived.value_flow_direction === "outgoing_supplier_payout"
|
||||||
|
? "Counterparty supplier-payout not_found result was treated as zero only inside the scoped checked 1C result set"
|
||||||
|
: "Counterparty incoming value-flow not_found result was treated as zero only inside the scoped checked 1C result set");
|
||||||
|
return facts;
|
||||||
|
}
|
||||||
if (derived.value_flow_direction === "outgoing_supplier_payout") {
|
if (derived.value_flow_direction === "outgoing_supplier_payout") {
|
||||||
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
|
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -261,13 +261,14 @@ function resolveAssistantMcpDiscoveryEvidence(input) {
|
||||||
const rowsMatched = probeRowsMatched(probeResults);
|
const rowsMatched = probeRowsMatched(probeResults);
|
||||||
const rowsReceived = probeRowsReceived(probeResults);
|
const rowsReceived = probeRowsReceived(probeResults);
|
||||||
const bypassDetected = hasProbeBypass(input.plan, probeResults);
|
const bypassDetected = hasProbeBypass(input.plan, probeResults);
|
||||||
|
const confirmedNoMatchFact = confirmedFacts.some((fact) => /\bnot_found\b/iu.test(String(fact ?? "")));
|
||||||
if (bypassDetected) {
|
if (bypassDetected) {
|
||||||
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
|
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
|
||||||
}
|
}
|
||||||
if (input.plan.plan_status !== "allowed") {
|
if (input.plan.plan_status !== "allowed") {
|
||||||
pushReason(reasonCodes, "plan_not_allowed_by_runtime");
|
pushReason(reasonCodes, "plan_not_allowed_by_runtime");
|
||||||
}
|
}
|
||||||
if (confirmedFacts.length > 0 && rowsMatched <= 0) {
|
if (confirmedFacts.length > 0 && rowsMatched <= 0 && !confirmedNoMatchFact) {
|
||||||
pushReason(reasonCodes, "confirmed_facts_without_matched_probe_rows");
|
pushReason(reasonCodes, "confirmed_facts_without_matched_probe_rows");
|
||||||
}
|
}
|
||||||
if (!sourceRowsSummary && rowsReceived > 0) {
|
if (!sourceRowsSummary && rowsReceived > 0) {
|
||||||
|
|
@ -287,6 +288,12 @@ function resolveAssistantMcpDiscoveryEvidence(input) {
|
||||||
answerPermission = "confirmed_answer";
|
answerPermission = "confirmed_answer";
|
||||||
pushReason(reasonCodes, "confirmed_facts_with_allowed_mcp_evidence");
|
pushReason(reasonCodes, "confirmed_facts_with_allowed_mcp_evidence");
|
||||||
}
|
}
|
||||||
|
else if (confirmedFacts.length > 0 && confirmedNoMatchFact && rowsMatched <= 0 && sourceRowsSummary) {
|
||||||
|
evidenceStatus = "confirmed";
|
||||||
|
coverageStatus = "full";
|
||||||
|
answerPermission = "confirmed_answer";
|
||||||
|
pushReason(reasonCodes, "confirmed_no_match_fact_with_allowed_mcp_evidence");
|
||||||
|
}
|
||||||
else if (inferredFacts.length > 0 && rowsReceived > 0) {
|
else if (inferredFacts.length > 0 && rowsReceived > 0) {
|
||||||
evidenceStatus = "inferred_only";
|
evidenceStatus = "inferred_only";
|
||||||
coverageStatus = "partial";
|
coverageStatus = "partial";
|
||||||
|
|
|
||||||
|
|
@ -211,9 +211,15 @@ function localizeLine(value) {
|
||||||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
|
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
|
||||||
}
|
}
|
||||||
|
if (/^1C value-flow rows were checked for the requested counterparty scope: not_found$/i.test(value)) {
|
||||||
|
return "В 1С проверены входящие денежные строки в запрошенном срезе: строки не найдены.";
|
||||||
|
}
|
||||||
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
|
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
|
||||||
}
|
}
|
||||||
|
if (/^1C supplier-payout rows were checked for the requested counterparty scope: not_found$/i.test(value)) {
|
||||||
|
return "В 1С проверены исходящие платежи/списания в запрошенном срезе: строки не найдены.";
|
||||||
|
}
|
||||||
const openScopeBidirectionalMatch = value.match(/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i);
|
const openScopeBidirectionalMatch = value.match(/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i);
|
||||||
if (openScopeBidirectionalMatch) {
|
if (openScopeBidirectionalMatch) {
|
||||||
const incoming = openScopeBidirectionalMatch[1] === "found"
|
const incoming = openScopeBidirectionalMatch[1] === "found"
|
||||||
|
|
@ -238,6 +244,10 @@ function localizeLine(value) {
|
||||||
if (valueFlowMatch) {
|
if (valueFlowMatch) {
|
||||||
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
|
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
|
||||||
}
|
}
|
||||||
|
const valueFlowNotFoundMatch = value.match(/^1C value-flow rows were checked for counterparty\s+(.+): not_found$/i);
|
||||||
|
if (valueFlowNotFoundMatch) {
|
||||||
|
return `В 1С проверены входящие денежные строки по контрагенту ${valueFlowNotFoundMatch[1]}: строки не найдены.`;
|
||||||
|
}
|
||||||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
|
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
|
||||||
}
|
}
|
||||||
|
|
@ -259,6 +269,10 @@ function localizeLine(value) {
|
||||||
if (supplierPayoutMatch) {
|
if (supplierPayoutMatch) {
|
||||||
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`;
|
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`;
|
||||||
}
|
}
|
||||||
|
const supplierPayoutNotFoundMatch = value.match(/^1C supplier-payout rows were checked for counterparty\s+(.+): not_found$/i);
|
||||||
|
if (supplierPayoutNotFoundMatch) {
|
||||||
|
return `В 1С проверены исходящие платежи/списания по контрагенту ${supplierPayoutNotFoundMatch[1]}: строки не найдены.`;
|
||||||
|
}
|
||||||
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
|
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
|
||||||
}
|
}
|
||||||
|
|
@ -292,6 +306,12 @@ function localizeLine(value) {
|
||||||
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
|
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
|
||||||
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
||||||
}
|
}
|
||||||
|
if (/^Counterparty supplier-payout not_found result was treated as zero only inside the scoped checked 1C result set$/i.test(value)) {
|
||||||
|
return "Нулевую сумму исходящих платежей можно трактовать только внутри этого проверенного среза 1С, где строки списаний не найдены.";
|
||||||
|
}
|
||||||
|
if (/^Counterparty incoming value-flow not_found result was treated as zero only inside the scoped checked 1C result set$/i.test(value)) {
|
||||||
|
return "Нулевую сумму входящих поступлений можно трактовать только внутри этого проверенного среза 1С, где строки поступлений не найдены.";
|
||||||
|
}
|
||||||
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
||||||
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
|
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
|
||||||
}
|
}
|
||||||
|
|
@ -703,6 +723,7 @@ function businessOverviewOrganizationScopeLabel(value) {
|
||||||
}
|
}
|
||||||
const comparable = compactComparable(text);
|
const comparable = compactComparable(text);
|
||||||
if (/^(?:с|без)\s+разбивк/.test(comparable) ||
|
if (/^(?:с|без)\s+разбивк/.test(comparable) ||
|
||||||
|
/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(comparable) ||
|
||||||
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)) {
|
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -1158,7 +1179,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
outgoingAmount ? `исходящие ${outgoingAmount}` : null,
|
outgoingAmount ? `исходящие ${outgoingAmount}` : null,
|
||||||
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
|
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
|
||||||
].filter((value) => Boolean(value));
|
].filter((value) => Boolean(value));
|
||||||
lines.push(`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, не аудиторское заключение и не подтвержденная чистая прибыль.`);
|
lines.push(`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, а не финансовый аудит и не подтверждение чистой прибыли.`);
|
||||||
lines.push("Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж.");
|
lines.push("Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж.");
|
||||||
lines.push("Что видно в ограниченном денежном срезе:");
|
lines.push("Что видно в ограниченном денежном срезе:");
|
||||||
if (incomingAmount) {
|
if (incomingAmount) {
|
||||||
|
|
@ -1193,7 +1214,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
lines.push(`- ${localizeLine(debtLine)}`);
|
lines.push(`- ${localizeLine(debtLine)}`);
|
||||||
}
|
}
|
||||||
if (!limitLine) {
|
if (!limitLine) {
|
||||||
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
|
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, а не финансовый аудит и не подтверждение чистой прибыли.");
|
||||||
}
|
}
|
||||||
const missingOverviewFamilies = [];
|
const missingOverviewFamilies = [];
|
||||||
if (!taxLine) {
|
if (!taxLine) {
|
||||||
|
|
|
||||||
|
|
@ -364,6 +364,51 @@ function hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarificati
|
||||||
const boundedMovementAnswerAllowed = bridge?.user_facing_response_allowed === true && bridge?.business_fact_answer_allowed === true;
|
const boundedMovementAnswerAllowed = bridge?.user_facing_response_allowed === true && bridge?.business_fact_answer_allowed === true;
|
||||||
return asksForMovements && staleInventoryItemClarification && mcpExecutionPerformed && boundedMovementAnswerAllowed;
|
return asksForMovements && staleInventoryItemClarification && mcpExecutionPerformed && boundedMovementAnswerAllowed;
|
||||||
}
|
}
|
||||||
|
function hasBusinessOverviewBoundaryCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate) {
|
||||||
|
if (!hasCurrentClarificationRequiredAddressReply(input)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (candidate.candidate_status !== "clarification_candidate" && candidate.candidate_status !== "ready_for_guarded_use") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (candidate.reply_text && hasInternalMechanics(candidate.reply_text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const graph = readDiscoveryDataNeedGraph(entryPoint);
|
||||||
|
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||||
|
const selectedChainId = readDiscoverySelectedChainId(entryPoint);
|
||||||
|
const discoveryFamily = toNonEmptyString(graph?.business_fact_family);
|
||||||
|
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
|
||||||
|
const separateCounterpartyCandidates = readStringArray(turnMeaning?.business_overview_separate_entity_candidates);
|
||||||
|
const hasPreviousCounterpartyBundle = Boolean(toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle) ||
|
||||||
|
toRecordObject(turnMeaning?.previous_counterparty_document_bundle));
|
||||||
|
const asksForBoundarySummary = Boolean((selectedChainId === "business_overview" ||
|
||||||
|
discoveryFamily === "business_overview" ||
|
||||||
|
askedDomain === "business_overview") &&
|
||||||
|
(separateCounterpartyCandidates.length > 0 || hasPreviousCounterpartyBundle));
|
||||||
|
const truthAnswerShape = readTruthAnswerShape(input);
|
||||||
|
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||||
|
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||||
|
const capabilityId = toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
|
||||||
|
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id) ??
|
||||||
|
toNonEmptyString(truthAnswerShape?.capability_contract_id);
|
||||||
|
const missingFilters = [
|
||||||
|
...readStringArray(input.addressRuntimeMeta?.missing_required_filters),
|
||||||
|
...readStringArray(input.addressRuntimeMeta?.missing_anchors),
|
||||||
|
...readStringArray(toRecordObject(input.addressRuntimeMeta?.capability_binding_contract)?.missing_anchors)
|
||||||
|
];
|
||||||
|
const staleInventoryItemClarification = Boolean(missingFilters.includes("item") &&
|
||||||
|
(detectedIntent?.startsWith("inventory_") ||
|
||||||
|
selectedRecipe?.includes("inventory_") ||
|
||||||
|
capabilityId?.includes("inventory_")));
|
||||||
|
return asksForBoundarySummary && staleInventoryItemClarification;
|
||||||
|
}
|
||||||
function hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate) {
|
function hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate) {
|
||||||
if (!hasCurrentClarificationRequiredAddressReply(input)) {
|
if (!hasCurrentClarificationRequiredAddressReply(input)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -570,6 +615,53 @@ function hasExactBankOperationsAddressReply(input, entryPoint) {
|
||||||
routeMode === "exact" ||
|
routeMode === "exact" ||
|
||||||
hasFullConfirmedTruth(input));
|
hasFullConfirmedTruth(input));
|
||||||
}
|
}
|
||||||
|
function extractExplicitDateScopeYear(value) {
|
||||||
|
const source = toNonEmptyString(value);
|
||||||
|
if (!source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = source.match(/\b((?:19|20)\d{2})\b/u);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
function extractDocumentReplyDateYears(value) {
|
||||||
|
const source = String(value ?? "");
|
||||||
|
if (!source) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const years = new Set();
|
||||||
|
for (const match of source.matchAll(/\b((?:19|20)\d{2})-\d{2}-\d{2}(?:T|\b)/gu)) {
|
||||||
|
years.add(match[1]);
|
||||||
|
}
|
||||||
|
for (const match of source.matchAll(/\b\d{1,2}\.\d{1,2}\.((?:19|20)\d{2})\b/gu)) {
|
||||||
|
years.add(match[1]);
|
||||||
|
}
|
||||||
|
return [...years];
|
||||||
|
}
|
||||||
|
function hasDocumentListTemporalConflictWithDiscovery(input, entryPoint) {
|
||||||
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||||
|
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||||
|
const isDocumentIntent = detectedIntent === "list_documents_by_counterparty" || detectedIntent === "list_documents_by_contract";
|
||||||
|
const isDocumentRecipe = selectedRecipe === "address_documents_by_counterparty_v1" ||
|
||||||
|
selectedRecipe === "address_documents_by_contract_v1";
|
||||||
|
if (!isDocumentIntent || !isDocumentRecipe) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||||
|
const expectedYear = extractExplicitDateScopeYear(turnMeaning?.explicit_date_scope);
|
||||||
|
if (!expectedYear) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const filters = toRecordObject(input.addressRuntimeMeta?.extracted_filters);
|
||||||
|
const periodFrom = toNonEmptyString(filters?.period_from);
|
||||||
|
const periodTo = toNonEmptyString(filters?.period_to);
|
||||||
|
const hasMatchingPeriod = Boolean(periodFrom?.startsWith(`${expectedYear}-`)) && Boolean(periodTo?.startsWith(`${expectedYear}-`));
|
||||||
|
const replyDateYears = extractDocumentReplyDateYears(input.currentReply);
|
||||||
|
const hasOutOfScopeReplyDates = replyDateYears.some((year) => year !== expectedYear);
|
||||||
|
return hasOutOfScopeReplyDates || (!hasMatchingPeriod && replyDateYears.length > 0);
|
||||||
|
}
|
||||||
function hasExactDocumentListAddressReply(input, entryPoint) {
|
function hasExactDocumentListAddressReply(input, entryPoint) {
|
||||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -923,7 +1015,8 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint);
|
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint);
|
||||||
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint);
|
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint);
|
||||||
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
||||||
const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint);
|
const documentListTemporalConflictWithDiscovery = hasDocumentListTemporalConflictWithDiscovery(input, entryPoint);
|
||||||
|
const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint) && !documentListTemporalConflictWithDiscovery;
|
||||||
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
|
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
|
||||||
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(input, entryPoint);
|
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(input, entryPoint);
|
||||||
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
||||||
|
|
@ -934,16 +1027,20 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
const discoveryClarificationCandidatePriority = hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(input, entryPoint, candidate);
|
const discoveryClarificationCandidatePriority = hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(input, entryPoint, candidate);
|
||||||
const groundedValueFlowCandidatePriority = hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate);
|
const groundedValueFlowCandidatePriority = hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate);
|
||||||
const groundedMovementCandidatePriority = hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
|
const groundedMovementCandidatePriority = hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
|
||||||
|
const businessOverviewBoundaryCandidatePriority = hasBusinessOverviewBoundaryCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
|
||||||
const groundedValueFlowRankingCandidatePriority = hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate);
|
const groundedValueFlowRankingCandidatePriority = hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate);
|
||||||
const currentClarificationProtectsCurrentReply = currentClarificationRequiredAddressReply &&
|
const currentClarificationProtectsCurrentReply = currentClarificationRequiredAddressReply &&
|
||||||
!discoveryClarificationCandidatePriority &&
|
!discoveryClarificationCandidatePriority &&
|
||||||
!groundedValueFlowCandidatePriority &&
|
!groundedValueFlowCandidatePriority &&
|
||||||
!groundedMovementCandidatePriority &&
|
!groundedMovementCandidatePriority &&
|
||||||
|
!businessOverviewBoundaryCandidatePriority &&
|
||||||
!groundedValueFlowRankingCandidatePriority;
|
!groundedValueFlowRankingCandidatePriority;
|
||||||
const alignedFactualAddressReplyProtectsCurrent = alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
const alignedFactualAddressReplyProtectsCurrent = alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||||
const matchedFactualAddressContinuationTargetProtectsCurrent = matchedFactualAddressContinuationTarget && !groundedValueFlowRankingCandidatePriority;
|
const matchedFactualAddressContinuationTargetProtectsCurrent = matchedFactualAddressContinuationTarget &&
|
||||||
const fullConfirmedFactualAddressReplyProtectsCurrent = fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
!groundedValueFlowRankingCandidatePriority &&
|
||||||
const exactMatchedFactualAddressReplyProtectsCurrent = exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
!documentListTemporalConflictWithDiscovery;
|
||||||
|
const fullConfirmedFactualAddressReplyProtectsCurrent = fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||||
|
const exactMatchedFactualAddressReplyProtectsCurrent = exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||||
const exactBankOperationsProtectsCurrent = exactBankOperationsAddressReply &&
|
const exactBankOperationsProtectsCurrent = exactBankOperationsAddressReply &&
|
||||||
!semanticConflictWithDiscoveryTurnMeaning &&
|
!semanticConflictWithDiscoveryTurnMeaning &&
|
||||||
!valueFlowActionConflictWithDiscoveryTurnMeaning;
|
!valueFlowActionConflictWithDiscoveryTurnMeaning;
|
||||||
|
|
@ -977,6 +1074,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
|
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
|
||||||
}
|
}
|
||||||
|
if (documentListTemporalConflictWithDiscovery) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_document_list_temporal_conflict_allows_candidate_override");
|
||||||
|
}
|
||||||
if (currentClarificationProtectsCurrentReply) {
|
if (currentClarificationProtectsCurrentReply) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
||||||
}
|
}
|
||||||
|
|
@ -989,6 +1089,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
if (groundedMovementCandidatePriority) {
|
if (groundedMovementCandidatePriority) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_grounded_movement_candidate_priority_over_inventory_item_clarification");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_grounded_movement_candidate_priority_over_inventory_item_clarification");
|
||||||
}
|
}
|
||||||
|
if (businessOverviewBoundaryCandidatePriority) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_business_overview_boundary_candidate_priority_over_inventory_item_clarification");
|
||||||
|
}
|
||||||
if (groundedValueFlowRankingCandidatePriority) {
|
if (groundedValueFlowRankingCandidatePriority) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_grounded_value_flow_ranking_candidate_priority_over_current_exact_reply");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_grounded_value_flow_ranking_candidate_priority_over_current_exact_reply");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,10 @@ function isReferentialOrganizationPlaceholder(value) {
|
||||||
if (isNegativeCounterpartyScopePlaceholder(value)) {
|
if (isNegativeCounterpartyScopePlaceholder(value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const text = compactLower((0, addressTextRepair_1.repairAddressMojibakeText)(value));
|
||||||
|
if (/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return new Set([
|
return new Set([
|
||||||
"эта организация",
|
"эта организация",
|
||||||
"этой организации",
|
"этой организации",
|
||||||
|
|
@ -882,6 +886,10 @@ function hasBidirectionalValueFlowSignal(text) {
|
||||||
if (/(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0431\u0430\u043b\u0430\u043d\u0441\s+(?:\u043f\u043b\u0430\u0442|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436)|\u0432\u0437\u0430\u0438\u043c\u043e\u0440\u0430\u0441\u0447[\u0435\u0451]\u0442|\u043f\u043e\u043b\u0443\u0447\p{L}*.*(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*|(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*.*\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\u044f\u0449.*\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449.*\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u0440\u0438\u0448\p{L}*.*\u0443\u0448\p{L}*|\u0443\u0448\p{L}*.*\u043f\u0440\u0438\u0448\p{L}*|\u043f\u043b\u044e\u0441.*\u043c\u0438\u043d\u0443\u0441|\u043c\u0438\u043d\u0443\u0441.*\u043f\u043b\u044e\u0441|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text)) {
|
if (/(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0431\u0430\u043b\u0430\u043d\u0441\s+(?:\u043f\u043b\u0430\u0442|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436)|\u0432\u0437\u0430\u0438\u043c\u043e\u0440\u0430\u0441\u0447[\u0435\u0451]\u0442|\u043f\u043e\u043b\u0443\u0447\p{L}*.*(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*|(?:\u0437\u0430)?\u043f\u043b\u0430\u0442\p{L}*.*\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\u044f\u0449.*\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449.*\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u0440\u0438\u0448\p{L}*.*\u0443\u0448\p{L}*|\u0443\u0448\p{L}*.*\u043f\u0440\u0438\u0448\p{L}*|\u043f\u043b\u044e\u0441.*\u043c\u0438\u043d\u0443\u0441|\u043c\u0438\u043d\u0443\u0441.*\u043f\u043b\u044e\u0441|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (/(?:\u043e\u0431\u0449\p{L}*[\s\S]{0,60}\u0434\u0435\u043d\p{L}*[\s\S]{0,60}\u0441\u043c\u044b\u0441\u043b|\u0434\u0435\u043d\p{L}*[\s\S]{0,40}\u0441\u043c\u044b\u0441\u043b|overall[\s\S]{0,40}(?:cash|money)[\s\S]{0,40}(?:meaning|sense)|money[\s\S]{0,40}(?:meaning|sense))/iu.test(text) &&
|
||||||
|
hasPayoutSignal(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text);
|
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text);
|
||||||
}
|
}
|
||||||
function hasValueRankingSignal(text) {
|
function hasValueRankingSignal(text) {
|
||||||
|
|
@ -921,7 +929,7 @@ function normalizeLooseOrganizationAlias(value) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (isNegativeCounterpartyScopePlaceholder(text)) {
|
if (isReferentialOrganizationPlaceholder(text)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const normalized = text
|
const normalized = text
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,10 @@ function detectScopedCounterpartyEntity(text) {
|
||||||
if (!rawEntity) {
|
if (!rawEntity) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const entity = rawEntity.replace(/^["'«»]+|["'«»]+$/gu, "").trim();
|
const entity = rawEntity
|
||||||
|
.replace(/^["'«»]+|["'«»]+$/gu, "")
|
||||||
|
.replace(/^(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a(?:\u0443|\u0430|\u043e\u043c|\u0435)?|\u043a\u043b\u0438\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435)?|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435)?)\s+/iu, "")
|
||||||
|
.trim();
|
||||||
if (entity.length >= 2 && !ignored.has(entity)) {
|
if (entity.length >= 2 && !ignored.has(entity)) {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
@ -195,7 +198,8 @@ function detectCounterpartyBidirectionalValueFlowFamily(text) {
|
||||||
const hasNetCue = /(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|net\s+(?:flow|cash|payment)|cash\s+net)/iu.test(text);
|
const hasNetCue = /(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|net\s+(?:flow|cash|payment)|cash\s+net)/iu.test(text);
|
||||||
const hasIncomingCue = /(?:\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|received|incoming)/iu.test(text);
|
const hasIncomingCue = /(?:\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|received|incoming)/iu.test(text);
|
||||||
const hasOutgoingCue = /(?:\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0441\u043f\u0438\u0441\u0430\u043d\p{L}*|paid|outgoing|payment)/iu.test(text);
|
const hasOutgoingCue = /(?:\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0441\u043f\u0438\u0441\u0430\u043d\p{L}*|paid|outgoing|payment)/iu.test(text);
|
||||||
if (!(hasNetCue || (hasIncomingCue && hasOutgoingCue))) {
|
const hasOverallMoneyMeaningCue = /(?:\u043e\u0431\u0449\p{L}*[\s\S]{0,60}\u0434\u0435\u043d\p{L}*[\s\S]{0,60}\u0441\u043c\u044b\u0441\u043b|\u0434\u0435\u043d\p{L}*[\s\S]{0,40}\u0441\u043c\u044b\u0441\u043b|overall[\s\S]{0,40}(?:cash|money)[\s\S]{0,40}(?:meaning|sense)|money[\s\S]{0,40}(?:meaning|sense))/iu.test(text);
|
||||||
|
if (!(hasNetCue || (hasIncomingCue && hasOutgoingCue) || (hasOverallMoneyMeaningCue && hasOutgoingCue))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const entity = detectScopedCounterpartyEntity(text);
|
const entity = detectScopedCounterpartyEntity(text);
|
||||||
|
|
@ -409,6 +413,8 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
? null
|
? null
|
||||||
: detectBroadBusinessEvaluation(joinedText);
|
: detectBroadBusinessEvaluation(joinedText);
|
||||||
const explicitIntentCandidate = broadBusinessEvaluation?.family
|
const explicitIntentCandidate = broadBusinessEvaluation?.family
|
||||||
|
? null
|
||||||
|
: counterpartyBidirectionalValueFlow?.family
|
||||||
? null
|
? null
|
||||||
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||||
const unsupportedFamily = broadBusinessEvaluation?.family
|
const unsupportedFamily = broadBusinessEvaluation?.family
|
||||||
|
|
|
||||||
|
|
@ -2200,7 +2200,11 @@ function hasNomenclatureMarginRankingSignal(text: string): boolean {
|
||||||
/(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(
|
/(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue);
|
const hasInspectionCue =
|
||||||
|
/(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
|
function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,11 @@ function hasInventoryMarginRankingSignal(text: string): boolean {
|
||||||
/(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(
|
/(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(
|
||||||
normalized
|
normalized
|
||||||
);
|
);
|
||||||
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue);
|
const hasInspectionCue =
|
||||||
|
/(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(
|
||||||
|
normalized
|
||||||
|
);
|
||||||
|
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasInventoryOnHandSignal(text: string): boolean {
|
function hasInventoryOnHandSignal(text: string): boolean {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import type { AssistantConversationItem } from "../types/assistant";
|
import type { AssistantConversationItem } from "../types/assistant";
|
||||||
import type { AddressIntent } from "../types/addressQuery";
|
import type { AddressIntent } from "../types/addressQuery";
|
||||||
|
import { normalizeRussianComparableText } from "./addressTextRepair";
|
||||||
import {
|
import {
|
||||||
readAddressDebugCounterparty,
|
readAddressDebugCounterparty,
|
||||||
readAddressDebugItem,
|
readAddressDebugItem,
|
||||||
|
|
@ -150,6 +151,56 @@ function readNavigationDiscoveryCounterparty(debug: Record<string, unknown>): st
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readDiscoveryPilot(debug: Record<string, unknown>): Record<string, unknown> | null {
|
||||||
|
const entry = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
||||||
|
const bridge = toObject(entry?.bridge);
|
||||||
|
return toObject(bridge?.pilot);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCurrentDiscoveryCounterparty(debug: Record<string, unknown>): string | null {
|
||||||
|
const pilot = readDiscoveryPilot(debug);
|
||||||
|
const entityResolution = toObject(pilot?.derived_entity_resolution);
|
||||||
|
const bidirectionalValueFlow = toObject(pilot?.derived_bidirectional_value_flow);
|
||||||
|
const valueFlow = toObject(pilot?.derived_value_flow);
|
||||||
|
return (
|
||||||
|
toNonEmptyString(entityResolution?.resolved_entity) ??
|
||||||
|
toNonEmptyString(bidirectionalValueFlow?.counterparty) ??
|
||||||
|
toNonEmptyString(valueFlow?.counterparty) ??
|
||||||
|
readAddressDebugCounterparty(debug, toNonEmptyString) ??
|
||||||
|
readNavigationDiscoveryCounterparty(debug)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCounterpartyDiscoveryChain(value: string | null): boolean {
|
||||||
|
return (
|
||||||
|
value === "entity_resolution" ||
|
||||||
|
value === "value_flow" ||
|
||||||
|
value === "value_flow_comparison" ||
|
||||||
|
value === "document_evidence" ||
|
||||||
|
value === "movement_evidence" ||
|
||||||
|
value === "lifecycle"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function intentFromDiscoveryChain(value: string | null): AddressIntent {
|
||||||
|
if (value === "business_overview") {
|
||||||
|
return "business_overview";
|
||||||
|
}
|
||||||
|
if (value === "value_flow" || value === "value_flow_comparison") {
|
||||||
|
return "customer_revenue_and_payments";
|
||||||
|
}
|
||||||
|
if (value === "document_evidence") {
|
||||||
|
return "list_documents_by_counterparty";
|
||||||
|
}
|
||||||
|
if (value === "movement_evidence") {
|
||||||
|
return "bank_operations_by_counterparty";
|
||||||
|
}
|
||||||
|
if (value === "lifecycle") {
|
||||||
|
return "counterparty_activity_lifecycle";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
function toAddressFocusObjectType(value: unknown): AddressFocusObjectType {
|
function toAddressFocusObjectType(value: unknown): AddressFocusObjectType {
|
||||||
const normalized = toNonEmptyString(value);
|
const normalized = toNonEmptyString(value);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -346,6 +397,43 @@ function normalizeFilters(value: unknown): Record<string, unknown> {
|
||||||
return { ...record };
|
return { ...record };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isBusinessOverviewRoleCounterpartyFragment(value: unknown): boolean {
|
||||||
|
const text = normalizeRussianComparableText(value);
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasRoleWord = /(?:^|\s)(?:клиент|поставщик|контрагент|получатель|источник|покупатель|заказчик)(?:\s|$)/u.test(text);
|
||||||
|
if (!hasRoleWord) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasRankingWord = /(?:^|\s)(?:главн[а-яёa-z0-9_-]*|крупнейш[а-яёa-z0-9_-]*|основн[а-яёa-z0-9_-]*|топ(?:-\d+)?)(?:\s|$)/u.test(text);
|
||||||
|
const hasRoleOnlyExplanation = /(?:^|\s)(?:клиент|поставщик)(?:\s|$).{0,40}(?:^|\s)как(?:\s|$).{0,30}(?:^|\s)рол/u.test(text);
|
||||||
|
const endsWithDanglingPreposition = /(?:^|\s)(?:в|за|по|на|от|для|к)$/u.test(text);
|
||||||
|
return hasRoleOnlyExplanation || (hasRankingWord && (endsWithDanglingPreposition || text.length <= 48));
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSuppressBusinessOverviewCounterpartyFocus(
|
||||||
|
debug: Record<string, unknown>,
|
||||||
|
counterparty: unknown
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
toNonEmptyString(debug.mcp_discovery_selected_chain_id) === "business_overview" &&
|
||||||
|
debug.mcp_discovery_response_applied === true &&
|
||||||
|
isBusinessOverviewRoleCounterpartyFragment(counterparty)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeBusinessOverviewNavigationFilters(
|
||||||
|
filters: Record<string, unknown>,
|
||||||
|
debug: Record<string, unknown>
|
||||||
|
): Record<string, unknown> {
|
||||||
|
if (!shouldSuppressBusinessOverviewCounterpartyFocus(debug, filters.counterparty)) {
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
const { counterparty: _counterparty, ...rest } = filters;
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveNavigationAction(debug: Record<string, unknown>, hasFocusObject: boolean): AddressNavigationAction {
|
function resolveNavigationAction(debug: Record<string, unknown>, hasFocusObject: boolean): AddressNavigationAction {
|
||||||
const continuationContract = toObject(debug.dialog_continuation_contract_v2);
|
const continuationContract = toObject(debug.dialog_continuation_contract_v2);
|
||||||
const decision = toNonEmptyString(continuationContract?.decision);
|
const decision = toNonEmptyString(continuationContract?.decision);
|
||||||
|
|
@ -399,8 +487,7 @@ function readBusinessOverviewComparisonProofBundles(debug: Record<string, unknow
|
||||||
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
||||||
const turnInput = toObject(entryPoint?.turn_input);
|
const turnInput = toObject(entryPoint?.turn_input);
|
||||||
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
|
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
|
||||||
const bridge = toObject(entryPoint?.bridge);
|
const pilot = readDiscoveryPilot(debug);
|
||||||
const pilot = toObject(bridge?.pilot);
|
|
||||||
const counterpartyValueFlowBundle =
|
const counterpartyValueFlowBundle =
|
||||||
cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
|
cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
|
||||||
cloneRecord(pilot?.derived_bidirectional_value_flow);
|
cloneRecord(pilot?.derived_bidirectional_value_flow);
|
||||||
|
|
@ -421,12 +508,25 @@ function readBusinessOverviewComparisonProofBundles(debug: Record<string, unknow
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readCounterpartyValueFlowProofBundles(debug: Record<string, unknown>): AddressComparisonProofBundles | null {
|
||||||
|
const pilot = readDiscoveryPilot(debug);
|
||||||
|
const counterpartyValueFlowBundle = cloneRecord(pilot?.derived_bidirectional_value_flow);
|
||||||
|
const counterparty = toNonEmptyString(counterpartyValueFlowBundle?.counterparty);
|
||||||
|
if (!counterparty || !counterpartyValueFlowBundle) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
|
||||||
|
counterparty_document_bundle: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId: string, createdAt: string): AddressFocusObject | null {
|
function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId: string, createdAt: string): AddressFocusObject | null {
|
||||||
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
||||||
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
||||||
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
|
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
|
||||||
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
||||||
if (counterparty) {
|
if (counterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, counterparty)) {
|
||||||
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
||||||
}
|
}
|
||||||
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
|
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
|
||||||
|
|
@ -434,9 +534,8 @@ function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId:
|
||||||
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
|
if (isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true) {
|
||||||
const counterparty =
|
const counterparty = readCurrentDiscoveryCounterparty(debug);
|
||||||
readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
|
||||||
if (counterparty) {
|
if (counterparty) {
|
||||||
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
||||||
}
|
}
|
||||||
|
|
@ -659,27 +758,28 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
||||||
}
|
}
|
||||||
const debug = item.debug as unknown as Record<string, unknown>;
|
const debug = item.debug as unknown as Record<string, unknown>;
|
||||||
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
||||||
const discoveryIntent =
|
|
||||||
selectedDiscoveryChain === "business_overview"
|
|
||||||
? "business_overview"
|
|
||||||
: selectedDiscoveryChain === "value_flow_comparison"
|
|
||||||
? "customer_revenue_and_payments"
|
|
||||||
: "unknown";
|
|
||||||
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
||||||
const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
|
const discoveryIntent = intentFromDiscoveryChain(selectedDiscoveryChain);
|
||||||
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
|
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
|
||||||
|
const shouldPreferDiscoveryIntent =
|
||||||
|
trackableDiscoveryTurn &&
|
||||||
|
discoveryIntent !== "unknown" &&
|
||||||
|
selectedDiscoveryChain !== "business_overview";
|
||||||
|
const intent = toAddressIntent(
|
||||||
|
shouldPreferDiscoveryIntent ? discoveryIntent : detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent
|
||||||
|
);
|
||||||
if (intent === "unknown" && !trackableDiscoveryTurn) {
|
if (intent === "unknown" && !trackableDiscoveryTurn) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
|
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
|
||||||
const resultSetId = `rs-${item.message_id}`;
|
const resultSetId = `rs-${item.message_id}`;
|
||||||
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
|
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
|
||||||
const filters = normalizeFilters(debug.extracted_filters);
|
const filters = sanitizeBusinessOverviewNavigationFilters(normalizeFilters(debug.extracted_filters), debug);
|
||||||
const derivedOrganizationScope =
|
const derivedOrganizationScope =
|
||||||
resolveDerivedOrganizationScope(debug, filters, item.text) ?? readAddressDebugOrganization(debug, toNonEmptyString);
|
resolveDerivedOrganizationScope(debug, filters, item.text) ?? readAddressDebugOrganization(debug, toNonEmptyString);
|
||||||
const derivedCounterpartyScope =
|
const derivedCounterpartyScope =
|
||||||
selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true
|
||||||
? readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
|
? readCurrentDiscoveryCounterparty(debug)
|
||||||
: null;
|
: null;
|
||||||
const filtersWithDerivedScope =
|
const filtersWithDerivedScope =
|
||||||
derivedOrganizationScope && !toNonEmptyString(filters.organization)
|
derivedOrganizationScope && !toNonEmptyString(filters.organization)
|
||||||
|
|
@ -719,9 +819,17 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
||||||
debugFocusObject,
|
debugFocusObject,
|
||||||
primaryEntityFocusObject
|
primaryEntityFocusObject
|
||||||
);
|
);
|
||||||
const comparisonCounterparty =
|
const rawComparisonCounterparty =
|
||||||
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
debug.mcp_discovery_response_applied === true
|
||||||
|
? selectedDiscoveryChain === "business_overview"
|
||||||
? readNavigationDiscoveryCounterparty(debug)
|
? readNavigationDiscoveryCounterparty(debug)
|
||||||
|
: isCounterpartyDiscoveryChain(selectedDiscoveryChain)
|
||||||
|
? readCurrentDiscoveryCounterparty(debug)
|
||||||
|
: null
|
||||||
|
: null;
|
||||||
|
const comparisonCounterparty =
|
||||||
|
rawComparisonCounterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, rawComparisonCounterparty)
|
||||||
|
? rawComparisonCounterparty
|
||||||
: null;
|
: null;
|
||||||
const comparisonOrganization =
|
const comparisonOrganization =
|
||||||
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||||
|
|
@ -730,6 +838,8 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
||||||
const currentComparisonProofBundles =
|
const currentComparisonProofBundles =
|
||||||
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||||
? readBusinessOverviewComparisonProofBundles(debug)
|
? readBusinessOverviewComparisonProofBundles(debug)
|
||||||
|
: selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
||||||
|
? readCounterpartyValueFlowProofBundles(debug)
|
||||||
: null;
|
: null;
|
||||||
const inheritedComparisonScope = state.session_context.comparison_scope;
|
const inheritedComparisonScope = state.session_context.comparison_scope;
|
||||||
const inheritedComparisonProofBundles =
|
const inheritedComparisonProofBundles =
|
||||||
|
|
|
||||||
|
|
@ -1629,6 +1629,16 @@ function mergeFollowupFilters(
|
||||||
intent === "vat_payable_confirmed_as_of_date";
|
intent === "vat_payable_confirmed_as_of_date";
|
||||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||||
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
||||||
|
const currentPeriodFromForCarryover = toNonEmptyString(merged.period_from);
|
||||||
|
const currentPeriodToForCarryover = toNonEmptyString(merged.period_to);
|
||||||
|
const todayIsoForPeriodCarryover = new Date().toISOString().slice(0, 10);
|
||||||
|
const currentValueFlowPeriodDefaultsToToday =
|
||||||
|
isValueCounterpartyIntent(intent) &&
|
||||||
|
hasFollowupSignal &&
|
||||||
|
!hasExplicitPeriodInMessage &&
|
||||||
|
!hasExplicitCurrentDateInMessage &&
|
||||||
|
!currentPeriodFromForCarryover &&
|
||||||
|
currentPeriodToForCarryover === todayIsoForPeriodCarryover;
|
||||||
const currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
|
const currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
|
||||||
const currentContractExplicit = toNonEmptyString(merged.contract);
|
const currentContractExplicit = toNonEmptyString(merged.contract);
|
||||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||||
|
|
@ -1717,7 +1727,7 @@ function mergeFollowupFilters(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!currentHasPeriod &&
|
(!currentHasPeriod || currentValueFlowPeriodDefaultsToToday) &&
|
||||||
previousHasPeriod &&
|
previousHasPeriod &&
|
||||||
hasFollowupSignal &&
|
hasFollowupSignal &&
|
||||||
!hasExplicitPeriodInMessage &&
|
!hasExplicitPeriodInMessage &&
|
||||||
|
|
|
||||||
|
|
@ -624,12 +624,27 @@ function normalizeCounterpartyCandidate(
|
||||||
toNonEmptyString: (value: unknown) => string | null
|
toNonEmptyString: (value: unknown) => string | null
|
||||||
): string | null {
|
): string | null {
|
||||||
const text = toNonEmptyString(value);
|
const text = toNonEmptyString(value);
|
||||||
if (!text || isReferentialCounterpartyPlaceholder(text)) {
|
if (!text || isReferentialCounterpartyPlaceholder(text) || isCounterpartyRoleFragment(text)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCounterpartyRoleFragment(value: unknown): boolean {
|
||||||
|
const text = normalizeRussianComparableText(value);
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasRoleWord = /(?:^|\s)(?:клиент|поставщик|контрагент|получатель|источник|покупатель|заказчик)(?:\s|$)/u.test(text);
|
||||||
|
if (!hasRoleWord) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasRankingWord = /(?:^|\s)(?:главн[а-яёa-z0-9_-]*|крупнейш[а-яёa-z0-9_-]*|основн[а-яёa-z0-9_-]*|топ(?:-\d+)?)(?:\s|$)/u.test(text);
|
||||||
|
const hasRoleOnlyExplanation = /(?:^|\s)(?:клиент|поставщик)(?:\s|$).{0,40}(?:^|\s)как(?:\s|$).{0,30}(?:^|\s)рол/u.test(text);
|
||||||
|
const endsWithDanglingPreposition = /(?:^|\s)(?:в|за|по|на|от|для|к)$/u.test(text);
|
||||||
|
return hasRoleOnlyExplanation || (hasRankingWord && (endsWithDanglingPreposition || text.length <= 48));
|
||||||
|
}
|
||||||
|
|
||||||
function sameCounterpartyCandidate(
|
function sameCounterpartyCandidate(
|
||||||
left: string | null,
|
left: string | null,
|
||||||
right: string | null
|
right: string | null
|
||||||
|
|
@ -877,6 +892,9 @@ export function resolveAddressDebugCarryoverFilters(
|
||||||
const preferGroundedDiscoveryCounterparty =
|
const preferGroundedDiscoveryCounterparty =
|
||||||
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty);
|
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty);
|
||||||
const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString);
|
const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString);
|
||||||
|
if (!existingCounterparty) {
|
||||||
|
delete nextFilters.counterparty;
|
||||||
|
}
|
||||||
if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) {
|
if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) {
|
||||||
nextFilters.counterparty = counterparty;
|
nextFilters.counterparty = counterparty;
|
||||||
}
|
}
|
||||||
|
|
@ -1140,6 +1158,10 @@ export function resolveFollowupTargetIntent(
|
||||||
export function shouldUseNavigationTemporalCarryover(sourceIntentHint: unknown): boolean {
|
export function shouldUseNavigationTemporalCarryover(sourceIntentHint: unknown): boolean {
|
||||||
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
|
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
|
||||||
return (
|
return (
|
||||||
|
normalizedIntent === "customer_revenue_and_payments" ||
|
||||||
|
normalizedIntent === "supplier_payouts_profile" ||
|
||||||
|
normalizedIntent === "contract_usage_and_value" ||
|
||||||
|
normalizedIntent === "counterparty_activity_lifecycle" ||
|
||||||
normalizedIntent === "inventory_on_hand_as_of_date" ||
|
normalizedIntent === "inventory_on_hand_as_of_date" ||
|
||||||
normalizedIntent === "inventory_supplier_stock_overlap_as_of_date" ||
|
normalizedIntent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
||||||
|
|
|
||||||
|
|
@ -281,7 +281,7 @@ function valueFlowDirectionLabelRu(pilot: AssistantMcpDiscoveryPilotExecutionCon
|
||||||
: "входящих денежных поступлений";
|
: "входящих денежных поступлений";
|
||||||
}
|
}
|
||||||
|
|
||||||
function valueFlowZeroResultConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
function valueFlowZeroResultInsufficiencyLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||||
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -293,7 +293,23 @@ function valueFlowZeroResultConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecu
|
||||||
const period = explicitDateScope(pilot);
|
const period = explicitDateScope(pilot);
|
||||||
const organizationPart = organization ? ` по организации ${organization}` : "";
|
const organizationPart = organization ? ` по организации ${organization}` : "";
|
||||||
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
|
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
|
||||||
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${valueFlowDirectionLabelRu(
|
const direction = valueFlowDirectionLabelRu(pilot);
|
||||||
|
return `Точную сумму ${direction} по контрагенту ${counterparty}${organizationPart}${periodPart} не подтверждаю: в выполненной проверке 1С строки ${direction} не найдены; это не доказывает отсутствие операций вне доступного банковского контура.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueFlowZeroResultCheckedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||||
|
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const counterparty = firstEntityCandidate(pilot);
|
||||||
|
if (!counterparty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const organization = explicitOrganizationScope(pilot);
|
||||||
|
const period = explicitDateScope(pilot);
|
||||||
|
const organizationPart = organization ? ` по организации ${organization}` : "";
|
||||||
|
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
|
||||||
|
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart} строки ${valueFlowDirectionLabelRu(
|
||||||
pilot
|
pilot
|
||||||
)} не найдено.`;
|
)} не найдено.`;
|
||||||
}
|
}
|
||||||
|
|
@ -312,11 +328,11 @@ function valueFlowZeroResultUnknownLine(pilot: AssistantMcpDiscoveryPilotExecuti
|
||||||
}
|
}
|
||||||
|
|
||||||
function valueFlowZeroResultHeadline(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
function valueFlowZeroResultHeadline(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||||
const confirmedLine = valueFlowZeroResultConfirmedLine(pilot);
|
const insufficiencyLine = valueFlowZeroResultInsufficiencyLine(pilot);
|
||||||
if (!confirmedLine) {
|
if (!insufficiencyLine) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return confirmedLine;
|
return insufficiencyLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasAllTimeScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
function hasAllTimeScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||||
|
|
@ -1097,6 +1113,13 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||||
}
|
}
|
||||||
return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном.";
|
return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном.";
|
||||||
}
|
}
|
||||||
|
if (mode === "checked_sources_only" && hasExecutedZeroValueFlowRows(pilot)) {
|
||||||
|
const flowLabel =
|
||||||
|
pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1"
|
||||||
|
? "исходящие платежи/списания"
|
||||||
|
: "входящие денежные поступления";
|
||||||
|
return `Можно следующим шагом проверить ${flowLabel} по этому контрагенту в соседних периодах, по договорам или документам, либо собрать двусторонний денежный поток, чтобы отделить отсутствие строк в текущем срезе от операций вне проверенного окна.`;
|
||||||
|
}
|
||||||
if (mode === "confirmed_with_bounded_inference" && isBusinessOverviewPilot(pilot)) {
|
if (mode === "confirmed_with_bounded_inference" && isBusinessOverviewPilot(pilot)) {
|
||||||
return pilot.derived_business_overview
|
return pilot.derived_business_overview
|
||||||
? businessOverviewNextStepLine(pilot.derived_business_overview)
|
? businessOverviewNextStepLine(pilot.derived_business_overview)
|
||||||
|
|
@ -2119,8 +2142,6 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||||
? [derivedValueLine]
|
? [derivedValueLine]
|
||||||
: derivedValueLine
|
: derivedValueLine
|
||||||
? [derivedValueLine, ...monthlyConfirmedLines]
|
? [derivedValueLine, ...monthlyConfirmedLines]
|
||||||
: valueFlowZeroResultConfirmedLine(pilot)
|
|
||||||
? [valueFlowZeroResultConfirmedLine(pilot)!]
|
|
||||||
: derivedEntityResolutionLine
|
: derivedEntityResolutionLine
|
||||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||||
: derivedMetadataLine
|
: derivedMetadataLine
|
||||||
|
|
@ -2132,7 +2153,12 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||||
? pilot.derived_metadata_surface.available_fields.length > 0
|
? pilot.derived_metadata_surface.available_fields.length > 0
|
||||||
? userFacingUnknowns(pilot.evidence.unknown_facts)
|
? userFacingUnknowns(pilot.evidence.unknown_facts)
|
||||||
: ["Детальный список полей этих объектов этим шагом не получен."]
|
: ["Детальный список полей этих объектов этим шагом не получен."]
|
||||||
: appendValueFlowZeroResultUnknown(rankedValueFlowUnknownLines(pilot), pilot);
|
: appendValueFlowZeroResultUnknown(
|
||||||
|
valueFlowZeroResultCheckedLine(pilot)
|
||||||
|
? [valueFlowZeroResultCheckedLine(pilot)!, ...rankedValueFlowUnknownLines(pilot)]
|
||||||
|
: rankedValueFlowUnknownLines(pilot),
|
||||||
|
pilot
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
||||||
|
import { normalizeRussianComparableText } from "./addressTextRepair";
|
||||||
|
|
||||||
export const ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION =
|
export const ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION =
|
||||||
"assistant_data_need_graph_v1" as const;
|
"assistant_data_need_graph_v1" as const;
|
||||||
|
|
@ -307,6 +308,40 @@ function hasDetailedBusinessOverviewMoneyBreakdownHint(rawUtterance: string): bo
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasWideBusinessOverviewSurfaceHint(rawUtterance: string): boolean {
|
||||||
|
if (!rawUtterance) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const textVariants = Array.from(new Set([rawUtterance, normalizeRussianComparableText(rawUtterance)]));
|
||||||
|
if (
|
||||||
|
textVariants.some((text) =>
|
||||||
|
/(?:\u043d\u0435\s+\u043e\u0431\u0437\u043e\u0440|\u043f\u0440\u043e\u0441\u0442\u043e\s+\u0434\u0435\u043d\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0434\u0435\u043d\p{L}*)/iu.test(
|
||||||
|
text
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasBroadCue =
|
||||||
|
textVariants.some((text) =>
|
||||||
|
/(?:\u0431\u0438\u0437\u043d\u0435\u0441[-\s]*\u043e\u0431\u0437\u043e\u0440|\u043e\u0431\u0437\u043e\u0440\s+\u0431\u0438\u0437\u043d\u0435\u0441|\u0432\u0437\u0440\u043e\u0441\u043b\p{L}*\s+\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\p{L}*\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0446\u0435\u043d\p{L}*\s+\u0431\u0438\u0437\u043d\u0435\u0441|business\s+overview|business\s+review|company\s+overview)/iu.test(
|
||||||
|
text
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (!hasBroadCue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const surfaceMarkers = [
|
||||||
|
/(?:\u043d\u0434\u0441|vat|\u043d\u0430\u043b\u043e\u0433\p{L}*)/iu,
|
||||||
|
/(?:\u0434\u043e\u043b\u0433\p{L}*|\u0437\u0430\u0434\u043e\u043b\u0436\p{L}*|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440)/iu,
|
||||||
|
/(?:\u0441\u043a\u043b\u0430\u0434|\u043e\u0441\u0442\u0430\u0442\u043a|\u0437\u0430\u043f\u0430\u0441|\u0442\u043e\u0432\u0430\u0440\p{L}*)/iu,
|
||||||
|
/(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b)/iu,
|
||||||
|
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b)/iu,
|
||||||
|
/(?:\u0447\u0442\u043e\s+(?:\u043f\u043e\u043a\u0430\s+)?\u043d\u0435\u043b\u044c\u0437\u044f\s+\u0443\u0442\u0432\u0435\u0440\u0436\u0434|\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d|\u0433\u0440\u0430\u043d\u0438\u0446\p{L}*)/iu
|
||||||
|
];
|
||||||
|
return textVariants.some((text) => surfaceMarkers.filter((marker) => marker.test(text)).length >= 3);
|
||||||
|
}
|
||||||
|
|
||||||
function supportsOrganizationScopedOpenTotal(action: string): boolean {
|
function supportsOrganizationScopedOpenTotal(action: string): boolean {
|
||||||
return action === "turnover" || action === "payout";
|
return action === "turnover" || action === "payout";
|
||||||
}
|
}
|
||||||
|
|
@ -565,7 +600,8 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
||||||
const rawAction = lower(turnMeaning?.asked_action_family);
|
const rawAction = lower(turnMeaning?.asked_action_family);
|
||||||
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
||||||
const rawUtterance = lower(input.rawUtterance);
|
const rawUtterance = lower(input.rawUtterance);
|
||||||
const rawQuestionSignal = lower([input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" "));
|
const rawQuestionSource = [input.rawUtterance, turnMeaning?.raw_message, turnMeaning?.effective_message].join(" ");
|
||||||
|
const rawQuestionSignal = lower(rawQuestionSource);
|
||||||
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
|
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
|
||||||
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
|
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
|
||||||
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
||||||
|
|
@ -583,12 +619,15 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
||||||
});
|
});
|
||||||
const oneSidedValueAction = inferredOpenScopeOneSidedValueAction(rawQuestionSignal);
|
const oneSidedValueAction = inferredOpenScopeOneSidedValueAction(rawQuestionSignal);
|
||||||
const detailedBusinessOverviewMoneyBreakdownHint = hasDetailedBusinessOverviewMoneyBreakdownHint(rawQuestionSignal);
|
const detailedBusinessOverviewMoneyBreakdownHint = hasDetailedBusinessOverviewMoneyBreakdownHint(rawQuestionSignal);
|
||||||
|
const wideBusinessOverviewSurfaceHint = hasWideBusinessOverviewSurfaceHint(rawQuestionSource);
|
||||||
const oneSidedBusinessOverviewValueFlowOverride =
|
const oneSidedBusinessOverviewValueFlowOverride =
|
||||||
initialBusinessFactFamily === "business_overview" &&
|
initialBusinessFactFamily === "business_overview" &&
|
||||||
Boolean(oneSidedValueAction) &&
|
Boolean(oneSidedValueAction) &&
|
||||||
!detailedBusinessOverviewMoneyBreakdownHint;
|
!detailedBusinessOverviewMoneyBreakdownHint;
|
||||||
const bidirectionalBusinessOverviewValueFlowOverride =
|
const bidirectionalBusinessOverviewValueFlowOverride =
|
||||||
initialBusinessFactFamily === "business_overview" && hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
|
initialBusinessFactFamily === "business_overview" &&
|
||||||
|
hasIncomingVsOutgoingComparisonHint(rawQuestionSignal) &&
|
||||||
|
!wideBusinessOverviewSurfaceHint;
|
||||||
const oneSidedValueFlowActionOverride =
|
const oneSidedValueFlowActionOverride =
|
||||||
initialBusinessFactFamily === "value_flow" &&
|
initialBusinessFactFamily === "value_flow" &&
|
||||||
Boolean(oneSidedValueAction) &&
|
Boolean(oneSidedValueAction) &&
|
||||||
|
|
@ -735,6 +774,9 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
||||||
if (businessFactFamily === "business_overview" && !explicitDateScope) {
|
if (businessFactFamily === "business_overview" && !explicitDateScope) {
|
||||||
pushReason(reasonCodes, "data_need_graph_business_overview_defaults_to_all_time_scope");
|
pushReason(reasonCodes, "data_need_graph_business_overview_defaults_to_all_time_scope");
|
||||||
}
|
}
|
||||||
|
if (initialBusinessFactFamily === "business_overview" && wideBusinessOverviewSurfaceHint) {
|
||||||
|
pushReason(reasonCodes, "data_need_graph_business_overview_wide_surface_preserved");
|
||||||
|
}
|
||||||
if (directBusinessOverviewMoneyAnswerHint) {
|
if (directBusinessOverviewMoneyAnswerHint) {
|
||||||
pushReason(reasonCodes, "data_need_graph_business_overview_direct_money_answer");
|
pushReason(reasonCodes, "data_need_graph_business_overview_direct_money_answer");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3217,9 +3217,28 @@ function deriveValueFlow(
|
||||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"],
|
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"],
|
||||||
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
|
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
|
||||||
): AssistantMcpDiscoveryDerivedValueFlow | null {
|
): AssistantMcpDiscoveryDerivedValueFlow | null {
|
||||||
if (!result || result.error || result.matched_rows <= 0) {
|
if (!result || result.error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (result.matched_rows <= 0) {
|
||||||
|
return {
|
||||||
|
value_flow_direction: direction,
|
||||||
|
counterparty,
|
||||||
|
period_scope: periodScope,
|
||||||
|
aggregation_axis: aggregationAxis,
|
||||||
|
rows_matched: 0,
|
||||||
|
rows_with_amount: 0,
|
||||||
|
total_amount: 0,
|
||||||
|
total_amount_human_ru: formatAmountHumanRu(0),
|
||||||
|
first_movement_date: null,
|
||||||
|
latest_movement_date: null,
|
||||||
|
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
|
||||||
|
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
|
||||||
|
period_chunking_granularity: result.period_chunking_granularity,
|
||||||
|
monthly_breakdown: [],
|
||||||
|
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
|
||||||
|
};
|
||||||
|
}
|
||||||
let totalAmount = 0;
|
let totalAmount = 0;
|
||||||
let rowsWithAmount = 0;
|
let rowsWithAmount = 0;
|
||||||
for (const row of result.rows) {
|
for (const row of result.rows) {
|
||||||
|
|
@ -5294,16 +5313,30 @@ function buildValueFlowConfirmedFacts(
|
||||||
counterparty: string | null,
|
counterparty: string | null,
|
||||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"]
|
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"]
|
||||||
): string[] {
|
): string[] {
|
||||||
if (result.error || result.matched_rows <= 0) {
|
if (result.error) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (direction === "outgoing_supplier_payout") {
|
if (direction === "outgoing_supplier_payout") {
|
||||||
|
if (result.matched_rows <= 0) {
|
||||||
|
return [
|
||||||
|
counterparty
|
||||||
|
? `1C supplier-payout rows were checked for counterparty ${counterparty}: not_found`
|
||||||
|
: "1C supplier-payout rows were checked for the requested counterparty scope: not_found"
|
||||||
|
];
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
counterparty
|
counterparty
|
||||||
? `1C supplier-payout rows were found for counterparty ${counterparty}`
|
? `1C supplier-payout rows were found for counterparty ${counterparty}`
|
||||||
: "1C supplier-payout rows were found for the requested counterparty scope"
|
: "1C supplier-payout rows were found for the requested counterparty scope"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if (result.matched_rows <= 0) {
|
||||||
|
return [
|
||||||
|
counterparty
|
||||||
|
? `1C value-flow rows were checked for counterparty ${counterparty}: not_found`
|
||||||
|
: "1C value-flow rows were checked for the requested counterparty scope: not_found"
|
||||||
|
];
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
counterparty
|
counterparty
|
||||||
? `1C value-flow rows were found for counterparty ${counterparty}`
|
? `1C value-flow rows were found for counterparty ${counterparty}`
|
||||||
|
|
@ -5521,6 +5554,14 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const facts: string[] = [];
|
const facts: string[] = [];
|
||||||
|
if (derived.rows_matched <= 0) {
|
||||||
|
facts.push(
|
||||||
|
derived.value_flow_direction === "outgoing_supplier_payout"
|
||||||
|
? "Counterparty supplier-payout not_found result was treated as zero only inside the scoped checked 1C result set"
|
||||||
|
: "Counterparty incoming value-flow not_found result was treated as zero only inside the scoped checked 1C result set"
|
||||||
|
);
|
||||||
|
return facts;
|
||||||
|
}
|
||||||
if (derived.value_flow_direction === "outgoing_supplier_payout") {
|
if (derived.value_flow_direction === "outgoing_supplier_payout") {
|
||||||
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
|
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -386,6 +386,7 @@ export function resolveAssistantMcpDiscoveryEvidence(
|
||||||
const rowsMatched = probeRowsMatched(probeResults);
|
const rowsMatched = probeRowsMatched(probeResults);
|
||||||
const rowsReceived = probeRowsReceived(probeResults);
|
const rowsReceived = probeRowsReceived(probeResults);
|
||||||
const bypassDetected = hasProbeBypass(input.plan, probeResults);
|
const bypassDetected = hasProbeBypass(input.plan, probeResults);
|
||||||
|
const confirmedNoMatchFact = confirmedFacts.some((fact) => /\bnot_found\b/iu.test(String(fact ?? "")));
|
||||||
|
|
||||||
if (bypassDetected) {
|
if (bypassDetected) {
|
||||||
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
|
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
|
||||||
|
|
@ -393,7 +394,7 @@ export function resolveAssistantMcpDiscoveryEvidence(
|
||||||
if (input.plan.plan_status !== "allowed") {
|
if (input.plan.plan_status !== "allowed") {
|
||||||
pushReason(reasonCodes, "plan_not_allowed_by_runtime");
|
pushReason(reasonCodes, "plan_not_allowed_by_runtime");
|
||||||
}
|
}
|
||||||
if (confirmedFacts.length > 0 && rowsMatched <= 0) {
|
if (confirmedFacts.length > 0 && rowsMatched <= 0 && !confirmedNoMatchFact) {
|
||||||
pushReason(reasonCodes, "confirmed_facts_without_matched_probe_rows");
|
pushReason(reasonCodes, "confirmed_facts_without_matched_probe_rows");
|
||||||
}
|
}
|
||||||
if (!sourceRowsSummary && rowsReceived > 0) {
|
if (!sourceRowsSummary && rowsReceived > 0) {
|
||||||
|
|
@ -413,6 +414,11 @@ export function resolveAssistantMcpDiscoveryEvidence(
|
||||||
coverageStatus = "full";
|
coverageStatus = "full";
|
||||||
answerPermission = "confirmed_answer";
|
answerPermission = "confirmed_answer";
|
||||||
pushReason(reasonCodes, "confirmed_facts_with_allowed_mcp_evidence");
|
pushReason(reasonCodes, "confirmed_facts_with_allowed_mcp_evidence");
|
||||||
|
} else if (confirmedFacts.length > 0 && confirmedNoMatchFact && rowsMatched <= 0 && sourceRowsSummary) {
|
||||||
|
evidenceStatus = "confirmed";
|
||||||
|
coverageStatus = "full";
|
||||||
|
answerPermission = "confirmed_answer";
|
||||||
|
pushReason(reasonCodes, "confirmed_no_match_fact_with_allowed_mcp_evidence");
|
||||||
} else if (inferredFacts.length > 0 && rowsReceived > 0) {
|
} else if (inferredFacts.length > 0 && rowsReceived > 0) {
|
||||||
evidenceStatus = "inferred_only";
|
evidenceStatus = "inferred_only";
|
||||||
coverageStatus = "partial";
|
coverageStatus = "partial";
|
||||||
|
|
|
||||||
|
|
@ -284,9 +284,15 @@ function localizeLine(value: string): string {
|
||||||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
|
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
|
||||||
}
|
}
|
||||||
|
if (/^1C value-flow rows were checked for the requested counterparty scope: not_found$/i.test(value)) {
|
||||||
|
return "В 1С проверены входящие денежные строки в запрошенном срезе: строки не найдены.";
|
||||||
|
}
|
||||||
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
|
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
|
||||||
}
|
}
|
||||||
|
if (/^1C supplier-payout rows were checked for the requested counterparty scope: not_found$/i.test(value)) {
|
||||||
|
return "В 1С проверены исходящие платежи/списания в запрошенном срезе: строки не найдены.";
|
||||||
|
}
|
||||||
const openScopeBidirectionalMatch = value.match(
|
const openScopeBidirectionalMatch = value.match(
|
||||||
/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i
|
/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i
|
||||||
);
|
);
|
||||||
|
|
@ -319,6 +325,10 @@ function localizeLine(value: string): string {
|
||||||
if (valueFlowMatch) {
|
if (valueFlowMatch) {
|
||||||
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
|
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
|
||||||
}
|
}
|
||||||
|
const valueFlowNotFoundMatch = value.match(/^1C value-flow rows were checked for counterparty\s+(.+): not_found$/i);
|
||||||
|
if (valueFlowNotFoundMatch) {
|
||||||
|
return `В 1С проверены входящие денежные строки по контрагенту ${valueFlowNotFoundMatch[1]}: строки не найдены.`;
|
||||||
|
}
|
||||||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
|
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
|
||||||
}
|
}
|
||||||
|
|
@ -340,6 +350,10 @@ function localizeLine(value: string): string {
|
||||||
if (supplierPayoutMatch) {
|
if (supplierPayoutMatch) {
|
||||||
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`;
|
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`;
|
||||||
}
|
}
|
||||||
|
const supplierPayoutNotFoundMatch = value.match(/^1C supplier-payout rows were checked for counterparty\s+(.+): not_found$/i);
|
||||||
|
if (supplierPayoutNotFoundMatch) {
|
||||||
|
return `В 1С проверены исходящие платежи/списания по контрагенту ${supplierPayoutNotFoundMatch[1]}: строки не найдены.`;
|
||||||
|
}
|
||||||
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
|
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
|
||||||
}
|
}
|
||||||
|
|
@ -377,6 +391,12 @@ function localizeLine(value: string): string {
|
||||||
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
|
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
|
||||||
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
||||||
}
|
}
|
||||||
|
if (/^Counterparty supplier-payout not_found result was treated as zero only inside the scoped checked 1C result set$/i.test(value)) {
|
||||||
|
return "Нулевую сумму исходящих платежей можно трактовать только внутри этого проверенного среза 1С, где строки списаний не найдены.";
|
||||||
|
}
|
||||||
|
if (/^Counterparty incoming value-flow not_found result was treated as zero only inside the scoped checked 1C result set$/i.test(value)) {
|
||||||
|
return "Нулевую сумму входящих поступлений можно трактовать только внутри этого проверенного среза 1С, где строки поступлений не найдены.";
|
||||||
|
}
|
||||||
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
||||||
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
|
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
|
||||||
}
|
}
|
||||||
|
|
@ -838,6 +858,9 @@ function businessOverviewOrganizationScopeLabel(value: unknown): string | null {
|
||||||
const comparable = compactComparable(text);
|
const comparable = compactComparable(text);
|
||||||
if (
|
if (
|
||||||
/^(?:с|без)\s+разбивк/.test(comparable) ||
|
/^(?:с|без)\s+разбивк/.test(comparable) ||
|
||||||
|
/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(
|
||||||
|
comparable
|
||||||
|
) ||
|
||||||
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)
|
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1426,7 +1449,7 @@ function buildCompactBusinessOverviewReply(
|
||||||
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
|
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
|
||||||
].filter((value): value is string => Boolean(value));
|
].filter((value): value is string => Boolean(value));
|
||||||
lines.push(
|
lines.push(
|
||||||
`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, не аудиторское заключение и не подтвержденная чистая прибыль.`
|
`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, а не финансовый аудит и не подтверждение чистой прибыли.`
|
||||||
);
|
);
|
||||||
lines.push(
|
lines.push(
|
||||||
"Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж."
|
"Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж."
|
||||||
|
|
@ -1469,7 +1492,7 @@ function buildCompactBusinessOverviewReply(
|
||||||
}
|
}
|
||||||
if (!limitLine) {
|
if (!limitLine) {
|
||||||
lines.push(
|
lines.push(
|
||||||
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
|
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, а не финансовый аудит и не подтверждение чистой прибыли."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const missingOverviewFamilies: string[] = [];
|
const missingOverviewFamilies: string[] = [];
|
||||||
|
|
|
||||||
|
|
@ -526,6 +526,66 @@ function hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarificati
|
||||||
return asksForMovements && staleInventoryItemClarification && mcpExecutionPerformed && boundedMovementAnswerAllowed;
|
return asksForMovements && staleInventoryItemClarification && mcpExecutionPerformed && boundedMovementAnswerAllowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasBusinessOverviewBoundaryCandidatePriorityOverCurrentInventoryItemClarification(
|
||||||
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||||
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null,
|
||||||
|
candidate: AssistantMcpDiscoveryResponseCandidateContract
|
||||||
|
): boolean {
|
||||||
|
if (!hasCurrentClarificationRequiredAddressReply(input)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (candidate.candidate_status !== "clarification_candidate" && candidate.candidate_status !== "ready_for_guarded_use") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (candidate.reply_text && hasInternalMechanics(candidate.reply_text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const graph = readDiscoveryDataNeedGraph(entryPoint);
|
||||||
|
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||||
|
const selectedChainId = readDiscoverySelectedChainId(entryPoint);
|
||||||
|
const discoveryFamily = toNonEmptyString(graph?.business_fact_family);
|
||||||
|
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
|
||||||
|
const separateCounterpartyCandidates = readStringArray(turnMeaning?.business_overview_separate_entity_candidates);
|
||||||
|
const hasPreviousCounterpartyBundle = Boolean(
|
||||||
|
toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle) ||
|
||||||
|
toRecordObject(turnMeaning?.previous_counterparty_document_bundle)
|
||||||
|
);
|
||||||
|
const asksForBoundarySummary = Boolean(
|
||||||
|
(selectedChainId === "business_overview" ||
|
||||||
|
discoveryFamily === "business_overview" ||
|
||||||
|
askedDomain === "business_overview") &&
|
||||||
|
(separateCounterpartyCandidates.length > 0 || hasPreviousCounterpartyBundle)
|
||||||
|
);
|
||||||
|
|
||||||
|
const truthAnswerShape = readTruthAnswerShape(input);
|
||||||
|
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||||
|
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||||
|
const capabilityId =
|
||||||
|
toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
|
||||||
|
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id) ??
|
||||||
|
toNonEmptyString(truthAnswerShape?.capability_contract_id);
|
||||||
|
const missingFilters = [
|
||||||
|
...readStringArray(input.addressRuntimeMeta?.missing_required_filters),
|
||||||
|
...readStringArray(input.addressRuntimeMeta?.missing_anchors),
|
||||||
|
...readStringArray(toRecordObject(input.addressRuntimeMeta?.capability_binding_contract)?.missing_anchors)
|
||||||
|
];
|
||||||
|
const staleInventoryItemClarification = Boolean(
|
||||||
|
missingFilters.includes("item") &&
|
||||||
|
(detectedIntent?.startsWith("inventory_") ||
|
||||||
|
selectedRecipe?.includes("inventory_") ||
|
||||||
|
capabilityId?.includes("inventory_"))
|
||||||
|
);
|
||||||
|
|
||||||
|
return asksForBoundarySummary && staleInventoryItemClarification;
|
||||||
|
}
|
||||||
|
|
||||||
function hasGroundedValueFlowCandidatePriorityOverCurrentClarification(
|
function hasGroundedValueFlowCandidatePriorityOverCurrentClarification(
|
||||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null,
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null,
|
||||||
|
|
@ -795,6 +855,65 @@ function hasExactBankOperationsAddressReply(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractExplicitDateScopeYear(value: unknown): string | null {
|
||||||
|
const source = toNonEmptyString(value);
|
||||||
|
if (!source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = source.match(/\b((?:19|20)\d{2})\b/u);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDocumentReplyDateYears(value: unknown): string[] {
|
||||||
|
const source = String(value ?? "");
|
||||||
|
if (!source) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const years = new Set<string>();
|
||||||
|
for (const match of source.matchAll(/\b((?:19|20)\d{2})-\d{2}-\d{2}(?:T|\b)/gu)) {
|
||||||
|
years.add(match[1]);
|
||||||
|
}
|
||||||
|
for (const match of source.matchAll(/\b\d{1,2}\.\d{1,2}\.((?:19|20)\d{2})\b/gu)) {
|
||||||
|
years.add(match[1]);
|
||||||
|
}
|
||||||
|
return [...years];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDocumentListTemporalConflictWithDiscovery(
|
||||||
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||||
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||||
|
): boolean {
|
||||||
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||||
|
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||||
|
const isDocumentIntent =
|
||||||
|
detectedIntent === "list_documents_by_counterparty" || detectedIntent === "list_documents_by_contract";
|
||||||
|
const isDocumentRecipe =
|
||||||
|
selectedRecipe === "address_documents_by_counterparty_v1" ||
|
||||||
|
selectedRecipe === "address_documents_by_contract_v1";
|
||||||
|
if (!isDocumentIntent || !isDocumentRecipe) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||||
|
const expectedYear = extractExplicitDateScopeYear(turnMeaning?.explicit_date_scope);
|
||||||
|
if (!expectedYear) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = toRecordObject(input.addressRuntimeMeta?.extracted_filters);
|
||||||
|
const periodFrom = toNonEmptyString(filters?.period_from);
|
||||||
|
const periodTo = toNonEmptyString(filters?.period_to);
|
||||||
|
const hasMatchingPeriod =
|
||||||
|
Boolean(periodFrom?.startsWith(`${expectedYear}-`)) && Boolean(periodTo?.startsWith(`${expectedYear}-`));
|
||||||
|
const replyDateYears = extractDocumentReplyDateYears(input.currentReply);
|
||||||
|
const hasOutOfScopeReplyDates = replyDateYears.some((year) => year !== expectedYear);
|
||||||
|
|
||||||
|
return hasOutOfScopeReplyDates || (!hasMatchingPeriod && replyDateYears.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
function hasExactDocumentListAddressReply(
|
function hasExactDocumentListAddressReply(
|
||||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||||
|
|
@ -1239,7 +1358,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
entryPoint
|
entryPoint
|
||||||
);
|
);
|
||||||
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
||||||
const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint);
|
const documentListTemporalConflictWithDiscovery = hasDocumentListTemporalConflictWithDiscovery(input, entryPoint);
|
||||||
|
const exactDocumentListAddressReply =
|
||||||
|
hasExactDocumentListAddressReply(input, entryPoint) && !documentListTemporalConflictWithDiscovery;
|
||||||
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
|
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
|
||||||
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(
|
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(
|
||||||
input,
|
input,
|
||||||
|
|
@ -1268,6 +1389,8 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
);
|
);
|
||||||
const groundedMovementCandidatePriority =
|
const groundedMovementCandidatePriority =
|
||||||
hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
|
hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
|
||||||
|
const businessOverviewBoundaryCandidatePriority =
|
||||||
|
hasBusinessOverviewBoundaryCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
|
||||||
const groundedValueFlowRankingCandidatePriority =
|
const groundedValueFlowRankingCandidatePriority =
|
||||||
hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate);
|
hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate);
|
||||||
const currentClarificationProtectsCurrentReply =
|
const currentClarificationProtectsCurrentReply =
|
||||||
|
|
@ -1275,15 +1398,18 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
!discoveryClarificationCandidatePriority &&
|
!discoveryClarificationCandidatePriority &&
|
||||||
!groundedValueFlowCandidatePriority &&
|
!groundedValueFlowCandidatePriority &&
|
||||||
!groundedMovementCandidatePriority &&
|
!groundedMovementCandidatePriority &&
|
||||||
|
!businessOverviewBoundaryCandidatePriority &&
|
||||||
!groundedValueFlowRankingCandidatePriority;
|
!groundedValueFlowRankingCandidatePriority;
|
||||||
const alignedFactualAddressReplyProtectsCurrent =
|
const alignedFactualAddressReplyProtectsCurrent =
|
||||||
alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||||
const matchedFactualAddressContinuationTargetProtectsCurrent =
|
const matchedFactualAddressContinuationTargetProtectsCurrent =
|
||||||
matchedFactualAddressContinuationTarget && !groundedValueFlowRankingCandidatePriority;
|
matchedFactualAddressContinuationTarget &&
|
||||||
|
!groundedValueFlowRankingCandidatePriority &&
|
||||||
|
!documentListTemporalConflictWithDiscovery;
|
||||||
const fullConfirmedFactualAddressReplyProtectsCurrent =
|
const fullConfirmedFactualAddressReplyProtectsCurrent =
|
||||||
fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||||
const exactMatchedFactualAddressReplyProtectsCurrent =
|
const exactMatchedFactualAddressReplyProtectsCurrent =
|
||||||
exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||||
const exactBankOperationsProtectsCurrent =
|
const exactBankOperationsProtectsCurrent =
|
||||||
exactBankOperationsAddressReply &&
|
exactBankOperationsAddressReply &&
|
||||||
!semanticConflictWithDiscoveryTurnMeaning &&
|
!semanticConflictWithDiscoveryTurnMeaning &&
|
||||||
|
|
@ -1320,6 +1446,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
|
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
|
||||||
}
|
}
|
||||||
|
if (documentListTemporalConflictWithDiscovery) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_document_list_temporal_conflict_allows_candidate_override");
|
||||||
|
}
|
||||||
if (currentClarificationProtectsCurrentReply) {
|
if (currentClarificationProtectsCurrentReply) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
||||||
}
|
}
|
||||||
|
|
@ -1341,6 +1470,12 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
"mcp_discovery_response_policy_grounded_movement_candidate_priority_over_inventory_item_clarification"
|
"mcp_discovery_response_policy_grounded_movement_candidate_priority_over_inventory_item_clarification"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (businessOverviewBoundaryCandidatePriority) {
|
||||||
|
pushReason(
|
||||||
|
reasonCodes,
|
||||||
|
"mcp_discovery_response_policy_business_overview_boundary_candidate_priority_over_inventory_item_clarification"
|
||||||
|
);
|
||||||
|
}
|
||||||
if (groundedValueFlowRankingCandidatePriority) {
|
if (groundedValueFlowRankingCandidatePriority) {
|
||||||
pushReason(
|
pushReason(
|
||||||
reasonCodes,
|
reasonCodes,
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,14 @@ function isReferentialOrganizationPlaceholder(value: string | null): boolean {
|
||||||
if (isNegativeCounterpartyScopePlaceholder(value)) {
|
if (isNegativeCounterpartyScopePlaceholder(value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const text = compactLower(repairAddressMojibakeText(value));
|
||||||
|
if (
|
||||||
|
/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(
|
||||||
|
text
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return new Set([
|
return new Set([
|
||||||
"эта организация",
|
"эта организация",
|
||||||
"этой организации",
|
"этой организации",
|
||||||
|
|
@ -1258,6 +1266,12 @@ function hasBidirectionalValueFlowSignal(text: string): boolean {
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
/(?:\u043e\u0431\u0449\p{L}*[\s\S]{0,60}\u0434\u0435\u043d\p{L}*[\s\S]{0,60}\u0441\u043c\u044b\u0441\u043b|\u0434\u0435\u043d\p{L}*[\s\S]{0,40}\u0441\u043c\u044b\u0441\u043b|overall[\s\S]{0,40}(?:cash|money)[\s\S]{0,40}(?:meaning|sense)|money[\s\S]{0,40}(?:meaning|sense))/iu.test(text) &&
|
||||||
|
hasPayoutSignal(text)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(
|
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(
|
||||||
text
|
text
|
||||||
);
|
);
|
||||||
|
|
@ -1320,7 +1334,7 @@ function normalizeLooseOrganizationAlias(value: string | null): string | null {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (isNegativeCounterpartyScopePlaceholder(text)) {
|
if (isReferentialOrganizationPlaceholder(text)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const normalized = text
|
const normalized = text
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,10 @@ function detectScopedCounterpartyEntity(text) {
|
||||||
if (!rawEntity) {
|
if (!rawEntity) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const entity = rawEntity.replace(/^["'«»]+|["'«»]+$/gu, "").trim();
|
const entity = rawEntity
|
||||||
|
.replace(/^["'«»]+|["'«»]+$/gu, "")
|
||||||
|
.replace(/^(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a(?:\u0443|\u0430|\u043e\u043c|\u0435)?|\u043a\u043b\u0438\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435)?|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442(?:\u0443|\u0430|\u043e\u043c|\u0435)?)\s+/iu, "")
|
||||||
|
.trim();
|
||||||
if (entity.length >= 2 && !ignored.has(entity)) {
|
if (entity.length >= 2 && !ignored.has(entity)) {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +220,11 @@ function detectCounterpartyBidirectionalValueFlowFamily(text) {
|
||||||
/(?:\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|received|incoming)/iu.test(text);
|
/(?:\u043f\u043e\u043b\u0443\u0447\p{L}*|\u0432\u0445\u043e\u0434\p{L}*|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*|received|incoming)/iu.test(text);
|
||||||
const hasOutgoingCue =
|
const hasOutgoingCue =
|
||||||
/(?:\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0441\u043f\u0438\u0441\u0430\u043d\p{L}*|paid|outgoing|payment)/iu.test(text);
|
/(?:\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0441\u043f\u0438\u0441\u0430\u043d\p{L}*|paid|outgoing|payment)/iu.test(text);
|
||||||
if (!(hasNetCue || (hasIncomingCue && hasOutgoingCue))) {
|
const hasOverallMoneyMeaningCue =
|
||||||
|
/(?:\u043e\u0431\u0449\p{L}*[\s\S]{0,60}\u0434\u0435\u043d\p{L}*[\s\S]{0,60}\u0441\u043c\u044b\u0441\u043b|\u0434\u0435\u043d\p{L}*[\s\S]{0,40}\u0441\u043c\u044b\u0441\u043b|overall[\s\S]{0,40}(?:cash|money)[\s\S]{0,40}(?:meaning|sense)|money[\s\S]{0,40}(?:meaning|sense))/iu.test(
|
||||||
|
text
|
||||||
|
);
|
||||||
|
if (!(hasNetCue || (hasIncomingCue && hasOutgoingCue) || (hasOverallMoneyMeaningCue && hasOutgoingCue))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const entity = detectScopedCounterpartyEntity(text);
|
const entity = detectScopedCounterpartyEntity(text);
|
||||||
|
|
@ -553,6 +560,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
||||||
const explicitIntentCandidate =
|
const explicitIntentCandidate =
|
||||||
broadBusinessEvaluation?.family
|
broadBusinessEvaluation?.family
|
||||||
? null
|
? null
|
||||||
|
: counterpartyBidirectionalValueFlow?.family
|
||||||
|
? null
|
||||||
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||||
const unsupportedFamily = broadBusinessEvaluation?.family
|
const unsupportedFamily = broadBusinessEvaluation?.family
|
||||||
? broadBusinessEvaluation.family
|
? broadBusinessEvaluation.family
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,28 @@ describe("address follow-up temporal regressions", () => {
|
||||||
expect(result?.baseReasons).not.toContain("counterparty_carryover_suppressed_for_short_debt_mirror");
|
expect(result?.baseReasons).not.toContain("counterparty_carryover_suppressed_for_short_debt_mirror");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps explicit year period for selected counterparty payout follow-up", () => {
|
||||||
|
const result = runAddressDecomposeStage("а теперь сколько заплатили?", {
|
||||||
|
previous_intent: "customer_revenue_and_payments" as const,
|
||||||
|
target_intent: "customer_revenue_and_payments" as const,
|
||||||
|
previous_filters: {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "counterparty" as const,
|
||||||
|
previous_anchor_value: "Группа СВК",
|
||||||
|
resolved_counterparty_from_display: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("customer_revenue_and_payments");
|
||||||
|
expect(result?.filters.extracted_filters.counterparty).toBe("Группа СВК");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31");
|
||||||
|
expect(result?.baseReasons).toContain("period_from_followup_context");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps same-date inventory pivot anchored to the previous VAT date", () => {
|
it("keeps same-date inventory pivot anchored to the previous VAT date", () => {
|
||||||
const result = runAddressDecomposeStage("какие остатки по складу на эту же дату", {
|
const result = runAddressDecomposeStage("какие остатки по складу на эту же дату", {
|
||||||
previous_intent: "vat_payable_confirmed_as_of_date",
|
previous_intent: "vat_payable_confirmed_as_of_date",
|
||||||
|
|
@ -454,6 +476,29 @@ describe("address follow-up temporal regressions", () => {
|
||||||
expect(movements?.baseReasons).toContain("counterparty_from_followup_context");
|
expect(movements?.baseReasons).toContain("counterparty_from_followup_context");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps value-flow period for counterparty document evidence follow-up", () => {
|
||||||
|
const followupContext = {
|
||||||
|
previous_intent: "customer_revenue_and_payments" as const,
|
||||||
|
target_intent: "list_documents_by_counterparty" as const,
|
||||||
|
previous_filters: {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "counterparty" as const,
|
||||||
|
previous_anchor_value: "Группа СВК",
|
||||||
|
resolved_counterparty_from_display: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const documents = runAddressDecomposeStage("а по документам?", followupContext);
|
||||||
|
|
||||||
|
expect(documents?.intent.intent).toBe("list_documents_by_counterparty");
|
||||||
|
expect(documents?.filters.extracted_filters.counterparty).toBe("Группа СВК");
|
||||||
|
expect(documents?.filters.extracted_filters.period_from).toBe("2020-01-01");
|
||||||
|
expect(documents?.filters.extracted_filters.period_to).toBe("2020-12-31");
|
||||||
|
expect(documents?.baseReasons).toContain("period_from_followup_context");
|
||||||
|
});
|
||||||
|
|
||||||
it("replaces pronoun chain anchors from counterparty follow-up context", () => {
|
it("replaces pronoun chain anchors from counterparty follow-up context", () => {
|
||||||
const followupContext = {
|
const followupContext = {
|
||||||
previous_intent: "customer_revenue_and_payments" as const,
|
previous_intent: "customer_revenue_and_payments" as const,
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,16 @@ describe("addressIntentResolver regression bridges", () => {
|
||||||
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
|
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes agent margin-inspection follow-up to inventory margin instead of unknown", () => {
|
||||||
|
const result = resolveAddressIntent(
|
||||||
|
"\u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u043c\u0430\u0440\u0436\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043f\u043e \u0442\u043e\u0432\u0430\u0440\u0430\u043c \u0437\u0430 2020?"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result.intent).not.toBe("unknown");
|
||||||
|
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
|
||||||
|
});
|
||||||
|
|
||||||
it("detects bare historical inventory root with explicit month-year", () => {
|
it("detects bare historical inventory root with explicit month-year", () => {
|
||||||
const result = resolveAddressIntent("остатки на март 2016");
|
const result = resolveAddressIntent("остатки на март 2016");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,15 @@ describe("addressInventoryIntentSignals", () => {
|
||||||
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("classifies margin inspection wording from the phase106 replay as inventory margin", () => {
|
||||||
|
const result = resolveInventoryAddressIntent(
|
||||||
|
"\u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u043c\u0430\u0440\u0436\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043f\u043e \u0442\u043e\u0432\u0430\u0440\u0430\u043c \u0437\u0430 2020?"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||||
|
expect(result?.reasons).toContain("inventory_margin_ranking_signal_detected");
|
||||||
|
});
|
||||||
|
|
||||||
it("classifies selected-object purchase provenance wording through the extracted inventory owner", () => {
|
it("classifies selected-object purchase provenance wording through the extracted inventory owner", () => {
|
||||||
const result = resolveInventoryAddressIntent("selected object supplier provenance");
|
const result = resolveInventoryAddressIntent("selected object supplier provenance");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,68 @@ describe("address navigation state", () => {
|
||||||
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
|
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not store broad money role wording as a counterparty focus object", () => {
|
||||||
|
const base = createEmptyAddressNavigationState("asst-bo-money", "2026-04-12T10:00:00.000Z");
|
||||||
|
const organization = 'ООО "Альтернатива Плюс"';
|
||||||
|
const assistantItem = {
|
||||||
|
message_id: "msg-bo-money",
|
||||||
|
session_id: "asst-bo-money",
|
||||||
|
role: "assistant",
|
||||||
|
text:
|
||||||
|
"По компании ООО \"Альтернатива Плюс\" за 2020 получили 47 628 853,03 руб.; крупнейший небанковский входящий контрагент: Группа СВК.",
|
||||||
|
reply_type: "partial_coverage",
|
||||||
|
created_at: "2026-04-12T10:11:00.000Z",
|
||||||
|
trace_id: "address-bo-money",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "address_query",
|
||||||
|
detected_intent: "unknown",
|
||||||
|
selected_recipe: null,
|
||||||
|
extracted_filters: {
|
||||||
|
counterparty: "и главный поставщик в",
|
||||||
|
organization,
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
anchor_type: "unknown",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
mcp_discovery_selected_chain_id: "business_overview",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||||
|
entry_status: "bridge_executed",
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
explicit_organization_scope: organization,
|
||||||
|
explicit_date_scope: "2020"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
subject_candidates: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
decision: "continue_previous"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 2);
|
||||||
|
|
||||||
|
expect(evolved.result_sets[0]?.filters.counterparty).toBeUndefined();
|
||||||
|
expect(evolved.result_sets[0]?.filters.organization).toBe(organization);
|
||||||
|
expect(evolved.session_context.active_focus_object?.object_type).toBe("organization");
|
||||||
|
expect(evolved.session_context.active_focus_object?.label).toBe(organization);
|
||||||
|
expect(evolved.navigation_history[0]?.target_object_id).toBe(`organization:${organization}`.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
it("captures counterparty focus from applied discovery turns even when the response stayed in chat mode", () => {
|
it("captures counterparty focus from applied discovery turns even when the response stayed in chat mode", () => {
|
||||||
const base = createEmptyAddressNavigationState("asst-vf", "2026-04-12T10:00:00.000Z");
|
const base = createEmptyAddressNavigationState("asst-vf", "2026-04-12T10:00:00.000Z");
|
||||||
const assistantItem = {
|
const assistantItem = {
|
||||||
|
|
@ -158,6 +220,262 @@ describe("address navigation state", () => {
|
||||||
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
|
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("captures resolved entity-resolution focus as fresh current discovery state", () => {
|
||||||
|
const base = createEmptyAddressNavigationState("asst-entity", "2026-04-12T10:00:00.000Z");
|
||||||
|
const assistantItem = {
|
||||||
|
message_id: "msg-entity",
|
||||||
|
session_id: "asst-entity",
|
||||||
|
role: "assistant",
|
||||||
|
text: "В проверенном каталожном срезе 1С найден наиболее вероятный контрагент: Группа СВК.",
|
||||||
|
created_at: "2026-04-12T10:12:30.000Z",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "chat",
|
||||||
|
detected_intent: "unknown",
|
||||||
|
extracted_filters: {},
|
||||||
|
anchor_type: "unknown",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
mcp_discovery_selected_chain_id: "entity_resolution",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "entity_resolution",
|
||||||
|
asked_action_family: "search_business_entity",
|
||||||
|
explicit_entity_candidates: ["СВК"],
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "entity_resolution_search_v1",
|
||||||
|
derived_entity_resolution: {
|
||||||
|
requested_entity: "СВК",
|
||||||
|
resolution_status: "resolved",
|
||||||
|
resolved_entity: "Группа СВК"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
decision: "new_topic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 1);
|
||||||
|
|
||||||
|
expect(evolved.session_context.active_focus_object?.object_type).toBe("counterparty");
|
||||||
|
expect(evolved.session_context.active_focus_object?.label).toBe("Группа СВК");
|
||||||
|
expect(evolved.result_sets[0]?.filters.counterparty).toBe("Группа СВК");
|
||||||
|
expect(evolved.navigation_history[0]?.target_object_id).toBe("counterparty:группа свк");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures single-direction value-flow focus before the net bundle exists", () => {
|
||||||
|
const base = createEmptyAddressNavigationState("asst-vf-single", "2026-04-12T10:00:00.000Z");
|
||||||
|
const assistantItem = {
|
||||||
|
message_id: "msg-vf-single",
|
||||||
|
session_id: "asst-vf-single",
|
||||||
|
role: "assistant",
|
||||||
|
text: "Входящие денежные поступления по контрагенту Группа СВК за период 2020: 12 093 465 руб.",
|
||||||
|
created_at: "2026-04-12T10:13:00.000Z",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "chat",
|
||||||
|
detected_intent: "unknown",
|
||||||
|
extracted_filters: {
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
anchor_type: "unknown",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
mcp_discovery_selected_chain_id: "value_flow",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "turnover",
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||||
|
derived_value_flow: {
|
||||||
|
value_flow_direction: "incoming_customer_revenue",
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_scope: "2020",
|
||||||
|
total_amount_human_ru: "12 093 465 руб."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
decision: "continue_previous"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 2);
|
||||||
|
|
||||||
|
expect(evolved.result_sets[0]?.intent).toBe("customer_revenue_and_payments");
|
||||||
|
expect(evolved.result_sets[0]?.filters.counterparty).toBe("Группа СВК");
|
||||||
|
expect(evolved.session_context.active_focus_object?.label).toBe("Группа СВК");
|
||||||
|
expect(evolved.session_context.date_scope.period_from).toBe("2020-01-01");
|
||||||
|
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores bidirectional value-flow proof bundle for adjacent evidence drilldowns", () => {
|
||||||
|
const base = createEmptyAddressNavigationState("asst-vf-bundle", "2026-04-12T10:00:00.000Z");
|
||||||
|
const valueFlowBundle = {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_scope: "2020",
|
||||||
|
incoming_customer_revenue: {
|
||||||
|
total_amount_human_ru: "12 093 465 руб."
|
||||||
|
},
|
||||||
|
outgoing_supplier_payout: {
|
||||||
|
total_amount_human_ru: "0 руб."
|
||||||
|
},
|
||||||
|
net_amount_human_ru: "12 093 465 руб.",
|
||||||
|
net_direction: "net_incoming"
|
||||||
|
};
|
||||||
|
const netItem = {
|
||||||
|
message_id: "msg-vf-net",
|
||||||
|
session_id: "asst-vf-bundle",
|
||||||
|
role: "assistant",
|
||||||
|
text: "По контрагенту Группа СВК за период 2020 получили 12 093 465 руб., заплатили 0 руб.; расчетное нетто в нашу сторону: 12 093 465 руб.",
|
||||||
|
created_at: "2026-04-12T10:14:00.000Z",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "chat",
|
||||||
|
detected_intent: "customer_revenue_and_payments",
|
||||||
|
extracted_filters: {
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
anchor_type: "unknown",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
mcp_discovery_selected_chain_id: "value_flow_comparison",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
|
||||||
|
derived_bidirectional_value_flow: valueFlowBundle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
decision: "continue_previous"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const afterNet = evolveAddressNavigationStateWithAssistantItem(base, netItem, 5);
|
||||||
|
expect(afterNet.session_context.comparison_scope?.counterparty?.label).toBe("Группа СВК");
|
||||||
|
expect(afterNet.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
|
||||||
|
valueFlowBundle
|
||||||
|
);
|
||||||
|
|
||||||
|
const docsItem = {
|
||||||
|
message_id: "msg-vf-docs",
|
||||||
|
session_id: "asst-vf-bundle",
|
||||||
|
role: "assistant",
|
||||||
|
text: "По документам по контрагенту Группа СВК за 2020 полный срез не подтвержден.",
|
||||||
|
created_at: "2026-04-12T10:15:00.000Z",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "chat",
|
||||||
|
detected_intent: "list_documents_by_counterparty",
|
||||||
|
extracted_filters: {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
anchor_type: "unknown",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
mcp_discovery_selected_chain_id: "document_evidence",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "documents",
|
||||||
|
asked_action_family: "list_documents",
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_document_evidence_query_documents_v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
decision: "continue_previous"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const afterDocs = evolveAddressNavigationStateWithAssistantItem(afterNet, docsItem, 6);
|
||||||
|
expect(afterDocs.session_context.active_focus_object?.label).toBe("Группа СВК");
|
||||||
|
expect(afterDocs.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
|
||||||
|
valueFlowBundle
|
||||||
|
);
|
||||||
|
|
||||||
|
const movementsItem = {
|
||||||
|
message_id: "msg-vf-movements",
|
||||||
|
session_id: "asst-vf-bundle",
|
||||||
|
role: "assistant",
|
||||||
|
text: "По движениям по контрагенту Группа СВК за 2020 полный срез не подтвержден.",
|
||||||
|
created_at: "2026-04-12T10:16:00.000Z",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "chat",
|
||||||
|
detected_intent: "inventory_purchase_documents_for_item",
|
||||||
|
extracted_filters: {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
anchor_type: "unknown",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
mcp_discovery_selected_chain_id: "movement_evidence",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "movements",
|
||||||
|
asked_action_family: "list_movements",
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_movement_evidence_query_movements_v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
decision: "continue_previous"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const afterMovements = evolveAddressNavigationStateWithAssistantItem(afterDocs, movementsItem, 7);
|
||||||
|
expect(afterMovements.result_sets.at(-1)?.intent).toBe("bank_operations_by_counterparty");
|
||||||
|
expect(afterMovements.result_sets.at(-1)?.type).toBe("bank_operations_list");
|
||||||
|
expect(afterMovements.session_context.active_focus_object?.label).toBe("Группа СВК");
|
||||||
|
expect(afterMovements.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
|
||||||
|
valueFlowBundle
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps selected counterparty focus for company boundary summaries", () => {
|
it("keeps selected counterparty focus for company boundary summaries", () => {
|
||||||
const initial = createEmptyAddressNavigationState("asst-boundary", "2026-04-12T10:00:00.000Z");
|
const initial = createEmptyAddressNavigationState("asst-boundary", "2026-04-12T10:00:00.000Z");
|
||||||
const assistantItem = {
|
const assistantItem = {
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,33 @@ describe("assistantContinuityPolicy organization authority", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not expose broad money role fragments as counterparty carryover", () => {
|
||||||
|
const debug = {
|
||||||
|
execution_lane: "address_query",
|
||||||
|
detected_intent: "business_overview",
|
||||||
|
extracted_filters: {
|
||||||
|
counterparty: "и главный поставщик в",
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
anchor_type: "counterparty",
|
||||||
|
anchor_value_raw: "и главный поставщик в",
|
||||||
|
anchor_value_resolved: "и главный поставщик в"
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(readAddressDebugCounterparty(debug)).toBeNull();
|
||||||
|
expect(resolveAddressDebugCarryoverFilters(debug)).toEqual({
|
||||||
|
organization: 'ООО "Альтернатива Плюс"',
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
});
|
||||||
|
expect(resolveAddressDebugAnchorContext(debug)).toEqual({
|
||||||
|
anchorType: "counterparty",
|
||||||
|
anchorValue: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("hydrates intent and carryover filters from grounded MCP discovery payout scope", () => {
|
it("hydrates intent and carryover filters from grounded MCP discovery payout scope", () => {
|
||||||
const debug = {
|
const debug = {
|
||||||
execution_lane: "living_chat",
|
execution_lane: "living_chat",
|
||||||
|
|
@ -657,6 +684,27 @@ describe("assistantContinuityPolicy organization authority", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("carries value-flow temporal scope into follow-up evidence drilldowns", () => {
|
||||||
|
const filters = applyTemporalCarryoverFilters(
|
||||||
|
{
|
||||||
|
counterparty: "Группа СВК"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
as_of_date: null,
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
"customer_revenue_and_payments"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(filters).toEqual({
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves inventory root pivots through one shared helper", () => {
|
it("resolves inventory root pivots through one shared helper", () => {
|
||||||
const flags = resolveInventoryFollowupPivotFlags(
|
const flags = resolveInventoryFollowupPivotFlags(
|
||||||
{ intent: "inventory_on_hand_as_of_date" },
|
{ intent: "inventory_on_hand_as_of_date" },
|
||||||
|
|
|
||||||
|
|
@ -130,13 +130,14 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||||
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
|
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
|
||||||
|
|
||||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||||
const answerText = [draft.headline, ...draft.confirmed_lines, ...draft.unknown_lines].join("\n");
|
const answerText = [draft.headline, ...draft.confirmed_lines, ...draft.inference_lines, ...draft.unknown_lines].join("\n");
|
||||||
|
|
||||||
expect(draft.answer_mode).toBe("checked_sources_only");
|
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||||
expect(answerText).toContain("0 руб.");
|
expect(answerText).toContain("0 руб.");
|
||||||
expect(answerText).toContain("исходящих платежей/списаний не найдено");
|
expect(answerText).toContain("not_found");
|
||||||
expect(answerText).toContain("Группа СВК");
|
expect(answerText).toContain("Группа СВК");
|
||||||
expect(answerText).toContain("2020");
|
expect(answerText).toContain("2020");
|
||||||
|
expect(draft.unknown_lines.join("\n")).toContain("outside the checked period");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("turns generic document evidence into a bounded document answer draft", async () => {
|
it("turns generic document evidence into a bounded document answer draft", async () => {
|
||||||
|
|
@ -1462,7 +1463,7 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||||
expect(draft.unknown_lines).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot");
|
expect(draft.unknown_lines).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders zero-row supplier payout as a checked negative with period and counterparty", async () => {
|
it("renders zero-row supplier payout as a confirmed scoped zero with period and counterparty", async () => {
|
||||||
const planner = planAssistantMcpDiscovery({
|
const planner = planAssistantMcpDiscovery({
|
||||||
turnMeaning: {
|
turnMeaning: {
|
||||||
asked_domain_family: "counterparty_value",
|
asked_domain_family: "counterparty_value",
|
||||||
|
|
@ -1477,18 +1478,18 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||||
|
|
||||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||||
const confirmedText = draft.confirmed_lines.join("\n");
|
const confirmedText = draft.confirmed_lines.join("\n");
|
||||||
|
const inferenceText = draft.inference_lines.join("\n");
|
||||||
const unknownText = draft.unknown_lines.join("\n");
|
const unknownText = draft.unknown_lines.join("\n");
|
||||||
|
|
||||||
expect(draft.answer_mode).toBe("checked_sources_only");
|
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||||
expect(draft.headline).toContain("Группа СВК");
|
expect(draft.headline).toContain("Группа СВК");
|
||||||
expect(draft.headline).toContain("2020");
|
expect(draft.headline).toContain("2020");
|
||||||
expect(draft.headline).toContain("исходящих платежей");
|
expect(draft.headline).toContain("0 руб.");
|
||||||
expect(confirmedText).toContain("Группа СВК");
|
expect(confirmedText).toContain("Группа СВК");
|
||||||
expect(confirmedText).toContain("ООО Альтернатива Плюс");
|
expect(confirmedText).toContain("0 руб.");
|
||||||
expect(confirmedText).toContain("2020");
|
expect(confirmedText).toContain("Граница");
|
||||||
expect(confirmedText).toContain("не найдено");
|
expect(inferenceText).toContain("not_found result was treated as zero");
|
||||||
expect(unknownText).toContain("вне периода 2020");
|
expect(unknownText).toContain("outside the checked period");
|
||||||
expect(unknownText).toContain("вне доступного банковского контура");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("turns bidirectional value-flow evidence into a bounded net cash answer draft", async () => {
|
it("turns bidirectional value-flow evidence into a bounded net cash answer draft", async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
|
import iconv from "iconv-lite";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { buildAssistantMcpDiscoveryDataNeedGraph } from "../src/services/assistantMcpDiscoveryDataNeedGraph";
|
import { buildAssistantMcpDiscoveryDataNeedGraph } from "../src/services/assistantMcpDiscoveryDataNeedGraph";
|
||||||
|
|
||||||
|
function toWin1251Mojibake(value: string): string {
|
||||||
|
return iconv.decode(Buffer.from(value, "utf8"), "win1251");
|
||||||
|
}
|
||||||
|
|
||||||
describe("assistant MCP discovery data need graph", () => {
|
describe("assistant MCP discovery data need graph", () => {
|
||||||
it("builds a monthly bidirectional value-flow graph from grounded turn meaning", () => {
|
it("builds a monthly bidirectional value-flow graph from grounded turn meaning", () => {
|
||||||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||||
|
|
@ -403,6 +408,59 @@ describe("assistant MCP discovery data need graph", () => {
|
||||||
expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing");
|
expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps a wide formal business overview on the business-overview graph even when money net is requested", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||||
|
semanticDataNeed: "business overview evidence with bounded analyst interpretation",
|
||||||
|
rawUtterance:
|
||||||
|
"\u0414\u0430\u0439 \u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u0431\u0437\u043e\u0440 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020 \u0433\u043e\u0434 \u043f\u043e \u0434\u0430\u043d\u043d\u044b\u043c 1\u0421: \u043e\u0431\u043e\u0440\u043e\u0442\u044b, \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u044c\u0433\u0438, \u043d\u0435\u0442\u0442\u043e, \u041d\u0414\u0421, \u0434\u043e\u043b\u0433\u0438, \u0441\u043a\u043b\u0430\u0434, \u043a\u043b\u0438\u0435\u043d\u0442\u044b, \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0438 \u0438 \u0447\u0442\u043e \u043f\u043e\u043a\u0430 \u043d\u0435\u043b\u044c\u0437\u044f \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0442\u044c.",
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
explicit_organization_scope:
|
||||||
|
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.business_fact_family).toBe("business_overview");
|
||||||
|
expect(result.action_family).toBe("broad_evaluation");
|
||||||
|
expect(result.comparison_need).toBeNull();
|
||||||
|
expect(result.subject_candidates).toEqual([]);
|
||||||
|
expect(result.reason_codes).toContain("data_need_graph_business_overview_wide_surface_preserved");
|
||||||
|
expect(result.reason_codes).not.toContain(
|
||||||
|
"data_need_graph_business_overview_bidirectional_money_comparison_routed_to_value_flow"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a wide formal business overview when the runtime signal is win1251 mojibake", () => {
|
||||||
|
const wideQuestion =
|
||||||
|
"\u0414\u0430\u0439 \u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u0431\u0437\u043e\u0440 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 2020 \u0433\u043e\u0434 \u043f\u043e \u0434\u0430\u043d\u043d\u044b\u043c 1\u0421: \u043e\u0431\u043e\u0440\u043e\u0442\u044b, \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u044c\u0433\u0438, \u043d\u0435\u0442\u0442\u043e, \u041d\u0414\u0421, \u0434\u043e\u043b\u0433\u0438, \u0441\u043a\u043b\u0430\u0434, \u043a\u043b\u0438\u0435\u043d\u0442\u044b, \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0438 \u0438 \u0447\u0442\u043e \u043f\u043e\u043a\u0430 \u043d\u0435\u043b\u044c\u0437\u044f \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0442\u044c.";
|
||||||
|
const legacyQuestion = toWin1251Mojibake(wideQuestion);
|
||||||
|
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||||
|
semanticDataNeed: "business overview evidence with bounded analyst interpretation",
|
||||||
|
rawUtterance: legacyQuestion,
|
||||||
|
turnMeaning: {
|
||||||
|
raw_message: legacyQuestion,
|
||||||
|
effective_message: legacyQuestion,
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
explicit_organization_scope:
|
||||||
|
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.business_fact_family).toBe("business_overview");
|
||||||
|
expect(result.action_family).toBe("broad_evaluation");
|
||||||
|
expect(result.comparison_need).toBeNull();
|
||||||
|
expect(result.reason_codes).toContain("data_need_graph_business_overview_wide_surface_preserved");
|
||||||
|
expect(result.reason_codes).not.toContain(
|
||||||
|
"data_need_graph_business_overview_bidirectional_money_comparison_routed_to_value_flow"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => {
|
it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => {
|
||||||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||||
semanticDataNeed: "counterparty value-flow evidence",
|
semanticDataNeed: "counterparty value-flow evidence",
|
||||||
|
|
|
||||||
|
|
@ -1650,6 +1650,38 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||||
expect(String(call?.query ?? "")).toContain("СписаниеСРасчетногоСчета");
|
expect(String(call?.query ?? "")).toContain("СписаниеСРасчетногоСчета");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats zero-row supplier payout query as confirmed not-found inside checked scope", async () => {
|
||||||
|
const planner = planAssistantMcpDiscovery({
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "payout",
|
||||||
|
explicit_entity_candidates: ["SVK"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const deps = buildDeps([]);
|
||||||
|
|
||||||
|
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||||
|
|
||||||
|
expect(result.pilot_status).toBe("executed");
|
||||||
|
expect(result.pilot_scope).toBe("counterparty_supplier_payout_query_movements_v1");
|
||||||
|
expect(result.evidence.evidence_status).toBe("confirmed");
|
||||||
|
expect(result.evidence.answer_permission).toBe("confirmed_answer");
|
||||||
|
expect(result.evidence.confirmed_facts[0]).toContain("not_found");
|
||||||
|
expect(result.reason_codes).toContain("pilot_derived_value_flow_from_confirmed_rows");
|
||||||
|
expect(result.evidence.reason_codes).toContain("confirmed_no_match_fact_with_allowed_mcp_evidence");
|
||||||
|
expect(result.derived_value_flow).toMatchObject({
|
||||||
|
value_flow_direction: "outgoing_supplier_payout",
|
||||||
|
counterparty: "SVK",
|
||||||
|
period_scope: "2020",
|
||||||
|
rows_matched: 0,
|
||||||
|
rows_with_amount: 0,
|
||||||
|
total_amount: 0,
|
||||||
|
total_amount_human_ru: "0 руб."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("marks value-flow coverage as limited when the probe row limit is reached", async () => {
|
it("marks value-flow coverage as limited when the probe row limit is reached", async () => {
|
||||||
const planner = planAssistantMcpDiscovery({
|
const planner = planAssistantMcpDiscovery({
|
||||||
turnMeaning: {
|
turnMeaning: {
|
||||||
|
|
|
||||||
|
|
@ -1132,6 +1132,55 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(candidate.reply_text).not.toContain("generic advice");
|
expect(candidate.reply_text).not.toContain("generic advice");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not render negative next-check instruction as organization scope", () => {
|
||||||
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
|
entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
turn_meaning_ref: {
|
||||||
|
raw_message:
|
||||||
|
"\u0447\u0442\u043e \u0434\u0430\u043b\u044c\u0448\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043d\u044f\u0442\u044c \u0437\u0434\u043e\u0440\u043e\u0432\u044c\u0435 \u0431\u0438\u0437\u043d\u0435\u0441\u0430, \u0431\u0435\u0437 \u0433\u0430\u0434\u0430\u043d\u0438\u0439?",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
explicit_organization_scope: "\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
ranking_need: null,
|
||||||
|
reason_codes: ["data_need_graph_family_business_overview"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "business_overview_route_template_v1",
|
||||||
|
derived_business_overview: {
|
||||||
|
incoming_customer_revenue: { total_amount_human_ru: "47 628 853,03 руб." },
|
||||||
|
outgoing_supplier_payout: { total_amount_human_ru: "43 763 351,53 руб." },
|
||||||
|
net_amount_human_ru: "3 865 501,50 руб.",
|
||||||
|
net_direction: "net_incoming"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "generic advice should not leak",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(candidate.reply_text).toContain("\u0412 \u043f\u0435\u0440\u0432\u0443\u044e \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438");
|
||||||
|
expect(candidate.reply_text).not.toContain("\u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
|
it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
|
||||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||||
entryPoint({
|
entryPoint({
|
||||||
|
|
@ -1553,8 +1602,9 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(firstLine).toContain("По подтвержденным строкам 1С");
|
expect(firstLine).toContain("По подтвержденным строкам 1С");
|
||||||
expect(firstLine).toContain("входящие 157 192 981,43 руб.");
|
expect(firstLine).toContain("входящие 157 192 981,43 руб.");
|
||||||
expect(firstLine).toContain("исходящие 35 439 044,74 руб.");
|
expect(firstLine).toContain("исходящие 35 439 044,74 руб.");
|
||||||
expect(firstLine).toContain("не аудиторское заключение");
|
expect(firstLine).toContain("не финансовый аудит");
|
||||||
expect(firstLine).toContain("не подтвержденная чистая прибыль");
|
expect(firstLine).toContain("не подтверждение чистой прибыли");
|
||||||
|
expect(firstLine).not.toContain("аудиторское заключение");
|
||||||
expect(candidate.reply_text).toContain("Интерпретация:");
|
expect(candidate.reply_text).toContain("Интерпретация:");
|
||||||
expect(candidate.reply_text).toContain("Что видно в ограниченном денежном срезе");
|
expect(candidate.reply_text).toContain("Что видно в ограниченном денежном срезе");
|
||||||
expect(candidate.reply_text).toContain("Что не подтверждено в этом срезе");
|
expect(candidate.reply_text).toContain("Что не подтверждено в этом срезе");
|
||||||
|
|
|
||||||
|
|
@ -525,6 +525,177 @@ describe("assistant MCP discovery response policy", () => {
|
||||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses a business-overview boundary clarification over a stale inventory-item clarification", () => {
|
||||||
|
const valueFlowBundle = {
|
||||||
|
counterparty: "Group SVK",
|
||||||
|
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 rub." },
|
||||||
|
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 rub." },
|
||||||
|
net_amount_human_ru: "18 523 839 rub.",
|
||||||
|
net_direction: "net_incoming"
|
||||||
|
};
|
||||||
|
const documentBundle = { counterparty: "Group SVK", document_count: 19 };
|
||||||
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply: "Need item before I can continue.",
|
||||||
|
currentReplySource: "address_query_runtime_v1",
|
||||||
|
currentReplyType: "partial_coverage",
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
detected_intent: "inventory_purchase_documents_for_item",
|
||||||
|
selected_recipe: "address_inventory_purchase_documents_for_item_v1",
|
||||||
|
capability_id: "inventory_inventory_purchase_documents_for_item",
|
||||||
|
truth_mode: "clarification_required",
|
||||||
|
answer_shape: "clarification_required",
|
||||||
|
missing_required_filters: ["item"],
|
||||||
|
answer_shape_contract: {
|
||||||
|
answer_shape: "clarification_required",
|
||||||
|
reply_type: "partial_coverage",
|
||||||
|
capability_contract_id: "inventory_inventory_purchase_documents_for_item"
|
||||||
|
},
|
||||||
|
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
should_run_discovery: true,
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
business_overview_separate_entity_candidates: ["Group SVK"],
|
||||||
|
previous_counterparty_value_flow_bundle: valueFlowBundle,
|
||||||
|
previous_counterparty_document_bundle: documentBundle,
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
action_family: "broad_evaluation",
|
||||||
|
clarification_gaps: ["organization"],
|
||||||
|
reason_codes: ["data_need_graph_family_business_overview"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "needs_clarification",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: false,
|
||||||
|
requires_user_clarification: true,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "needs_clarification",
|
||||||
|
headline: "Specify organization.",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: "Specify organization."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.applied).toBe(true);
|
||||||
|
expect(result.decision).toBe("apply_candidate");
|
||||||
|
expect(result.reply_text).toContain("Group SVK");
|
||||||
|
expect(result.reply_text).toContain("20 653 490 rub.");
|
||||||
|
expect(result.reply_text).toContain("19");
|
||||||
|
expect(result.reply_text).not.toContain("Need item");
|
||||||
|
expect(result.reason_codes).toContain(
|
||||||
|
"mcp_discovery_response_policy_business_overview_boundary_candidate_priority_over_inventory_item_clarification"
|
||||||
|
);
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses a ready business-overview boundary answer over a stale inventory-item clarification", () => {
|
||||||
|
const valueFlowBundle = {
|
||||||
|
counterparty: "Group SVK",
|
||||||
|
incoming_customer_revenue: { total_amount_human_ru: "20 653 490 rub." },
|
||||||
|
outgoing_supplier_payout: { total_amount_human_ru: "2 129 651 rub." },
|
||||||
|
net_amount_human_ru: "18 523 839 rub.",
|
||||||
|
net_direction: "net_incoming"
|
||||||
|
};
|
||||||
|
const documentBundle = { counterparty: "Group SVK", document_count: 19 };
|
||||||
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply: "Need item before I can continue.",
|
||||||
|
currentReplySource: "address_query_runtime_v1",
|
||||||
|
currentReplyType: "partial_coverage",
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
detected_intent: "inventory_purchase_documents_for_item",
|
||||||
|
selected_recipe: "address_inventory_purchase_documents_for_item_v1",
|
||||||
|
capability_id: "inventory_inventory_purchase_documents_for_item",
|
||||||
|
truth_mode: "clarification_required",
|
||||||
|
answer_shape: "clarification_required",
|
||||||
|
missing_required_filters: ["item"],
|
||||||
|
answer_shape_contract: {
|
||||||
|
answer_shape: "clarification_required",
|
||||||
|
reply_type: "partial_coverage",
|
||||||
|
capability_contract_id: "inventory_inventory_purchase_documents_for_item"
|
||||||
|
},
|
||||||
|
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||||
|
hot_runtime_wired: true,
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
should_run_discovery: true,
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
explicit_organization_scope: "Alternative Plus",
|
||||||
|
business_overview_separate_entity_candidates: ["Group SVK"],
|
||||||
|
previous_counterparty_value_flow_bundle: valueFlowBundle,
|
||||||
|
previous_counterparty_document_bundle: documentBundle,
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "business_overview",
|
||||||
|
action_family: "broad_evaluation",
|
||||||
|
clarification_gaps: [],
|
||||||
|
reason_codes: ["data_need_graph_family_business_overview"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
hot_runtime_wired: true,
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
execution_handoff: {
|
||||||
|
mcp_execution_performed: true,
|
||||||
|
can_use_guarded_response: true,
|
||||||
|
must_keep_internal_mechanics_hidden: true
|
||||||
|
},
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "business_overview_route_template_v1",
|
||||||
|
mcp_execution_performed: true,
|
||||||
|
derived_business_overview: {
|
||||||
|
period_scope: "all_time",
|
||||||
|
incoming_customer_revenue: { total_amount_human_ru: "285 819 547 rub." },
|
||||||
|
outgoing_supplier_payout: { total_amount_human_ru: "137 963 720 rub." },
|
||||||
|
net_amount_human_ru: "147 855 827 rub.",
|
||||||
|
net_direction: "net_incoming",
|
||||||
|
top_customers: [],
|
||||||
|
top_suppliers: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "confirmed_with_bounded_inference",
|
||||||
|
headline: "Company summary.",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: [],
|
||||||
|
unknown_lines: [],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.applied).toBe(true);
|
||||||
|
expect(result.decision).toBe("apply_candidate");
|
||||||
|
expect(result.reply_text).toContain("Alternative Plus");
|
||||||
|
expect(result.reply_text).toContain("Group SVK");
|
||||||
|
expect(result.reply_text).toContain("20 653 490 rub.");
|
||||||
|
expect(result.reply_text).not.toContain("Need item");
|
||||||
|
expect(result.reason_codes).toContain(
|
||||||
|
"mcp_discovery_response_policy_business_overview_boundary_candidate_priority_over_inventory_item_clarification"
|
||||||
|
);
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
||||||
|
});
|
||||||
|
|
||||||
it("overrides exact document-list replies for explicit counterparty received-paid-net questions", () => {
|
it("overrides exact document-list replies for explicit counterparty received-paid-net questions", () => {
|
||||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
currentReply: "Контрагент: Группа СВК. Найдено документов: 19.",
|
currentReply: "Контрагент: Группа СВК. Найдено документов: 19.",
|
||||||
|
|
@ -586,6 +757,72 @@ describe("assistant MCP discovery response policy", () => {
|
||||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_document_list_address_reply");
|
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_document_list_address_reply");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("overrides exact document-list replies when they contradict the discovery period scope", () => {
|
||||||
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply:
|
||||||
|
"Контрагент: Группа СВК. Найдено документов: 19.\n" +
|
||||||
|
"1. 2021-11-10T12:00:07Z | Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07 | аналитика: Группа СВК",
|
||||||
|
currentReplySource: "address_query_runtime_v1",
|
||||||
|
currentReplyType: "factual",
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
detected_intent: "list_documents_by_counterparty",
|
||||||
|
selected_recipe: "address_documents_by_counterparty_v1",
|
||||||
|
mcp_call_status: "matched_non_empty",
|
||||||
|
response_type: "FACTUAL_LIST",
|
||||||
|
truth_mode: "confirmed",
|
||||||
|
extracted_filters: {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
as_of_date: "2026-06-15"
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
target_intent: "list_documents_by_counterparty"
|
||||||
|
},
|
||||||
|
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
should_run_discovery: true,
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "documents",
|
||||||
|
asked_action_family: "list_documents",
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "document_evidence"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "document_evidence",
|
||||||
|
action_family: "list_documents",
|
||||||
|
subject_candidates: ["Группа СВК"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "bounded_inference_only",
|
||||||
|
headline: "По документам по контрагенту Группа СВК за 2020 полный срез не подтвержден.",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: ["Подтвержденных строк документов за 2020 этим поиском не найдено."],
|
||||||
|
unknown_lines: ["Полный исторический срез вне 2020 этим поиском не подтвержден."],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.applied).toBe(true);
|
||||||
|
expect(result.decision).toBe("apply_candidate");
|
||||||
|
expect(result.reply_text).toContain("за 2020");
|
||||||
|
expect(result.reply_text).not.toContain("2021-11-10");
|
||||||
|
expect(result.reason_codes).toContain(
|
||||||
|
"mcp_discovery_response_policy_document_list_temporal_conflict_allows_candidate_override"
|
||||||
|
);
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_document_list_address_reply");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
|
it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
|
||||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",
|
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,42 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||||
expect(result.reason_codes).not.toContain("mcp_discovery_payout_signal_detected");
|
expect(result.reason_codes).not.toContain("mcp_discovery_payout_signal_detected");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats supplier payout plus overall money meaning as bidirectional value-flow discovery", () => {
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage:
|
||||||
|
"А теперь поставщику Группа СВК за 2020 сколько мы ему заплатили и какой общий денежный смысл?",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_entity_candidates: [{ value: "группа свк" }],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
},
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: { counterparty: "Группа СВК" },
|
||||||
|
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.adapter_status).toBe("ready");
|
||||||
|
expect(result.turn_meaning_ref).toMatchObject({
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_entity_candidates: ["группа свк"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
});
|
||||||
|
expect(result.data_need_graph).toMatchObject({
|
||||||
|
business_fact_family: "value_flow",
|
||||||
|
action_family: "net_value_flow",
|
||||||
|
comparison_need: "incoming_vs_outgoing"
|
||||||
|
});
|
||||||
|
expect(result.reason_codes).toContain("mcp_discovery_bidirectional_value_flow_signal_detected");
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_payout_signal_detected");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps explicit counterparty money-flow basis questions executable without requiring a period", () => {
|
it("keeps explicit counterparty money-flow basis questions executable without requiring a period", () => {
|
||||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
userMessage: "How much money passed with SVK, incoming and outgoing, and what documents or movements prove it?",
|
userMessage: "How much money passed with SVK, incoming and outgoing, and what documents or movements prove it?",
|
||||||
|
|
@ -4076,4 +4112,39 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||||
);
|
);
|
||||||
expect(result.reason_codes).not.toContain("mcp_discovery_business_overview_suppressed_stale_counterparty");
|
expect(result.reason_codes).not.toContain("mcp_discovery_business_overview_suppressed_stale_counterparty");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses retained organization scope instead of negative next-check instruction text", () => {
|
||||||
|
const orgName = "\u041e\u041e\u041e \"\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441\"";
|
||||||
|
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||||
|
userMessage:
|
||||||
|
"\u0447\u0442\u043e \u0434\u0430\u043b\u044c\u0448\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043d\u044f\u0442\u044c \u0437\u0434\u043e\u0440\u043e\u0432\u044c\u0435 \u0431\u0438\u0437\u043d\u0435\u0441\u0430, \u0431\u0435\u0437 \u0433\u0430\u0434\u0430\u043d\u0438\u0439?",
|
||||||
|
assistantTurnMeaning: {
|
||||||
|
asked_domain_family: "business_overview",
|
||||||
|
asked_action_family: "broad_evaluation",
|
||||||
|
explicit_organization_scope: "\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439",
|
||||||
|
unsupported_but_understood_family: "broad_business_evaluation",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
},
|
||||||
|
predecomposeContract: {
|
||||||
|
entities: {
|
||||||
|
counterparty: "\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
followupContext: {
|
||||||
|
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||||||
|
previous_filters: { organization: orgName },
|
||||||
|
root_filters: { organization: orgName },
|
||||||
|
previous_anchor_type: "organization",
|
||||||
|
previous_anchor_value: orgName,
|
||||||
|
previous_intent: "business_overview",
|
||||||
|
root_intent: "business_overview"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe(orgName);
|
||||||
|
expect(result.turn_meaning_ref?.explicit_organization_scope).not.toBe(
|
||||||
|
"\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439"
|
||||||
|
);
|
||||||
|
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,31 @@ describe("assistantTurnMeaningPolicy", () => {
|
||||||
expect(meaning.stale_replay_forbidden).toBe(true);
|
expect(meaning.stale_replay_forbidden).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats payout plus overall money meaning as bidirectional value-flow", () => {
|
||||||
|
const policy = buildPolicy({
|
||||||
|
resolveAddressIntent: () => ({ intent: "supplier_payouts_profile", confidence: "high" })
|
||||||
|
});
|
||||||
|
|
||||||
|
const meaning = policy.resolveAssistantTurnMeaning({
|
||||||
|
rawUserMessage:
|
||||||
|
"\u0410 \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0443 \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u0437\u0430 2020: \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u044b \u0435\u043c\u0443 \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438 \u0438 \u043a\u0430\u043a\u043e\u0439 \u043e\u0431\u0449\u0438\u0439 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u0441\u043c\u044b\u0441\u043b?"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(meaning.explicit_intent_candidate).toBeNull();
|
||||||
|
expect(meaning.asked_domain_family).toBe("counterparty_value");
|
||||||
|
expect(meaning.asked_action_family).toBe("net_value_flow");
|
||||||
|
expect(meaning.unsupported_but_understood_family).toBe("counterparty_bidirectional_value_flow_or_netting");
|
||||||
|
expect(meaning.explicit_entity_candidates).toEqual([
|
||||||
|
{
|
||||||
|
type: "counterparty",
|
||||||
|
value: "\u0433\u0440\u0443\u043f\u043f\u0430 \u0441\u0432\u043a",
|
||||||
|
source: "current_turn_loose_entity_tail"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(meaning.reason_codes).toContain("counterparty_bidirectional_value_flow_current_turn_signal");
|
||||||
|
expect(meaning.stale_replay_forbidden).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("ignores temporal tail words in all-time revenue ranking questions", () => {
|
it("ignores temporal tail words in all-time revenue ranking questions", () => {
|
||||||
const policy = buildPolicy({
|
const policy = buildPolicy({
|
||||||
resolveAddressIntent: (text: string) =>
|
resolveAddressIntent: (text: string) =>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,170 @@
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"generation_id": "gen-ag06151952-5d8f12",
|
||||||
|
"created_at": "2026-06-15T19:52:40+00:00",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "AGENT | Phase107 mixed planner brain dogfood",
|
||||||
|
"count": 9,
|
||||||
|
"domain": "address_phase107_mixed_planner_brain_dogfood",
|
||||||
|
"questions": [
|
||||||
|
"привет, ты на связи?",
|
||||||
|
"найди в 1С контрагента СВК",
|
||||||
|
"сколько получили по нему за 2020 год",
|
||||||
|
"а теперь сколько заплатили?",
|
||||||
|
"а какое нетто?",
|
||||||
|
"а по документам?",
|
||||||
|
"а по движениям?",
|
||||||
|
"Как ты оценишь деятельность компании ООО Альтернатива Плюс по данным 1С?",
|
||||||
|
"а чем капибара отличается от утки?"
|
||||||
|
],
|
||||||
|
"generated_by": "codex_agent",
|
||||||
|
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260615195240_gen-ag06151952-5d8f12.json",
|
||||||
|
"context": {
|
||||||
|
"llm_provider": null,
|
||||||
|
"model": null,
|
||||||
|
"assistant_prompt_version": null,
|
||||||
|
"decomposition_prompt_version": null,
|
||||||
|
"prompt_fingerprint": null,
|
||||||
|
"autogen_personality_id": null,
|
||||||
|
"autogen_personality_prompt": null,
|
||||||
|
"source_session_id": null,
|
||||||
|
"saved_session_file": "assistant_saved_session_20260615195240_gen-ag06151952-5d8f12.json",
|
||||||
|
"saved_case_set_kind": "agent_semantic_scenario",
|
||||||
|
"agent_run": true,
|
||||||
|
"agent_focus": "mixed planner autonomy and counterparty evidence carryover",
|
||||||
|
"architecture_phase": "assistant-stage1 phase107",
|
||||||
|
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
|
||||||
|
"scenario_id": null,
|
||||||
|
"semantic_tags": [
|
||||||
|
"alias_grounding",
|
||||||
|
"broad_business_evaluation",
|
||||||
|
"context_boundary",
|
||||||
|
"context_switch_from_counterparty_to_company",
|
||||||
|
"date_carryover",
|
||||||
|
"document_evidence",
|
||||||
|
"document_pivot",
|
||||||
|
"entity_resolution",
|
||||||
|
"followup_anchor",
|
||||||
|
"followup_reuse",
|
||||||
|
"grounded_summary",
|
||||||
|
"human_answer",
|
||||||
|
"incoming_value_flow",
|
||||||
|
"mcp_discovery_gate_sanity",
|
||||||
|
"meta_smalltalk",
|
||||||
|
"movement_evidence",
|
||||||
|
"net_value_flow",
|
||||||
|
"off_domain_living_chat",
|
||||||
|
"payout_switch",
|
||||||
|
"planner_catalog_alignment",
|
||||||
|
"stale_replay_forbidden",
|
||||||
|
"value_flow_pivot"
|
||||||
|
],
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
|
||||||
|
"saved_after_validated_replay": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"generation_id": "gen-ag06151746-7d3a57",
|
||||||
|
"created_at": "2026-06-15T17:46:11+00:00",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "AGENT | Phase106 profit cashflow margin next checks",
|
||||||
|
"count": 5,
|
||||||
|
"domain": "address_phase106_profit_cashflow_margin_next_step",
|
||||||
|
"questions": [
|
||||||
|
"ООО Альтернатива Плюс",
|
||||||
|
"скока денег альтернатива заработала за 20 год?",
|
||||||
|
"а это чистая прибыль?",
|
||||||
|
"тогда какую прибыль или маржу можно проверить по товарам за 2020?",
|
||||||
|
"что дальше проверить, чтобы понять здоровье бизнеса, без гаданий?"
|
||||||
|
],
|
||||||
|
"generated_by": "codex_agent",
|
||||||
|
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260615174611_gen-ag06151746-7d3a57.json",
|
||||||
|
"context": {
|
||||||
|
"llm_provider": null,
|
||||||
|
"model": null,
|
||||||
|
"assistant_prompt_version": null,
|
||||||
|
"decomposition_prompt_version": null,
|
||||||
|
"prompt_fingerprint": null,
|
||||||
|
"autogen_personality_id": null,
|
||||||
|
"autogen_personality_prompt": null,
|
||||||
|
"source_session_id": null,
|
||||||
|
"saved_session_file": "assistant_saved_session_20260615174611_gen-ag06151746-7d3a57.json",
|
||||||
|
"saved_case_set_kind": "agent_semantic_scenario",
|
||||||
|
"agent_run": true,
|
||||||
|
"agent_focus": "Profit/cashflow boundary, inventory margin pivot, organization carryover, and concrete next-check plan after accepted phase106 p03 live replay.",
|
||||||
|
"architecture_phase": "turnaround_11",
|
||||||
|
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
|
||||||
|
"scenario_id": null,
|
||||||
|
"semantic_tags": [
|
||||||
|
"bare_org_scope",
|
||||||
|
"business_next_step",
|
||||||
|
"business_overview",
|
||||||
|
"cashflow_not_profit",
|
||||||
|
"cashflow_to_margin_pivot",
|
||||||
|
"colloquial_cashflow",
|
||||||
|
"company_scope",
|
||||||
|
"evidence_plan",
|
||||||
|
"followup_boundary",
|
||||||
|
"inventory",
|
||||||
|
"margin_profitability",
|
||||||
|
"no_generic_advice",
|
||||||
|
"profitability_boundary",
|
||||||
|
"wrong_domain_trap"
|
||||||
|
],
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
|
||||||
|
"saved_after_validated_replay": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"generation_id": "gen-ag06151720-c4b54f",
|
||||||
|
"created_at": "2026-06-15T17:20:44+00:00",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "AGENT | Phase104 role-tail anchor hygiene and bidirectional value-flow",
|
||||||
|
"count": 4,
|
||||||
|
"domain": "address_phase104_generic_role_tail_anchor_hygiene",
|
||||||
|
"questions": [
|
||||||
|
"ООО Альтернатива Плюс",
|
||||||
|
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
|
||||||
|
"А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
|
||||||
|
"А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?"
|
||||||
|
],
|
||||||
|
"generated_by": "codex_agent",
|
||||||
|
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260615172044_gen-ag06151720-c4b54f.json",
|
||||||
|
"context": {
|
||||||
|
"llm_provider": null,
|
||||||
|
"model": null,
|
||||||
|
"assistant_prompt_version": null,
|
||||||
|
"decomposition_prompt_version": null,
|
||||||
|
"prompt_fingerprint": null,
|
||||||
|
"autogen_personality_id": null,
|
||||||
|
"autogen_personality_prompt": null,
|
||||||
|
"source_session_id": null,
|
||||||
|
"saved_session_file": "assistant_saved_session_20260615172044_gen-ag06151720-c4b54f.json",
|
||||||
|
"saved_case_set_kind": "agent_semantic_scenario",
|
||||||
|
"agent_run": true,
|
||||||
|
"agent_focus": "Role-tail anchor hygiene, bank boundary, and supplier-prefix bidirectional money-flow after live domain-case-loop p04.",
|
||||||
|
"architecture_phase": "turnaround_11",
|
||||||
|
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
|
||||||
|
"scenario_id": null,
|
||||||
|
"semantic_tags": [
|
||||||
|
"bank_like_counterparty",
|
||||||
|
"bare_org_scope",
|
||||||
|
"business_overview",
|
||||||
|
"company_scope",
|
||||||
|
"counterparty_net_cash_flow",
|
||||||
|
"financial_role_purpose",
|
||||||
|
"generic_role_tail_anchor_hygiene",
|
||||||
|
"post_overview_anchor_integrity",
|
||||||
|
"stale_scope_guard",
|
||||||
|
"supplier_prefix_canary"
|
||||||
|
],
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
|
||||||
|
"saved_after_validated_replay": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"generation_id": "gen-ag06031328-3e1dd1",
|
"generation_id": "gen-ag06031328-3e1dd1",
|
||||||
"created_at": "2026-06-03T13:28:09+00:00",
|
"created_at": "2026-06-03T13:28:09+00:00",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
{
|
||||||
|
"saved_at": "2026-06-15T17:20:44+00:00",
|
||||||
|
"generation_id": "gen-ag06151720-c4b54f",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "AGENT | Phase104 role-tail anchor hygiene and bidirectional value-flow",
|
||||||
|
"agent_run": true,
|
||||||
|
"questions": [
|
||||||
|
"ООО Альтернатива Плюс",
|
||||||
|
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
|
||||||
|
"А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
|
||||||
|
"А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"assistant_prompt_version": null,
|
||||||
|
"decomposition_prompt_version": null,
|
||||||
|
"prompt_fingerprint": null,
|
||||||
|
"agent_focus": "Role-tail anchor hygiene, bank boundary, and supplier-prefix bidirectional money-flow after live domain-case-loop p04.",
|
||||||
|
"architecture_phase": "turnaround_11",
|
||||||
|
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
|
||||||
|
"scenario_id": null,
|
||||||
|
"semantic_tags": [
|
||||||
|
"bank_like_counterparty",
|
||||||
|
"bare_org_scope",
|
||||||
|
"business_overview",
|
||||||
|
"company_scope",
|
||||||
|
"counterparty_net_cash_flow",
|
||||||
|
"financial_role_purpose",
|
||||||
|
"generic_role_tail_anchor_hygiene",
|
||||||
|
"post_overview_anchor_integrity",
|
||||||
|
"stale_scope_guard",
|
||||||
|
"supplier_prefix_canary"
|
||||||
|
],
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
|
||||||
|
"saved_after_validated_replay": true,
|
||||||
|
"save_gate": {
|
||||||
|
"schema_version": "agent_semantic_save_gate_v1",
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
|
||||||
|
"final_status": "accepted",
|
||||||
|
"loop_id": "phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
|
||||||
|
"target_score": 88,
|
||||||
|
"iterations_ran": 1,
|
||||||
|
"quality_score": 91,
|
||||||
|
"repair_target_count": 0,
|
||||||
|
"repair_target_severity_counts": {
|
||||||
|
"P0": 0,
|
||||||
|
"P1": 0,
|
||||||
|
"P2": 0
|
||||||
|
},
|
||||||
|
"accepted_gate": true,
|
||||||
|
"effective_runtime": {
|
||||||
|
"manifest_path": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop\\effective_runtime.json",
|
||||||
|
"runner": "domain_case_loop.run-pack-loop",
|
||||||
|
"git_sha": "bbb3620956f85aa05bb0c2b76c109cb05a066185",
|
||||||
|
"backend_url": "http://127.0.0.1:8787",
|
||||||
|
"mcp_proxy_url": "http://127.0.0.1:6003",
|
||||||
|
"llm_provider": "local",
|
||||||
|
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
|
||||||
|
"temperature": 0.8,
|
||||||
|
"max_output_tokens": 900,
|
||||||
|
"prompt_version": "normalizer_v2_0_2",
|
||||||
|
"prompt_source": "file",
|
||||||
|
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
|
||||||
|
"prompt_registry_status": "pass"
|
||||||
|
},
|
||||||
|
"saved_after_validated_replay": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source_session_id": null,
|
||||||
|
"session": {
|
||||||
|
"session_id": null,
|
||||||
|
"mode": "agent_semantic_run",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-001",
|
||||||
|
"role": "user",
|
||||||
|
"text": "ООО Альтернатива Плюс",
|
||||||
|
"created_at": "2026-06-15T17:20:44+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-002",
|
||||||
|
"role": "user",
|
||||||
|
"text": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
|
||||||
|
"created_at": "2026-06-15T17:20:44+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-003",
|
||||||
|
"role": "user",
|
||||||
|
"text": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
|
||||||
|
"created_at": "2026-06-15T17:20:44+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-004",
|
||||||
|
"role": "user",
|
||||||
|
"text": "А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?",
|
||||||
|
"created_at": "2026-06-15T17:20:44+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"agent_run": true,
|
||||||
|
"metadata": {
|
||||||
|
"assistant_prompt_version": null,
|
||||||
|
"decomposition_prompt_version": null,
|
||||||
|
"prompt_fingerprint": null,
|
||||||
|
"agent_focus": "Role-tail anchor hygiene, bank boundary, and supplier-prefix bidirectional money-flow after live domain-case-loop p04.",
|
||||||
|
"architecture_phase": "turnaround_11",
|
||||||
|
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
|
||||||
|
"scenario_id": null,
|
||||||
|
"semantic_tags": [
|
||||||
|
"bank_like_counterparty",
|
||||||
|
"bare_org_scope",
|
||||||
|
"business_overview",
|
||||||
|
"company_scope",
|
||||||
|
"counterparty_net_cash_flow",
|
||||||
|
"financial_role_purpose",
|
||||||
|
"generic_role_tail_anchor_hygiene",
|
||||||
|
"post_overview_anchor_integrity",
|
||||||
|
"stale_scope_guard",
|
||||||
|
"supplier_prefix_canary"
|
||||||
|
],
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
|
||||||
|
"saved_after_validated_replay": true,
|
||||||
|
"save_gate": {
|
||||||
|
"schema_version": "agent_semantic_save_gate_v1",
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
|
||||||
|
"final_status": "accepted",
|
||||||
|
"loop_id": "phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
|
||||||
|
"target_score": 88,
|
||||||
|
"iterations_ran": 1,
|
||||||
|
"quality_score": 91,
|
||||||
|
"repair_target_count": 0,
|
||||||
|
"repair_target_severity_counts": {
|
||||||
|
"P0": 0,
|
||||||
|
"P1": 0,
|
||||||
|
"P2": 0
|
||||||
|
},
|
||||||
|
"accepted_gate": true,
|
||||||
|
"effective_runtime": {
|
||||||
|
"manifest_path": "artifacts\\domain_runs\\phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop\\effective_runtime.json",
|
||||||
|
"runner": "domain_case_loop.run-pack-loop",
|
||||||
|
"git_sha": "bbb3620956f85aa05bb0c2b76c109cb05a066185",
|
||||||
|
"backend_url": "http://127.0.0.1:8787",
|
||||||
|
"mcp_proxy_url": "http://127.0.0.1:6003",
|
||||||
|
"llm_provider": "local",
|
||||||
|
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
|
||||||
|
"temperature": 0.8,
|
||||||
|
"max_output_tokens": 900,
|
||||||
|
"prompt_version": "normalizer_v2_0_2",
|
||||||
|
"prompt_source": "file",
|
||||||
|
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
|
||||||
|
"prompt_registry_status": "pass"
|
||||||
|
},
|
||||||
|
"saved_after_validated_replay": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
{
|
||||||
|
"saved_at": "2026-06-15T17:46:11+00:00",
|
||||||
|
"generation_id": "gen-ag06151746-7d3a57",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "AGENT | Phase106 profit cashflow margin next checks",
|
||||||
|
"agent_run": true,
|
||||||
|
"questions": [
|
||||||
|
"ООО Альтернатива Плюс",
|
||||||
|
"скока денег альтернатива заработала за 20 год?",
|
||||||
|
"а это чистая прибыль?",
|
||||||
|
"тогда какую прибыль или маржу можно проверить по товарам за 2020?",
|
||||||
|
"что дальше проверить, чтобы понять здоровье бизнеса, без гаданий?"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"assistant_prompt_version": null,
|
||||||
|
"decomposition_prompt_version": null,
|
||||||
|
"prompt_fingerprint": null,
|
||||||
|
"agent_focus": "Profit/cashflow boundary, inventory margin pivot, organization carryover, and concrete next-check plan after accepted phase106 p03 live replay.",
|
||||||
|
"architecture_phase": "turnaround_11",
|
||||||
|
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
|
||||||
|
"scenario_id": null,
|
||||||
|
"semantic_tags": [
|
||||||
|
"bare_org_scope",
|
||||||
|
"business_next_step",
|
||||||
|
"business_overview",
|
||||||
|
"cashflow_not_profit",
|
||||||
|
"cashflow_to_margin_pivot",
|
||||||
|
"colloquial_cashflow",
|
||||||
|
"company_scope",
|
||||||
|
"evidence_plan",
|
||||||
|
"followup_boundary",
|
||||||
|
"inventory",
|
||||||
|
"margin_profitability",
|
||||||
|
"no_generic_advice",
|
||||||
|
"profitability_boundary",
|
||||||
|
"wrong_domain_trap"
|
||||||
|
],
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
|
||||||
|
"saved_after_validated_replay": true,
|
||||||
|
"save_gate": {
|
||||||
|
"schema_version": "agent_semantic_save_gate_v1",
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
|
||||||
|
"final_status": "accepted",
|
||||||
|
"loop_id": "phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
|
||||||
|
"target_score": 88,
|
||||||
|
"iterations_ran": 1,
|
||||||
|
"quality_score": 92,
|
||||||
|
"repair_target_count": 0,
|
||||||
|
"repair_target_severity_counts": {
|
||||||
|
"P0": 0,
|
||||||
|
"P1": 0,
|
||||||
|
"P2": 0
|
||||||
|
},
|
||||||
|
"accepted_gate": true,
|
||||||
|
"effective_runtime": {
|
||||||
|
"manifest_path": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop\\effective_runtime.json",
|
||||||
|
"runner": "domain_case_loop.run-pack-loop",
|
||||||
|
"git_sha": "d0d7962b65ea699bc5b6dfc82308efaf08d513e7",
|
||||||
|
"backend_url": "http://127.0.0.1:8787",
|
||||||
|
"mcp_proxy_url": "http://127.0.0.1:6003",
|
||||||
|
"llm_provider": "local",
|
||||||
|
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
|
||||||
|
"temperature": 0.8,
|
||||||
|
"max_output_tokens": 900,
|
||||||
|
"prompt_version": "normalizer_v2_0_2",
|
||||||
|
"prompt_source": "file",
|
||||||
|
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
|
||||||
|
"prompt_registry_status": "pass"
|
||||||
|
},
|
||||||
|
"saved_after_validated_replay": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source_session_id": null,
|
||||||
|
"session": {
|
||||||
|
"session_id": null,
|
||||||
|
"mode": "agent_semantic_run",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-001",
|
||||||
|
"role": "user",
|
||||||
|
"text": "ООО Альтернатива Плюс",
|
||||||
|
"created_at": "2026-06-15T17:46:11+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-002",
|
||||||
|
"role": "user",
|
||||||
|
"text": "скока денег альтернатива заработала за 20 год?",
|
||||||
|
"created_at": "2026-06-15T17:46:11+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-003",
|
||||||
|
"role": "user",
|
||||||
|
"text": "а это чистая прибыль?",
|
||||||
|
"created_at": "2026-06-15T17:46:11+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-004",
|
||||||
|
"role": "user",
|
||||||
|
"text": "тогда какую прибыль или маржу можно проверить по товарам за 2020?",
|
||||||
|
"created_at": "2026-06-15T17:46:11+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-005",
|
||||||
|
"role": "user",
|
||||||
|
"text": "что дальше проверить, чтобы понять здоровье бизнеса, без гаданий?",
|
||||||
|
"created_at": "2026-06-15T17:46:11+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"agent_run": true,
|
||||||
|
"metadata": {
|
||||||
|
"assistant_prompt_version": null,
|
||||||
|
"decomposition_prompt_version": null,
|
||||||
|
"prompt_fingerprint": null,
|
||||||
|
"agent_focus": "Profit/cashflow boundary, inventory margin pivot, organization carryover, and concrete next-check plan after accepted phase106 p03 live replay.",
|
||||||
|
"architecture_phase": "turnaround_11",
|
||||||
|
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
|
||||||
|
"scenario_id": null,
|
||||||
|
"semantic_tags": [
|
||||||
|
"bare_org_scope",
|
||||||
|
"business_next_step",
|
||||||
|
"business_overview",
|
||||||
|
"cashflow_not_profit",
|
||||||
|
"cashflow_to_margin_pivot",
|
||||||
|
"colloquial_cashflow",
|
||||||
|
"company_scope",
|
||||||
|
"evidence_plan",
|
||||||
|
"followup_boundary",
|
||||||
|
"inventory",
|
||||||
|
"margin_profitability",
|
||||||
|
"no_generic_advice",
|
||||||
|
"profitability_boundary",
|
||||||
|
"wrong_domain_trap"
|
||||||
|
],
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
|
||||||
|
"saved_after_validated_replay": true,
|
||||||
|
"save_gate": {
|
||||||
|
"schema_version": "agent_semantic_save_gate_v1",
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
|
||||||
|
"final_status": "accepted",
|
||||||
|
"loop_id": "phase106_profit_cashflow_margin_next_step_20260615_p03_loop",
|
||||||
|
"target_score": 88,
|
||||||
|
"iterations_ran": 1,
|
||||||
|
"quality_score": 92,
|
||||||
|
"repair_target_count": 0,
|
||||||
|
"repair_target_severity_counts": {
|
||||||
|
"P0": 0,
|
||||||
|
"P1": 0,
|
||||||
|
"P2": 0
|
||||||
|
},
|
||||||
|
"accepted_gate": true,
|
||||||
|
"effective_runtime": {
|
||||||
|
"manifest_path": "artifacts\\domain_runs\\phase106_profit_cashflow_margin_next_step_20260615_p03_loop\\effective_runtime.json",
|
||||||
|
"runner": "domain_case_loop.run-pack-loop",
|
||||||
|
"git_sha": "d0d7962b65ea699bc5b6dfc82308efaf08d513e7",
|
||||||
|
"backend_url": "http://127.0.0.1:8787",
|
||||||
|
"mcp_proxy_url": "http://127.0.0.1:6003",
|
||||||
|
"llm_provider": "local",
|
||||||
|
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
|
||||||
|
"temperature": 0.8,
|
||||||
|
"max_output_tokens": 900,
|
||||||
|
"prompt_version": "normalizer_v2_0_2",
|
||||||
|
"prompt_source": "file",
|
||||||
|
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
|
||||||
|
"prompt_registry_status": "pass"
|
||||||
|
},
|
||||||
|
"saved_after_validated_replay": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
{
|
||||||
|
"saved_at": "2026-06-15T19:52:40+00:00",
|
||||||
|
"generation_id": "gen-ag06151952-5d8f12",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "AGENT | Phase107 mixed planner brain dogfood",
|
||||||
|
"agent_run": true,
|
||||||
|
"questions": [
|
||||||
|
"привет, ты на связи?",
|
||||||
|
"найди в 1С контрагента СВК",
|
||||||
|
"сколько получили по нему за 2020 год",
|
||||||
|
"а теперь сколько заплатили?",
|
||||||
|
"а какое нетто?",
|
||||||
|
"а по документам?",
|
||||||
|
"а по движениям?",
|
||||||
|
"Как ты оценишь деятельность компании ООО Альтернатива Плюс по данным 1С?",
|
||||||
|
"а чем капибара отличается от утки?"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"assistant_prompt_version": null,
|
||||||
|
"decomposition_prompt_version": null,
|
||||||
|
"prompt_fingerprint": null,
|
||||||
|
"agent_focus": "mixed planner autonomy and counterparty evidence carryover",
|
||||||
|
"architecture_phase": "assistant-stage1 phase107",
|
||||||
|
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
|
||||||
|
"scenario_id": null,
|
||||||
|
"semantic_tags": [
|
||||||
|
"alias_grounding",
|
||||||
|
"broad_business_evaluation",
|
||||||
|
"context_boundary",
|
||||||
|
"context_switch_from_counterparty_to_company",
|
||||||
|
"date_carryover",
|
||||||
|
"document_evidence",
|
||||||
|
"document_pivot",
|
||||||
|
"entity_resolution",
|
||||||
|
"followup_anchor",
|
||||||
|
"followup_reuse",
|
||||||
|
"grounded_summary",
|
||||||
|
"human_answer",
|
||||||
|
"incoming_value_flow",
|
||||||
|
"mcp_discovery_gate_sanity",
|
||||||
|
"meta_smalltalk",
|
||||||
|
"movement_evidence",
|
||||||
|
"net_value_flow",
|
||||||
|
"off_domain_living_chat",
|
||||||
|
"payout_switch",
|
||||||
|
"planner_catalog_alignment",
|
||||||
|
"stale_replay_forbidden",
|
||||||
|
"value_flow_pivot"
|
||||||
|
],
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
|
||||||
|
"saved_after_validated_replay": true,
|
||||||
|
"save_gate": {
|
||||||
|
"schema_version": "agent_semantic_save_gate_v1",
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
|
||||||
|
"final_status": "accepted",
|
||||||
|
"loop_id": "phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
|
||||||
|
"target_score": 88,
|
||||||
|
"iterations_ran": 1,
|
||||||
|
"quality_score": 91,
|
||||||
|
"repair_target_count": 0,
|
||||||
|
"repair_target_severity_counts": {
|
||||||
|
"P0": 0,
|
||||||
|
"P1": 0,
|
||||||
|
"P2": 0
|
||||||
|
},
|
||||||
|
"accepted_gate": true,
|
||||||
|
"effective_runtime": {
|
||||||
|
"manifest_path": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop\\effective_runtime.json",
|
||||||
|
"runner": "domain_case_loop.run-pack-loop",
|
||||||
|
"git_sha": "7e70ec1d464548a9f422e35b717834211f8d7683",
|
||||||
|
"backend_url": "http://127.0.0.1:8787",
|
||||||
|
"mcp_proxy_url": "http://127.0.0.1:6003",
|
||||||
|
"llm_provider": "local",
|
||||||
|
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
|
||||||
|
"temperature": 0.8,
|
||||||
|
"max_output_tokens": 900,
|
||||||
|
"prompt_version": "normalizer_v2_0_2",
|
||||||
|
"prompt_source": "file",
|
||||||
|
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
|
||||||
|
"prompt_registry_status": "pass"
|
||||||
|
},
|
||||||
|
"saved_after_validated_replay": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source_session_id": null,
|
||||||
|
"session": {
|
||||||
|
"session_id": null,
|
||||||
|
"mode": "agent_semantic_run",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-001",
|
||||||
|
"role": "user",
|
||||||
|
"text": "привет, ты на связи?",
|
||||||
|
"created_at": "2026-06-15T19:52:40+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-002",
|
||||||
|
"role": "user",
|
||||||
|
"text": "найди в 1С контрагента СВК",
|
||||||
|
"created_at": "2026-06-15T19:52:40+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-003",
|
||||||
|
"role": "user",
|
||||||
|
"text": "сколько получили по нему за 2020 год",
|
||||||
|
"created_at": "2026-06-15T19:52:40+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-004",
|
||||||
|
"role": "user",
|
||||||
|
"text": "а теперь сколько заплатили?",
|
||||||
|
"created_at": "2026-06-15T19:52:40+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-005",
|
||||||
|
"role": "user",
|
||||||
|
"text": "а какое нетто?",
|
||||||
|
"created_at": "2026-06-15T19:52:40+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-006",
|
||||||
|
"role": "user",
|
||||||
|
"text": "а по документам?",
|
||||||
|
"created_at": "2026-06-15T19:52:40+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-007",
|
||||||
|
"role": "user",
|
||||||
|
"text": "а по движениям?",
|
||||||
|
"created_at": "2026-06-15T19:52:40+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-008",
|
||||||
|
"role": "user",
|
||||||
|
"text": "Как ты оценишь деятельность компании ООО Альтернатива Плюс по данным 1С?",
|
||||||
|
"created_at": "2026-06-15T19:52:40+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "agent-user-009",
|
||||||
|
"role": "user",
|
||||||
|
"text": "а чем капибара отличается от утки?",
|
||||||
|
"created_at": "2026-06-15T19:52:40+00:00",
|
||||||
|
"reply_type": null,
|
||||||
|
"trace_id": null,
|
||||||
|
"debug": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"agent_run": true,
|
||||||
|
"metadata": {
|
||||||
|
"assistant_prompt_version": null,
|
||||||
|
"decomposition_prompt_version": null,
|
||||||
|
"prompt_fingerprint": null,
|
||||||
|
"agent_focus": "mixed planner autonomy and counterparty evidence carryover",
|
||||||
|
"architecture_phase": "assistant-stage1 phase107",
|
||||||
|
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
|
||||||
|
"scenario_id": null,
|
||||||
|
"semantic_tags": [
|
||||||
|
"alias_grounding",
|
||||||
|
"broad_business_evaluation",
|
||||||
|
"context_boundary",
|
||||||
|
"context_switch_from_counterparty_to_company",
|
||||||
|
"date_carryover",
|
||||||
|
"document_evidence",
|
||||||
|
"document_pivot",
|
||||||
|
"entity_resolution",
|
||||||
|
"followup_anchor",
|
||||||
|
"followup_reuse",
|
||||||
|
"grounded_summary",
|
||||||
|
"human_answer",
|
||||||
|
"incoming_value_flow",
|
||||||
|
"mcp_discovery_gate_sanity",
|
||||||
|
"meta_smalltalk",
|
||||||
|
"movement_evidence",
|
||||||
|
"net_value_flow",
|
||||||
|
"off_domain_living_chat",
|
||||||
|
"payout_switch",
|
||||||
|
"planner_catalog_alignment",
|
||||||
|
"stale_replay_forbidden",
|
||||||
|
"value_flow_pivot"
|
||||||
|
],
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
|
||||||
|
"saved_after_validated_replay": true,
|
||||||
|
"save_gate": {
|
||||||
|
"schema_version": "agent_semantic_save_gate_v1",
|
||||||
|
"validation_status": "accepted_domain_pack_loop",
|
||||||
|
"validated_run_dir": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
|
||||||
|
"final_status": "accepted",
|
||||||
|
"loop_id": "phase107_mixed_planner_brain_dogfood_20260615_p11_loop",
|
||||||
|
"target_score": 88,
|
||||||
|
"iterations_ran": 1,
|
||||||
|
"quality_score": 91,
|
||||||
|
"repair_target_count": 0,
|
||||||
|
"repair_target_severity_counts": {
|
||||||
|
"P0": 0,
|
||||||
|
"P1": 0,
|
||||||
|
"P2": 0
|
||||||
|
},
|
||||||
|
"accepted_gate": true,
|
||||||
|
"effective_runtime": {
|
||||||
|
"manifest_path": "artifacts\\domain_runs\\phase107_mixed_planner_brain_dogfood_20260615_p11_loop\\effective_runtime.json",
|
||||||
|
"runner": "domain_case_loop.run-pack-loop",
|
||||||
|
"git_sha": "7e70ec1d464548a9f422e35b717834211f8d7683",
|
||||||
|
"backend_url": "http://127.0.0.1:8787",
|
||||||
|
"mcp_proxy_url": "http://127.0.0.1:6003",
|
||||||
|
"llm_provider": "local",
|
||||||
|
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
|
||||||
|
"temperature": 0.8,
|
||||||
|
"max_output_tokens": 900,
|
||||||
|
"prompt_version": "normalizer_v2_0_2",
|
||||||
|
"prompt_source": "file",
|
||||||
|
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
|
||||||
|
"prompt_registry_status": "pass"
|
||||||
|
},
|
||||||
|
"saved_after_validated_replay": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"suite_id": "assistant_saved_session_gen-ag06151720-c4b54f",
|
||||||
|
"suite_version": "0.1.0",
|
||||||
|
"schema_version": "assistant_saved_session_suite_v0_1",
|
||||||
|
"generated_at": "2026-06-15T17:20:44+00:00",
|
||||||
|
"generation_id": "gen-ag06151720-c4b54f",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "AGENT | Phase104 role-tail anchor hygiene and bidirectional value-flow",
|
||||||
|
"domain": "address_phase104_generic_role_tail_anchor_hygiene",
|
||||||
|
"scenario_count": 1,
|
||||||
|
"case_ids": [
|
||||||
|
"SAVED-001"
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"case_id": "SAVED-001",
|
||||||
|
"scenario_tag": "agent_saved_user_sessions",
|
||||||
|
"title": "AGENT | Phase104 role-tail anchor hygiene and bidirectional value-flow",
|
||||||
|
"question_type": "followup",
|
||||||
|
"broadness_level": "medium",
|
||||||
|
"turns": [
|
||||||
|
{
|
||||||
|
"user_message": "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"suite_id": "assistant_saved_session_gen-ag06151746-7d3a57",
|
||||||
|
"suite_version": "0.1.0",
|
||||||
|
"schema_version": "assistant_saved_session_suite_v0_1",
|
||||||
|
"generated_at": "2026-06-15T17:46:11+00:00",
|
||||||
|
"generation_id": "gen-ag06151746-7d3a57",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "AGENT | Phase106 profit cashflow margin next checks",
|
||||||
|
"domain": "address_phase106_profit_cashflow_margin_next_step",
|
||||||
|
"scenario_count": 1,
|
||||||
|
"case_ids": [
|
||||||
|
"SAVED-001"
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"case_id": "SAVED-001",
|
||||||
|
"scenario_tag": "agent_saved_user_sessions",
|
||||||
|
"title": "AGENT | Phase106 profit cashflow margin next checks",
|
||||||
|
"question_type": "followup",
|
||||||
|
"broadness_level": "medium",
|
||||||
|
"turns": [
|
||||||
|
{
|
||||||
|
"user_message": "ООО Альтернатива Плюс"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "скока денег альтернатива заработала за 20 год?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а это чистая прибыль?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "тогда какую прибыль или маржу можно проверить по товарам за 2020?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "что дальше проверить, чтобы понять здоровье бизнеса, без гаданий?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"suite_id": "assistant_saved_session_gen-ag06151952-5d8f12",
|
||||||
|
"suite_version": "0.1.0",
|
||||||
|
"schema_version": "assistant_saved_session_suite_v0_1",
|
||||||
|
"generated_at": "2026-06-15T19:52:40+00:00",
|
||||||
|
"generation_id": "gen-ag06151952-5d8f12",
|
||||||
|
"mode": "saved_user_sessions",
|
||||||
|
"title": "AGENT | Phase107 mixed planner brain dogfood",
|
||||||
|
"domain": "address_phase107_mixed_planner_brain_dogfood",
|
||||||
|
"scenario_count": 1,
|
||||||
|
"case_ids": [
|
||||||
|
"SAVED-001"
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"case_id": "SAVED-001",
|
||||||
|
"scenario_tag": "agent_saved_user_sessions",
|
||||||
|
"title": "AGENT | Phase107 mixed planner brain dogfood",
|
||||||
|
"question_type": "followup",
|
||||||
|
"broadness_level": "medium",
|
||||||
|
"turns": [
|
||||||
|
{
|
||||||
|
"user_message": "привет, ты на связи?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "найди в 1С контрагента СВК"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "сколько получили по нему за 2020 год"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а теперь сколько заплатили?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а какое нетто?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а по документам?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а по движениям?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "Как ты оценишь деятельность компании ООО Альтернатива Плюс по данным 1С?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а чем капибара отличается от утки?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -13,10 +14,40 @@ DETECTOR_REGISTRY_PATH = REPO_ROOT / "docs" / "orchestration" / "detector_regist
|
||||||
ISSUE_CATALOG_PATH = REPO_ROOT / "docs" / "orchestration" / "issue_catalog.json"
|
ISSUE_CATALOG_PATH = REPO_ROOT / "docs" / "orchestration" / "issue_catalog.json"
|
||||||
DEFAULT_GLOBAL_DETECTORS = ["missing_effective_runtime_json"]
|
DEFAULT_GLOBAL_DETECTORS = ["missing_effective_runtime_json"]
|
||||||
DETECTOR_RESULTS_SCHEMA_VERSION = "agent_detector_results_v1"
|
DETECTOR_RESULTS_SCHEMA_VERSION = "agent_detector_results_v1"
|
||||||
|
DEFAULT_LIMITED_NEXT_ACTION_EXTRA_PATTERNS = [
|
||||||
|
r"(?i)(что\s+проверить\s+дальше|для\s+ответа.{0,120}нужно|нужно\s+отдельно\s+(?:считать|посчитать|проверить)|проверить\s+дальше)"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def path_for_io(path: Path) -> str:
|
||||||
|
if os.name != "nt":
|
||||||
|
return str(path)
|
||||||
|
raw_value = str(path)
|
||||||
|
if raw_value.startswith("\\\\?\\"):
|
||||||
|
return raw_value
|
||||||
|
absolute_path = path if path.is_absolute() else Path.cwd() / path
|
||||||
|
absolute_value = str(absolute_path.absolute())
|
||||||
|
if absolute_value.startswith("\\\\?\\"):
|
||||||
|
return absolute_value
|
||||||
|
if absolute_value.startswith("\\\\"):
|
||||||
|
return "\\\\?\\UNC\\" + absolute_value.lstrip("\\")
|
||||||
|
return "\\\\?\\" + absolute_value
|
||||||
|
|
||||||
|
|
||||||
|
def path_exists(path: Path) -> bool:
|
||||||
|
try:
|
||||||
|
return os.path.exists(path_for_io(path))
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def read_text(path: Path) -> str:
|
||||||
|
with open(path_for_io(path), "r", encoding="utf-8") as handle:
|
||||||
|
return handle.read()
|
||||||
|
|
||||||
|
|
||||||
def read_json(path: Path) -> Any:
|
def read_json(path: Path) -> Any:
|
||||||
return json.loads(path.read_text(encoding="utf-8"))
|
return json.loads(read_text(path))
|
||||||
|
|
||||||
|
|
||||||
def read_json_object(path: Path) -> dict[str, Any]:
|
def read_json_object(path: Path) -> dict[str, Any]:
|
||||||
|
|
@ -28,8 +59,9 @@ def read_json_object(path: Path) -> dict[str, Any]:
|
||||||
|
|
||||||
|
|
||||||
def write_json(path: Path, payload: Any) -> None:
|
def write_json(path: Path, payload: Any) -> None:
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
os.makedirs(path_for_io(path.parent), exist_ok=True)
|
||||||
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
with open(path_for_io(path), "w", encoding="utf-8") as handle:
|
||||||
|
handle.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
||||||
|
|
||||||
|
|
||||||
def utc_now() -> str:
|
def utc_now() -> str:
|
||||||
|
|
@ -161,13 +193,50 @@ def select_detectors(
|
||||||
|
|
||||||
def read_text_or_empty(path: Path) -> str:
|
def read_text_or_empty(path: Path) -> str:
|
||||||
try:
|
try:
|
||||||
return path.read_text(encoding="utf-8")
|
return read_text(path)
|
||||||
except OSError:
|
except OSError:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def decode_win1251_utf8_mojibake(value: str) -> str | None:
|
||||||
|
try:
|
||||||
|
return value.encode("cp1251").decode("utf-8")
|
||||||
|
except UnicodeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def decode_latin1_utf8_mojibake(value: str) -> str | None:
|
||||||
|
try:
|
||||||
|
return value.encode("latin1").decode("utf-8")
|
||||||
|
except UnicodeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def text_match_variants(value: str) -> list[str]:
|
||||||
|
variants: list[str] = []
|
||||||
|
queue = [str(value or "")]
|
||||||
|
while queue:
|
||||||
|
current = queue.pop(0)
|
||||||
|
if current in variants:
|
||||||
|
continue
|
||||||
|
variants.append(current)
|
||||||
|
for decoded in (decode_win1251_utf8_mojibake(current), decode_latin1_utf8_mojibake(current)):
|
||||||
|
if decoded and decoded not in variants and decoded not in queue:
|
||||||
|
queue.append(decoded)
|
||||||
|
return variants
|
||||||
|
|
||||||
|
|
||||||
|
def first_pattern_search(patterns: list[re.Pattern[str]], text: str) -> re.Match[str] | None:
|
||||||
|
for variant in text_match_variants(text):
|
||||||
|
for pattern in patterns:
|
||||||
|
match = pattern.search(variant)
|
||||||
|
if match:
|
||||||
|
return match
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def assistant_text_from_turn_path(path: Path | None) -> str:
|
def assistant_text_from_turn_path(path: Path | None) -> str:
|
||||||
if path is None or not path.exists():
|
if path is None or not path_exists(path):
|
||||||
return ""
|
return ""
|
||||||
payload = read_json_object(path)
|
payload = read_json_object(path)
|
||||||
assistant_message = payload.get("assistant_message") if isinstance(payload.get("assistant_message"), dict) else {}
|
assistant_message = payload.get("assistant_message") if isinstance(payload.get("assistant_message"), dict) else {}
|
||||||
|
|
@ -224,14 +293,21 @@ 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":
|
||||||
candidate = output_path.with_name("turn.json")
|
candidate = output_path.with_name("turn.json")
|
||||||
return candidate if candidate.exists() else None
|
return candidate if path_exists(candidate) else None
|
||||||
if name.endswith("_output.md"):
|
if name.endswith("_output.md"):
|
||||||
prefix = name[: -len("_output.md")]
|
prefix = name[: -len("_output.md")]
|
||||||
candidate = output_path.with_name(f"{prefix}_turn.json")
|
candidate = output_path.with_name(f"{prefix}_turn.json")
|
||||||
return candidate if candidate.exists() else None
|
return candidate if path_exists(candidate) else None
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def output_step_state_path(output_path: Path) -> Path | None:
|
||||||
|
if output_path.name != "output.md":
|
||||||
|
return None
|
||||||
|
candidate = output_path.with_name("step_state.json")
|
||||||
|
return candidate if path_exists(candidate) else None
|
||||||
|
|
||||||
|
|
||||||
def collect_output_artifacts(artifact_dir: Path) -> list[dict[str, Any]]:
|
def collect_output_artifacts(artifact_dir: Path) -> list[dict[str, Any]]:
|
||||||
outputs: list[dict[str, Any]] = []
|
outputs: list[dict[str, Any]] = []
|
||||||
seen: set[Path] = set()
|
seen: set[Path] = set()
|
||||||
|
|
@ -252,6 +328,7 @@ def collect_output_artifacts(artifact_dir: Path) -> list[dict[str, Any]]:
|
||||||
"artifact_path": str(path.relative_to(artifact_dir)),
|
"artifact_path": str(path.relative_to(artifact_dir)),
|
||||||
"text": read_text_or_empty(path),
|
"text": read_text_or_empty(path),
|
||||||
"turn_path": turn_path,
|
"turn_path": turn_path,
|
||||||
|
"step_state_path": output_step_state_path(path),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return outputs
|
return outputs
|
||||||
|
|
@ -384,7 +461,7 @@ def evaluate_forbidden_regex(
|
||||||
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])
|
||||||
for pattern in patterns:
|
for pattern in patterns:
|
||||||
match = pattern.search(text)
|
match = first_pattern_search([pattern], text)
|
||||||
if match:
|
if match:
|
||||||
evidence.append(
|
evidence.append(
|
||||||
{
|
{
|
||||||
|
|
@ -411,7 +488,7 @@ def evaluate_required_any(
|
||||||
matched: list[dict[str, Any]] = []
|
matched: list[dict[str, Any]] = []
|
||||||
for output in outputs:
|
for output in outputs:
|
||||||
text = current_answer_text_for_output(output)
|
text = current_answer_text_for_output(output)
|
||||||
match = next((pattern.search(text) for pattern in patterns if pattern.search(text)), None)
|
match = first_pattern_search(patterns, text)
|
||||||
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]})
|
||||||
else:
|
else:
|
||||||
|
|
@ -428,18 +505,24 @@ def evaluate_limited_next_action(
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
check = detector.get("check") if isinstance(detector.get("check"), dict) else {}
|
check = detector.get("check") if isinstance(detector.get("check"), dict) else {}
|
||||||
limited_patterns = compile_patterns(normalize_string_list(check.get("limited_patterns")))
|
limited_patterns = compile_patterns(normalize_string_list(check.get("limited_patterns")))
|
||||||
next_action_patterns = compile_patterns(normalize_string_list(check.get("required_next_action_patterns_any")))
|
next_action_pattern_sources = [
|
||||||
|
*normalize_string_list(check.get("required_next_action_patterns_any")),
|
||||||
|
*normalize_string_list(check.get("required_next_action_patterns_extra_any")),
|
||||||
|
]
|
||||||
|
if detector_name == "limited_answer_without_next_action":
|
||||||
|
next_action_pattern_sources.extend(DEFAULT_LIMITED_NEXT_ACTION_EXTRA_PATTERNS)
|
||||||
|
next_action_patterns = compile_patterns(next_action_pattern_sources)
|
||||||
if not outputs:
|
if not outputs:
|
||||||
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")
|
||||||
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 = current_answer_text_for_output(output)
|
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 = first_pattern_search(limited_patterns, text)
|
||||||
if not limited_match:
|
if not limited_match:
|
||||||
continue
|
continue
|
||||||
limited_outputs += 1
|
limited_outputs += 1
|
||||||
if not any(pattern.search(text) for pattern in next_action_patterns):
|
if not first_pattern_search(next_action_patterns, text):
|
||||||
failures.append({"path": output["repo_path"], "limited_match": limited_match.group(0)[:240]})
|
failures.append({"path": output["repo_path"], "limited_match": limited_match.group(0)[:240]})
|
||||||
status = "fail" if failures else "pass"
|
status = "fail" if failures else "pass"
|
||||||
if failures:
|
if failures:
|
||||||
|
|
@ -451,6 +534,52 @@ def evaluate_limited_next_action(
|
||||||
return build_result(detector_name, detector, status, message, evidence=failures)
|
return build_result(detector_name, detector, status, message, evidence=failures)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_answer_text_shape(
|
||||||
|
detector_name: str,
|
||||||
|
detector: dict[str, Any],
|
||||||
|
outputs: list[dict[str, Any]],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if not outputs:
|
||||||
|
return build_result(detector_name, detector, "skipped", "no output.md-style artifacts matched detector scope")
|
||||||
|
failures: list[dict[str, Any]] = []
|
||||||
|
reviewed: list[dict[str, Any]] = []
|
||||||
|
unknown: list[dict[str, Any]] = []
|
||||||
|
for output in outputs:
|
||||||
|
step_state_path = output.get("step_state_path")
|
||||||
|
step_state = read_json_object(step_state_path) if isinstance(step_state_path, Path) else {}
|
||||||
|
review = step_state.get("business_first_review") if isinstance(step_state.get("business_first_review"), dict) else {}
|
||||||
|
direct_answer_first_ok = review.get("direct_answer_first_ok")
|
||||||
|
if direct_answer_first_ok is True:
|
||||||
|
reviewed.append({"path": output["repo_path"], "direct_answer_first_ok": True})
|
||||||
|
elif direct_answer_first_ok is False:
|
||||||
|
failures.append({"path": output["repo_path"], "direct_answer_first_ok": False})
|
||||||
|
else:
|
||||||
|
unknown.append({"path": output["repo_path"], "reason": "business_first_review_missing"})
|
||||||
|
if failures:
|
||||||
|
return build_result(
|
||||||
|
detector_name,
|
||||||
|
detector,
|
||||||
|
"fail",
|
||||||
|
"first-line direct answer check failed",
|
||||||
|
evidence=[*failures, *unknown],
|
||||||
|
)
|
||||||
|
if unknown:
|
||||||
|
return build_result(
|
||||||
|
detector_name,
|
||||||
|
detector,
|
||||||
|
"review",
|
||||||
|
"direct-answer shape requires business review",
|
||||||
|
evidence=[*reviewed, *unknown],
|
||||||
|
)
|
||||||
|
return build_result(
|
||||||
|
detector_name,
|
||||||
|
detector,
|
||||||
|
"pass",
|
||||||
|
"business-first step reviews confirm direct-answer-first shape",
|
||||||
|
evidence=reviewed,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def evaluate_trace_guard(
|
def evaluate_trace_guard(
|
||||||
detector_name: str,
|
detector_name: str,
|
||||||
detector: dict[str, Any],
|
detector: dict[str, Any],
|
||||||
|
|
@ -640,7 +769,7 @@ def evaluate_detector(
|
||||||
):
|
):
|
||||||
return evaluate_composite(detector_name, detector, results_by_name)
|
return evaluate_composite(detector_name, detector, results_by_name)
|
||||||
if kind == "answer_text_shape":
|
if kind == "answer_text_shape":
|
||||||
return evaluate_manual_review(detector_name, detector, "direct-answer shape requires business review")
|
return evaluate_answer_text_shape(detector_name, detector, scoped_outputs)
|
||||||
return build_result(detector_name, detector, "skipped", f"detector kind is not executable yet: {kind}")
|
return build_result(detector_name, detector, "skipped", f"detector kind is not executable yet: {kind}")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,7 @@ DEFAULT_INVARIANT_SEVERITY: dict[str, str] = {
|
||||||
"forbidden_recipe_selected": "P0",
|
"forbidden_recipe_selected": "P0",
|
||||||
"focus_object_missing": "P0",
|
"focus_object_missing": "P0",
|
||||||
"wrong_date_scope_state": "P0",
|
"wrong_date_scope_state": "P0",
|
||||||
|
"out_of_window_date_in_answer": "P0",
|
||||||
"direct_answer_missing": "P0",
|
"direct_answer_missing": "P0",
|
||||||
"top_level_noise_present": "P0",
|
"top_level_noise_present": "P0",
|
||||||
"business_direct_answer_missing": "P0",
|
"business_direct_answer_missing": "P0",
|
||||||
|
|
@ -930,6 +931,40 @@ def first_non_empty_lines(text: str, limit: int = 3) -> list[str]:
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def expected_single_year_from_date_scope(date_scope: Any) -> str | None:
|
||||||
|
if not isinstance(date_scope, dict):
|
||||||
|
return None
|
||||||
|
if str(date_scope.get("scope") or "").strip().lower() == "all_time":
|
||||||
|
return None
|
||||||
|
period_from = normalize_iso_date(date_scope.get("period_from"))
|
||||||
|
period_to = normalize_iso_date(date_scope.get("period_to"))
|
||||||
|
if not period_from or not period_to:
|
||||||
|
return None
|
||||||
|
from_year = period_from[:4]
|
||||||
|
to_year = period_to[:4]
|
||||||
|
if from_year != to_year:
|
||||||
|
return None
|
||||||
|
return from_year if re.fullmatch(r"(?:19|20)\d{2}", from_year) else None
|
||||||
|
|
||||||
|
|
||||||
|
def dated_evidence_years_from_answer(text: Any) -> list[str]:
|
||||||
|
source = repair_text_mojibake(str(text or ""))
|
||||||
|
years: list[str] = []
|
||||||
|
for match in re.finditer(r"\b((?:19|20)\d{2})-\d{2}-\d{2}(?:T|\b)", source):
|
||||||
|
years.append(match.group(1))
|
||||||
|
for match in re.finditer(r"\b\d{1,2}\.\d{1,2}\.((?:19|20)\d{2})\b", source):
|
||||||
|
years.append(match.group(1))
|
||||||
|
return list(dict.fromkeys(years))
|
||||||
|
|
||||||
|
|
||||||
|
def answer_has_out_of_window_dates_for_scope(text: Any, date_scope: Any) -> bool:
|
||||||
|
expected_year = expected_single_year_from_date_scope(date_scope)
|
||||||
|
if not expected_year:
|
||||||
|
return False
|
||||||
|
years = dated_evidence_years_from_answer(text)
|
||||||
|
return any(year != expected_year for year in years)
|
||||||
|
|
||||||
|
|
||||||
def build_node_contract_index(raw_contract: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
def build_node_contract_index(raw_contract: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||||
scenario_tree = raw_contract.get("scenario_tree")
|
scenario_tree = raw_contract.get("scenario_tree")
|
||||||
if not isinstance(scenario_tree, dict):
|
if not isinstance(scenario_tree, dict):
|
||||||
|
|
@ -1695,6 +1730,47 @@ def resolve_question_template(question_template: str, scenario_state: dict[str,
|
||||||
return pattern.sub(replace, question_template).strip()
|
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(
|
def build_failed_step_state(
|
||||||
*,
|
*,
|
||||||
scenario_id: str,
|
scenario_id: str,
|
||||||
|
|
@ -2313,6 +2389,9 @@ def step_intent_matches_expected(state: dict[str, Any], expected_intents: list[s
|
||||||
observed_intents = [
|
observed_intents = [
|
||||||
str(state.get("detected_intent") or "").strip(),
|
str(state.get("detected_intent") or "").strip(),
|
||||||
*normalize_string_list(state.get("mcp_discovery_effective_intents")),
|
*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)
|
return any(identifier_in_list(intent, expected_intents) for intent in observed_intents if intent)
|
||||||
|
|
||||||
|
|
@ -2551,6 +2630,8 @@ def is_validated_memory_checkpoint_answer(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if is_validated_comparison_boundary_memory_checkpoint(state, business_review, violations):
|
if is_validated_comparison_boundary_memory_checkpoint(state, business_review, violations):
|
||||||
return True
|
return True
|
||||||
|
if is_validated_bare_organization_scope_checkpoint(state, business_review, violations):
|
||||||
|
return True
|
||||||
tags = set(normalize_string_list(state.get("semantic_tags")))
|
tags = set(normalize_string_list(state.get("semantic_tags")))
|
||||||
if "memory" not in tags:
|
if "memory" not in tags:
|
||||||
return False
|
return False
|
||||||
|
|
@ -2566,6 +2647,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(
|
def is_validated_comparison_boundary_memory_checkpoint(
|
||||||
state: dict[str, Any],
|
state: dict[str, Any],
|
||||||
business_review: dict[str, Any],
|
business_review: dict[str, Any],
|
||||||
|
|
@ -2788,7 +2902,12 @@ def is_validated_clean_meta_chat_answer(
|
||||||
return False
|
return False
|
||||||
semantic_tags = set(normalize_string_list(state.get("semantic_tags")))
|
semantic_tags = set(normalize_string_list(state.get("semantic_tags")))
|
||||||
allowed_tags = {
|
allowed_tags = {
|
||||||
|
"human_answer",
|
||||||
"meta_smalltalk",
|
"meta_smalltalk",
|
||||||
|
"mcp_discovery_gate_sanity",
|
||||||
|
"off_domain_living_chat",
|
||||||
|
"stale_replay_forbidden",
|
||||||
|
"context_boundary",
|
||||||
"company_selected",
|
"company_selected",
|
||||||
"organization_authority",
|
"organization_authority",
|
||||||
"meta_capability",
|
"meta_capability",
|
||||||
|
|
@ -3033,6 +3152,10 @@ def validate_step_contract(step_state: dict[str, Any]) -> dict[str, Any]:
|
||||||
if current_date_scope and current_date_scope != required_filters["as_of_date"]:
|
if current_date_scope and current_date_scope != required_filters["as_of_date"]:
|
||||||
violated_invariants.append("wrong_date_scope_state")
|
violated_invariants.append("wrong_date_scope_state")
|
||||||
|
|
||||||
|
if answer_has_out_of_window_dates_for_scope(assistant_text, date_scope):
|
||||||
|
violated_invariants.append("out_of_window_date_in_answer")
|
||||||
|
warnings.append("out_of_window_date_in_answer")
|
||||||
|
|
||||||
if should_require_direct_answer(state):
|
if should_require_direct_answer(state):
|
||||||
if not actual_direct_answer or is_top_level_noise_line(actual_direct_answer):
|
if not actual_direct_answer or is_top_level_noise_line(actual_direct_answer):
|
||||||
violated_invariants.append("direct_answer_missing")
|
violated_invariants.append("direct_answer_missing")
|
||||||
|
|
@ -3114,8 +3237,10 @@ def validate_step_contract(step_state: dict[str, Any]) -> dict[str, Any]:
|
||||||
state["clarification_answer_validated"] = clarification_validated
|
state["clarification_answer_validated"] = clarification_validated
|
||||||
state["missing_axis_clarification_validated"] = missing_axis_clarification_validated
|
state["missing_axis_clarification_validated"] = missing_axis_clarification_validated
|
||||||
state["clean_meta_chat_answer_validated"] = clean_meta_chat_validated
|
state["clean_meta_chat_answer_validated"] = clean_meta_chat_validated
|
||||||
|
effective_execution_status = "exact" if clean_meta_chat_validated else execution_status
|
||||||
|
state["execution_status"] = effective_execution_status
|
||||||
state["acceptance_status"] = acceptance_status_from_execution(
|
state["acceptance_status"] = acceptance_status_from_execution(
|
||||||
execution_status,
|
effective_execution_status,
|
||||||
hard_fail,
|
hard_fail,
|
||||||
(
|
(
|
||||||
bounded_validated
|
bounded_validated
|
||||||
|
|
@ -3235,6 +3360,10 @@ def build_scenario_step_state(
|
||||||
"detected_intent": debug.get("detected_intent"),
|
"detected_intent": debug.get("detected_intent"),
|
||||||
"selected_recipe": debug.get("selected_recipe"),
|
"selected_recipe": debug.get("selected_recipe"),
|
||||||
"capability_id": debug.get("capability_id"),
|
"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(
|
"capability_contract_id": first_non_empty_string(
|
||||||
debug.get("capability_contract_id"),
|
debug.get("capability_contract_id"),
|
||||||
runtime_contract.get("capability_contract_id"),
|
runtime_contract.get("capability_contract_id"),
|
||||||
|
|
@ -3600,6 +3729,7 @@ def execute_scenario_manifest(
|
||||||
step_dir = steps_dir / step["step_id"]
|
step_dir = steps_dir / step["step_id"]
|
||||||
try:
|
try:
|
||||||
resolved_question = resolve_question_template(step["question_template"], scenario_state)
|
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 = merge_analysis_context(manifest.get("analysis_context"), step.get("analysis_context"))
|
||||||
step_analysis_context = carry_forward_analysis_context(
|
step_analysis_context = carry_forward_analysis_context(
|
||||||
scenario_state,
|
scenario_state,
|
||||||
|
|
@ -3611,7 +3741,7 @@ def execute_scenario_manifest(
|
||||||
args=args,
|
args=args,
|
||||||
domain=manifest["domain"],
|
domain=manifest["domain"],
|
||||||
scenario_id=manifest["scenario_id"],
|
scenario_id=manifest["scenario_id"],
|
||||||
step=step,
|
step=resolved_step,
|
||||||
step_index=step_index,
|
step_index=step_index,
|
||||||
session_id=scenario_state.get("session_id"),
|
session_id=scenario_state.get("session_id"),
|
||||||
question_resolved=resolved_question,
|
question_resolved=resolved_question,
|
||||||
|
|
@ -6225,7 +6355,22 @@ def summarize_detector_results(detector_results: dict[str, Any] | None, *, limit
|
||||||
return names[:limit]
|
return names[:limit]
|
||||||
|
|
||||||
status = str(summary.get("status") or "skipped")
|
status = str(summary.get("status") or "skipped")
|
||||||
declared_detector_count = len(normalize_string_list(detector_results.get("declared_detectors_under_test")))
|
declared_detectors = normalize_string_list(detector_results.get("declared_detectors_under_test"))
|
||||||
|
declared_detector_count = len(declared_detectors)
|
||||||
|
|
||||||
|
def _declared_with_status(status: str) -> list[str]:
|
||||||
|
names: list[str] = []
|
||||||
|
declared = set(declared_detectors)
|
||||||
|
if not declared:
|
||||||
|
return names
|
||||||
|
for item in results:
|
||||||
|
if not isinstance(item, dict) or str(item.get("status") or "") != status:
|
||||||
|
continue
|
||||||
|
detector_name = str(item.get("detector") or "").strip()
|
||||||
|
if detector_name in declared and detector_name not in names:
|
||||||
|
names.append(detector_name)
|
||||||
|
return names[:limit]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": status,
|
"status": status,
|
||||||
"detector_count": int(summary.get("detector_count") or len(results)),
|
"detector_count": int(summary.get("detector_count") or len(results)),
|
||||||
|
|
@ -6237,6 +6382,7 @@ def summarize_detector_results(detector_results: dict[str, Any] | None, *, limit
|
||||||
"failed_detectors": _detectors_with_status("fail"),
|
"failed_detectors": _detectors_with_status("fail"),
|
||||||
"review_detectors": _detectors_with_status("review"),
|
"review_detectors": _detectors_with_status("review"),
|
||||||
"skipped_detectors": _detectors_with_status("skipped"),
|
"skipped_detectors": _detectors_with_status("skipped"),
|
||||||
|
"declared_skipped_detectors": _declared_with_status("skipped"),
|
||||||
"signal_ok_for_auto_coder": status in {"fail", "review"},
|
"signal_ok_for_auto_coder": status in {"fail", "review"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6247,6 +6393,9 @@ def evaluate_detector_results_gate(detector_results_summary: dict[str, Any]) ->
|
||||||
detector_count = int(detector_results_summary.get("detector_count") or 0)
|
detector_count = int(detector_results_summary.get("detector_count") or 0)
|
||||||
if declared_detector_count > 0 and detector_count == 0:
|
if declared_detector_count > 0 and detector_count == 0:
|
||||||
return False, "declared_detector_results_missing"
|
return False, "declared_detector_results_missing"
|
||||||
|
declared_skipped = normalize_string_list(detector_results_summary.get("declared_skipped_detectors"))
|
||||||
|
if declared_skipped:
|
||||||
|
return False, f"declared_detector_results_skipped:{','.join(declared_skipped)}"
|
||||||
if declared_detector_count > 0 and status == "skipped":
|
if declared_detector_count > 0 and status == "skipped":
|
||||||
return False, "declared_detector_results_skipped"
|
return False, "declared_detector_results_skipped"
|
||||||
if status == "fail":
|
if status == "fail":
|
||||||
|
|
|
||||||
|
|
@ -720,7 +720,11 @@ def parse_args() -> argparse.Namespace:
|
||||||
parser.add_argument("--prompt-fingerprint", help="Optional prompt fingerprint metadata.")
|
parser.add_argument("--prompt-fingerprint", help="Optional prompt fingerprint metadata.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--validated-run-dir",
|
"--validated-run-dir",
|
||||||
help="Accepted truth-harness artifact directory containing pack_state.json, truth_review.json, and business_review.json.",
|
help=(
|
||||||
|
"Accepted replay artifact directory. Supports truth-harness runs "
|
||||||
|
"(pack_state/truth_review/business_review), domain-case-loop pack/scenario dirs, "
|
||||||
|
"and run-pack-loop dirs with loop_state.json."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--allow-unvalidated",
|
"--allow-unvalidated",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
@ -22,6 +23,10 @@ def write_text(path: Path, text: str) -> None:
|
||||||
path.write_text(text, encoding="utf-8")
|
path.write_text(text, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def to_win1251_mojibake(value: str) -> str:
|
||||||
|
return value.encode("utf-8").decode("cp1251")
|
||||||
|
|
||||||
|
|
||||||
class AgentDetectorRunnerTests(unittest.TestCase):
|
class AgentDetectorRunnerTests(unittest.TestCase):
|
||||||
def test_default_runner_fails_missing_effective_runtime(self) -> None:
|
def test_default_runner_fails_missing_effective_runtime(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
|
@ -102,6 +107,59 @@ class AgentDetectorRunnerTests(unittest.TestCase):
|
||||||
self.assertEqual(results["summary"]["status"], "pass")
|
self.assertEqual(results["summary"]["status"], "pass")
|
||||||
self.assertEqual(results["results"][0]["status"], "pass")
|
self.assertEqual(results["results"][0]["status"], "pass")
|
||||||
|
|
||||||
|
def test_limited_next_action_accepts_mojibake_what_to_check_next(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
artifact_dir = root / "run"
|
||||||
|
answer = (
|
||||||
|
"Это не подтвержденная чистая прибыль.\n"
|
||||||
|
"Что проверить дальше: чистую прибыль через счета 90/91/99 и закрытие периода."
|
||||||
|
)
|
||||||
|
write_text(artifact_dir / "steps" / "s01" / "output.md", to_win1251_mojibake(answer))
|
||||||
|
registry_path = root / "detector_registry.json"
|
||||||
|
issue_catalog_path = root / "issue_catalog.json"
|
||||||
|
write_json(
|
||||||
|
registry_path,
|
||||||
|
{
|
||||||
|
"schema_version": "agent_detector_registry_v1",
|
||||||
|
"detectors": {
|
||||||
|
"limited_answer_without_next_action": {
|
||||||
|
"kind": "answer_text_required_when_limited",
|
||||||
|
"automation_level": "semi_automatic",
|
||||||
|
"description": "Limited answers need a next action.",
|
||||||
|
"issue_codes": ["business_next_step_missing"],
|
||||||
|
"inputs": ["output.md"],
|
||||||
|
"check": {
|
||||||
|
"limited_patterns": ["(?i)не подтвержден"],
|
||||||
|
"required_next_action_patterns_any": ["(?i)(можно|проверь)"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
write_json(
|
||||||
|
issue_catalog_path,
|
||||||
|
{
|
||||||
|
"schema_version": "agent_issue_catalog_v1",
|
||||||
|
"issues": {
|
||||||
|
"business_next_step_missing": {
|
||||||
|
"detectors": ["limited_answer_without_next_action"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
results = runner.build_detector_results(
|
||||||
|
artifact_dir,
|
||||||
|
issue_codes=["business_next_step_missing"],
|
||||||
|
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]["message"], "limited answer includes a next action")
|
||||||
|
|
||||||
def test_candidate_forbidden_regex_is_limited_to_evidence_path(self) -> None:
|
def test_candidate_forbidden_regex_is_limited_to_evidence_path(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp)
|
root = Path(tmp)
|
||||||
|
|
@ -171,7 +229,7 @@ class AgentDetectorRunnerTests(unittest.TestCase):
|
||||||
root = Path(tmp)
|
root = Path(tmp)
|
||||||
artifact_dir = root / "run"
|
artifact_dir = root / "run"
|
||||||
write_text(
|
write_text(
|
||||||
artifact_dir / "scenarios" / "margin_root" / "steps" / "step_01" / "output.md",
|
artifact_dir / "scenarios" / "profit_cashflow_boundary" / "steps" / "step_01_margin_root" / "output.md",
|
||||||
"Top margin item: revenue 100, cogs 60, gross profit 40, margin 40%.",
|
"Top margin item: revenue 100, cogs 60, gross profit 40, margin 40%.",
|
||||||
)
|
)
|
||||||
write_text(
|
write_text(
|
||||||
|
|
@ -183,6 +241,15 @@ class AgentDetectorRunnerTests(unittest.TestCase):
|
||||||
/ "output.md",
|
/ "output.md",
|
||||||
"Supplier for the selected item: Example LLC.",
|
"Supplier for the selected item: Example LLC.",
|
||||||
)
|
)
|
||||||
|
write_text(
|
||||||
|
artifact_dir
|
||||||
|
/ "scenarios"
|
||||||
|
/ "business_health"
|
||||||
|
/ "steps"
|
||||||
|
/ "step_05_next_checks"
|
||||||
|
/ "output.md",
|
||||||
|
"Check profit, debt, inventory, taxes, and customer concentration next.",
|
||||||
|
)
|
||||||
registry_path = root / "detector_registry.json"
|
registry_path = root / "detector_registry.json"
|
||||||
issue_catalog_path = root / "issue_catalog.json"
|
issue_catalog_path = root / "issue_catalog.json"
|
||||||
write_json(
|
write_json(
|
||||||
|
|
@ -198,6 +265,7 @@ class AgentDetectorRunnerTests(unittest.TestCase):
|
||||||
"inputs": ["output.md"],
|
"inputs": ["output.md"],
|
||||||
"check": {
|
"check": {
|
||||||
"required_patterns_any": ["(?i)(revenue|cogs|gross profit|margin)"],
|
"required_patterns_any": ["(?i)(revenue|cogs|gross profit|margin)"],
|
||||||
|
"artifact_path_include_patterns": ["(?i)steps[\\\\/][^\\\\/]*(?:margin|profitability)"],
|
||||||
"artifact_path_exclude_patterns": ["(?i)selected_item"],
|
"artifact_path_exclude_patterns": ["(?i)selected_item"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +286,183 @@ class AgentDetectorRunnerTests(unittest.TestCase):
|
||||||
self.assertEqual(results["results"][0]["status"], "pass")
|
self.assertEqual(results["results"][0]["status"], "pass")
|
||||||
evidence_paths = [item["path"] for item in results["results"][0]["evidence"]]
|
evidence_paths = [item["path"] for item in results["results"][0]["evidence"]]
|
||||||
self.assertEqual(len(evidence_paths), 1)
|
self.assertEqual(len(evidence_paths), 1)
|
||||||
self.assertIn("margin_root", evidence_paths[0])
|
self.assertIn("step_01_margin_root", evidence_paths[0])
|
||||||
|
|
||||||
|
def test_counterparty_value_flow_required_surface_scopes_to_current_net_step(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
artifact_dir = root / "run"
|
||||||
|
write_text(
|
||||||
|
artifact_dir
|
||||||
|
/ "scenarios"
|
||||||
|
/ "mixed_planner_counterparty_evidence_and_living_guard"
|
||||||
|
/ "steps"
|
||||||
|
/ "step_03_incoming_by_resolved_entity"
|
||||||
|
/ "output.md",
|
||||||
|
"Входящие денежные поступления по контрагенту Группа СВК за 2020: 12 093 465 руб.",
|
||||||
|
)
|
||||||
|
write_text(
|
||||||
|
artifact_dir
|
||||||
|
/ "scenarios"
|
||||||
|
/ "mixed_planner_counterparty_evidence_and_living_guard"
|
||||||
|
/ "steps"
|
||||||
|
/ "step_05_net_after_payout"
|
||||||
|
/ "output.md",
|
||||||
|
"По контрагенту Группа СВК за период 2020 получили 12 093 465 руб., заплатили 0 руб.; расчетное нетто в нашу сторону: 12 093 465 руб.",
|
||||||
|
)
|
||||||
|
registry_path = root / "detector_registry.json"
|
||||||
|
issue_catalog_path = root / "issue_catalog.json"
|
||||||
|
write_json(
|
||||||
|
registry_path,
|
||||||
|
{
|
||||||
|
"schema_version": "agent_detector_registry_v1",
|
||||||
|
"detectors": {
|
||||||
|
"counterparty_value_flow_required_surface": {
|
||||||
|
"kind": "answer_text_required_any",
|
||||||
|
"automation_level": "automatic",
|
||||||
|
"description": "Net value-flow answer must surface all money directions.",
|
||||||
|
"issue_codes": ["counterparty_value_flow_misrouted_to_company_profit"],
|
||||||
|
"inputs": ["output.md"],
|
||||||
|
"check": {
|
||||||
|
"artifact_path_include_patterns": ["(?i)step_05_net_after_payout"],
|
||||||
|
"required_patterns_any": [
|
||||||
|
"(?is)(?=.*(СВК|Группа\\s+СВК))(?=.*(входящ|получил|получено|получили))(?=.*(исходящ|заплатил|заплачено|заплатили|ушло))(?=.*(нетто|сальдо|разниц|чистый\\s+денежный))"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
write_json(issue_catalog_path, {"schema_version": "agent_issue_catalog_v1", "issues": {}})
|
||||||
|
|
||||||
|
results = runner.build_detector_results(
|
||||||
|
artifact_dir,
|
||||||
|
detector_names=["counterparty_value_flow_required_surface"],
|
||||||
|
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")
|
||||||
|
evidence_paths = [item["path"] for item in results["results"][0]["evidence"]]
|
||||||
|
self.assertEqual(len(evidence_paths), 1)
|
||||||
|
self.assertIn("step_05_net_after_payout", evidence_paths[0])
|
||||||
|
|
||||||
|
def test_answer_text_shape_uses_business_first_step_review(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
artifact_dir = root / "run"
|
||||||
|
step_dir = artifact_dir / "scenarios" / "mixed" / "steps" / "step_01"
|
||||||
|
write_text(step_dir / "output.md", "Direct business answer first.")
|
||||||
|
write_json(step_dir / "step_state.json", {"business_first_review": {"direct_answer_first_ok": True}})
|
||||||
|
registry_path = root / "detector_registry.json"
|
||||||
|
issue_catalog_path = root / "issue_catalog.json"
|
||||||
|
write_json(
|
||||||
|
registry_path,
|
||||||
|
{
|
||||||
|
"schema_version": "agent_detector_registry_v1",
|
||||||
|
"detectors": {
|
||||||
|
"first_line_not_direct_answer": {
|
||||||
|
"kind": "answer_text_shape",
|
||||||
|
"automation_level": "semi_automatic",
|
||||||
|
"description": "First line should be direct.",
|
||||||
|
"issue_codes": ["business_direct_answer_missing"],
|
||||||
|
"inputs": ["output.md"],
|
||||||
|
"check": {"first_line_should_be": "business_answer_or_honest_boundary"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
write_json(issue_catalog_path, {"schema_version": "agent_issue_catalog_v1", "issues": {}})
|
||||||
|
|
||||||
|
results = runner.build_detector_results(
|
||||||
|
artifact_dir,
|
||||||
|
detector_names=["first_line_not_direct_answer"],
|
||||||
|
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_answer_text_shape_fails_when_business_first_review_rejects_first_line(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
artifact_dir = root / "run"
|
||||||
|
step_dir = artifact_dir / "scenarios" / "mixed" / "steps" / "step_01"
|
||||||
|
write_text(step_dir / "output.md", "Let me inspect the route first.")
|
||||||
|
write_json(step_dir / "step_state.json", {"business_first_review": {"direct_answer_first_ok": False}})
|
||||||
|
registry_path = root / "detector_registry.json"
|
||||||
|
issue_catalog_path = root / "issue_catalog.json"
|
||||||
|
write_json(
|
||||||
|
registry_path,
|
||||||
|
{
|
||||||
|
"schema_version": "agent_detector_registry_v1",
|
||||||
|
"detectors": {
|
||||||
|
"first_line_not_direct_answer": {
|
||||||
|
"kind": "answer_text_shape",
|
||||||
|
"automation_level": "semi_automatic",
|
||||||
|
"description": "First line should be direct.",
|
||||||
|
"issue_codes": ["business_direct_answer_missing"],
|
||||||
|
"inputs": ["output.md"],
|
||||||
|
"check": {"first_line_should_be": "business_answer_or_honest_boundary"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
write_json(issue_catalog_path, {"schema_version": "agent_issue_catalog_v1", "issues": {}})
|
||||||
|
|
||||||
|
results = runner.build_detector_results(
|
||||||
|
artifact_dir,
|
||||||
|
detector_names=["first_line_not_direct_answer"],
|
||||||
|
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_step_state_lookup_handles_long_artifact_paths(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
step_dir = (
|
||||||
|
root
|
||||||
|
/ "run"
|
||||||
|
/ "scenarios"
|
||||||
|
/ ("mixed_planner_counterparty_evidence_and_living_guard_" + "s" * 48)
|
||||||
|
/ "steps"
|
||||||
|
/ ("step_04_payout_switch_by_resolved_entity_" + "x" * 80)
|
||||||
|
)
|
||||||
|
output_path = step_dir / "output.md"
|
||||||
|
step_state_path = step_dir / "step_state.json"
|
||||||
|
try:
|
||||||
|
os.makedirs(runner.path_for_io(step_dir), exist_ok=True)
|
||||||
|
with open(runner.path_for_io(output_path), "w", encoding="utf-8") as handle:
|
||||||
|
handle.write("Direct business answer first.")
|
||||||
|
runner.write_json(step_state_path, {"business_first_review": {"direct_answer_first_ok": True}})
|
||||||
|
|
||||||
|
self.assertGreater(len(str(step_state_path.absolute())), 260)
|
||||||
|
self.assertEqual(runner.output_step_state_path(output_path), step_state_path)
|
||||||
|
self.assertEqual(
|
||||||
|
runner.read_json_object(step_state_path).get("business_first_review"),
|
||||||
|
{"direct_answer_first_ok": True},
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
for path in (output_path, step_state_path):
|
||||||
|
try:
|
||||||
|
os.remove(runner.path_for_io(path))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
current = step_dir
|
||||||
|
while current != root:
|
||||||
|
try:
|
||||||
|
os.rmdir(runner.path_for_io(current))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
current = current.parent
|
||||||
|
|
||||||
def test_composite_detector_fails_after_child_detector_fails(self) -> None:
|
def test_composite_detector_fails_after_child_detector_fails(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
|
|
||||||
|
|
@ -404,6 +404,24 @@ class DomainCaseLoopLeadHandoffTests(unittest.TestCase):
|
||||||
self.assertEqual(summary["declared_detector_count"], 1)
|
self.assertEqual(summary["declared_detector_count"], 1)
|
||||||
self.assertEqual(reason, "declared_detector_results_missing")
|
self.assertEqual(reason, "declared_detector_results_missing")
|
||||||
|
|
||||||
|
def test_detector_gate_blocks_declared_detector_skipped_inside_mixed_pass_run(self) -> None:
|
||||||
|
summary = dcl.summarize_detector_results(
|
||||||
|
{
|
||||||
|
"summary": {"status": "pass", "detector_count": 2, "pass": 1, "fail": 0, "review": 0, "skipped": 1},
|
||||||
|
"results": [
|
||||||
|
{"detector": "runtime_tokens_in_user_answer", "status": "pass"},
|
||||||
|
{"detector": "counterparty_value_flow_profit_mismatch", "status": "skipped"},
|
||||||
|
],
|
||||||
|
"declared_detectors_under_test": ["counterparty_value_flow_profit_mismatch"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ok, reason = dcl.evaluate_detector_results_gate(summary)
|
||||||
|
|
||||||
|
self.assertFalse(ok)
|
||||||
|
self.assertEqual(summary["declared_skipped_detectors"], ["counterparty_value_flow_profit_mismatch"])
|
||||||
|
self.assertEqual(reason, "declared_detector_results_skipped:counterparty_value_flow_profit_mismatch")
|
||||||
|
|
||||||
def test_auto_coder_gate_allows_when_detector_results_confirm_failure(self) -> None:
|
def test_auto_coder_gate_allows_when_detector_results_confirm_failure(self) -> None:
|
||||||
repair_targets = {
|
repair_targets = {
|
||||||
"targets": [
|
"targets": [
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,81 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
|
||||||
self.assertEqual(dcl.derive_scenario_execution_status(step_outputs), "partial")
|
self.assertEqual(dcl.derive_scenario_execution_status(step_outputs), "partial")
|
||||||
self.assertEqual(dcl.derive_scenario_status(step_outputs), "accepted")
|
self.assertEqual(dcl.derive_scenario_status(step_outputs), "accepted")
|
||||||
|
|
||||||
|
def test_clean_meta_smalltalk_is_exact_without_1c_capability(self) -> None:
|
||||||
|
step_state = dcl.build_scenario_step_state(
|
||||||
|
scenario_id="meta_chat_demo",
|
||||||
|
domain="agentic_loop",
|
||||||
|
step={
|
||||||
|
"step_id": "step_01",
|
||||||
|
"title": "Human smalltalk",
|
||||||
|
"depends_on": [],
|
||||||
|
"question_template": "привет, ты на связи?",
|
||||||
|
"semantic_tags": ["human_answer", "meta_smalltalk", "mcp_discovery_gate_sanity"],
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
"fallback_type": "none",
|
||||||
|
"living_chat_response_source": "llm_chat",
|
||||||
|
},
|
||||||
|
"session_summary": {},
|
||||||
|
},
|
||||||
|
entries=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(step_state["clean_meta_chat_answer_validated"])
|
||||||
|
self.assertEqual(step_state["execution_status"], "exact")
|
||||||
|
self.assertEqual(step_state["acceptance_status"], "validated")
|
||||||
|
|
||||||
|
def test_off_domain_living_chat_is_exact_without_1c_capability(self) -> None:
|
||||||
|
step_state = dcl.build_scenario_step_state(
|
||||||
|
scenario_id="off_domain_demo",
|
||||||
|
domain="agentic_loop",
|
||||||
|
step={
|
||||||
|
"step_id": "step_09",
|
||||||
|
"title": "Off-domain living chat",
|
||||||
|
"depends_on": [],
|
||||||
|
"question_template": "а чем капибара отличается от утки?",
|
||||||
|
"semantic_tags": ["off_domain_living_chat", "stale_replay_forbidden", "context_boundary"],
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
},
|
||||||
|
step_index=9,
|
||||||
|
question_resolved="а чем капибара отличается от утки?",
|
||||||
|
analysis_context={},
|
||||||
|
turn_artifact={
|
||||||
|
"assistant_message": {
|
||||||
|
"reply_type": "factual_with_explanation",
|
||||||
|
"text": (
|
||||||
|
"Капибара и утка отличаются принципиально: капибара - млекопитающее-грызун, "
|
||||||
|
"а утка - птица. Поэтому у них разные тело, среда обитания и способ передвижения."
|
||||||
|
),
|
||||||
|
"message_id": "msg-2",
|
||||||
|
"trace_id": "trace-2",
|
||||||
|
},
|
||||||
|
"technical_debug_payload": {
|
||||||
|
"detected_mode": "chat",
|
||||||
|
"fallback_type": "none",
|
||||||
|
"living_chat_response_source": "llm_chat",
|
||||||
|
},
|
||||||
|
"session_summary": {},
|
||||||
|
},
|
||||||
|
entries=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(step_state["clean_meta_chat_answer_validated"])
|
||||||
|
self.assertEqual(step_state["execution_status"], "exact")
|
||||||
|
self.assertEqual(step_state["acceptance_status"], "validated")
|
||||||
|
|
||||||
def test_today_scope_required_filter_and_direct_patterns_are_enforced(self) -> None:
|
def test_today_scope_required_filter_and_direct_patterns_are_enforced(self) -> None:
|
||||||
self.assertTrue(dcl.question_resets_temporal_scope("мы должны комуто денег на сегодня?"))
|
self.assertTrue(dcl.question_resets_temporal_scope("мы должны комуто денег на сегодня?"))
|
||||||
|
|
||||||
|
|
@ -190,6 +265,109 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
|
||||||
self.assertIn("required_answer_patterns_any_missing", step_state["violated_invariants"])
|
self.assertIn("required_answer_patterns_any_missing", step_state["violated_invariants"])
|
||||||
self.assertEqual(step_state["acceptance_status"], "rejected")
|
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:
|
def test_repair_targets_promote_route_candidate_enablement_gaps(self) -> None:
|
||||||
repair_targets = dcl.build_deterministic_repair_targets(
|
repair_targets = dcl.build_deterministic_repair_targets(
|
||||||
{"pack_id": "route_candidate_pack", "domain": "open_world", "final_status": "accepted"},
|
{"pack_id": "route_candidate_pack", "domain": "open_world", "final_status": "accepted"},
|
||||||
|
|
@ -507,6 +685,32 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
|
||||||
self.assertIsNone(step_state["date_scope"]["as_of_date"])
|
self.assertIsNone(step_state["date_scope"]["as_of_date"])
|
||||||
self.assertEqual(step_state["date_scope"]["source"], "question_temporal_scope_reset")
|
self.assertEqual(step_state["date_scope"]["source"], "question_temporal_scope_reset")
|
||||||
|
|
||||||
|
def test_out_of_window_document_dates_reject_validated_step(self) -> None:
|
||||||
|
validated = dcl.validate_step_contract(
|
||||||
|
{
|
||||||
|
"execution_status": "exact",
|
||||||
|
"required_answer_shape": "direct_answer_first",
|
||||||
|
"reply_type": "factual",
|
||||||
|
"actual_direct_answer": "Контрагент: Группа СВК. Найдено документов: 19.",
|
||||||
|
"assistant_text": (
|
||||||
|
"Контрагент: Группа СВК. Найдено документов: 19.\n"
|
||||||
|
"1. 2021-11-10T12:00:07Z | Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07"
|
||||||
|
),
|
||||||
|
"top_non_empty_lines": [
|
||||||
|
"Контрагент: Группа СВК. Найдено документов: 19.",
|
||||||
|
"1. 2021-11-10T12:00:07Z | Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07",
|
||||||
|
],
|
||||||
|
"date_scope": {
|
||||||
|
"period_from": "2020-01-01",
|
||||||
|
"period_to": "2020-12-31",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(validated["status"], "rejected")
|
||||||
|
self.assertIn("out_of_window_date_in_answer", validated["violated_invariants"])
|
||||||
|
self.assertIn("out_of_window_date_in_answer", validated["warnings"])
|
||||||
|
|
||||||
def test_open_items_exact_negative_answer_validates_without_rows(self) -> None:
|
def test_open_items_exact_negative_answer_validates_without_rows(self) -> None:
|
||||||
step_state = dcl.build_scenario_step_state(
|
step_state = dcl.build_scenario_step_state(
|
||||||
scenario_id="open_items_negative_demo",
|
scenario_id="open_items_negative_demo",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue