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": [
|
||||
"(?i)(выруч|себестоим|валов|марж|не хватает|не могу подтвердить|не подтвержден|unknown)"
|
||||
],
|
||||
"artifact_path_include_patterns": [
|
||||
"(?i)steps[\\\\/][^\\\\/]*(?:margin|profitability|марж)"
|
||||
],
|
||||
"artifact_path_exclude_patterns": [
|
||||
"(?i)selected_item",
|
||||
"(?i)supplier",
|
||||
|
|
@ -208,6 +211,45 @@
|
|||
"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": {
|
||||
"kind": "stage_review_signal",
|
||||
"automation_level": "semi_automatic",
|
||||
|
|
|
|||
|
|
@ -411,7 +411,11 @@
|
|||
"severity": "P0",
|
||||
"business_meaning": "Контрагентский вопрос про получили/заплатили/нетто отвечен company-level прибылью или 90/91/99 вместо контрагентского денежного потока.",
|
||||
"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": [
|
||||
"llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts",
|
||||
"llm_normalizer/backend/src/services/assistantMcpDiscoveryRuntimeBridge.ts",
|
||||
|
|
@ -489,7 +493,10 @@
|
|||
"severity": "P1",
|
||||
"business_meaning": "Банк/финансовая организация ошибочно подана как обычный клиент/поставщик без роли, назначения платежа и договорного контекста.",
|
||||
"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": [
|
||||
"llm_normalizer/backend/src/services/address_runtime/composeStage.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 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);
|
||||
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) {
|
||||
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 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);
|
||||
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) {
|
||||
const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(text);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ exports.cloneAddressNavigationState = cloneAddressNavigationState;
|
|||
exports.normalizeAddressNavigationState = normalizeAddressNavigationState;
|
||||
exports.evolveAddressNavigationStateWithAssistantItem = evolveAddressNavigationStateWithAssistantItem;
|
||||
const nanoid_1 = require("nanoid");
|
||||
const addressTextRepair_1 = require("./addressTextRepair");
|
||||
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
|
||||
const addressNavigation_1 = require("../types/addressNavigation");
|
||||
const MAX_RESULT_SETS = 40;
|
||||
|
|
@ -126,6 +127,48 @@ function readNavigationDiscoveryCounterparty(debug) {
|
|||
}
|
||||
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) {
|
||||
const normalized = toNonEmptyString(value);
|
||||
if (!normalized) {
|
||||
|
|
@ -295,6 +338,32 @@ function normalizeFilters(value) {
|
|||
}
|
||||
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) {
|
||||
const continuationContract = toObject(debug.dialog_continuation_contract_v2);
|
||||
const decision = toNonEmptyString(continuationContract?.decision);
|
||||
|
|
@ -337,8 +406,7 @@ function readBusinessOverviewComparisonProofBundles(debug) {
|
|||
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
||||
const turnInput = toObject(entryPoint?.turn_input);
|
||||
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
|
||||
const bridge = toObject(entryPoint?.bridge);
|
||||
const pilot = toObject(bridge?.pilot);
|
||||
const pilot = readDiscoveryPilot(debug);
|
||||
const counterpartyValueFlowBundle = cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
|
||||
cloneRecord(pilot?.derived_bidirectional_value_flow);
|
||||
const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle);
|
||||
|
|
@ -356,12 +424,24 @@ function readBusinessOverviewComparisonProofBundles(debug) {
|
|||
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) {
|
||||
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
||||
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
||||
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
|
||||
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
||||
if (counterparty) {
|
||||
if (counterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, counterparty)) {
|
||||
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
||||
}
|
||||
const organization = (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
|
||||
|
|
@ -369,8 +449,8 @@ function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
|
|||
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
||||
}
|
||||
}
|
||||
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
|
||||
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
||||
if (isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true) {
|
||||
const counterparty = readCurrentDiscoveryCounterparty(debug);
|
||||
if (counterparty) {
|
||||
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
||||
}
|
||||
|
|
@ -560,24 +640,23 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
|
|||
}
|
||||
const debug = item.debug;
|
||||
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 intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
|
||||
const discoveryIntent = intentFromDiscoveryChain(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) {
|
||||
return state;
|
||||
}
|
||||
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
|
||||
const resultSetId = `rs-${item.message_id}`;
|
||||
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 derivedCounterpartyScope = selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
||||
? (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
|
||||
const derivedCounterpartyScope = isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true
|
||||
? readCurrentDiscoveryCounterparty(debug)
|
||||
: null;
|
||||
const filtersWithDerivedScope = derivedOrganizationScope && !toNonEmptyString(filters.organization)
|
||||
? {
|
||||
|
|
@ -610,15 +689,24 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
|
|||
const debugFocusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
|
||||
const primaryEntityFocusObject = buildFocusObjectFromPrimaryEntityRef(resultSet, createdAt);
|
||||
const focusObject = resolveFocusObjectForNavigation(state, intent, resultSet, debugFocusObject, primaryEntityFocusObject);
|
||||
const comparisonCounterparty = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||
? readNavigationDiscoveryCounterparty(debug)
|
||||
const rawComparisonCounterparty = debug.mcp_discovery_response_applied === true
|
||||
? selectedDiscoveryChain === "business_overview"
|
||||
? readNavigationDiscoveryCounterparty(debug)
|
||||
: isCounterpartyDiscoveryChain(selectedDiscoveryChain)
|
||||
? readCurrentDiscoveryCounterparty(debug)
|
||||
: null
|
||||
: null;
|
||||
const comparisonCounterparty = rawComparisonCounterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, rawComparisonCounterparty)
|
||||
? rawComparisonCounterparty
|
||||
: null;
|
||||
const comparisonOrganization = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||
? derivedOrganizationScope ?? toNonEmptyString(filtersWithDerivedScope.organization)
|
||||
: null;
|
||||
const currentComparisonProofBundles = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||
? readBusinessOverviewComparisonProofBundles(debug)
|
||||
: null;
|
||||
: selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
||||
? readCounterpartyValueFlowProofBundles(debug)
|
||||
: null;
|
||||
const inheritedComparisonScope = state.session_context.comparison_scope;
|
||||
const inheritedComparisonProofBundles = comparisonCounterparty &&
|
||||
sameBusinessLabel(inheritedComparisonScope?.counterparty?.label, comparisonCounterparty)
|
||||
|
|
|
|||
|
|
@ -1313,6 +1313,15 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
intent === "vat_payable_confirmed_as_of_date";
|
||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||
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 currentContractExplicit = toNonEmptyString(merged.contract);
|
||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||
|
|
@ -1387,7 +1396,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
}
|
||||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
if (!currentHasPeriod &&
|
||||
if ((!currentHasPeriod || currentValueFlowPeriodDefaultsToToday) &&
|
||||
previousHasPeriod &&
|
||||
hasFollowupSignal &&
|
||||
!hasExplicitPeriodInMessage &&
|
||||
|
|
|
|||
|
|
@ -411,11 +411,25 @@ function isReferentialCounterpartyPlaceholder(value) {
|
|||
}
|
||||
function normalizeCounterpartyCandidate(value, toNonEmptyString) {
|
||||
const text = toNonEmptyString(value);
|
||||
if (!text || isReferentialCounterpartyPlaceholder(text)) {
|
||||
if (!text || isReferentialCounterpartyPlaceholder(text) || isCounterpartyRoleFragment(text)) {
|
||||
return null;
|
||||
}
|
||||
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) {
|
||||
return Boolean(left &&
|
||||
right &&
|
||||
|
|
@ -590,6 +604,9 @@ function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackT
|
|||
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
|
||||
const preferGroundedDiscoveryCounterparty = hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty);
|
||||
const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString);
|
||||
if (!existingCounterparty) {
|
||||
delete nextFilters.counterparty;
|
||||
}
|
||||
if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) {
|
||||
nextFilters.counterparty = counterparty;
|
||||
}
|
||||
|
|
@ -761,7 +778,11 @@ function resolveFollowupTargetIntent(inventoryPurchaseDateVatBridge, selectedObj
|
|||
}
|
||||
function shouldUseNavigationTemporalCarryover(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_purchase_provenance_for_item" ||
|
||||
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ function valueFlowDirectionLabelRu(pilot) {
|
|||
? "исходящих платежей/списаний"
|
||||
: "входящих денежных поступлений";
|
||||
}
|
||||
function valueFlowZeroResultConfirmedLine(pilot) {
|
||||
function valueFlowZeroResultInsufficiencyLine(pilot) {
|
||||
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -224,7 +224,22 @@ function valueFlowZeroResultConfirmedLine(pilot) {
|
|||
const period = explicitDateScope(pilot);
|
||||
const organizationPart = organization ? ` по организации ${organization}` : "";
|
||||
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) {
|
||||
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
||||
|
|
@ -239,11 +254,11 @@ function valueFlowZeroResultUnknownLine(pilot) {
|
|||
return `Это не доказывает отсутствие операций с контрагентом ${counterparty}${periodPart} или вне доступного банковского контура.`;
|
||||
}
|
||||
function valueFlowZeroResultHeadline(pilot) {
|
||||
const confirmedLine = valueFlowZeroResultConfirmedLine(pilot);
|
||||
if (!confirmedLine) {
|
||||
const insufficiencyLine = valueFlowZeroResultInsufficiencyLine(pilot);
|
||||
if (!insufficiencyLine) {
|
||||
return null;
|
||||
}
|
||||
return confirmedLine;
|
||||
return insufficiencyLine;
|
||||
}
|
||||
function hasAllTimeScope(pilot) {
|
||||
return (dryRunHasAxis(pilot, "all_time_scope") ||
|
||||
|
|
@ -938,6 +953,12 @@ function nextStepFor(mode, pilot) {
|
|||
}
|
||||
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)) {
|
||||
return pilot.derived_business_overview
|
||||
? businessOverviewNextStepLine(pilot.derived_business_overview)
|
||||
|
|
@ -1840,20 +1861,20 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
|||
? [derivedValueLine]
|
||||
: derivedValueLine
|
||||
? [derivedValueLine, ...monthlyConfirmedLines]
|
||||
: valueFlowZeroResultConfirmedLine(pilot)
|
||||
? [valueFlowZeroResultConfirmedLine(pilot)]
|
||||
: derivedEntityResolutionLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||
: derivedMetadataLine
|
||||
? [derivedMetadataLine]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
: derivedEntityResolutionLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||
: derivedMetadataLine
|
||||
? [derivedMetadataLine]
|
||||
: pilot.evidence.confirmed_facts;
|
||||
const unknownLines = pilot.derived_business_overview
|
||||
? businessOverviewUnknownLines(pilot)
|
||||
: pilot.derived_metadata_surface
|
||||
? pilot.derived_metadata_surface.available_fields.length > 0
|
||||
? userFacingUnknowns(pilot.evidence.unknown_facts)
|
||||
: ["Детальный список полей этих объектов этим шагом не получен."]
|
||||
: appendValueFlowZeroResultUnknown(rankedValueFlowUnknownLines(pilot), pilot);
|
||||
: appendValueFlowZeroResultUnknown(valueFlowZeroResultCheckedLine(pilot)
|
||||
? [valueFlowZeroResultCheckedLine(pilot), ...rankedValueFlowUnknownLines(pilot)]
|
||||
: rankedValueFlowUnknownLines(pilot), pilot);
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpDiscoveryDataNeedGraph = buildAssistantMcpDiscoveryDataNeedGraph;
|
||||
const addressTextRepair_1 = require("./addressTextRepair");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = "assistant_data_need_graph_v1";
|
||||
function toNonEmptyString(value) {
|
||||
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);
|
||||
}
|
||||
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) {
|
||||
return action === "turnover" || action === "payout";
|
||||
}
|
||||
|
|
@ -421,7 +444,8 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
|||
const rawAction = lower(turnMeaning?.asked_action_family);
|
||||
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
||||
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 seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
|
||||
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
||||
|
|
@ -439,10 +463,13 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
|||
});
|
||||
const oneSidedValueAction = inferredOpenScopeOneSidedValueAction(rawQuestionSignal);
|
||||
const detailedBusinessOverviewMoneyBreakdownHint = hasDetailedBusinessOverviewMoneyBreakdownHint(rawQuestionSignal);
|
||||
const wideBusinessOverviewSurfaceHint = hasWideBusinessOverviewSurfaceHint(rawQuestionSource);
|
||||
const oneSidedBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" &&
|
||||
Boolean(oneSidedValueAction) &&
|
||||
!detailedBusinessOverviewMoneyBreakdownHint;
|
||||
const bidirectionalBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" && hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
|
||||
const bidirectionalBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" &&
|
||||
hasIncomingVsOutgoingComparisonHint(rawQuestionSignal) &&
|
||||
!wideBusinessOverviewSurfaceHint;
|
||||
const oneSidedValueFlowActionOverride = initialBusinessFactFamily === "value_flow" &&
|
||||
Boolean(oneSidedValueAction) &&
|
||||
(rawAction === "counterparty_value_or_turnover" || rawAction === "counterparty_value");
|
||||
|
|
@ -574,6 +601,9 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
|
|||
if (businessFactFamily === "business_overview" && !explicitDateScope) {
|
||||
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) {
|
||||
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) {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
if (!result || result.error) {
|
||||
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 rowsWithAmount = 0;
|
||||
for (const row of result.rows) {
|
||||
|
|
@ -3926,16 +3945,30 @@ function buildMovementConfirmedFacts(result, counterparty, periodScope) {
|
|||
return [`В 1С найдены строки движений${checkedCounterpartySuffixRu(counterparty)}${checkedPeriodSuffixRu(periodScope)}.`];
|
||||
}
|
||||
function buildValueFlowConfirmedFacts(result, counterparty, direction) {
|
||||
if (result.error || result.matched_rows <= 0) {
|
||||
if (result.error) {
|
||||
return [];
|
||||
}
|
||||
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 [
|
||||
counterparty
|
||||
? `1C supplier-payout rows were found for counterparty ${counterparty}`
|
||||
: "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 [
|
||||
counterparty
|
||||
? `1C value-flow rows were found for counterparty ${counterparty}`
|
||||
|
|
@ -4115,6 +4148,12 @@ function buildValueFlowInferredFacts(derived) {
|
|||
return [];
|
||||
}
|
||||
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") {
|
||||
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 rowsReceived = probeRowsReceived(probeResults);
|
||||
const bypassDetected = hasProbeBypass(input.plan, probeResults);
|
||||
const confirmedNoMatchFact = confirmedFacts.some((fact) => /\bnot_found\b/iu.test(String(fact ?? "")));
|
||||
if (bypassDetected) {
|
||||
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
|
||||
}
|
||||
if (input.plan.plan_status !== "allowed") {
|
||||
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");
|
||||
}
|
||||
if (!sourceRowsSummary && rowsReceived > 0) {
|
||||
|
|
@ -287,6 +288,12 @@ function resolveAssistantMcpDiscoveryEvidence(input) {
|
|||
answerPermission = "confirmed_answer";
|
||||
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) {
|
||||
evidenceStatus = "inferred_only";
|
||||
coverageStatus = "partial";
|
||||
|
|
|
|||
|
|
@ -211,9 +211,15 @@ function localizeLine(value) {
|
|||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
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)) {
|
||||
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);
|
||||
if (openScopeBidirectionalMatch) {
|
||||
const incoming = openScopeBidirectionalMatch[1] === "found"
|
||||
|
|
@ -238,6 +244,10 @@ function localizeLine(value) {
|
|||
if (valueFlowMatch) {
|
||||
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)) {
|
||||
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
|
||||
}
|
||||
|
|
@ -259,6 +269,10 @@ function localizeLine(value) {
|
|||
if (supplierPayoutMatch) {
|
||||
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)) {
|
||||
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)) {
|
||||
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)) {
|
||||
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
|
||||
}
|
||||
|
|
@ -703,6 +723,7 @@ function businessOverviewOrganizationScopeLabel(value) {
|
|||
}
|
||||
const comparable = compactComparable(text);
|
||||
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)) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1158,7 +1179,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
outgoingAmount ? `исходящие ${outgoingAmount}` : null,
|
||||
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
|
||||
].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("Что видно в ограниченном денежном срезе:");
|
||||
if (incomingAmount) {
|
||||
|
|
@ -1193,7 +1214,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
lines.push(`- ${localizeLine(debtLine)}`);
|
||||
}
|
||||
if (!limitLine) {
|
||||
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
|
||||
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, а не финансовый аудит и не подтверждение чистой прибыли.");
|
||||
}
|
||||
const missingOverviewFamilies = [];
|
||||
if (!taxLine) {
|
||||
|
|
|
|||
|
|
@ -364,6 +364,51 @@ function hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarificati
|
|||
const boundedMovementAnswerAllowed = bridge?.user_facing_response_allowed === true && bridge?.business_fact_answer_allowed === true;
|
||||
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) {
|
||||
if (!hasCurrentClarificationRequiredAddressReply(input)) {
|
||||
return false;
|
||||
|
|
@ -570,6 +615,53 @@ function hasExactBankOperationsAddressReply(input, entryPoint) {
|
|||
routeMode === "exact" ||
|
||||
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) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
|
|
@ -923,7 +1015,8 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint);
|
||||
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(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 exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(input, entryPoint);
|
||||
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
||||
|
|
@ -934,16 +1027,20 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
const discoveryClarificationCandidatePriority = hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(input, entryPoint, candidate);
|
||||
const groundedValueFlowCandidatePriority = hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate);
|
||||
const groundedMovementCandidatePriority = hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
|
||||
const businessOverviewBoundaryCandidatePriority = hasBusinessOverviewBoundaryCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
|
||||
const groundedValueFlowRankingCandidatePriority = hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate);
|
||||
const currentClarificationProtectsCurrentReply = currentClarificationRequiredAddressReply &&
|
||||
!discoveryClarificationCandidatePriority &&
|
||||
!groundedValueFlowCandidatePriority &&
|
||||
!groundedMovementCandidatePriority &&
|
||||
!businessOverviewBoundaryCandidatePriority &&
|
||||
!groundedValueFlowRankingCandidatePriority;
|
||||
const alignedFactualAddressReplyProtectsCurrent = alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
||||
const matchedFactualAddressContinuationTargetProtectsCurrent = matchedFactualAddressContinuationTarget && !groundedValueFlowRankingCandidatePriority;
|
||||
const fullConfirmedFactualAddressReplyProtectsCurrent = fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
||||
const exactMatchedFactualAddressReplyProtectsCurrent = exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
||||
const alignedFactualAddressReplyProtectsCurrent = alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||
const matchedFactualAddressContinuationTargetProtectsCurrent = matchedFactualAddressContinuationTarget &&
|
||||
!groundedValueFlowRankingCandidatePriority &&
|
||||
!documentListTemporalConflictWithDiscovery;
|
||||
const fullConfirmedFactualAddressReplyProtectsCurrent = fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||
const exactMatchedFactualAddressReplyProtectsCurrent = exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||
const exactBankOperationsProtectsCurrent = exactBankOperationsAddressReply &&
|
||||
!semanticConflictWithDiscoveryTurnMeaning &&
|
||||
!valueFlowActionConflictWithDiscoveryTurnMeaning;
|
||||
|
|
@ -977,6 +1074,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
|
||||
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) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
||||
}
|
||||
|
|
@ -989,6 +1089,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
if (groundedMovementCandidatePriority) {
|
||||
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) {
|
||||
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)) {
|
||||
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([
|
||||
"эта организация",
|
||||
"этой организации",
|
||||
|
|
@ -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)) {
|
||||
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);
|
||||
}
|
||||
function hasValueRankingSignal(text) {
|
||||
|
|
@ -921,7 +929,7 @@ function normalizeLooseOrganizationAlias(value) {
|
|||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
if (isNegativeCounterpartyScopePlaceholder(text)) {
|
||||
if (isReferentialOrganizationPlaceholder(text)) {
|
||||
return null;
|
||||
}
|
||||
const normalized = text
|
||||
|
|
|
|||
|
|
@ -184,7 +184,10 @@ function detectScopedCounterpartyEntity(text) {
|
|||
if (!rawEntity) {
|
||||
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)) {
|
||||
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 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);
|
||||
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;
|
||||
}
|
||||
const entity = detectScopedCounterpartyEntity(text);
|
||||
|
|
@ -410,7 +414,9 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||
: detectBroadBusinessEvaluation(joinedText);
|
||||
const explicitIntentCandidate = broadBusinessEvaluation?.family
|
||||
? null
|
||||
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||
: counterpartyBidirectionalValueFlow?.family
|
||||
? null
|
||||
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||
const unsupportedFamily = broadBusinessEvaluation?.family
|
||||
? broadBusinessEvaluation.family
|
||||
: !explicitIntentCandidate && counterpartyBidirectionalValueFlow?.family
|
||||
|
|
|
|||
|
|
@ -2200,7 +2200,11 @@ function hasNomenclatureMarginRankingSignal(text: string): boolean {
|
|||
/(?:посчита\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: string): boolean {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,11 @@ function hasInventoryMarginRankingSignal(text: string): boolean {
|
|||
/(?:посчита\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: string): boolean {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { nanoid } from "nanoid";
|
||||
import type { AssistantConversationItem } from "../types/assistant";
|
||||
import type { AddressIntent } from "../types/addressQuery";
|
||||
import { normalizeRussianComparableText } from "./addressTextRepair";
|
||||
import {
|
||||
readAddressDebugCounterparty,
|
||||
readAddressDebugItem,
|
||||
|
|
@ -150,6 +151,56 @@ function readNavigationDiscoveryCounterparty(debug: Record<string, unknown>): st
|
|||
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 {
|
||||
const normalized = toNonEmptyString(value);
|
||||
if (!normalized) {
|
||||
|
|
@ -346,6 +397,43 @@ function normalizeFilters(value: unknown): Record<string, unknown> {
|
|||
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 {
|
||||
const continuationContract = toObject(debug.dialog_continuation_contract_v2);
|
||||
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 turnInput = toObject(entryPoint?.turn_input);
|
||||
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
|
||||
const bridge = toObject(entryPoint?.bridge);
|
||||
const pilot = toObject(bridge?.pilot);
|
||||
const pilot = readDiscoveryPilot(debug);
|
||||
const counterpartyValueFlowBundle =
|
||||
cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
|
||||
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 {
|
||||
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
||||
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
||||
if (selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true) {
|
||||
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
||||
if (counterparty) {
|
||||
if (counterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, counterparty)) {
|
||||
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
||||
}
|
||||
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
|
||||
|
|
@ -434,9 +534,8 @@ function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId:
|
|||
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
||||
}
|
||||
}
|
||||
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
|
||||
const counterparty =
|
||||
readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
||||
if (isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true) {
|
||||
const counterparty = readCurrentDiscoveryCounterparty(debug);
|
||||
if (counterparty) {
|
||||
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
||||
}
|
||||
|
|
@ -659,27 +758,28 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
|||
}
|
||||
const debug = item.debug as unknown as Record<string, unknown>;
|
||||
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 intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
|
||||
const discoveryIntent = intentFromDiscoveryChain(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) {
|
||||
return state;
|
||||
}
|
||||
const createdAt = toNonEmptyString(item.created_at) ?? new Date().toISOString();
|
||||
const resultSetId = `rs-${item.message_id}`;
|
||||
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) ?? readAddressDebugOrganization(debug, toNonEmptyString);
|
||||
const derivedCounterpartyScope =
|
||||
selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
||||
? readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
|
||||
isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true
|
||||
? readCurrentDiscoveryCounterparty(debug)
|
||||
: null;
|
||||
const filtersWithDerivedScope =
|
||||
derivedOrganizationScope && !toNonEmptyString(filters.organization)
|
||||
|
|
@ -719,9 +819,17 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
|||
debugFocusObject,
|
||||
primaryEntityFocusObject
|
||||
);
|
||||
const rawComparisonCounterparty =
|
||||
debug.mcp_discovery_response_applied === true
|
||||
? selectedDiscoveryChain === "business_overview"
|
||||
? readNavigationDiscoveryCounterparty(debug)
|
||||
: isCounterpartyDiscoveryChain(selectedDiscoveryChain)
|
||||
? readCurrentDiscoveryCounterparty(debug)
|
||||
: null
|
||||
: null;
|
||||
const comparisonCounterparty =
|
||||
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||
? readNavigationDiscoveryCounterparty(debug)
|
||||
rawComparisonCounterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, rawComparisonCounterparty)
|
||||
? rawComparisonCounterparty
|
||||
: null;
|
||||
const comparisonOrganization =
|
||||
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||
|
|
@ -730,6 +838,8 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
|||
const currentComparisonProofBundles =
|
||||
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||
? readBusinessOverviewComparisonProofBundles(debug)
|
||||
: selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
||||
? readCounterpartyValueFlowProofBundles(debug)
|
||||
: null;
|
||||
const inheritedComparisonScope = state.session_context.comparison_scope;
|
||||
const inheritedComparisonProofBundles =
|
||||
|
|
|
|||
|
|
@ -1629,6 +1629,16 @@ function mergeFollowupFilters(
|
|||
intent === "vat_payable_confirmed_as_of_date";
|
||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||
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 currentContractExplicit = toNonEmptyString(merged.contract);
|
||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||
|
|
@ -1717,7 +1727,7 @@ function mergeFollowupFilters(
|
|||
}
|
||||
|
||||
if (
|
||||
!currentHasPeriod &&
|
||||
(!currentHasPeriod || currentValueFlowPeriodDefaultsToToday) &&
|
||||
previousHasPeriod &&
|
||||
hasFollowupSignal &&
|
||||
!hasExplicitPeriodInMessage &&
|
||||
|
|
|
|||
|
|
@ -624,12 +624,27 @@ function normalizeCounterpartyCandidate(
|
|||
toNonEmptyString: (value: unknown) => string | null
|
||||
): string | null {
|
||||
const text = toNonEmptyString(value);
|
||||
if (!text || isReferentialCounterpartyPlaceholder(text)) {
|
||||
if (!text || isReferentialCounterpartyPlaceholder(text) || isCounterpartyRoleFragment(text)) {
|
||||
return null;
|
||||
}
|
||||
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(
|
||||
left: string | null,
|
||||
right: string | null
|
||||
|
|
@ -877,6 +892,9 @@ export function resolveAddressDebugCarryoverFilters(
|
|||
const preferGroundedDiscoveryCounterparty =
|
||||
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) && Boolean(counterparty);
|
||||
const existingCounterparty = normalizeCounterpartyCandidate(nextFilters.counterparty, toNonEmptyString);
|
||||
if (!existingCounterparty) {
|
||||
delete nextFilters.counterparty;
|
||||
}
|
||||
if (counterparty && (preferGroundedDiscoveryCounterparty || !existingCounterparty)) {
|
||||
nextFilters.counterparty = counterparty;
|
||||
}
|
||||
|
|
@ -1140,6 +1158,10 @@ export function resolveFollowupTargetIntent(
|
|||
export function shouldUseNavigationTemporalCarryover(sourceIntentHint: unknown): boolean {
|
||||
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
|
||||
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_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)) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -293,7 +293,23 @@ function valueFlowZeroResultConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecu
|
|||
const period = explicitDateScope(pilot);
|
||||
const organizationPart = organization ? ` по организации ${organization}` : "";
|
||||
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
|
||||
)} не найдено.`;
|
||||
}
|
||||
|
|
@ -312,11 +328,11 @@ function valueFlowZeroResultUnknownLine(pilot: AssistantMcpDiscoveryPilotExecuti
|
|||
}
|
||||
|
||||
function valueFlowZeroResultHeadline(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
const confirmedLine = valueFlowZeroResultConfirmedLine(pilot);
|
||||
if (!confirmedLine) {
|
||||
const insufficiencyLine = valueFlowZeroResultInsufficiencyLine(pilot);
|
||||
if (!insufficiencyLine) {
|
||||
return null;
|
||||
}
|
||||
return confirmedLine;
|
||||
return insufficiencyLine;
|
||||
}
|
||||
|
||||
function hasAllTimeScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
|
|
@ -1097,6 +1113,13 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
}
|
||||
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)) {
|
||||
return pilot.derived_business_overview
|
||||
? businessOverviewNextStepLine(pilot.derived_business_overview)
|
||||
|
|
@ -2119,8 +2142,6 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
? [derivedValueLine]
|
||||
: derivedValueLine
|
||||
? [derivedValueLine, ...monthlyConfirmedLines]
|
||||
: valueFlowZeroResultConfirmedLine(pilot)
|
||||
? [valueFlowZeroResultConfirmedLine(pilot)!]
|
||||
: derivedEntityResolutionLine
|
||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||
: derivedMetadataLine
|
||||
|
|
@ -2132,7 +2153,12 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
? pilot.derived_metadata_surface.available_fields.length > 0
|
||||
? userFacingUnknowns(pilot.evidence.unknown_facts)
|
||||
: ["Детальный список полей этих объектов этим шагом не получен."]
|
||||
: appendValueFlowZeroResultUnknown(rankedValueFlowUnknownLines(pilot), pilot);
|
||||
: appendValueFlowZeroResultUnknown(
|
||||
valueFlowZeroResultCheckedLine(pilot)
|
||||
? [valueFlowZeroResultCheckedLine(pilot)!, ...rankedValueFlowUnknownLines(pilot)]
|
||||
: rankedValueFlowUnknownLines(pilot),
|
||||
pilot
|
||||
);
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
|
||||
import { normalizeRussianComparableText } from "./addressTextRepair";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION =
|
||||
"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 {
|
||||
return action === "turnover" || action === "payout";
|
||||
}
|
||||
|
|
@ -565,7 +600,8 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
|||
const rawAction = lower(turnMeaning?.asked_action_family);
|
||||
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
|
||||
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 seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
|
||||
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
|
||||
|
|
@ -583,12 +619,15 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
|||
});
|
||||
const oneSidedValueAction = inferredOpenScopeOneSidedValueAction(rawQuestionSignal);
|
||||
const detailedBusinessOverviewMoneyBreakdownHint = hasDetailedBusinessOverviewMoneyBreakdownHint(rawQuestionSignal);
|
||||
const wideBusinessOverviewSurfaceHint = hasWideBusinessOverviewSurfaceHint(rawQuestionSource);
|
||||
const oneSidedBusinessOverviewValueFlowOverride =
|
||||
initialBusinessFactFamily === "business_overview" &&
|
||||
Boolean(oneSidedValueAction) &&
|
||||
!detailedBusinessOverviewMoneyBreakdownHint;
|
||||
const bidirectionalBusinessOverviewValueFlowOverride =
|
||||
initialBusinessFactFamily === "business_overview" && hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
|
||||
initialBusinessFactFamily === "business_overview" &&
|
||||
hasIncomingVsOutgoingComparisonHint(rawQuestionSignal) &&
|
||||
!wideBusinessOverviewSurfaceHint;
|
||||
const oneSidedValueFlowActionOverride =
|
||||
initialBusinessFactFamily === "value_flow" &&
|
||||
Boolean(oneSidedValueAction) &&
|
||||
|
|
@ -735,6 +774,9 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
|
|||
if (businessFactFamily === "business_overview" && !explicitDateScope) {
|
||||
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) {
|
||||
pushReason(reasonCodes, "data_need_graph_business_overview_direct_money_answer");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3217,9 +3217,28 @@ function deriveValueFlow(
|
|||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"],
|
||||
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
|
||||
): AssistantMcpDiscoveryDerivedValueFlow | null {
|
||||
if (!result || result.error || result.matched_rows <= 0) {
|
||||
if (!result || result.error) {
|
||||
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 rowsWithAmount = 0;
|
||||
for (const row of result.rows) {
|
||||
|
|
@ -5294,16 +5313,30 @@ function buildValueFlowConfirmedFacts(
|
|||
counterparty: string | null,
|
||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"]
|
||||
): string[] {
|
||||
if (result.error || result.matched_rows <= 0) {
|
||||
if (result.error) {
|
||||
return [];
|
||||
}
|
||||
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 [
|
||||
counterparty
|
||||
? `1C supplier-payout rows were found for counterparty ${counterparty}`
|
||||
: "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 [
|
||||
counterparty
|
||||
? `1C value-flow rows were found for counterparty ${counterparty}`
|
||||
|
|
@ -5521,6 +5554,14 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
|
|||
return [];
|
||||
}
|
||||
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") {
|
||||
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -386,6 +386,7 @@ export function resolveAssistantMcpDiscoveryEvidence(
|
|||
const rowsMatched = probeRowsMatched(probeResults);
|
||||
const rowsReceived = probeRowsReceived(probeResults);
|
||||
const bypassDetected = hasProbeBypass(input.plan, probeResults);
|
||||
const confirmedNoMatchFact = confirmedFacts.some((fact) => /\bnot_found\b/iu.test(String(fact ?? "")));
|
||||
|
||||
if (bypassDetected) {
|
||||
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
|
||||
|
|
@ -393,7 +394,7 @@ export function resolveAssistantMcpDiscoveryEvidence(
|
|||
if (input.plan.plan_status !== "allowed") {
|
||||
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");
|
||||
}
|
||||
if (!sourceRowsSummary && rowsReceived > 0) {
|
||||
|
|
@ -413,6 +414,11 @@ export function resolveAssistantMcpDiscoveryEvidence(
|
|||
coverageStatus = "full";
|
||||
answerPermission = "confirmed_answer";
|
||||
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) {
|
||||
evidenceStatus = "inferred_only";
|
||||
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)) {
|
||||
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)) {
|
||||
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
|
||||
);
|
||||
|
|
@ -319,6 +325,10 @@ function localizeLine(value: string): string {
|
|||
if (valueFlowMatch) {
|
||||
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)) {
|
||||
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
|
||||
}
|
||||
|
|
@ -340,6 +350,10 @@ function localizeLine(value: string): string {
|
|||
if (supplierPayoutMatch) {
|
||||
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)) {
|
||||
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)) {
|
||||
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)) {
|
||||
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
|
||||
}
|
||||
|
|
@ -838,6 +858,9 @@ function businessOverviewOrganizationScopeLabel(value: unknown): string | null {
|
|||
const comparable = compactComparable(text);
|
||||
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)
|
||||
) {
|
||||
return null;
|
||||
|
|
@ -1426,7 +1449,7 @@ function buildCompactBusinessOverviewReply(
|
|||
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
|
||||
].filter((value): value is string => Boolean(value));
|
||||
lines.push(
|
||||
`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, не аудиторское заключение и не подтвержденная чистая прибыль.`
|
||||
`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, а не финансовый аудит и не подтверждение чистой прибыли.`
|
||||
);
|
||||
lines.push(
|
||||
"Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж."
|
||||
|
|
@ -1469,7 +1492,7 @@ function buildCompactBusinessOverviewReply(
|
|||
}
|
||||
if (!limitLine) {
|
||||
lines.push(
|
||||
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
|
||||
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, а не финансовый аудит и не подтверждение чистой прибыли."
|
||||
);
|
||||
}
|
||||
const missingOverviewFamilies: string[] = [];
|
||||
|
|
|
|||
|
|
@ -526,6 +526,66 @@ function hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarificati
|
|||
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(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
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(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
|
|
@ -1239,7 +1358,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
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 exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(
|
||||
input,
|
||||
|
|
@ -1268,6 +1389,8 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
);
|
||||
const groundedMovementCandidatePriority =
|
||||
hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
|
||||
const businessOverviewBoundaryCandidatePriority =
|
||||
hasBusinessOverviewBoundaryCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
|
||||
const groundedValueFlowRankingCandidatePriority =
|
||||
hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate);
|
||||
const currentClarificationProtectsCurrentReply =
|
||||
|
|
@ -1275,15 +1398,18 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
!discoveryClarificationCandidatePriority &&
|
||||
!groundedValueFlowCandidatePriority &&
|
||||
!groundedMovementCandidatePriority &&
|
||||
!businessOverviewBoundaryCandidatePriority &&
|
||||
!groundedValueFlowRankingCandidatePriority;
|
||||
const alignedFactualAddressReplyProtectsCurrent =
|
||||
alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
||||
alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||
const matchedFactualAddressContinuationTargetProtectsCurrent =
|
||||
matchedFactualAddressContinuationTarget && !groundedValueFlowRankingCandidatePriority;
|
||||
matchedFactualAddressContinuationTarget &&
|
||||
!groundedValueFlowRankingCandidatePriority &&
|
||||
!documentListTemporalConflictWithDiscovery;
|
||||
const fullConfirmedFactualAddressReplyProtectsCurrent =
|
||||
fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
||||
fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||
const exactMatchedFactualAddressReplyProtectsCurrent =
|
||||
exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
||||
exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||
const exactBankOperationsProtectsCurrent =
|
||||
exactBankOperationsAddressReply &&
|
||||
!semanticConflictWithDiscoveryTurnMeaning &&
|
||||
|
|
@ -1320,6 +1446,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
|
||||
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) {
|
||||
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"
|
||||
);
|
||||
}
|
||||
if (businessOverviewBoundaryCandidatePriority) {
|
||||
pushReason(
|
||||
reasonCodes,
|
||||
"mcp_discovery_response_policy_business_overview_boundary_candidate_priority_over_inventory_item_clarification"
|
||||
);
|
||||
}
|
||||
if (groundedValueFlowRankingCandidatePriority) {
|
||||
pushReason(
|
||||
reasonCodes,
|
||||
|
|
|
|||
|
|
@ -126,6 +126,14 @@ function isReferentialOrganizationPlaceholder(value: string | null): boolean {
|
|||
if (isNegativeCounterpartyScopePlaceholder(value)) {
|
||||
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([
|
||||
"эта организация",
|
||||
"этой организации",
|
||||
|
|
@ -1258,6 +1266,12 @@ function hasBidirectionalValueFlowSignal(text: string): boolean {
|
|||
) {
|
||||
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
|
||||
);
|
||||
|
|
@ -1320,7 +1334,7 @@ function normalizeLooseOrganizationAlias(value: string | null): string | null {
|
|||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
if (isNegativeCounterpartyScopePlaceholder(text)) {
|
||||
if (isReferentialOrganizationPlaceholder(text)) {
|
||||
return null;
|
||||
}
|
||||
const normalized = text
|
||||
|
|
|
|||
|
|
@ -202,7 +202,10 @@ function detectScopedCounterpartyEntity(text) {
|
|||
if (!rawEntity) {
|
||||
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)) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
const entity = detectScopedCounterpartyEntity(text);
|
||||
|
|
@ -553,6 +560,8 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
|
|||
const explicitIntentCandidate =
|
||||
broadBusinessEvaluation?.family
|
||||
? null
|
||||
: counterpartyBidirectionalValueFlow?.family
|
||||
? null
|
||||
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
|
||||
const unsupportedFamily = 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");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const result = runAddressDecomposeStage("какие остатки по складу на эту же дату", {
|
||||
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");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const followupContext = {
|
||||
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");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const result = resolveAddressIntent("остатки на март 2016");
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,15 @@ describe("addressInventoryIntentSignals", () => {
|
|||
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", () => {
|
||||
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");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const base = createEmptyAddressNavigationState("asst-vf", "2026-04-12T10:00:00.000Z");
|
||||
const assistantItem = {
|
||||
|
|
@ -158,6 +220,262 @@ describe("address navigation state", () => {
|
|||
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", () => {
|
||||
const initial = createEmptyAddressNavigationState("asst-boundary", "2026-04-12T10:00:00.000Z");
|
||||
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", () => {
|
||||
const debug = {
|
||||
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", () => {
|
||||
const flags = resolveInventoryFollowupPivotFlags(
|
||||
{ intent: "inventory_on_hand_as_of_date" },
|
||||
|
|
|
|||
|
|
@ -130,13 +130,14 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
|
||||
|
||||
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("исходящих платежей/списаний не найдено");
|
||||
expect(answerText).toContain("not_found");
|
||||
expect(answerText).toContain("Группа СВК");
|
||||
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 () => {
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
||||
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({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
|
|
@ -1477,18 +1478,18 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
const confirmedText = draft.confirmed_lines.join("\n");
|
||||
const inferenceText = draft.inference_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("2020");
|
||||
expect(draft.headline).toContain("исходящих платежей");
|
||||
expect(draft.headline).toContain("0 руб.");
|
||||
expect(confirmedText).toContain("Группа СВК");
|
||||
expect(confirmedText).toContain("ООО Альтернатива Плюс");
|
||||
expect(confirmedText).toContain("2020");
|
||||
expect(confirmedText).toContain("не найдено");
|
||||
expect(unknownText).toContain("вне периода 2020");
|
||||
expect(unknownText).toContain("вне доступного банковского контура");
|
||||
expect(confirmedText).toContain("0 руб.");
|
||||
expect(confirmedText).toContain("Граница");
|
||||
expect(inferenceText).toContain("not_found result was treated as zero");
|
||||
expect(unknownText).toContain("outside the checked period");
|
||||
});
|
||||
|
||||
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 { 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", () => {
|
||||
it("builds a monthly bidirectional value-flow graph from grounded turn meaning", () => {
|
||||
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");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const result = buildAssistantMcpDiscoveryDataNeedGraph({
|
||||
semanticDataNeed: "counterparty value-flow evidence",
|
||||
|
|
|
|||
|
|
@ -1650,6 +1650,38 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
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 () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
|
|||
|
|
@ -1132,6 +1132,55 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
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", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
|
|
@ -1553,8 +1602,9 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
expect(firstLine).toContain("По подтвержденным строкам 1С");
|
||||
expect(firstLine).toContain("входящие 157 192 981,43 руб.");
|
||||
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("Что не подтверждено в этом срезе");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||
currentReply: "Контрагент: Группа СВК. Найдено документов: 19.",
|
||||
|
|
@ -582,7 +753,73 @@ describe("assistant MCP discovery response policy", () => {
|
|||
expect(result.reply_text).toContain("20 653 490");
|
||||
expect(result.reply_text).toContain("2 129 651");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_value_flow_action_conflict_allows_candidate_override");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
|
||||
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");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -163,6 +163,42 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
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", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
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");
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const policy = buildPolicy({
|
||||
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",
|
||||
"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 json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
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"
|
||||
DEFAULT_GLOBAL_DETECTORS = ["missing_effective_runtime_json"]
|
||||
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:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
return json.loads(read_text(path))
|
||||
|
||||
|
||||
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:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
os.makedirs(path_for_io(path.parent), exist_ok=True)
|
||||
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:
|
||||
|
|
@ -161,13 +193,50 @@ def select_detectors(
|
|||
|
||||
def read_text_or_empty(path: Path) -> str:
|
||||
try:
|
||||
return path.read_text(encoding="utf-8")
|
||||
return read_text(path)
|
||||
except OSError:
|
||||
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:
|
||||
if path is None or not path.exists():
|
||||
if path is None or not path_exists(path):
|
||||
return ""
|
||||
payload = read_json_object(path)
|
||||
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
|
||||
if name == "output.md":
|
||||
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"):
|
||||
prefix = name[: -len("_output.md")]
|
||||
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
|
||||
|
||||
|
||||
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]]:
|
||||
outputs: list[dict[str, Any]] = []
|
||||
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)),
|
||||
"text": read_text_or_empty(path),
|
||||
"turn_path": turn_path,
|
||||
"step_state_path": output_step_state_path(path),
|
||||
}
|
||||
)
|
||||
return outputs
|
||||
|
|
@ -384,7 +461,7 @@ def evaluate_forbidden_regex(
|
|||
lines = [line for line in text.splitlines() if line.strip()]
|
||||
text = "\n".join(lines[:prefix_line_count])
|
||||
for pattern in patterns:
|
||||
match = pattern.search(text)
|
||||
match = first_pattern_search([pattern], text)
|
||||
if match:
|
||||
evidence.append(
|
||||
{
|
||||
|
|
@ -411,7 +488,7 @@ def evaluate_required_any(
|
|||
matched: list[dict[str, Any]] = []
|
||||
for output in outputs:
|
||||
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:
|
||||
matched.append({"path": output["repo_path"], "match": match.group(0)[:240]})
|
||||
else:
|
||||
|
|
@ -428,18 +505,24 @@ def evaluate_limited_next_action(
|
|||
) -> dict[str, Any]:
|
||||
check = detector.get("check") if isinstance(detector.get("check"), dict) else {}
|
||||
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:
|
||||
return build_result(detector_name, detector, "skipped", "no output.md-style artifacts matched detector scope")
|
||||
failures: list[dict[str, Any]] = []
|
||||
limited_outputs = 0
|
||||
for output in outputs:
|
||||
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:
|
||||
continue
|
||||
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]})
|
||||
status = "fail" if failures else "pass"
|
||||
if failures:
|
||||
|
|
@ -451,6 +534,52 @@ def evaluate_limited_next_action(
|
|||
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(
|
||||
detector_name: str,
|
||||
detector: dict[str, Any],
|
||||
|
|
@ -640,7 +769,7 @@ def evaluate_detector(
|
|||
):
|
||||
return evaluate_composite(detector_name, detector, results_by_name)
|
||||
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}")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -317,6 +317,7 @@ DEFAULT_INVARIANT_SEVERITY: dict[str, str] = {
|
|||
"forbidden_recipe_selected": "P0",
|
||||
"focus_object_missing": "P0",
|
||||
"wrong_date_scope_state": "P0",
|
||||
"out_of_window_date_in_answer": "P0",
|
||||
"direct_answer_missing": "P0",
|
||||
"top_level_noise_present": "P0",
|
||||
"business_direct_answer_missing": "P0",
|
||||
|
|
@ -930,6 +931,40 @@ def first_non_empty_lines(text: str, limit: int = 3) -> list[str]:
|
|||
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]]:
|
||||
scenario_tree = raw_contract.get("scenario_tree")
|
||||
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()
|
||||
|
||||
|
||||
def resolve_scenario_contract_placeholders(value: Any, scenario_state: dict[str, Any]) -> Any:
|
||||
if isinstance(value, str):
|
||||
runtime_resolved = str(resolve_runtime_placeholders(value))
|
||||
if "{{" not in runtime_resolved:
|
||||
return runtime_resolved
|
||||
pattern = re.compile(r"{{\s*([^{}]+?)\s*}}")
|
||||
|
||||
def replace(match: re.Match[str]) -> str:
|
||||
value = lookup_placeholder_value(match.group(1), scenario_state)
|
||||
if isinstance(value, (dict, list)):
|
||||
return dump_json(value)
|
||||
return str(value)
|
||||
|
||||
return pattern.sub(replace, runtime_resolved)
|
||||
if isinstance(value, list):
|
||||
return [resolve_scenario_contract_placeholders(item, scenario_state) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: resolve_scenario_contract_placeholders(item, scenario_state)
|
||||
for key, item in value.items()
|
||||
}
|
||||
return value
|
||||
|
||||
|
||||
def resolve_step_contract_placeholders(step: dict[str, Any], scenario_state: dict[str, Any]) -> dict[str, Any]:
|
||||
resolved_step = dict(step)
|
||||
resolved_step["required_filters"] = normalize_validation_filters(
|
||||
resolve_scenario_contract_placeholders(step.get("required_filters") or {}, scenario_state)
|
||||
)
|
||||
for pattern_field in (
|
||||
"forbidden_answer_patterns",
|
||||
"required_answer_patterns_any",
|
||||
"required_answer_patterns_all",
|
||||
):
|
||||
if pattern_field in step:
|
||||
resolved_step[pattern_field] = normalize_pattern_list(
|
||||
resolve_scenario_contract_placeholders(step.get(pattern_field) or [], scenario_state)
|
||||
)
|
||||
return resolved_step
|
||||
|
||||
|
||||
def build_failed_step_state(
|
||||
*,
|
||||
scenario_id: str,
|
||||
|
|
@ -2313,6 +2389,9 @@ def step_intent_matches_expected(state: dict[str, Any], expected_intents: list[s
|
|||
observed_intents = [
|
||||
str(state.get("detected_intent") or "").strip(),
|
||||
*normalize_string_list(state.get("mcp_discovery_effective_intents")),
|
||||
str(state.get("mcp_discovery_selected_chain_id") or "").strip(),
|
||||
str(state.get("mcp_discovery_route_candidate_fact_family") or "").strip(),
|
||||
str(state.get("mcp_discovery_route_candidate_action_family") or "").strip(),
|
||||
]
|
||||
return any(identifier_in_list(intent, expected_intents) for intent in observed_intents if intent)
|
||||
|
||||
|
|
@ -2551,6 +2630,8 @@ def is_validated_memory_checkpoint_answer(
|
|||
) -> bool:
|
||||
if is_validated_comparison_boundary_memory_checkpoint(state, business_review, violations):
|
||||
return True
|
||||
if is_validated_bare_organization_scope_checkpoint(state, business_review, violations):
|
||||
return True
|
||||
tags = set(normalize_string_list(state.get("semantic_tags")))
|
||||
if "memory" not in tags:
|
||||
return False
|
||||
|
|
@ -2566,6 +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(
|
||||
state: dict[str, Any],
|
||||
business_review: dict[str, Any],
|
||||
|
|
@ -2788,7 +2902,12 @@ def is_validated_clean_meta_chat_answer(
|
|||
return False
|
||||
semantic_tags = set(normalize_string_list(state.get("semantic_tags")))
|
||||
allowed_tags = {
|
||||
"human_answer",
|
||||
"meta_smalltalk",
|
||||
"mcp_discovery_gate_sanity",
|
||||
"off_domain_living_chat",
|
||||
"stale_replay_forbidden",
|
||||
"context_boundary",
|
||||
"company_selected",
|
||||
"organization_authority",
|
||||
"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"]:
|
||||
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 not actual_direct_answer or is_top_level_noise_line(actual_direct_answer):
|
||||
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["missing_axis_clarification_validated"] = missing_axis_clarification_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(
|
||||
execution_status,
|
||||
effective_execution_status,
|
||||
hard_fail,
|
||||
(
|
||||
bounded_validated
|
||||
|
|
@ -3235,6 +3360,10 @@ def build_scenario_step_state(
|
|||
"detected_intent": debug.get("detected_intent"),
|
||||
"selected_recipe": debug.get("selected_recipe"),
|
||||
"capability_id": debug.get("capability_id"),
|
||||
"living_chat_response_source": debug.get("living_chat_response_source"),
|
||||
"living_chat_data_scope_probe_status": debug.get("living_chat_data_scope_probe_status"),
|
||||
"living_chat_selected_organization": debug.get("living_chat_selected_organization"),
|
||||
"assistant_active_organization": debug.get("assistant_active_organization"),
|
||||
"capability_contract_id": first_non_empty_string(
|
||||
debug.get("capability_contract_id"),
|
||||
runtime_contract.get("capability_contract_id"),
|
||||
|
|
@ -3600,6 +3729,7 @@ def execute_scenario_manifest(
|
|||
step_dir = steps_dir / step["step_id"]
|
||||
try:
|
||||
resolved_question = resolve_question_template(step["question_template"], scenario_state)
|
||||
resolved_step = resolve_step_contract_placeholders(step, scenario_state)
|
||||
step_analysis_context = merge_analysis_context(manifest.get("analysis_context"), step.get("analysis_context"))
|
||||
step_analysis_context = carry_forward_analysis_context(
|
||||
scenario_state,
|
||||
|
|
@ -3611,7 +3741,7 @@ def execute_scenario_manifest(
|
|||
args=args,
|
||||
domain=manifest["domain"],
|
||||
scenario_id=manifest["scenario_id"],
|
||||
step=step,
|
||||
step=resolved_step,
|
||||
step_index=step_index,
|
||||
session_id=scenario_state.get("session_id"),
|
||||
question_resolved=resolved_question,
|
||||
|
|
@ -6225,7 +6355,22 @@ def summarize_detector_results(detector_results: dict[str, Any] | None, *, limit
|
|||
return names[:limit]
|
||||
|
||||
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 {
|
||||
"status": status,
|
||||
"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"),
|
||||
"review_detectors": _detectors_with_status("review"),
|
||||
"skipped_detectors": _detectors_with_status("skipped"),
|
||||
"declared_skipped_detectors": _declared_with_status("skipped"),
|
||||
"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)
|
||||
if declared_detector_count > 0 and detector_count == 0:
|
||||
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":
|
||||
return False, "declared_detector_results_skipped"
|
||||
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(
|
||||
"--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(
|
||||
"--allow-unvalidated",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
|
@ -22,6 +23,10 @@ def write_text(path: Path, text: str) -> None:
|
|||
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):
|
||||
def test_default_runner_fails_missing_effective_runtime(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
|
|
@ -102,6 +107,59 @@ class AgentDetectorRunnerTests(unittest.TestCase):
|
|||
self.assertEqual(results["summary"]["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:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
|
|
@ -171,7 +229,7 @@ class AgentDetectorRunnerTests(unittest.TestCase):
|
|||
root = Path(tmp)
|
||||
artifact_dir = root / "run"
|
||||
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%.",
|
||||
)
|
||||
write_text(
|
||||
|
|
@ -183,6 +241,15 @@ class AgentDetectorRunnerTests(unittest.TestCase):
|
|||
/ "output.md",
|
||||
"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"
|
||||
issue_catalog_path = root / "issue_catalog.json"
|
||||
write_json(
|
||||
|
|
@ -198,6 +265,7 @@ class AgentDetectorRunnerTests(unittest.TestCase):
|
|||
"inputs": ["output.md"],
|
||||
"check": {
|
||||
"required_patterns_any": ["(?i)(revenue|cogs|gross profit|margin)"],
|
||||
"artifact_path_include_patterns": ["(?i)steps[\\\\/][^\\\\/]*(?:margin|profitability)"],
|
||||
"artifact_path_exclude_patterns": ["(?i)selected_item"],
|
||||
},
|
||||
}
|
||||
|
|
@ -218,7 +286,183 @@ class AgentDetectorRunnerTests(unittest.TestCase):
|
|||
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("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:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
|
|
|
|||
|
|
@ -404,6 +404,24 @@ class DomainCaseLoopLeadHandoffTests(unittest.TestCase):
|
|||
self.assertEqual(summary["declared_detector_count"], 1)
|
||||
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:
|
||||
repair_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_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:
|
||||
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.assertEqual(step_state["acceptance_status"], "rejected")
|
||||
|
||||
def test_binding_placeholder_required_filter_is_resolved_before_step_validation(self) -> None:
|
||||
step = dcl.normalize_step_definition(
|
||||
1,
|
||||
{
|
||||
"step_id": "counterparty_binding_filter",
|
||||
"title": "Counterparty binding filter",
|
||||
"question": "Отдельно по {{bindings.counterparty}}: какой это поток?",
|
||||
"required_filters": {"counterparty": "{{bindings.counterparty}}"},
|
||||
"required_answer_patterns_all": ["(?i){{bindings.counterparty}}|банк"],
|
||||
},
|
||||
)
|
||||
scenario_state = {
|
||||
"bindings": {"counterparty": "СБЕРБАНК"},
|
||||
"step_outputs": {},
|
||||
"semantic_memory": {},
|
||||
}
|
||||
|
||||
resolved_step = dcl.resolve_step_contract_placeholders(step, scenario_state)
|
||||
|
||||
self.assertEqual(resolved_step["required_filters"]["counterparty"], "СБЕРБАНК")
|
||||
self.assertEqual(resolved_step["required_answer_patterns_all"], ["(?i)СБЕРБАНК|банк"])
|
||||
|
||||
step_state = dcl.build_scenario_step_state(
|
||||
scenario_id="binding_filter_demo",
|
||||
domain="agentic_loop",
|
||||
step=resolved_step,
|
||||
step_index=1,
|
||||
question_resolved="Отдельно по СБЕРБАНК: какой это поток?",
|
||||
analysis_context={},
|
||||
turn_artifact={
|
||||
"assistant_message": {
|
||||
"reply_type": "factual",
|
||||
"text": "СБЕРБАНК подтвержден как банковский/финансовый поток.",
|
||||
"message_id": "msg-1",
|
||||
"trace_id": "trace-1",
|
||||
},
|
||||
"technical_debug_payload": {
|
||||
"detected_mode": "address_query",
|
||||
"detected_intent": "bank_operations_by_counterparty",
|
||||
"selected_recipe": "address_bank_operations_by_counterparty_v1",
|
||||
"capability_id": "bank_operations_drilldown",
|
||||
"truth_gate_contract_status": "full_confirmed",
|
||||
"mcp_call_status": "matched_non_empty",
|
||||
"extracted_filters": {"counterparty": "СБЕРБАНК"},
|
||||
},
|
||||
"session_summary": {},
|
||||
},
|
||||
entries=[],
|
||||
)
|
||||
|
||||
self.assertNotIn("missing_required_filter", step_state["violated_invariants"])
|
||||
|
||||
def test_expected_intent_accepts_discovery_chain_id(self) -> None:
|
||||
self.assertTrue(
|
||||
dcl.step_intent_matches_expected(
|
||||
{
|
||||
"detected_intent": "supplier_payouts_profile",
|
||||
"mcp_discovery_selected_chain_id": "value_flow",
|
||||
"mcp_discovery_route_candidate_fact_family": "value_flow",
|
||||
"mcp_discovery_route_candidate_action_family": "payout",
|
||||
},
|
||||
["value_flow"],
|
||||
)
|
||||
)
|
||||
|
||||
def test_bare_organization_scope_selection_is_validated_memory_checkpoint(self) -> None:
|
||||
step_state = dcl.build_scenario_step_state(
|
||||
scenario_id="org_scope_demo",
|
||||
domain="agentic_loop",
|
||||
step={
|
||||
"step_id": "step_01",
|
||||
"title": "Choose organization",
|
||||
"depends_on": [],
|
||||
"question_template": "ООО Альтернатива Плюс",
|
||||
"semantic_tags": ["bare_org_scope", "company_scope"],
|
||||
"required_answer_patterns_all": ["(?i)фиксир|рабоч.*организац|контур"],
|
||||
},
|
||||
step_index=1,
|
||||
question_resolved="ООО Альтернатива Плюс",
|
||||
analysis_context={},
|
||||
turn_artifact={
|
||||
"assistant_message": {
|
||||
"reply_type": "factual_with_explanation",
|
||||
"text": 'Отлично, фиксирую рабочую организацию: ООО "Альтернатива Плюс".',
|
||||
"message_id": "msg-1",
|
||||
"trace_id": "trace-1",
|
||||
},
|
||||
"technical_debug_payload": {
|
||||
"detected_mode": "chat",
|
||||
"fallback_type": "none",
|
||||
"living_chat_response_source": "deterministic_data_scope_selection_contract",
|
||||
"living_chat_data_scope_probe_status": "resolved",
|
||||
"living_chat_selected_organization": 'ООО "Альтернатива Плюс"',
|
||||
"assistant_active_organization": 'ООО "Альтернатива Плюс"',
|
||||
},
|
||||
"session_summary": {},
|
||||
},
|
||||
entries=[],
|
||||
)
|
||||
|
||||
self.assertEqual(step_state["status"], "validated")
|
||||
self.assertTrue(step_state["memory_checkpoint_validated"])
|
||||
|
||||
def test_repair_targets_promote_route_candidate_enablement_gaps(self) -> None:
|
||||
repair_targets = dcl.build_deterministic_repair_targets(
|
||||
{"pack_id": "route_candidate_pack", "domain": "open_world", "final_status": "accepted"},
|
||||
|
|
@ -507,6 +685,32 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
|
|||
self.assertIsNone(step_state["date_scope"]["as_of_date"])
|
||||
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:
|
||||
step_state = dcl.build_scenario_step_state(
|
||||
scenario_id="open_items_negative_demo",
|
||||
|
|
|
|||
Loading…
Reference in New Issue