Удержать контекст СВК в follow-up ассистента
This commit is contained in:
parent
7e70ec1d46
commit
b3f2e405bb
|
|
@ -127,6 +127,48 @@ function readNavigationDiscoveryCounterparty(debug) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function readDiscoveryPilot(debug) {
|
||||||
|
const entry = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
||||||
|
const bridge = toObject(entry?.bridge);
|
||||||
|
return toObject(bridge?.pilot);
|
||||||
|
}
|
||||||
|
function readCurrentDiscoveryCounterparty(debug) {
|
||||||
|
const pilot = readDiscoveryPilot(debug);
|
||||||
|
const entityResolution = toObject(pilot?.derived_entity_resolution);
|
||||||
|
const bidirectionalValueFlow = toObject(pilot?.derived_bidirectional_value_flow);
|
||||||
|
const valueFlow = toObject(pilot?.derived_value_flow);
|
||||||
|
return (toNonEmptyString(entityResolution?.resolved_entity) ??
|
||||||
|
toNonEmptyString(bidirectionalValueFlow?.counterparty) ??
|
||||||
|
toNonEmptyString(valueFlow?.counterparty) ??
|
||||||
|
(0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ??
|
||||||
|
readNavigationDiscoveryCounterparty(debug));
|
||||||
|
}
|
||||||
|
function isCounterpartyDiscoveryChain(value) {
|
||||||
|
return (value === "entity_resolution" ||
|
||||||
|
value === "value_flow" ||
|
||||||
|
value === "value_flow_comparison" ||
|
||||||
|
value === "document_evidence" ||
|
||||||
|
value === "movement_evidence" ||
|
||||||
|
value === "lifecycle");
|
||||||
|
}
|
||||||
|
function intentFromDiscoveryChain(value) {
|
||||||
|
if (value === "business_overview") {
|
||||||
|
return "business_overview";
|
||||||
|
}
|
||||||
|
if (value === "value_flow" || value === "value_flow_comparison") {
|
||||||
|
return "customer_revenue_and_payments";
|
||||||
|
}
|
||||||
|
if (value === "document_evidence") {
|
||||||
|
return "list_documents_by_counterparty";
|
||||||
|
}
|
||||||
|
if (value === "movement_evidence") {
|
||||||
|
return "bank_operations_by_counterparty";
|
||||||
|
}
|
||||||
|
if (value === "lifecycle") {
|
||||||
|
return "counterparty_activity_lifecycle";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
function toAddressFocusObjectType(value) {
|
function toAddressFocusObjectType(value) {
|
||||||
const normalized = toNonEmptyString(value);
|
const normalized = toNonEmptyString(value);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -364,8 +406,7 @@ function readBusinessOverviewComparisonProofBundles(debug) {
|
||||||
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
||||||
const turnInput = toObject(entryPoint?.turn_input);
|
const turnInput = toObject(entryPoint?.turn_input);
|
||||||
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
|
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
|
||||||
const bridge = toObject(entryPoint?.bridge);
|
const pilot = readDiscoveryPilot(debug);
|
||||||
const pilot = toObject(bridge?.pilot);
|
|
||||||
const counterpartyValueFlowBundle = cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
|
const counterpartyValueFlowBundle = cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
|
||||||
cloneRecord(pilot?.derived_bidirectional_value_flow);
|
cloneRecord(pilot?.derived_bidirectional_value_flow);
|
||||||
const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle);
|
const counterpartyDocumentBundle = cloneRecord(turnMeaning?.previous_counterparty_document_bundle);
|
||||||
|
|
@ -383,6 +424,18 @@ function readBusinessOverviewComparisonProofBundles(debug) {
|
||||||
counterparty_document_bundle: counterpartyDocumentBundle
|
counterparty_document_bundle: counterpartyDocumentBundle
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function readCounterpartyValueFlowProofBundles(debug) {
|
||||||
|
const pilot = readDiscoveryPilot(debug);
|
||||||
|
const counterpartyValueFlowBundle = cloneRecord(pilot?.derived_bidirectional_value_flow);
|
||||||
|
const counterparty = toNonEmptyString(counterpartyValueFlowBundle?.counterparty);
|
||||||
|
if (!counterparty || !counterpartyValueFlowBundle) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
|
||||||
|
counterparty_document_bundle: null
|
||||||
|
};
|
||||||
|
}
|
||||||
function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
|
function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
|
||||||
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
||||||
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
||||||
|
|
@ -396,8 +449,8 @@ function buildFocusObjectFromDebug(debug, resultSetId, createdAt) {
|
||||||
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
|
if (isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true) {
|
||||||
const counterparty = (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
const counterparty = readCurrentDiscoveryCounterparty(debug);
|
||||||
if (counterparty) {
|
if (counterparty) {
|
||||||
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
||||||
}
|
}
|
||||||
|
|
@ -587,14 +640,13 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
|
||||||
}
|
}
|
||||||
const debug = item.debug;
|
const debug = item.debug;
|
||||||
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
||||||
const discoveryIntent = selectedDiscoveryChain === "business_overview"
|
|
||||||
? "business_overview"
|
|
||||||
: selectedDiscoveryChain === "value_flow_comparison"
|
|
||||||
? "customer_revenue_and_payments"
|
|
||||||
: "unknown";
|
|
||||||
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
||||||
const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
|
const discoveryIntent = intentFromDiscoveryChain(selectedDiscoveryChain);
|
||||||
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
|
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
|
||||||
|
const shouldPreferDiscoveryIntent = trackableDiscoveryTurn &&
|
||||||
|
discoveryIntent !== "unknown" &&
|
||||||
|
selectedDiscoveryChain !== "business_overview";
|
||||||
|
const intent = toAddressIntent(shouldPreferDiscoveryIntent ? discoveryIntent : detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
|
||||||
if (intent === "unknown" && !trackableDiscoveryTurn) {
|
if (intent === "unknown" && !trackableDiscoveryTurn) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
@ -603,8 +655,8 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
|
||||||
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
|
const routeId = toNonEmptyString(debug.selected_recipe) ?? selectedDiscoveryChain;
|
||||||
const filters = sanitizeBusinessOverviewNavigationFilters(normalizeFilters(debug.extracted_filters), debug);
|
const filters = sanitizeBusinessOverviewNavigationFilters(normalizeFilters(debug.extracted_filters), debug);
|
||||||
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text) ?? (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
|
const derivedOrganizationScope = resolveDerivedOrganizationScope(debug, filters, item.text) ?? (0, assistantContinuityPolicy_1.readAddressDebugOrganization)(debug, toNonEmptyString);
|
||||||
const derivedCounterpartyScope = selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
const derivedCounterpartyScope = isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true
|
||||||
? (0, assistantContinuityPolicy_1.readAddressDebugCounterparty)(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
|
? readCurrentDiscoveryCounterparty(debug)
|
||||||
: null;
|
: null;
|
||||||
const filtersWithDerivedScope = derivedOrganizationScope && !toNonEmptyString(filters.organization)
|
const filtersWithDerivedScope = derivedOrganizationScope && !toNonEmptyString(filters.organization)
|
||||||
? {
|
? {
|
||||||
|
|
@ -637,8 +689,12 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
|
||||||
const debugFocusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
|
const debugFocusObject = buildFocusObjectFromDebug(debug, resultSetId, createdAt);
|
||||||
const primaryEntityFocusObject = buildFocusObjectFromPrimaryEntityRef(resultSet, createdAt);
|
const primaryEntityFocusObject = buildFocusObjectFromPrimaryEntityRef(resultSet, createdAt);
|
||||||
const focusObject = resolveFocusObjectForNavigation(state, intent, resultSet, debugFocusObject, primaryEntityFocusObject);
|
const focusObject = resolveFocusObjectForNavigation(state, intent, resultSet, debugFocusObject, primaryEntityFocusObject);
|
||||||
const rawComparisonCounterparty = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
const rawComparisonCounterparty = debug.mcp_discovery_response_applied === true
|
||||||
|
? selectedDiscoveryChain === "business_overview"
|
||||||
? readNavigationDiscoveryCounterparty(debug)
|
? readNavigationDiscoveryCounterparty(debug)
|
||||||
|
: isCounterpartyDiscoveryChain(selectedDiscoveryChain)
|
||||||
|
? readCurrentDiscoveryCounterparty(debug)
|
||||||
|
: null
|
||||||
: null;
|
: null;
|
||||||
const comparisonCounterparty = rawComparisonCounterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, rawComparisonCounterparty)
|
const comparisonCounterparty = rawComparisonCounterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, rawComparisonCounterparty)
|
||||||
? rawComparisonCounterparty
|
? rawComparisonCounterparty
|
||||||
|
|
@ -648,6 +704,8 @@ function evolveAddressNavigationStateWithAssistantItem(state, item, turnIndex) {
|
||||||
: null;
|
: null;
|
||||||
const currentComparisonProofBundles = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
const currentComparisonProofBundles = selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||||
? readBusinessOverviewComparisonProofBundles(debug)
|
? readBusinessOverviewComparisonProofBundles(debug)
|
||||||
|
: selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
||||||
|
? readCounterpartyValueFlowProofBundles(debug)
|
||||||
: null;
|
: null;
|
||||||
const inheritedComparisonScope = state.session_context.comparison_scope;
|
const inheritedComparisonScope = state.session_context.comparison_scope;
|
||||||
const inheritedComparisonProofBundles = comparisonCounterparty &&
|
const inheritedComparisonProofBundles = comparisonCounterparty &&
|
||||||
|
|
|
||||||
|
|
@ -1313,6 +1313,15 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
intent === "vat_payable_confirmed_as_of_date";
|
intent === "vat_payable_confirmed_as_of_date";
|
||||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||||
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
||||||
|
const currentPeriodFromForCarryover = toNonEmptyString(merged.period_from);
|
||||||
|
const currentPeriodToForCarryover = toNonEmptyString(merged.period_to);
|
||||||
|
const todayIsoForPeriodCarryover = new Date().toISOString().slice(0, 10);
|
||||||
|
const currentValueFlowPeriodDefaultsToToday = isValueCounterpartyIntent(intent) &&
|
||||||
|
hasFollowupSignal &&
|
||||||
|
!hasExplicitPeriodInMessage &&
|
||||||
|
!hasExplicitCurrentDateInMessage &&
|
||||||
|
!currentPeriodFromForCarryover &&
|
||||||
|
currentPeriodToForCarryover === todayIsoForPeriodCarryover;
|
||||||
const currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
|
const currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
|
||||||
const currentContractExplicit = toNonEmptyString(merged.contract);
|
const currentContractExplicit = toNonEmptyString(merged.contract);
|
||||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||||
|
|
@ -1387,7 +1396,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
}
|
}
|
||||||
reasons.push("period_from_followup_context");
|
reasons.push("period_from_followup_context");
|
||||||
}
|
}
|
||||||
if (!currentHasPeriod &&
|
if ((!currentHasPeriod || currentValueFlowPeriodDefaultsToToday) &&
|
||||||
previousHasPeriod &&
|
previousHasPeriod &&
|
||||||
hasFollowupSignal &&
|
hasFollowupSignal &&
|
||||||
!hasExplicitPeriodInMessage &&
|
!hasExplicitPeriodInMessage &&
|
||||||
|
|
|
||||||
|
|
@ -778,7 +778,11 @@ function resolveFollowupTargetIntent(inventoryPurchaseDateVatBridge, selectedObj
|
||||||
}
|
}
|
||||||
function shouldUseNavigationTemporalCarryover(sourceIntentHint) {
|
function shouldUseNavigationTemporalCarryover(sourceIntentHint) {
|
||||||
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
|
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
|
||||||
return (normalizedIntent === "inventory_on_hand_as_of_date" ||
|
return (normalizedIntent === "customer_revenue_and_payments" ||
|
||||||
|
normalizedIntent === "supplier_payouts_profile" ||
|
||||||
|
normalizedIntent === "contract_usage_and_value" ||
|
||||||
|
normalizedIntent === "counterparty_activity_lifecycle" ||
|
||||||
|
normalizedIntent === "inventory_on_hand_as_of_date" ||
|
||||||
normalizedIntent === "inventory_supplier_stock_overlap_as_of_date" ||
|
normalizedIntent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
||||||
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ function valueFlowDirectionLabelRu(pilot) {
|
||||||
? "исходящих платежей/списаний"
|
? "исходящих платежей/списаний"
|
||||||
: "входящих денежных поступлений";
|
: "входящих денежных поступлений";
|
||||||
}
|
}
|
||||||
function valueFlowZeroResultConfirmedLine(pilot) {
|
function valueFlowZeroResultInsufficiencyLine(pilot) {
|
||||||
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +224,22 @@ function valueFlowZeroResultConfirmedLine(pilot) {
|
||||||
const period = explicitDateScope(pilot);
|
const period = explicitDateScope(pilot);
|
||||||
const organizationPart = organization ? ` по организации ${organization}` : "";
|
const organizationPart = organization ? ` по организации ${organization}` : "";
|
||||||
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
|
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
|
||||||
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${valueFlowDirectionLabelRu(pilot)} не найдено.`;
|
const direction = valueFlowDirectionLabelRu(pilot);
|
||||||
|
return `Точную сумму ${direction} по контрагенту ${counterparty}${organizationPart}${periodPart} не подтверждаю: в выполненной проверке 1С строки ${direction} не найдены; это не доказывает отсутствие операций вне доступного банковского контура.`;
|
||||||
|
}
|
||||||
|
function valueFlowZeroResultCheckedLine(pilot) {
|
||||||
|
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const counterparty = firstEntityCandidate(pilot);
|
||||||
|
if (!counterparty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const organization = explicitOrganizationScope(pilot);
|
||||||
|
const period = explicitDateScope(pilot);
|
||||||
|
const organizationPart = organization ? ` по организации ${organization}` : "";
|
||||||
|
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
|
||||||
|
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart} строки ${valueFlowDirectionLabelRu(pilot)} не найдено.`;
|
||||||
}
|
}
|
||||||
function valueFlowZeroResultUnknownLine(pilot) {
|
function valueFlowZeroResultUnknownLine(pilot) {
|
||||||
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
||||||
|
|
@ -239,11 +254,11 @@ function valueFlowZeroResultUnknownLine(pilot) {
|
||||||
return `Это не доказывает отсутствие операций с контрагентом ${counterparty}${periodPart} или вне доступного банковского контура.`;
|
return `Это не доказывает отсутствие операций с контрагентом ${counterparty}${periodPart} или вне доступного банковского контура.`;
|
||||||
}
|
}
|
||||||
function valueFlowZeroResultHeadline(pilot) {
|
function valueFlowZeroResultHeadline(pilot) {
|
||||||
const confirmedLine = valueFlowZeroResultConfirmedLine(pilot);
|
const insufficiencyLine = valueFlowZeroResultInsufficiencyLine(pilot);
|
||||||
if (!confirmedLine) {
|
if (!insufficiencyLine) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return confirmedLine;
|
return insufficiencyLine;
|
||||||
}
|
}
|
||||||
function hasAllTimeScope(pilot) {
|
function hasAllTimeScope(pilot) {
|
||||||
return (dryRunHasAxis(pilot, "all_time_scope") ||
|
return (dryRunHasAxis(pilot, "all_time_scope") ||
|
||||||
|
|
@ -1846,8 +1861,6 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||||
? [derivedValueLine]
|
? [derivedValueLine]
|
||||||
: derivedValueLine
|
: derivedValueLine
|
||||||
? [derivedValueLine, ...monthlyConfirmedLines]
|
? [derivedValueLine, ...monthlyConfirmedLines]
|
||||||
: valueFlowZeroResultConfirmedLine(pilot)
|
|
||||||
? [valueFlowZeroResultConfirmedLine(pilot)]
|
|
||||||
: derivedEntityResolutionLine
|
: derivedEntityResolutionLine
|
||||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||||
: derivedMetadataLine
|
: derivedMetadataLine
|
||||||
|
|
@ -1859,7 +1872,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||||
? pilot.derived_metadata_surface.available_fields.length > 0
|
? pilot.derived_metadata_surface.available_fields.length > 0
|
||||||
? userFacingUnknowns(pilot.evidence.unknown_facts)
|
? userFacingUnknowns(pilot.evidence.unknown_facts)
|
||||||
: ["Детальный список полей этих объектов этим шагом не получен."]
|
: ["Детальный список полей этих объектов этим шагом не получен."]
|
||||||
: appendValueFlowZeroResultUnknown(rankedValueFlowUnknownLines(pilot), pilot);
|
: appendValueFlowZeroResultUnknown(valueFlowZeroResultCheckedLine(pilot)
|
||||||
|
? [valueFlowZeroResultCheckedLine(pilot), ...rankedValueFlowUnknownLines(pilot)]
|
||||||
|
: rankedValueFlowUnknownLines(pilot), pilot);
|
||||||
return {
|
return {
|
||||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||||
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
|
||||||
|
|
|
||||||
|
|
@ -2209,9 +2209,28 @@ function deriveBusinessOverviewContractUsageProfile(result, periodScope) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function deriveValueFlow(result, counterparty, periodScope, direction, aggregationAxis) {
|
function deriveValueFlow(result, counterparty, periodScope, direction, aggregationAxis) {
|
||||||
if (!result || result.error || result.matched_rows <= 0) {
|
if (!result || result.error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (result.matched_rows <= 0) {
|
||||||
|
return {
|
||||||
|
value_flow_direction: direction,
|
||||||
|
counterparty,
|
||||||
|
period_scope: periodScope,
|
||||||
|
aggregation_axis: aggregationAxis,
|
||||||
|
rows_matched: 0,
|
||||||
|
rows_with_amount: 0,
|
||||||
|
total_amount: 0,
|
||||||
|
total_amount_human_ru: formatAmountHumanRu(0),
|
||||||
|
first_movement_date: null,
|
||||||
|
latest_movement_date: null,
|
||||||
|
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
|
||||||
|
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
|
||||||
|
period_chunking_granularity: result.period_chunking_granularity,
|
||||||
|
monthly_breakdown: [],
|
||||||
|
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
|
||||||
|
};
|
||||||
|
}
|
||||||
let totalAmount = 0;
|
let totalAmount = 0;
|
||||||
let rowsWithAmount = 0;
|
let rowsWithAmount = 0;
|
||||||
for (const row of result.rows) {
|
for (const row of result.rows) {
|
||||||
|
|
@ -3926,16 +3945,30 @@ function buildMovementConfirmedFacts(result, counterparty, periodScope) {
|
||||||
return [`В 1С найдены строки движений${checkedCounterpartySuffixRu(counterparty)}${checkedPeriodSuffixRu(periodScope)}.`];
|
return [`В 1С найдены строки движений${checkedCounterpartySuffixRu(counterparty)}${checkedPeriodSuffixRu(periodScope)}.`];
|
||||||
}
|
}
|
||||||
function buildValueFlowConfirmedFacts(result, counterparty, direction) {
|
function buildValueFlowConfirmedFacts(result, counterparty, direction) {
|
||||||
if (result.error || result.matched_rows <= 0) {
|
if (result.error) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (direction === "outgoing_supplier_payout") {
|
if (direction === "outgoing_supplier_payout") {
|
||||||
|
if (result.matched_rows <= 0) {
|
||||||
|
return [
|
||||||
|
counterparty
|
||||||
|
? `1C supplier-payout rows were checked for counterparty ${counterparty}: not_found`
|
||||||
|
: "1C supplier-payout rows were checked for the requested counterparty scope: not_found"
|
||||||
|
];
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
counterparty
|
counterparty
|
||||||
? `1C supplier-payout rows were found for counterparty ${counterparty}`
|
? `1C supplier-payout rows were found for counterparty ${counterparty}`
|
||||||
: "1C supplier-payout rows were found for the requested counterparty scope"
|
: "1C supplier-payout rows were found for the requested counterparty scope"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if (result.matched_rows <= 0) {
|
||||||
|
return [
|
||||||
|
counterparty
|
||||||
|
? `1C value-flow rows were checked for counterparty ${counterparty}: not_found`
|
||||||
|
: "1C value-flow rows were checked for the requested counterparty scope: not_found"
|
||||||
|
];
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
counterparty
|
counterparty
|
||||||
? `1C value-flow rows were found for counterparty ${counterparty}`
|
? `1C value-flow rows were found for counterparty ${counterparty}`
|
||||||
|
|
@ -4115,6 +4148,12 @@ function buildValueFlowInferredFacts(derived) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const facts = [];
|
const facts = [];
|
||||||
|
if (derived.rows_matched <= 0) {
|
||||||
|
facts.push(derived.value_flow_direction === "outgoing_supplier_payout"
|
||||||
|
? "Counterparty supplier-payout not_found result was treated as zero only inside the scoped checked 1C result set"
|
||||||
|
: "Counterparty incoming value-flow not_found result was treated as zero only inside the scoped checked 1C result set");
|
||||||
|
return facts;
|
||||||
|
}
|
||||||
if (derived.value_flow_direction === "outgoing_supplier_payout") {
|
if (derived.value_flow_direction === "outgoing_supplier_payout") {
|
||||||
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
|
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -261,13 +261,14 @@ function resolveAssistantMcpDiscoveryEvidence(input) {
|
||||||
const rowsMatched = probeRowsMatched(probeResults);
|
const rowsMatched = probeRowsMatched(probeResults);
|
||||||
const rowsReceived = probeRowsReceived(probeResults);
|
const rowsReceived = probeRowsReceived(probeResults);
|
||||||
const bypassDetected = hasProbeBypass(input.plan, probeResults);
|
const bypassDetected = hasProbeBypass(input.plan, probeResults);
|
||||||
|
const confirmedNoMatchFact = confirmedFacts.some((fact) => /\bnot_found\b/iu.test(String(fact ?? "")));
|
||||||
if (bypassDetected) {
|
if (bypassDetected) {
|
||||||
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
|
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
|
||||||
}
|
}
|
||||||
if (input.plan.plan_status !== "allowed") {
|
if (input.plan.plan_status !== "allowed") {
|
||||||
pushReason(reasonCodes, "plan_not_allowed_by_runtime");
|
pushReason(reasonCodes, "plan_not_allowed_by_runtime");
|
||||||
}
|
}
|
||||||
if (confirmedFacts.length > 0 && rowsMatched <= 0) {
|
if (confirmedFacts.length > 0 && rowsMatched <= 0 && !confirmedNoMatchFact) {
|
||||||
pushReason(reasonCodes, "confirmed_facts_without_matched_probe_rows");
|
pushReason(reasonCodes, "confirmed_facts_without_matched_probe_rows");
|
||||||
}
|
}
|
||||||
if (!sourceRowsSummary && rowsReceived > 0) {
|
if (!sourceRowsSummary && rowsReceived > 0) {
|
||||||
|
|
@ -287,6 +288,12 @@ function resolveAssistantMcpDiscoveryEvidence(input) {
|
||||||
answerPermission = "confirmed_answer";
|
answerPermission = "confirmed_answer";
|
||||||
pushReason(reasonCodes, "confirmed_facts_with_allowed_mcp_evidence");
|
pushReason(reasonCodes, "confirmed_facts_with_allowed_mcp_evidence");
|
||||||
}
|
}
|
||||||
|
else if (confirmedFacts.length > 0 && confirmedNoMatchFact && rowsMatched <= 0 && sourceRowsSummary) {
|
||||||
|
evidenceStatus = "confirmed";
|
||||||
|
coverageStatus = "full";
|
||||||
|
answerPermission = "confirmed_answer";
|
||||||
|
pushReason(reasonCodes, "confirmed_no_match_fact_with_allowed_mcp_evidence");
|
||||||
|
}
|
||||||
else if (inferredFacts.length > 0 && rowsReceived > 0) {
|
else if (inferredFacts.length > 0 && rowsReceived > 0) {
|
||||||
evidenceStatus = "inferred_only";
|
evidenceStatus = "inferred_only";
|
||||||
coverageStatus = "partial";
|
coverageStatus = "partial";
|
||||||
|
|
|
||||||
|
|
@ -211,9 +211,15 @@ function localizeLine(value) {
|
||||||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
|
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
|
||||||
}
|
}
|
||||||
|
if (/^1C value-flow rows were checked for the requested counterparty scope: not_found$/i.test(value)) {
|
||||||
|
return "В 1С проверены входящие денежные строки в запрошенном срезе: строки не найдены.";
|
||||||
|
}
|
||||||
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
|
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
|
||||||
}
|
}
|
||||||
|
if (/^1C supplier-payout rows were checked for the requested counterparty scope: not_found$/i.test(value)) {
|
||||||
|
return "В 1С проверены исходящие платежи/списания в запрошенном срезе: строки не найдены.";
|
||||||
|
}
|
||||||
const openScopeBidirectionalMatch = value.match(/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i);
|
const openScopeBidirectionalMatch = value.match(/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i);
|
||||||
if (openScopeBidirectionalMatch) {
|
if (openScopeBidirectionalMatch) {
|
||||||
const incoming = openScopeBidirectionalMatch[1] === "found"
|
const incoming = openScopeBidirectionalMatch[1] === "found"
|
||||||
|
|
@ -238,6 +244,10 @@ function localizeLine(value) {
|
||||||
if (valueFlowMatch) {
|
if (valueFlowMatch) {
|
||||||
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
|
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
|
||||||
}
|
}
|
||||||
|
const valueFlowNotFoundMatch = value.match(/^1C value-flow rows were checked for counterparty\s+(.+): not_found$/i);
|
||||||
|
if (valueFlowNotFoundMatch) {
|
||||||
|
return `В 1С проверены входящие денежные строки по контрагенту ${valueFlowNotFoundMatch[1]}: строки не найдены.`;
|
||||||
|
}
|
||||||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
|
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
|
||||||
}
|
}
|
||||||
|
|
@ -259,6 +269,10 @@ function localizeLine(value) {
|
||||||
if (supplierPayoutMatch) {
|
if (supplierPayoutMatch) {
|
||||||
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`;
|
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`;
|
||||||
}
|
}
|
||||||
|
const supplierPayoutNotFoundMatch = value.match(/^1C supplier-payout rows were checked for counterparty\s+(.+): not_found$/i);
|
||||||
|
if (supplierPayoutNotFoundMatch) {
|
||||||
|
return `В 1С проверены исходящие платежи/списания по контрагенту ${supplierPayoutNotFoundMatch[1]}: строки не найдены.`;
|
||||||
|
}
|
||||||
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
|
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
|
||||||
}
|
}
|
||||||
|
|
@ -292,6 +306,12 @@ function localizeLine(value) {
|
||||||
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
|
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
|
||||||
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
||||||
}
|
}
|
||||||
|
if (/^Counterparty supplier-payout not_found result was treated as zero only inside the scoped checked 1C result set$/i.test(value)) {
|
||||||
|
return "Нулевую сумму исходящих платежей можно трактовать только внутри этого проверенного среза 1С, где строки списаний не найдены.";
|
||||||
|
}
|
||||||
|
if (/^Counterparty incoming value-flow not_found result was treated as zero only inside the scoped checked 1C result set$/i.test(value)) {
|
||||||
|
return "Нулевую сумму входящих поступлений можно трактовать только внутри этого проверенного среза 1С, где строки поступлений не найдены.";
|
||||||
|
}
|
||||||
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
||||||
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
|
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
|
||||||
}
|
}
|
||||||
|
|
@ -1159,7 +1179,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
outgoingAmount ? `исходящие ${outgoingAmount}` : null,
|
outgoingAmount ? `исходящие ${outgoingAmount}` : null,
|
||||||
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
|
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
|
||||||
].filter((value) => Boolean(value));
|
].filter((value) => Boolean(value));
|
||||||
lines.push(`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, не аудиторское заключение и не подтвержденная чистая прибыль.`);
|
lines.push(`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, а не финансовый аудит и не подтверждение чистой прибыли.`);
|
||||||
lines.push("Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж.");
|
lines.push("Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж.");
|
||||||
lines.push("Что видно в ограниченном денежном срезе:");
|
lines.push("Что видно в ограниченном денежном срезе:");
|
||||||
if (incomingAmount) {
|
if (incomingAmount) {
|
||||||
|
|
@ -1194,7 +1214,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||||
lines.push(`- ${localizeLine(debtLine)}`);
|
lines.push(`- ${localizeLine(debtLine)}`);
|
||||||
}
|
}
|
||||||
if (!limitLine) {
|
if (!limitLine) {
|
||||||
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
|
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, а не финансовый аудит и не подтверждение чистой прибыли.");
|
||||||
}
|
}
|
||||||
const missingOverviewFamilies = [];
|
const missingOverviewFamilies = [];
|
||||||
if (!taxLine) {
|
if (!taxLine) {
|
||||||
|
|
|
||||||
|
|
@ -615,6 +615,53 @@ function hasExactBankOperationsAddressReply(input, entryPoint) {
|
||||||
routeMode === "exact" ||
|
routeMode === "exact" ||
|
||||||
hasFullConfirmedTruth(input));
|
hasFullConfirmedTruth(input));
|
||||||
}
|
}
|
||||||
|
function extractExplicitDateScopeYear(value) {
|
||||||
|
const source = toNonEmptyString(value);
|
||||||
|
if (!source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = source.match(/\b((?:19|20)\d{2})\b/u);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
function extractDocumentReplyDateYears(value) {
|
||||||
|
const source = String(value ?? "");
|
||||||
|
if (!source) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const years = new Set();
|
||||||
|
for (const match of source.matchAll(/\b((?:19|20)\d{2})-\d{2}-\d{2}(?:T|\b)/gu)) {
|
||||||
|
years.add(match[1]);
|
||||||
|
}
|
||||||
|
for (const match of source.matchAll(/\b\d{1,2}\.\d{1,2}\.((?:19|20)\d{2})\b/gu)) {
|
||||||
|
years.add(match[1]);
|
||||||
|
}
|
||||||
|
return [...years];
|
||||||
|
}
|
||||||
|
function hasDocumentListTemporalConflictWithDiscovery(input, entryPoint) {
|
||||||
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||||
|
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||||
|
const isDocumentIntent = detectedIntent === "list_documents_by_counterparty" || detectedIntent === "list_documents_by_contract";
|
||||||
|
const isDocumentRecipe = selectedRecipe === "address_documents_by_counterparty_v1" ||
|
||||||
|
selectedRecipe === "address_documents_by_contract_v1";
|
||||||
|
if (!isDocumentIntent || !isDocumentRecipe) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||||
|
const expectedYear = extractExplicitDateScopeYear(turnMeaning?.explicit_date_scope);
|
||||||
|
if (!expectedYear) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const filters = toRecordObject(input.addressRuntimeMeta?.extracted_filters);
|
||||||
|
const periodFrom = toNonEmptyString(filters?.period_from);
|
||||||
|
const periodTo = toNonEmptyString(filters?.period_to);
|
||||||
|
const hasMatchingPeriod = Boolean(periodFrom?.startsWith(`${expectedYear}-`)) && Boolean(periodTo?.startsWith(`${expectedYear}-`));
|
||||||
|
const replyDateYears = extractDocumentReplyDateYears(input.currentReply);
|
||||||
|
const hasOutOfScopeReplyDates = replyDateYears.some((year) => year !== expectedYear);
|
||||||
|
return hasOutOfScopeReplyDates || (!hasMatchingPeriod && replyDateYears.length > 0);
|
||||||
|
}
|
||||||
function hasExactDocumentListAddressReply(input, entryPoint) {
|
function hasExactDocumentListAddressReply(input, entryPoint) {
|
||||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -968,7 +1015,8 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint);
|
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint);
|
||||||
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint);
|
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint);
|
||||||
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
||||||
const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint);
|
const documentListTemporalConflictWithDiscovery = hasDocumentListTemporalConflictWithDiscovery(input, entryPoint);
|
||||||
|
const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint) && !documentListTemporalConflictWithDiscovery;
|
||||||
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
|
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
|
||||||
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(input, entryPoint);
|
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(input, entryPoint);
|
||||||
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
||||||
|
|
@ -987,10 +1035,12 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
!groundedMovementCandidatePriority &&
|
!groundedMovementCandidatePriority &&
|
||||||
!businessOverviewBoundaryCandidatePriority &&
|
!businessOverviewBoundaryCandidatePriority &&
|
||||||
!groundedValueFlowRankingCandidatePriority;
|
!groundedValueFlowRankingCandidatePriority;
|
||||||
const alignedFactualAddressReplyProtectsCurrent = alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
const alignedFactualAddressReplyProtectsCurrent = alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||||
const matchedFactualAddressContinuationTargetProtectsCurrent = matchedFactualAddressContinuationTarget && !groundedValueFlowRankingCandidatePriority;
|
const matchedFactualAddressContinuationTargetProtectsCurrent = matchedFactualAddressContinuationTarget &&
|
||||||
const fullConfirmedFactualAddressReplyProtectsCurrent = fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
!groundedValueFlowRankingCandidatePriority &&
|
||||||
const exactMatchedFactualAddressReplyProtectsCurrent = exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
!documentListTemporalConflictWithDiscovery;
|
||||||
|
const fullConfirmedFactualAddressReplyProtectsCurrent = fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||||
|
const exactMatchedFactualAddressReplyProtectsCurrent = exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||||
const exactBankOperationsProtectsCurrent = exactBankOperationsAddressReply &&
|
const exactBankOperationsProtectsCurrent = exactBankOperationsAddressReply &&
|
||||||
!semanticConflictWithDiscoveryTurnMeaning &&
|
!semanticConflictWithDiscoveryTurnMeaning &&
|
||||||
!valueFlowActionConflictWithDiscoveryTurnMeaning;
|
!valueFlowActionConflictWithDiscoveryTurnMeaning;
|
||||||
|
|
@ -1024,6 +1074,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
||||||
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
|
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
|
||||||
}
|
}
|
||||||
|
if (documentListTemporalConflictWithDiscovery) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_document_list_temporal_conflict_allows_candidate_override");
|
||||||
|
}
|
||||||
if (currentClarificationProtectsCurrentReply) {
|
if (currentClarificationProtectsCurrentReply) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,56 @@ function readNavigationDiscoveryCounterparty(debug: Record<string, unknown>): st
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readDiscoveryPilot(debug: Record<string, unknown>): Record<string, unknown> | null {
|
||||||
|
const entry = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
||||||
|
const bridge = toObject(entry?.bridge);
|
||||||
|
return toObject(bridge?.pilot);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCurrentDiscoveryCounterparty(debug: Record<string, unknown>): string | null {
|
||||||
|
const pilot = readDiscoveryPilot(debug);
|
||||||
|
const entityResolution = toObject(pilot?.derived_entity_resolution);
|
||||||
|
const bidirectionalValueFlow = toObject(pilot?.derived_bidirectional_value_flow);
|
||||||
|
const valueFlow = toObject(pilot?.derived_value_flow);
|
||||||
|
return (
|
||||||
|
toNonEmptyString(entityResolution?.resolved_entity) ??
|
||||||
|
toNonEmptyString(bidirectionalValueFlow?.counterparty) ??
|
||||||
|
toNonEmptyString(valueFlow?.counterparty) ??
|
||||||
|
readAddressDebugCounterparty(debug, toNonEmptyString) ??
|
||||||
|
readNavigationDiscoveryCounterparty(debug)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCounterpartyDiscoveryChain(value: string | null): boolean {
|
||||||
|
return (
|
||||||
|
value === "entity_resolution" ||
|
||||||
|
value === "value_flow" ||
|
||||||
|
value === "value_flow_comparison" ||
|
||||||
|
value === "document_evidence" ||
|
||||||
|
value === "movement_evidence" ||
|
||||||
|
value === "lifecycle"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function intentFromDiscoveryChain(value: string | null): AddressIntent {
|
||||||
|
if (value === "business_overview") {
|
||||||
|
return "business_overview";
|
||||||
|
}
|
||||||
|
if (value === "value_flow" || value === "value_flow_comparison") {
|
||||||
|
return "customer_revenue_and_payments";
|
||||||
|
}
|
||||||
|
if (value === "document_evidence") {
|
||||||
|
return "list_documents_by_counterparty";
|
||||||
|
}
|
||||||
|
if (value === "movement_evidence") {
|
||||||
|
return "bank_operations_by_counterparty";
|
||||||
|
}
|
||||||
|
if (value === "lifecycle") {
|
||||||
|
return "counterparty_activity_lifecycle";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
function toAddressFocusObjectType(value: unknown): AddressFocusObjectType {
|
function toAddressFocusObjectType(value: unknown): AddressFocusObjectType {
|
||||||
const normalized = toNonEmptyString(value);
|
const normalized = toNonEmptyString(value);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
|
|
@ -437,8 +487,7 @@ function readBusinessOverviewComparisonProofBundles(debug: Record<string, unknow
|
||||||
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
const entryPoint = toObject(debug.assistant_mcp_discovery_entry_point_v1);
|
||||||
const turnInput = toObject(entryPoint?.turn_input);
|
const turnInput = toObject(entryPoint?.turn_input);
|
||||||
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
|
const turnMeaning = toObject(turnInput?.turn_meaning_ref);
|
||||||
const bridge = toObject(entryPoint?.bridge);
|
const pilot = readDiscoveryPilot(debug);
|
||||||
const pilot = toObject(bridge?.pilot);
|
|
||||||
const counterpartyValueFlowBundle =
|
const counterpartyValueFlowBundle =
|
||||||
cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
|
cloneRecord(turnMeaning?.previous_counterparty_value_flow_bundle) ??
|
||||||
cloneRecord(pilot?.derived_bidirectional_value_flow);
|
cloneRecord(pilot?.derived_bidirectional_value_flow);
|
||||||
|
|
@ -459,6 +508,19 @@ function readBusinessOverviewComparisonProofBundles(debug: Record<string, unknow
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readCounterpartyValueFlowProofBundles(debug: Record<string, unknown>): AddressComparisonProofBundles | null {
|
||||||
|
const pilot = readDiscoveryPilot(debug);
|
||||||
|
const counterpartyValueFlowBundle = cloneRecord(pilot?.derived_bidirectional_value_flow);
|
||||||
|
const counterparty = toNonEmptyString(counterpartyValueFlowBundle?.counterparty);
|
||||||
|
if (!counterparty || !counterpartyValueFlowBundle) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
counterparty_value_flow_bundle: counterpartyValueFlowBundle,
|
||||||
|
counterparty_document_bundle: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId: string, createdAt: string): AddressFocusObject | null {
|
function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId: string, createdAt: string): AddressFocusObject | null {
|
||||||
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
const extractedFilters = toObject(debug.extracted_filters) ?? {};
|
||||||
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
||||||
|
|
@ -472,9 +534,8 @@ function buildFocusObjectFromDebug(debug: Record<string, unknown>, resultSetId:
|
||||||
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
return buildFocusObject("organization", organization, resultSetId, createdAt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true) {
|
if (isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true) {
|
||||||
const counterparty =
|
const counterparty = readCurrentDiscoveryCounterparty(debug);
|
||||||
readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug);
|
|
||||||
if (counterparty) {
|
if (counterparty) {
|
||||||
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
return buildFocusObject("counterparty", counterparty, resultSetId, createdAt);
|
||||||
}
|
}
|
||||||
|
|
@ -697,15 +758,16 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
||||||
}
|
}
|
||||||
const debug = item.debug as unknown as Record<string, unknown>;
|
const debug = item.debug as unknown as Record<string, unknown>;
|
||||||
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
const selectedDiscoveryChain = toNonEmptyString(debug.mcp_discovery_selected_chain_id);
|
||||||
const discoveryIntent =
|
|
||||||
selectedDiscoveryChain === "business_overview"
|
|
||||||
? "business_overview"
|
|
||||||
: selectedDiscoveryChain === "value_flow_comparison"
|
|
||||||
? "customer_revenue_and_payments"
|
|
||||||
: "unknown";
|
|
||||||
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
||||||
const intent = toAddressIntent(detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent);
|
const discoveryIntent = intentFromDiscoveryChain(selectedDiscoveryChain);
|
||||||
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
|
const trackableDiscoveryTurn = debug.mcp_discovery_response_applied === true && Boolean(selectedDiscoveryChain);
|
||||||
|
const shouldPreferDiscoveryIntent =
|
||||||
|
trackableDiscoveryTurn &&
|
||||||
|
discoveryIntent !== "unknown" &&
|
||||||
|
selectedDiscoveryChain !== "business_overview";
|
||||||
|
const intent = toAddressIntent(
|
||||||
|
shouldPreferDiscoveryIntent ? discoveryIntent : detectedIntent && detectedIntent !== "unknown" ? detectedIntent : discoveryIntent
|
||||||
|
);
|
||||||
if (intent === "unknown" && !trackableDiscoveryTurn) {
|
if (intent === "unknown" && !trackableDiscoveryTurn) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
@ -716,8 +778,8 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
||||||
const derivedOrganizationScope =
|
const derivedOrganizationScope =
|
||||||
resolveDerivedOrganizationScope(debug, filters, item.text) ?? readAddressDebugOrganization(debug, toNonEmptyString);
|
resolveDerivedOrganizationScope(debug, filters, item.text) ?? readAddressDebugOrganization(debug, toNonEmptyString);
|
||||||
const derivedCounterpartyScope =
|
const derivedCounterpartyScope =
|
||||||
selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
isCounterpartyDiscoveryChain(selectedDiscoveryChain) && debug.mcp_discovery_response_applied === true
|
||||||
? readAddressDebugCounterparty(debug, toNonEmptyString) ?? readNavigationDiscoveryCounterparty(debug)
|
? readCurrentDiscoveryCounterparty(debug)
|
||||||
: null;
|
: null;
|
||||||
const filtersWithDerivedScope =
|
const filtersWithDerivedScope =
|
||||||
derivedOrganizationScope && !toNonEmptyString(filters.organization)
|
derivedOrganizationScope && !toNonEmptyString(filters.organization)
|
||||||
|
|
@ -758,8 +820,12 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
||||||
primaryEntityFocusObject
|
primaryEntityFocusObject
|
||||||
);
|
);
|
||||||
const rawComparisonCounterparty =
|
const rawComparisonCounterparty =
|
||||||
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
debug.mcp_discovery_response_applied === true
|
||||||
|
? selectedDiscoveryChain === "business_overview"
|
||||||
? readNavigationDiscoveryCounterparty(debug)
|
? readNavigationDiscoveryCounterparty(debug)
|
||||||
|
: isCounterpartyDiscoveryChain(selectedDiscoveryChain)
|
||||||
|
? readCurrentDiscoveryCounterparty(debug)
|
||||||
|
: null
|
||||||
: null;
|
: null;
|
||||||
const comparisonCounterparty =
|
const comparisonCounterparty =
|
||||||
rawComparisonCounterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, rawComparisonCounterparty)
|
rawComparisonCounterparty && !shouldSuppressBusinessOverviewCounterpartyFocus(debug, rawComparisonCounterparty)
|
||||||
|
|
@ -772,6 +838,8 @@ export function evolveAddressNavigationStateWithAssistantItem(
|
||||||
const currentComparisonProofBundles =
|
const currentComparisonProofBundles =
|
||||||
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
selectedDiscoveryChain === "business_overview" && debug.mcp_discovery_response_applied === true
|
||||||
? readBusinessOverviewComparisonProofBundles(debug)
|
? readBusinessOverviewComparisonProofBundles(debug)
|
||||||
|
: selectedDiscoveryChain === "value_flow_comparison" && debug.mcp_discovery_response_applied === true
|
||||||
|
? readCounterpartyValueFlowProofBundles(debug)
|
||||||
: null;
|
: null;
|
||||||
const inheritedComparisonScope = state.session_context.comparison_scope;
|
const inheritedComparisonScope = state.session_context.comparison_scope;
|
||||||
const inheritedComparisonProofBundles =
|
const inheritedComparisonProofBundles =
|
||||||
|
|
|
||||||
|
|
@ -1629,6 +1629,16 @@ function mergeFollowupFilters(
|
||||||
intent === "vat_payable_confirmed_as_of_date";
|
intent === "vat_payable_confirmed_as_of_date";
|
||||||
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
const currentHasPeriod = hasExplicitPeriodWindow(merged);
|
||||||
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
const previousHasPeriod = hasExplicitPeriodWindow(previous);
|
||||||
|
const currentPeriodFromForCarryover = toNonEmptyString(merged.period_from);
|
||||||
|
const currentPeriodToForCarryover = toNonEmptyString(merged.period_to);
|
||||||
|
const todayIsoForPeriodCarryover = new Date().toISOString().slice(0, 10);
|
||||||
|
const currentValueFlowPeriodDefaultsToToday =
|
||||||
|
isValueCounterpartyIntent(intent) &&
|
||||||
|
hasFollowupSignal &&
|
||||||
|
!hasExplicitPeriodInMessage &&
|
||||||
|
!hasExplicitCurrentDateInMessage &&
|
||||||
|
!currentPeriodFromForCarryover &&
|
||||||
|
currentPeriodToForCarryover === todayIsoForPeriodCarryover;
|
||||||
const currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
|
const currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
|
||||||
const currentContractExplicit = toNonEmptyString(merged.contract);
|
const currentContractExplicit = toNonEmptyString(merged.contract);
|
||||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||||
|
|
@ -1717,7 +1727,7 @@ function mergeFollowupFilters(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!currentHasPeriod &&
|
(!currentHasPeriod || currentValueFlowPeriodDefaultsToToday) &&
|
||||||
previousHasPeriod &&
|
previousHasPeriod &&
|
||||||
hasFollowupSignal &&
|
hasFollowupSignal &&
|
||||||
!hasExplicitPeriodInMessage &&
|
!hasExplicitPeriodInMessage &&
|
||||||
|
|
|
||||||
|
|
@ -1158,6 +1158,10 @@ export function resolveFollowupTargetIntent(
|
||||||
export function shouldUseNavigationTemporalCarryover(sourceIntentHint: unknown): boolean {
|
export function shouldUseNavigationTemporalCarryover(sourceIntentHint: unknown): boolean {
|
||||||
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
|
const normalizedIntent = fallbackToNonEmptyString(sourceIntentHint);
|
||||||
return (
|
return (
|
||||||
|
normalizedIntent === "customer_revenue_and_payments" ||
|
||||||
|
normalizedIntent === "supplier_payouts_profile" ||
|
||||||
|
normalizedIntent === "contract_usage_and_value" ||
|
||||||
|
normalizedIntent === "counterparty_activity_lifecycle" ||
|
||||||
normalizedIntent === "inventory_on_hand_as_of_date" ||
|
normalizedIntent === "inventory_on_hand_as_of_date" ||
|
||||||
normalizedIntent === "inventory_supplier_stock_overlap_as_of_date" ||
|
normalizedIntent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
||||||
|
|
|
||||||
|
|
@ -281,7 +281,7 @@ function valueFlowDirectionLabelRu(pilot: AssistantMcpDiscoveryPilotExecutionCon
|
||||||
: "входящих денежных поступлений";
|
: "входящих денежных поступлений";
|
||||||
}
|
}
|
||||||
|
|
||||||
function valueFlowZeroResultConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
function valueFlowZeroResultInsufficiencyLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||||
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -293,7 +293,23 @@ function valueFlowZeroResultConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecu
|
||||||
const period = explicitDateScope(pilot);
|
const period = explicitDateScope(pilot);
|
||||||
const organizationPart = organization ? ` по организации ${organization}` : "";
|
const organizationPart = organization ? ` по организации ${organization}` : "";
|
||||||
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
|
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
|
||||||
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${valueFlowDirectionLabelRu(
|
const direction = valueFlowDirectionLabelRu(pilot);
|
||||||
|
return `Точную сумму ${direction} по контрагенту ${counterparty}${organizationPart}${periodPart} не подтверждаю: в выполненной проверке 1С строки ${direction} не найдены; это не доказывает отсутствие операций вне доступного банковского контура.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueFlowZeroResultCheckedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||||
|
if (!hasExecutedZeroValueFlowRows(pilot)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const counterparty = firstEntityCandidate(pilot);
|
||||||
|
if (!counterparty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const organization = explicitOrganizationScope(pilot);
|
||||||
|
const period = explicitDateScope(pilot);
|
||||||
|
const organizationPart = organization ? ` по организации ${organization}` : "";
|
||||||
|
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
|
||||||
|
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart} строки ${valueFlowDirectionLabelRu(
|
||||||
pilot
|
pilot
|
||||||
)} не найдено.`;
|
)} не найдено.`;
|
||||||
}
|
}
|
||||||
|
|
@ -312,11 +328,11 @@ function valueFlowZeroResultUnknownLine(pilot: AssistantMcpDiscoveryPilotExecuti
|
||||||
}
|
}
|
||||||
|
|
||||||
function valueFlowZeroResultHeadline(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
function valueFlowZeroResultHeadline(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||||
const confirmedLine = valueFlowZeroResultConfirmedLine(pilot);
|
const insufficiencyLine = valueFlowZeroResultInsufficiencyLine(pilot);
|
||||||
if (!confirmedLine) {
|
if (!insufficiencyLine) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return confirmedLine;
|
return insufficiencyLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasAllTimeScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
function hasAllTimeScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||||
|
|
@ -2126,8 +2142,6 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||||
? [derivedValueLine]
|
? [derivedValueLine]
|
||||||
: derivedValueLine
|
: derivedValueLine
|
||||||
? [derivedValueLine, ...monthlyConfirmedLines]
|
? [derivedValueLine, ...monthlyConfirmedLines]
|
||||||
: valueFlowZeroResultConfirmedLine(pilot)
|
|
||||||
? [valueFlowZeroResultConfirmedLine(pilot)!]
|
|
||||||
: derivedEntityResolutionLine
|
: derivedEntityResolutionLine
|
||||||
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
|
||||||
: derivedMetadataLine
|
: derivedMetadataLine
|
||||||
|
|
@ -2139,7 +2153,12 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||||
? pilot.derived_metadata_surface.available_fields.length > 0
|
? pilot.derived_metadata_surface.available_fields.length > 0
|
||||||
? userFacingUnknowns(pilot.evidence.unknown_facts)
|
? userFacingUnknowns(pilot.evidence.unknown_facts)
|
||||||
: ["Детальный список полей этих объектов этим шагом не получен."]
|
: ["Детальный список полей этих объектов этим шагом не получен."]
|
||||||
: appendValueFlowZeroResultUnknown(rankedValueFlowUnknownLines(pilot), pilot);
|
: appendValueFlowZeroResultUnknown(
|
||||||
|
valueFlowZeroResultCheckedLine(pilot)
|
||||||
|
? [valueFlowZeroResultCheckedLine(pilot)!, ...rankedValueFlowUnknownLines(pilot)]
|
||||||
|
: rankedValueFlowUnknownLines(pilot),
|
||||||
|
pilot
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
|
||||||
|
|
|
||||||
|
|
@ -3217,9 +3217,28 @@ function deriveValueFlow(
|
||||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"],
|
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"],
|
||||||
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
|
aggregationAxis: AssistantMcpDiscoveryAggregationAxis | null
|
||||||
): AssistantMcpDiscoveryDerivedValueFlow | null {
|
): AssistantMcpDiscoveryDerivedValueFlow | null {
|
||||||
if (!result || result.error || result.matched_rows <= 0) {
|
if (!result || result.error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (result.matched_rows <= 0) {
|
||||||
|
return {
|
||||||
|
value_flow_direction: direction,
|
||||||
|
counterparty,
|
||||||
|
period_scope: periodScope,
|
||||||
|
aggregation_axis: aggregationAxis,
|
||||||
|
rows_matched: 0,
|
||||||
|
rows_with_amount: 0,
|
||||||
|
total_amount: 0,
|
||||||
|
total_amount_human_ru: formatAmountHumanRu(0),
|
||||||
|
first_movement_date: null,
|
||||||
|
latest_movement_date: null,
|
||||||
|
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
|
||||||
|
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
|
||||||
|
period_chunking_granularity: result.period_chunking_granularity,
|
||||||
|
monthly_breakdown: [],
|
||||||
|
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
|
||||||
|
};
|
||||||
|
}
|
||||||
let totalAmount = 0;
|
let totalAmount = 0;
|
||||||
let rowsWithAmount = 0;
|
let rowsWithAmount = 0;
|
||||||
for (const row of result.rows) {
|
for (const row of result.rows) {
|
||||||
|
|
@ -5294,16 +5313,30 @@ function buildValueFlowConfirmedFacts(
|
||||||
counterparty: string | null,
|
counterparty: string | null,
|
||||||
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"]
|
direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"]
|
||||||
): string[] {
|
): string[] {
|
||||||
if (result.error || result.matched_rows <= 0) {
|
if (result.error) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (direction === "outgoing_supplier_payout") {
|
if (direction === "outgoing_supplier_payout") {
|
||||||
|
if (result.matched_rows <= 0) {
|
||||||
|
return [
|
||||||
|
counterparty
|
||||||
|
? `1C supplier-payout rows were checked for counterparty ${counterparty}: not_found`
|
||||||
|
: "1C supplier-payout rows were checked for the requested counterparty scope: not_found"
|
||||||
|
];
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
counterparty
|
counterparty
|
||||||
? `1C supplier-payout rows were found for counterparty ${counterparty}`
|
? `1C supplier-payout rows were found for counterparty ${counterparty}`
|
||||||
: "1C supplier-payout rows were found for the requested counterparty scope"
|
: "1C supplier-payout rows were found for the requested counterparty scope"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if (result.matched_rows <= 0) {
|
||||||
|
return [
|
||||||
|
counterparty
|
||||||
|
? `1C value-flow rows were checked for counterparty ${counterparty}: not_found`
|
||||||
|
: "1C value-flow rows were checked for the requested counterparty scope: not_found"
|
||||||
|
];
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
counterparty
|
counterparty
|
||||||
? `1C value-flow rows were found for counterparty ${counterparty}`
|
? `1C value-flow rows were found for counterparty ${counterparty}`
|
||||||
|
|
@ -5521,6 +5554,14 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const facts: string[] = [];
|
const facts: string[] = [];
|
||||||
|
if (derived.rows_matched <= 0) {
|
||||||
|
facts.push(
|
||||||
|
derived.value_flow_direction === "outgoing_supplier_payout"
|
||||||
|
? "Counterparty supplier-payout not_found result was treated as zero only inside the scoped checked 1C result set"
|
||||||
|
: "Counterparty incoming value-flow not_found result was treated as zero only inside the scoped checked 1C result set"
|
||||||
|
);
|
||||||
|
return facts;
|
||||||
|
}
|
||||||
if (derived.value_flow_direction === "outgoing_supplier_payout") {
|
if (derived.value_flow_direction === "outgoing_supplier_payout") {
|
||||||
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
|
facts.push("Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -386,6 +386,7 @@ export function resolveAssistantMcpDiscoveryEvidence(
|
||||||
const rowsMatched = probeRowsMatched(probeResults);
|
const rowsMatched = probeRowsMatched(probeResults);
|
||||||
const rowsReceived = probeRowsReceived(probeResults);
|
const rowsReceived = probeRowsReceived(probeResults);
|
||||||
const bypassDetected = hasProbeBypass(input.plan, probeResults);
|
const bypassDetected = hasProbeBypass(input.plan, probeResults);
|
||||||
|
const confirmedNoMatchFact = confirmedFacts.some((fact) => /\bnot_found\b/iu.test(String(fact ?? "")));
|
||||||
|
|
||||||
if (bypassDetected) {
|
if (bypassDetected) {
|
||||||
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
|
pushReason(reasonCodes, "probe_result_used_primitive_outside_runtime_plan");
|
||||||
|
|
@ -393,7 +394,7 @@ export function resolveAssistantMcpDiscoveryEvidence(
|
||||||
if (input.plan.plan_status !== "allowed") {
|
if (input.plan.plan_status !== "allowed") {
|
||||||
pushReason(reasonCodes, "plan_not_allowed_by_runtime");
|
pushReason(reasonCodes, "plan_not_allowed_by_runtime");
|
||||||
}
|
}
|
||||||
if (confirmedFacts.length > 0 && rowsMatched <= 0) {
|
if (confirmedFacts.length > 0 && rowsMatched <= 0 && !confirmedNoMatchFact) {
|
||||||
pushReason(reasonCodes, "confirmed_facts_without_matched_probe_rows");
|
pushReason(reasonCodes, "confirmed_facts_without_matched_probe_rows");
|
||||||
}
|
}
|
||||||
if (!sourceRowsSummary && rowsReceived > 0) {
|
if (!sourceRowsSummary && rowsReceived > 0) {
|
||||||
|
|
@ -413,6 +414,11 @@ export function resolveAssistantMcpDiscoveryEvidence(
|
||||||
coverageStatus = "full";
|
coverageStatus = "full";
|
||||||
answerPermission = "confirmed_answer";
|
answerPermission = "confirmed_answer";
|
||||||
pushReason(reasonCodes, "confirmed_facts_with_allowed_mcp_evidence");
|
pushReason(reasonCodes, "confirmed_facts_with_allowed_mcp_evidence");
|
||||||
|
} else if (confirmedFacts.length > 0 && confirmedNoMatchFact && rowsMatched <= 0 && sourceRowsSummary) {
|
||||||
|
evidenceStatus = "confirmed";
|
||||||
|
coverageStatus = "full";
|
||||||
|
answerPermission = "confirmed_answer";
|
||||||
|
pushReason(reasonCodes, "confirmed_no_match_fact_with_allowed_mcp_evidence");
|
||||||
} else if (inferredFacts.length > 0 && rowsReceived > 0) {
|
} else if (inferredFacts.length > 0 && rowsReceived > 0) {
|
||||||
evidenceStatus = "inferred_only";
|
evidenceStatus = "inferred_only";
|
||||||
coverageStatus = "partial";
|
coverageStatus = "partial";
|
||||||
|
|
|
||||||
|
|
@ -284,9 +284,15 @@ function localizeLine(value: string): string {
|
||||||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
|
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
|
||||||
}
|
}
|
||||||
|
if (/^1C value-flow rows were checked for the requested counterparty scope: not_found$/i.test(value)) {
|
||||||
|
return "В 1С проверены входящие денежные строки в запрошенном срезе: строки не найдены.";
|
||||||
|
}
|
||||||
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
|
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
|
||||||
}
|
}
|
||||||
|
if (/^1C supplier-payout rows were checked for the requested counterparty scope: not_found$/i.test(value)) {
|
||||||
|
return "В 1С проверены исходящие платежи/списания в запрошенном срезе: строки не найдены.";
|
||||||
|
}
|
||||||
const openScopeBidirectionalMatch = value.match(
|
const openScopeBidirectionalMatch = value.match(
|
||||||
/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i
|
/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i
|
||||||
);
|
);
|
||||||
|
|
@ -319,6 +325,10 @@ function localizeLine(value: string): string {
|
||||||
if (valueFlowMatch) {
|
if (valueFlowMatch) {
|
||||||
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
|
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
|
||||||
}
|
}
|
||||||
|
const valueFlowNotFoundMatch = value.match(/^1C value-flow rows were checked for counterparty\s+(.+): not_found$/i);
|
||||||
|
if (valueFlowNotFoundMatch) {
|
||||||
|
return `В 1С проверены входящие денежные строки по контрагенту ${valueFlowNotFoundMatch[1]}: строки не найдены.`;
|
||||||
|
}
|
||||||
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
|
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
|
||||||
}
|
}
|
||||||
|
|
@ -340,6 +350,10 @@ function localizeLine(value: string): string {
|
||||||
if (supplierPayoutMatch) {
|
if (supplierPayoutMatch) {
|
||||||
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`;
|
return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`;
|
||||||
}
|
}
|
||||||
|
const supplierPayoutNotFoundMatch = value.match(/^1C supplier-payout rows were checked for counterparty\s+(.+): not_found$/i);
|
||||||
|
if (supplierPayoutNotFoundMatch) {
|
||||||
|
return `В 1С проверены исходящие платежи/списания по контрагенту ${supplierPayoutNotFoundMatch[1]}: строки не найдены.`;
|
||||||
|
}
|
||||||
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
|
||||||
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
|
return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру.";
|
||||||
}
|
}
|
||||||
|
|
@ -377,6 +391,12 @@ function localizeLine(value: string): string {
|
||||||
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
|
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
|
||||||
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
|
||||||
}
|
}
|
||||||
|
if (/^Counterparty supplier-payout not_found result was treated as zero only inside the scoped checked 1C result set$/i.test(value)) {
|
||||||
|
return "Нулевую сумму исходящих платежей можно трактовать только внутри этого проверенного среза 1С, где строки списаний не найдены.";
|
||||||
|
}
|
||||||
|
if (/^Counterparty incoming value-flow not_found result was treated as zero only inside the scoped checked 1C result set$/i.test(value)) {
|
||||||
|
return "Нулевую сумму входящих поступлений можно трактовать только внутри этого проверенного среза 1С, где строки поступлений не найдены.";
|
||||||
|
}
|
||||||
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
|
||||||
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
|
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
|
||||||
}
|
}
|
||||||
|
|
@ -1429,7 +1449,7 @@ function buildCompactBusinessOverviewReply(
|
||||||
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
|
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
|
||||||
].filter((value): value is string => Boolean(value));
|
].filter((value): value is string => Boolean(value));
|
||||||
lines.push(
|
lines.push(
|
||||||
`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, не аудиторское заключение и не подтвержденная чистая прибыль.`
|
`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, а не финансовый аудит и не подтверждение чистой прибыли.`
|
||||||
);
|
);
|
||||||
lines.push(
|
lines.push(
|
||||||
"Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж."
|
"Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж."
|
||||||
|
|
@ -1472,7 +1492,7 @@ function buildCompactBusinessOverviewReply(
|
||||||
}
|
}
|
||||||
if (!limitLine) {
|
if (!limitLine) {
|
||||||
lines.push(
|
lines.push(
|
||||||
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
|
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, а не финансовый аудит и не подтверждение чистой прибыли."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const missingOverviewFamilies: string[] = [];
|
const missingOverviewFamilies: string[] = [];
|
||||||
|
|
|
||||||
|
|
@ -855,6 +855,65 @@ function hasExactBankOperationsAddressReply(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractExplicitDateScopeYear(value: unknown): string | null {
|
||||||
|
const source = toNonEmptyString(value);
|
||||||
|
if (!source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = source.match(/\b((?:19|20)\d{2})\b/u);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDocumentReplyDateYears(value: unknown): string[] {
|
||||||
|
const source = String(value ?? "");
|
||||||
|
if (!source) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const years = new Set<string>();
|
||||||
|
for (const match of source.matchAll(/\b((?:19|20)\d{2})-\d{2}-\d{2}(?:T|\b)/gu)) {
|
||||||
|
years.add(match[1]);
|
||||||
|
}
|
||||||
|
for (const match of source.matchAll(/\b\d{1,2}\.\d{1,2}\.((?:19|20)\d{2})\b/gu)) {
|
||||||
|
years.add(match[1]);
|
||||||
|
}
|
||||||
|
return [...years];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDocumentListTemporalConflictWithDiscovery(
|
||||||
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||||
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||||
|
): boolean {
|
||||||
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||||
|
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||||
|
const isDocumentIntent =
|
||||||
|
detectedIntent === "list_documents_by_counterparty" || detectedIntent === "list_documents_by_contract";
|
||||||
|
const isDocumentRecipe =
|
||||||
|
selectedRecipe === "address_documents_by_counterparty_v1" ||
|
||||||
|
selectedRecipe === "address_documents_by_contract_v1";
|
||||||
|
if (!isDocumentIntent || !isDocumentRecipe) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||||
|
const expectedYear = extractExplicitDateScopeYear(turnMeaning?.explicit_date_scope);
|
||||||
|
if (!expectedYear) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = toRecordObject(input.addressRuntimeMeta?.extracted_filters);
|
||||||
|
const periodFrom = toNonEmptyString(filters?.period_from);
|
||||||
|
const periodTo = toNonEmptyString(filters?.period_to);
|
||||||
|
const hasMatchingPeriod =
|
||||||
|
Boolean(periodFrom?.startsWith(`${expectedYear}-`)) && Boolean(periodTo?.startsWith(`${expectedYear}-`));
|
||||||
|
const replyDateYears = extractDocumentReplyDateYears(input.currentReply);
|
||||||
|
const hasOutOfScopeReplyDates = replyDateYears.some((year) => year !== expectedYear);
|
||||||
|
|
||||||
|
return hasOutOfScopeReplyDates || (!hasMatchingPeriod && replyDateYears.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
function hasExactDocumentListAddressReply(
|
function hasExactDocumentListAddressReply(
|
||||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||||
|
|
@ -1299,7 +1358,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
entryPoint
|
entryPoint
|
||||||
);
|
);
|
||||||
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
||||||
const exactDocumentListAddressReply = hasExactDocumentListAddressReply(input, entryPoint);
|
const documentListTemporalConflictWithDiscovery = hasDocumentListTemporalConflictWithDiscovery(input, entryPoint);
|
||||||
|
const exactDocumentListAddressReply =
|
||||||
|
hasExactDocumentListAddressReply(input, entryPoint) && !documentListTemporalConflictWithDiscovery;
|
||||||
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
|
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
|
||||||
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(
|
const exactInventoryPurchaseToSaleChainAddressReply = hasExactInventoryPurchaseToSaleChainAddressReply(
|
||||||
input,
|
input,
|
||||||
|
|
@ -1340,13 +1401,15 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
!businessOverviewBoundaryCandidatePriority &&
|
!businessOverviewBoundaryCandidatePriority &&
|
||||||
!groundedValueFlowRankingCandidatePriority;
|
!groundedValueFlowRankingCandidatePriority;
|
||||||
const alignedFactualAddressReplyProtectsCurrent =
|
const alignedFactualAddressReplyProtectsCurrent =
|
||||||
alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||||
const matchedFactualAddressContinuationTargetProtectsCurrent =
|
const matchedFactualAddressContinuationTargetProtectsCurrent =
|
||||||
matchedFactualAddressContinuationTarget && !groundedValueFlowRankingCandidatePriority;
|
matchedFactualAddressContinuationTarget &&
|
||||||
|
!groundedValueFlowRankingCandidatePriority &&
|
||||||
|
!documentListTemporalConflictWithDiscovery;
|
||||||
const fullConfirmedFactualAddressReplyProtectsCurrent =
|
const fullConfirmedFactualAddressReplyProtectsCurrent =
|
||||||
fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||||
const exactMatchedFactualAddressReplyProtectsCurrent =
|
const exactMatchedFactualAddressReplyProtectsCurrent =
|
||||||
exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
|
exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority && !documentListTemporalConflictWithDiscovery;
|
||||||
const exactBankOperationsProtectsCurrent =
|
const exactBankOperationsProtectsCurrent =
|
||||||
exactBankOperationsAddressReply &&
|
exactBankOperationsAddressReply &&
|
||||||
!semanticConflictWithDiscoveryTurnMeaning &&
|
!semanticConflictWithDiscoveryTurnMeaning &&
|
||||||
|
|
@ -1383,6 +1446,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||||
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
|
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
|
||||||
}
|
}
|
||||||
|
if (documentListTemporalConflictWithDiscovery) {
|
||||||
|
pushReason(reasonCodes, "mcp_discovery_response_policy_document_list_temporal_conflict_allows_candidate_override");
|
||||||
|
}
|
||||||
if (currentClarificationProtectsCurrentReply) {
|
if (currentClarificationProtectsCurrentReply) {
|
||||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,28 @@ describe("address follow-up temporal regressions", () => {
|
||||||
expect(result?.baseReasons).not.toContain("counterparty_carryover_suppressed_for_short_debt_mirror");
|
expect(result?.baseReasons).not.toContain("counterparty_carryover_suppressed_for_short_debt_mirror");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps explicit year period for selected counterparty payout follow-up", () => {
|
||||||
|
const result = runAddressDecomposeStage("а теперь сколько заплатили?", {
|
||||||
|
previous_intent: "customer_revenue_and_payments" as const,
|
||||||
|
target_intent: "customer_revenue_and_payments" as const,
|
||||||
|
previous_filters: {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "counterparty" as const,
|
||||||
|
previous_anchor_value: "Группа СВК",
|
||||||
|
resolved_counterparty_from_display: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.intent.intent).toBe("customer_revenue_and_payments");
|
||||||
|
expect(result?.filters.extracted_filters.counterparty).toBe("Группа СВК");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31");
|
||||||
|
expect(result?.baseReasons).toContain("period_from_followup_context");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps same-date inventory pivot anchored to the previous VAT date", () => {
|
it("keeps same-date inventory pivot anchored to the previous VAT date", () => {
|
||||||
const result = runAddressDecomposeStage("какие остатки по складу на эту же дату", {
|
const result = runAddressDecomposeStage("какие остатки по складу на эту же дату", {
|
||||||
previous_intent: "vat_payable_confirmed_as_of_date",
|
previous_intent: "vat_payable_confirmed_as_of_date",
|
||||||
|
|
@ -454,6 +476,29 @@ describe("address follow-up temporal regressions", () => {
|
||||||
expect(movements?.baseReasons).toContain("counterparty_from_followup_context");
|
expect(movements?.baseReasons).toContain("counterparty_from_followup_context");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps value-flow period for counterparty document evidence follow-up", () => {
|
||||||
|
const followupContext = {
|
||||||
|
previous_intent: "customer_revenue_and_payments" as const,
|
||||||
|
target_intent: "list_documents_by_counterparty" as const,
|
||||||
|
previous_filters: {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "counterparty" as const,
|
||||||
|
previous_anchor_value: "Группа СВК",
|
||||||
|
resolved_counterparty_from_display: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const documents = runAddressDecomposeStage("а по документам?", followupContext);
|
||||||
|
|
||||||
|
expect(documents?.intent.intent).toBe("list_documents_by_counterparty");
|
||||||
|
expect(documents?.filters.extracted_filters.counterparty).toBe("Группа СВК");
|
||||||
|
expect(documents?.filters.extracted_filters.period_from).toBe("2020-01-01");
|
||||||
|
expect(documents?.filters.extracted_filters.period_to).toBe("2020-12-31");
|
||||||
|
expect(documents?.baseReasons).toContain("period_from_followup_context");
|
||||||
|
});
|
||||||
|
|
||||||
it("replaces pronoun chain anchors from counterparty follow-up context", () => {
|
it("replaces pronoun chain anchors from counterparty follow-up context", () => {
|
||||||
const followupContext = {
|
const followupContext = {
|
||||||
previous_intent: "customer_revenue_and_payments" as const,
|
previous_intent: "customer_revenue_and_payments" as const,
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,262 @@ describe("address navigation state", () => {
|
||||||
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
|
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("captures resolved entity-resolution focus as fresh current discovery state", () => {
|
||||||
|
const base = createEmptyAddressNavigationState("asst-entity", "2026-04-12T10:00:00.000Z");
|
||||||
|
const assistantItem = {
|
||||||
|
message_id: "msg-entity",
|
||||||
|
session_id: "asst-entity",
|
||||||
|
role: "assistant",
|
||||||
|
text: "В проверенном каталожном срезе 1С найден наиболее вероятный контрагент: Группа СВК.",
|
||||||
|
created_at: "2026-04-12T10:12:30.000Z",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "chat",
|
||||||
|
detected_intent: "unknown",
|
||||||
|
extracted_filters: {},
|
||||||
|
anchor_type: "unknown",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
mcp_discovery_selected_chain_id: "entity_resolution",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "entity_resolution",
|
||||||
|
asked_action_family: "search_business_entity",
|
||||||
|
explicit_entity_candidates: ["СВК"],
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "entity_resolution_search_v1",
|
||||||
|
derived_entity_resolution: {
|
||||||
|
requested_entity: "СВК",
|
||||||
|
resolution_status: "resolved",
|
||||||
|
resolved_entity: "Группа СВК"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
decision: "new_topic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 1);
|
||||||
|
|
||||||
|
expect(evolved.session_context.active_focus_object?.object_type).toBe("counterparty");
|
||||||
|
expect(evolved.session_context.active_focus_object?.label).toBe("Группа СВК");
|
||||||
|
expect(evolved.result_sets[0]?.filters.counterparty).toBe("Группа СВК");
|
||||||
|
expect(evolved.navigation_history[0]?.target_object_id).toBe("counterparty:группа свк");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures single-direction value-flow focus before the net bundle exists", () => {
|
||||||
|
const base = createEmptyAddressNavigationState("asst-vf-single", "2026-04-12T10:00:00.000Z");
|
||||||
|
const assistantItem = {
|
||||||
|
message_id: "msg-vf-single",
|
||||||
|
session_id: "asst-vf-single",
|
||||||
|
role: "assistant",
|
||||||
|
text: "Входящие денежные поступления по контрагенту Группа СВК за период 2020: 12 093 465 руб.",
|
||||||
|
created_at: "2026-04-12T10:13:00.000Z",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "chat",
|
||||||
|
detected_intent: "unknown",
|
||||||
|
extracted_filters: {
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
anchor_type: "unknown",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
mcp_discovery_selected_chain_id: "value_flow",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "turnover",
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_value_flow_query_movements_v1",
|
||||||
|
derived_value_flow: {
|
||||||
|
value_flow_direction: "incoming_customer_revenue",
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_scope: "2020",
|
||||||
|
total_amount_human_ru: "12 093 465 руб."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
decision: "continue_previous"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const evolved = evolveAddressNavigationStateWithAssistantItem(base, assistantItem, 2);
|
||||||
|
|
||||||
|
expect(evolved.result_sets[0]?.intent).toBe("customer_revenue_and_payments");
|
||||||
|
expect(evolved.result_sets[0]?.filters.counterparty).toBe("Группа СВК");
|
||||||
|
expect(evolved.session_context.active_focus_object?.label).toBe("Группа СВК");
|
||||||
|
expect(evolved.session_context.date_scope.period_from).toBe("2020-01-01");
|
||||||
|
expect(evolved.session_context.date_scope.period_to).toBe("2020-12-31");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores bidirectional value-flow proof bundle for adjacent evidence drilldowns", () => {
|
||||||
|
const base = createEmptyAddressNavigationState("asst-vf-bundle", "2026-04-12T10:00:00.000Z");
|
||||||
|
const valueFlowBundle = {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_scope: "2020",
|
||||||
|
incoming_customer_revenue: {
|
||||||
|
total_amount_human_ru: "12 093 465 руб."
|
||||||
|
},
|
||||||
|
outgoing_supplier_payout: {
|
||||||
|
total_amount_human_ru: "0 руб."
|
||||||
|
},
|
||||||
|
net_amount_human_ru: "12 093 465 руб.",
|
||||||
|
net_direction: "net_incoming"
|
||||||
|
};
|
||||||
|
const netItem = {
|
||||||
|
message_id: "msg-vf-net",
|
||||||
|
session_id: "asst-vf-bundle",
|
||||||
|
role: "assistant",
|
||||||
|
text: "По контрагенту Группа СВК за период 2020 получили 12 093 465 руб., заплатили 0 руб.; расчетное нетто в нашу сторону: 12 093 465 руб.",
|
||||||
|
created_at: "2026-04-12T10:14:00.000Z",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "chat",
|
||||||
|
detected_intent: "customer_revenue_and_payments",
|
||||||
|
extracted_filters: {
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
anchor_type: "unknown",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
mcp_discovery_selected_chain_id: "value_flow_comparison",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "net_value_flow",
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
|
||||||
|
derived_bidirectional_value_flow: valueFlowBundle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
decision: "continue_previous"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const afterNet = evolveAddressNavigationStateWithAssistantItem(base, netItem, 5);
|
||||||
|
expect(afterNet.session_context.comparison_scope?.counterparty?.label).toBe("Группа СВК");
|
||||||
|
expect(afterNet.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
|
||||||
|
valueFlowBundle
|
||||||
|
);
|
||||||
|
|
||||||
|
const docsItem = {
|
||||||
|
message_id: "msg-vf-docs",
|
||||||
|
session_id: "asst-vf-bundle",
|
||||||
|
role: "assistant",
|
||||||
|
text: "По документам по контрагенту Группа СВК за 2020 полный срез не подтвержден.",
|
||||||
|
created_at: "2026-04-12T10:15:00.000Z",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "chat",
|
||||||
|
detected_intent: "list_documents_by_counterparty",
|
||||||
|
extracted_filters: {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
anchor_type: "unknown",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
mcp_discovery_selected_chain_id: "document_evidence",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "documents",
|
||||||
|
asked_action_family: "list_documents",
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_document_evidence_query_documents_v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
decision: "continue_previous"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const afterDocs = evolveAddressNavigationStateWithAssistantItem(afterNet, docsItem, 6);
|
||||||
|
expect(afterDocs.session_context.active_focus_object?.label).toBe("Группа СВК");
|
||||||
|
expect(afterDocs.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
|
||||||
|
valueFlowBundle
|
||||||
|
);
|
||||||
|
|
||||||
|
const movementsItem = {
|
||||||
|
message_id: "msg-vf-movements",
|
||||||
|
session_id: "asst-vf-bundle",
|
||||||
|
role: "assistant",
|
||||||
|
text: "По движениям по контрагенту Группа СВК за 2020 полный срез не подтвержден.",
|
||||||
|
created_at: "2026-04-12T10:16:00.000Z",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "chat",
|
||||||
|
detected_intent: "inventory_purchase_documents_for_item",
|
||||||
|
extracted_filters: {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
anchor_type: "unknown",
|
||||||
|
mcp_discovery_response_applied: true,
|
||||||
|
mcp_discovery_selected_chain_id: "movement_evidence",
|
||||||
|
assistant_mcp_discovery_entry_point_v1: {
|
||||||
|
turn_input: {
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "movements",
|
||||||
|
asked_action_family: "list_movements",
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
stale_replay_forbidden: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
pilot: {
|
||||||
|
pilot_scope: "counterparty_movement_evidence_query_movements_v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
decision: "continue_previous"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const afterMovements = evolveAddressNavigationStateWithAssistantItem(afterDocs, movementsItem, 7);
|
||||||
|
expect(afterMovements.result_sets.at(-1)?.intent).toBe("bank_operations_by_counterparty");
|
||||||
|
expect(afterMovements.result_sets.at(-1)?.type).toBe("bank_operations_list");
|
||||||
|
expect(afterMovements.session_context.active_focus_object?.label).toBe("Группа СВК");
|
||||||
|
expect(afterMovements.session_context.comparison_scope?.proof_bundles?.counterparty_value_flow_bundle).toEqual(
|
||||||
|
valueFlowBundle
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps selected counterparty focus for company boundary summaries", () => {
|
it("keeps selected counterparty focus for company boundary summaries", () => {
|
||||||
const initial = createEmptyAddressNavigationState("asst-boundary", "2026-04-12T10:00:00.000Z");
|
const initial = createEmptyAddressNavigationState("asst-boundary", "2026-04-12T10:00:00.000Z");
|
||||||
const assistantItem = {
|
const assistantItem = {
|
||||||
|
|
|
||||||
|
|
@ -684,6 +684,27 @@ describe("assistantContinuityPolicy organization authority", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("carries value-flow temporal scope into follow-up evidence drilldowns", () => {
|
||||||
|
const filters = applyTemporalCarryoverFilters(
|
||||||
|
{
|
||||||
|
counterparty: "Группа СВК"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
as_of_date: null,
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
"customer_revenue_and_payments"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(filters).toEqual({
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
period_from: "2020-01-01",
|
||||||
|
period_to: "2020-12-31"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves inventory root pivots through one shared helper", () => {
|
it("resolves inventory root pivots through one shared helper", () => {
|
||||||
const flags = resolveInventoryFollowupPivotFlags(
|
const flags = resolveInventoryFollowupPivotFlags(
|
||||||
{ intent: "inventory_on_hand_as_of_date" },
|
{ intent: "inventory_on_hand_as_of_date" },
|
||||||
|
|
|
||||||
|
|
@ -130,15 +130,14 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||||
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
|
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
|
||||||
|
|
||||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||||
const answerText = [draft.headline, ...draft.confirmed_lines, ...draft.unknown_lines].join("\n");
|
const answerText = [draft.headline, ...draft.confirmed_lines, ...draft.inference_lines, ...draft.unknown_lines].join("\n");
|
||||||
|
|
||||||
expect(draft.answer_mode).toBe("checked_sources_only");
|
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||||
expect(answerText).toContain("0 руб.");
|
expect(answerText).toContain("0 руб.");
|
||||||
expect(answerText).toContain("исходящих платежей/списаний не найдено");
|
expect(answerText).toContain("not_found");
|
||||||
expect(answerText).toContain("Группа СВК");
|
expect(answerText).toContain("Группа СВК");
|
||||||
expect(answerText).toContain("2020");
|
expect(answerText).toContain("2020");
|
||||||
expect(draft.next_step_line).toContain("соседних периодах");
|
expect(draft.unknown_lines.join("\n")).toContain("outside the checked period");
|
||||||
expect(draft.next_step_line).toContain("двусторонний денежный поток");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("turns generic document evidence into a bounded document answer draft", async () => {
|
it("turns generic document evidence into a bounded document answer draft", async () => {
|
||||||
|
|
@ -1464,7 +1463,7 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||||
expect(draft.unknown_lines).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot");
|
expect(draft.unknown_lines).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders zero-row supplier payout as a checked negative with period and counterparty", async () => {
|
it("renders zero-row supplier payout as a confirmed scoped zero with period and counterparty", async () => {
|
||||||
const planner = planAssistantMcpDiscovery({
|
const planner = planAssistantMcpDiscovery({
|
||||||
turnMeaning: {
|
turnMeaning: {
|
||||||
asked_domain_family: "counterparty_value",
|
asked_domain_family: "counterparty_value",
|
||||||
|
|
@ -1479,18 +1478,18 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||||
|
|
||||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||||
const confirmedText = draft.confirmed_lines.join("\n");
|
const confirmedText = draft.confirmed_lines.join("\n");
|
||||||
|
const inferenceText = draft.inference_lines.join("\n");
|
||||||
const unknownText = draft.unknown_lines.join("\n");
|
const unknownText = draft.unknown_lines.join("\n");
|
||||||
|
|
||||||
expect(draft.answer_mode).toBe("checked_sources_only");
|
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||||
expect(draft.headline).toContain("Группа СВК");
|
expect(draft.headline).toContain("Группа СВК");
|
||||||
expect(draft.headline).toContain("2020");
|
expect(draft.headline).toContain("2020");
|
||||||
expect(draft.headline).toContain("исходящих платежей");
|
expect(draft.headline).toContain("0 руб.");
|
||||||
expect(confirmedText).toContain("Группа СВК");
|
expect(confirmedText).toContain("Группа СВК");
|
||||||
expect(confirmedText).toContain("ООО Альтернатива Плюс");
|
expect(confirmedText).toContain("0 руб.");
|
||||||
expect(confirmedText).toContain("2020");
|
expect(confirmedText).toContain("Граница");
|
||||||
expect(confirmedText).toContain("не найдено");
|
expect(inferenceText).toContain("not_found result was treated as zero");
|
||||||
expect(unknownText).toContain("вне периода 2020");
|
expect(unknownText).toContain("outside the checked period");
|
||||||
expect(unknownText).toContain("вне доступного банковского контура");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("turns bidirectional value-flow evidence into a bounded net cash answer draft", async () => {
|
it("turns bidirectional value-flow evidence into a bounded net cash answer draft", async () => {
|
||||||
|
|
|
||||||
|
|
@ -1650,6 +1650,38 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||||
expect(String(call?.query ?? "")).toContain("СписаниеСРасчетногоСчета");
|
expect(String(call?.query ?? "")).toContain("СписаниеСРасчетногоСчета");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats zero-row supplier payout query as confirmed not-found inside checked scope", async () => {
|
||||||
|
const planner = planAssistantMcpDiscovery({
|
||||||
|
turnMeaning: {
|
||||||
|
asked_domain_family: "counterparty_value",
|
||||||
|
asked_action_family: "payout",
|
||||||
|
explicit_entity_candidates: ["SVK"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const deps = buildDeps([]);
|
||||||
|
|
||||||
|
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||||
|
|
||||||
|
expect(result.pilot_status).toBe("executed");
|
||||||
|
expect(result.pilot_scope).toBe("counterparty_supplier_payout_query_movements_v1");
|
||||||
|
expect(result.evidence.evidence_status).toBe("confirmed");
|
||||||
|
expect(result.evidence.answer_permission).toBe("confirmed_answer");
|
||||||
|
expect(result.evidence.confirmed_facts[0]).toContain("not_found");
|
||||||
|
expect(result.reason_codes).toContain("pilot_derived_value_flow_from_confirmed_rows");
|
||||||
|
expect(result.evidence.reason_codes).toContain("confirmed_no_match_fact_with_allowed_mcp_evidence");
|
||||||
|
expect(result.derived_value_flow).toMatchObject({
|
||||||
|
value_flow_direction: "outgoing_supplier_payout",
|
||||||
|
counterparty: "SVK",
|
||||||
|
period_scope: "2020",
|
||||||
|
rows_matched: 0,
|
||||||
|
rows_with_amount: 0,
|
||||||
|
total_amount: 0,
|
||||||
|
total_amount_human_ru: "0 руб."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("marks value-flow coverage as limited when the probe row limit is reached", async () => {
|
it("marks value-flow coverage as limited when the probe row limit is reached", async () => {
|
||||||
const planner = planAssistantMcpDiscovery({
|
const planner = planAssistantMcpDiscovery({
|
||||||
turnMeaning: {
|
turnMeaning: {
|
||||||
|
|
|
||||||
|
|
@ -1602,8 +1602,9 @@ describe("assistant MCP discovery response candidate", () => {
|
||||||
expect(firstLine).toContain("По подтвержденным строкам 1С");
|
expect(firstLine).toContain("По подтвержденным строкам 1С");
|
||||||
expect(firstLine).toContain("входящие 157 192 981,43 руб.");
|
expect(firstLine).toContain("входящие 157 192 981,43 руб.");
|
||||||
expect(firstLine).toContain("исходящие 35 439 044,74 руб.");
|
expect(firstLine).toContain("исходящие 35 439 044,74 руб.");
|
||||||
expect(firstLine).toContain("не аудиторское заключение");
|
expect(firstLine).toContain("не финансовый аудит");
|
||||||
expect(firstLine).toContain("не подтвержденная чистая прибыль");
|
expect(firstLine).toContain("не подтверждение чистой прибыли");
|
||||||
|
expect(firstLine).not.toContain("аудиторское заключение");
|
||||||
expect(candidate.reply_text).toContain("Интерпретация:");
|
expect(candidate.reply_text).toContain("Интерпретация:");
|
||||||
expect(candidate.reply_text).toContain("Что видно в ограниченном денежном срезе");
|
expect(candidate.reply_text).toContain("Что видно в ограниченном денежном срезе");
|
||||||
expect(candidate.reply_text).toContain("Что не подтверждено в этом срезе");
|
expect(candidate.reply_text).toContain("Что не подтверждено в этом срезе");
|
||||||
|
|
|
||||||
|
|
@ -757,6 +757,72 @@ describe("assistant MCP discovery response policy", () => {
|
||||||
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_document_list_address_reply");
|
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_document_list_address_reply");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("overrides exact document-list replies when they contradict the discovery period scope", () => {
|
||||||
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
|
currentReply:
|
||||||
|
"Контрагент: Группа СВК. Найдено документов: 19.\n" +
|
||||||
|
"1. 2021-11-10T12:00:07Z | Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07 | аналитика: Группа СВК",
|
||||||
|
currentReplySource: "address_query_runtime_v1",
|
||||||
|
currentReplyType: "factual",
|
||||||
|
addressRuntimeMeta: {
|
||||||
|
detected_intent: "list_documents_by_counterparty",
|
||||||
|
selected_recipe: "address_documents_by_counterparty_v1",
|
||||||
|
mcp_call_status: "matched_non_empty",
|
||||||
|
response_type: "FACTUAL_LIST",
|
||||||
|
truth_mode: "confirmed",
|
||||||
|
extracted_filters: {
|
||||||
|
counterparty: "Группа СВК",
|
||||||
|
as_of_date: "2026-06-15"
|
||||||
|
},
|
||||||
|
dialog_continuation_contract_v2: {
|
||||||
|
target_intent: "list_documents_by_counterparty"
|
||||||
|
},
|
||||||
|
assistant_mcp_discovery_entry_point_v1: entryPoint({
|
||||||
|
turn_input: {
|
||||||
|
adapter_status: "ready",
|
||||||
|
should_run_discovery: true,
|
||||||
|
turn_meaning_ref: {
|
||||||
|
asked_domain_family: "documents",
|
||||||
|
asked_action_family: "list_documents",
|
||||||
|
explicit_entity_candidates: ["Группа СВК"],
|
||||||
|
explicit_date_scope: "2020",
|
||||||
|
unsupported_but_understood_family: "document_evidence"
|
||||||
|
},
|
||||||
|
data_need_graph: {
|
||||||
|
business_fact_family: "document_evidence",
|
||||||
|
action_family: "list_documents",
|
||||||
|
subject_candidates: ["Группа СВК"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bridge: {
|
||||||
|
bridge_status: "answer_draft_ready",
|
||||||
|
user_facing_response_allowed: true,
|
||||||
|
business_fact_answer_allowed: true,
|
||||||
|
requires_user_clarification: false,
|
||||||
|
answer_draft: {
|
||||||
|
answer_mode: "bounded_inference_only",
|
||||||
|
headline: "По документам по контрагенту Группа СВК за 2020 полный срез не подтвержден.",
|
||||||
|
confirmed_lines: [],
|
||||||
|
inference_lines: ["Подтвержденных строк документов за 2020 этим поиском не найдено."],
|
||||||
|
unknown_lines: ["Полный исторический срез вне 2020 этим поиском не подтвержден."],
|
||||||
|
limitation_lines: [],
|
||||||
|
next_step_line: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.applied).toBe(true);
|
||||||
|
expect(result.decision).toBe("apply_candidate");
|
||||||
|
expect(result.reply_text).toContain("за 2020");
|
||||||
|
expect(result.reply_text).not.toContain("2021-11-10");
|
||||||
|
expect(result.reason_codes).toContain(
|
||||||
|
"mcp_discovery_response_policy_document_list_temporal_conflict_allows_candidate_override"
|
||||||
|
);
|
||||||
|
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_document_list_address_reply");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
|
it("keeps exact matched inventory address replies over stale metadata discovery candidates", () => {
|
||||||
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
const result = applyAssistantMcpDiscoveryResponsePolicy({
|
||||||
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",
|
currentReply: "По товару Шкаф картотечный 1000*400*2100 цепочка поставки и продажи подтверждена.",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue