Укрепить business overview и память по СВК

This commit is contained in:
dctouch 2026-06-01 22:47:45 +03:00
parent ede15c6080
commit 987419fade
13 changed files with 172 additions and 29 deletions

View File

@ -3159,7 +3159,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
: `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`; : `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`;
const lines = [ const lines = [
directVatLine, directVatLine,
"Расчет сделан по книгам продаж и покупок.", "Расчет сделан по подтвержденным строкам книг продаж и покупок.",
"", "",
"Что вошло в расчет:", "Что вошло в расчет:",
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []), ...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
@ -3169,6 +3169,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`, `- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.` `- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
]; ];
lines.push("", "Граница вывода: это подтвержденный баланс по найденным строкам книг продаж/покупок, но не полный налоговый вердикт и не сверка декларации; для финального вывода нужно отдельно проверить корректировки, применимость вычетов, закрытие периода и первичные документы.");
if (rows.length === 0) { if (rows.length === 0) {
lines.push("", "За выбранный налоговый период не найдены строки книг продаж/покупок, поэтому подтвержденная сумма к уплате равна 0."); lines.push("", "За выбранный налоговый период не найдены строки книг продаж/покупок, поэтому подтвержденная сумма к уплате равна 0.");
} }

View File

@ -327,9 +327,6 @@ function parseBusinessOverviewProofBundlesFromTextV2(value, counterpartyHint, to
return valueBundle || documentBundle ? { valueBundle, documentBundle } : null; return valueBundle || documentBundle ? { valueBundle, documentBundle } : null;
} }
function findRecentBusinessOverviewProofBundles(input) { function findRecentBusinessOverviewProofBundles(input) {
if (!input.counterpartyHint) {
return null;
}
for (let index = input.sessionItems.length - 1; index >= 0; index -= 1) { for (let index = input.sessionItems.length - 1; index >= 0; index -= 1) {
const item = toRecordObject(input.sessionItems[index]); const item = toRecordObject(input.sessionItems[index]);
if (input.toNonEmptyString(item?.role) !== "assistant") { if (input.toNonEmptyString(item?.role) !== "assistant") {
@ -344,7 +341,7 @@ function findRecentBusinessOverviewProofBundles(input) {
const documentBundle = toRecordObject(turnMeaningRef.previous_counterparty_document_bundle); const documentBundle = toRecordObject(turnMeaningRef.previous_counterparty_document_bundle);
if (valueBundle || documentBundle) { if (valueBundle || documentBundle) {
const bundleCounterparty = turnMeaningCounterpartyName(turnMeaningRef, valueBundle, documentBundle, input.toNonEmptyString); const bundleCounterparty = turnMeaningCounterpartyName(turnMeaningRef, valueBundle, documentBundle, input.toNonEmptyString);
if (sameEntityHint(input.counterpartyHint, bundleCounterparty)) { if (!input.counterpartyHint || sameEntityHint(input.counterpartyHint, bundleCounterparty)) {
return { valueBundle, documentBundle }; return { valueBundle, documentBundle };
} }
} }
@ -356,7 +353,7 @@ function findRecentBusinessOverviewProofBundles(input) {
} }
const parsedCounterparty = input.toNonEmptyString(parsedBundles.valueBundle?.counterparty) ?? const parsedCounterparty = input.toNonEmptyString(parsedBundles.valueBundle?.counterparty) ??
input.toNonEmptyString(parsedBundles.documentBundle?.counterparty); input.toNonEmptyString(parsedBundles.documentBundle?.counterparty);
if (!sameEntityHint(input.counterpartyHint, parsedCounterparty)) { if (input.counterpartyHint && !sameEntityHint(input.counterpartyHint, parsedCounterparty)) {
continue; continue;
} }
return parsedBundles; return parsedBundles;
@ -381,7 +378,7 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input) {
const entities = toRecordObject(input.predecomposeContract?.entities); const entities = toRecordObject(input.predecomposeContract?.entities);
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization)); const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString); const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
if (!hasComparisonScopeProofCue(input.userMessage)) { if (!hasComparisonScopeProofCue(input.userMessage) && !hasCurrentOrganizationSelection) {
return input.followupContext; return input.followupContext;
} }
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) { if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
@ -416,10 +413,13 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input) {
}; };
} }
function mergeBusinessOverviewProofBundlesFromSessionItems(input) { function mergeBusinessOverviewProofBundlesFromSessionItems(input) {
if (!hasComparisonScopeProofCue(input.userMessage)) { const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
const entities = toRecordObject(input.predecomposeContract?.entities);
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
if (!hasComparisonScopeProofCue(input.userMessage) && !businessOverviewFollowup && !hasCurrentOrganizationSelection) {
return input.followupContext; return input.followupContext;
} }
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) { if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
return input.followupContext; return input.followupContext;
} }
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow); const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
@ -438,8 +438,21 @@ function mergeBusinessOverviewProofBundlesFromSessionItems(input) {
if (!proofBundles) { if (!proofBundles) {
return input.followupContext; return input.followupContext;
} }
const recoveredCounterparty = counterpartyHint ??
input.toNonEmptyString(proofBundles.valueBundle?.counterparty) ??
input.toNonEmptyString(proofBundles.documentBundle?.counterparty);
return { return {
...(input.followupContext ?? {}), ...(input.followupContext ?? {}),
previous_intent: input.toNonEmptyString(input.followupContext?.previous_intent) ?? "business_overview",
target_intent: input.toNonEmptyString(input.followupContext?.target_intent) ?? "business_overview",
previous_discovery_pilot_scope: input.toNonEmptyString(input.followupContext?.previous_discovery_pilot_scope) ??
"business_overview_route_template_v1",
previous_discovery_loop_selected_chain_id: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_selected_chain_id) ?? "business_overview",
previous_discovery_loop_asked_domain_family: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_domain_family) ?? "business_overview",
previous_discovery_loop_asked_action_family: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_action_family) ?? "broad_evaluation",
previous_discovery_loop_metadata_scope_hint: input.toNonEmptyString(input.followupContext?.previous_discovery_loop_metadata_scope_hint) ?? recoveredCounterparty ?? undefined,
previous_anchor_type: input.toNonEmptyString(input.followupContext?.previous_anchor_type) ?? (recoveredCounterparty ? "counterparty" : undefined),
previous_anchor_value: input.toNonEmptyString(input.followupContext?.previous_anchor_value) ?? recoveredCounterparty ?? undefined,
previous_discovery_bidirectional_value_flow: currentValueBundle ?? proofBundles.valueBundle ?? undefined, previous_discovery_bidirectional_value_flow: currentValueBundle ?? proofBundles.valueBundle ?? undefined,
previous_discovery_document_summary: currentDocumentBundle ?? proofBundles.documentBundle ?? undefined previous_discovery_document_summary: currentDocumentBundle ?? proofBundles.documentBundle ?? undefined
}; };
@ -620,6 +633,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({ const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
followupContext: discoveryFollowupContext, followupContext: discoveryFollowupContext,
userMessage: input.userMessage, userMessage: input.userMessage,
predecomposeContract,
sessionItems: input.sessionItems, sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString toNonEmptyString: input.toNonEmptyString
}); });

View File

@ -933,7 +933,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
return joinBusinessReplyLines(lines); return joinBusinessReplyLines(lines);
} }
if (plainOrganizationClarificationSelection && (incomingAmount || outgoingAmount || netAmount)) { if (plainOrganizationClarificationSelection && (incomingAmount || outgoingAmount || netAmount)) {
lines.push(`Коротко: по компании ${organizationScope} ${period} подтвержден company-level денежный срез: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`); lines.push(`по компании ${organizationScope} ${period} подтвержден company-level денежный срез: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`);
if (previousCounterpartySummary) { if (previousCounterpartySummary) {
lines.push(previousCounterpartySummary.line); lines.push(previousCounterpartySummary.line);
} }
@ -1137,7 +1137,13 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
(actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) { (actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
const subject = organizationScope ?? "компания"; const subject = organizationScope ?? "компания";
const periodWithoutPrefix = period.replace(/^за\s+/iu, ""); const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
lines.push(`Коротко: по ограниченному проверенному 1С-срезу ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж; это не аудиторское заключение и не подтвержденная чистая прибыль.`); const executiveFacts = [
incomingAmount ? `входящие ${incomingAmount}` : null,
outgoingAmount ? `исходящие ${outgoingAmount}` : null,
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
].filter((value) => Boolean(value));
lines.push(`По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, не аудиторское заключение и не подтвержденная чистая прибыль.`);
lines.push("Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж.");
lines.push("Что видно в ограниченном денежном срезе:"); lines.push("Что видно в ограниченном денежном срезе:");
if (incomingAmount) { if (incomingAmount) {
lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`); lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`);

View File

@ -2088,10 +2088,13 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: undefined, : undefined,
explicit_entity_candidates: businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty ? [] : entityCandidates, explicit_entity_candidates: businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty ? [] : entityCandidates,
business_overview_separate_entity_candidates: businessOverviewSeparateEntityCandidates, business_overview_separate_entity_candidates: businessOverviewSeparateEntityCandidates,
previous_counterparty_value_flow_bundle: businessOverviewSignal && followupSeed.previousBidirectionalValueFlow previous_counterparty_value_flow_bundle: (businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty) &&
followupSeed.previousBidirectionalValueFlow
? followupSeed.previousBidirectionalValueFlow ? followupSeed.previousBidirectionalValueFlow
: undefined, : undefined,
previous_counterparty_document_bundle: businessOverviewSignal && followupSeed.previousDocumentSummary ? followupSeed.previousDocumentSummary : undefined, previous_counterparty_document_bundle: (businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty) && followupSeed.previousDocumentSummary
? followupSeed.previousDocumentSummary
: undefined,
metadata_ambiguity_entity_sets: metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0 metadata_ambiguity_entity_sets: metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
? followupSeed.metadataAmbiguityEntitySets ? followupSeed.metadataAmbiguityEntitySets
: undefined, : undefined,

View File

@ -409,7 +409,9 @@ function createAssistantTransitionPolicy(deps) {
if (!text) { if (!text) {
return null; return null;
} }
const match = text.match(/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu); const boundaryMatch = text.match(/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu);
const directMatch = text.match(/по\s+контрагенту\s+([^.\n]+?)\s+в\s+проверенном\s+окне[\s\S]{0,160}?получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^;\n]+?руб\.?);\s*расчетное\s+нетто\s+в\s+нашу\s+сторону:?\s*([^.\n]+?руб\.?)/iu);
const match = boundaryMatch ?? directMatch;
if (!match?.[1] || !match?.[2] || !match?.[3] || !match?.[4]) { if (!match?.[1] || !match?.[2] || !match?.[3] || !match?.[4]) {
return null; return null;
} }

View File

@ -4053,7 +4053,7 @@ function composeFactualReplyBody(
const lines = [ const lines = [
directVatLine, directVatLine,
"Расчет сделан по книгам продаж и покупок.", "Расчет сделан по подтвержденным строкам книг продаж и покупок.",
"", "",
"Что вошло в расчет:", "Что вошло в расчет:",
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []), ...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
@ -4063,6 +4063,10 @@ function composeFactualReplyBody(
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`, `- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.` `- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
]; ];
lines.push(
"",
"Граница вывода: это подтвержденный баланс по найденным строкам книг продаж/покупок, но не полный налоговый вердикт и не сверка декларации; для финального вывода нужно отдельно проверить корректировки, применимость вычетов, закрытие периода и первичные документы."
);
if (rows.length === 0) { if (rows.length === 0) {
lines.push( lines.push(

View File

@ -483,9 +483,6 @@ function findRecentBusinessOverviewProofBundles(input: {
counterpartyHint: string | null; counterpartyHint: string | null;
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]; toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
}): { valueBundle: Record<string, unknown> | null; documentBundle: Record<string, unknown> | null } | null { }): { valueBundle: Record<string, unknown> | null; documentBundle: Record<string, unknown> | null } | null {
if (!input.counterpartyHint) {
return null;
}
for (let index = input.sessionItems.length - 1; index >= 0; index -= 1) { for (let index = input.sessionItems.length - 1; index >= 0; index -= 1) {
const item = toRecordObject(input.sessionItems[index]); const item = toRecordObject(input.sessionItems[index]);
if (input.toNonEmptyString(item?.role) !== "assistant") { if (input.toNonEmptyString(item?.role) !== "assistant") {
@ -505,7 +502,7 @@ function findRecentBusinessOverviewProofBundles(input: {
documentBundle, documentBundle,
input.toNonEmptyString input.toNonEmptyString
); );
if (sameEntityHint(input.counterpartyHint, bundleCounterparty)) { if (!input.counterpartyHint || sameEntityHint(input.counterpartyHint, bundleCounterparty)) {
return { valueBundle, documentBundle }; return { valueBundle, documentBundle };
} }
} }
@ -519,7 +516,7 @@ function findRecentBusinessOverviewProofBundles(input: {
const parsedCounterparty = const parsedCounterparty =
input.toNonEmptyString(parsedBundles.valueBundle?.counterparty) ?? input.toNonEmptyString(parsedBundles.valueBundle?.counterparty) ??
input.toNonEmptyString(parsedBundles.documentBundle?.counterparty); input.toNonEmptyString(parsedBundles.documentBundle?.counterparty);
if (!sameEntityHint(input.counterpartyHint, parsedCounterparty)) { if (input.counterpartyHint && !sameEntityHint(input.counterpartyHint, parsedCounterparty)) {
continue; continue;
} }
return parsedBundles; return parsedBundles;
@ -551,7 +548,7 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
const entities = toRecordObject(input.predecomposeContract?.entities); const entities = toRecordObject(input.predecomposeContract?.entities);
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization)); const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString); const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
if (!hasComparisonScopeProofCue(input.userMessage)) { if (!hasComparisonScopeProofCue(input.userMessage) && !hasCurrentOrganizationSelection) {
return input.followupContext; return input.followupContext;
} }
if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) { if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
@ -597,13 +594,17 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: {
function mergeBusinessOverviewProofBundlesFromSessionItems(input: { function mergeBusinessOverviewProofBundlesFromSessionItems(input: {
followupContext: Record<string, unknown> | null; followupContext: Record<string, unknown> | null;
userMessage: string; userMessage: string;
predecomposeContract: Record<string, unknown> | null;
sessionItems: unknown[]; sessionItems: unknown[];
toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]; toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"];
}): Record<string, unknown> | null { }): Record<string, unknown> | null {
if (!hasComparisonScopeProofCue(input.userMessage)) { const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString);
const entities = toRecordObject(input.predecomposeContract?.entities);
const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization));
if (!hasComparisonScopeProofCue(input.userMessage) && !businessOverviewFollowup && !hasCurrentOrganizationSelection) {
return input.followupContext; return input.followupContext;
} }
if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) { if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) {
return input.followupContext; return input.followupContext;
} }
const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow); const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow);
@ -622,8 +623,27 @@ function mergeBusinessOverviewProofBundlesFromSessionItems(input: {
if (!proofBundles) { if (!proofBundles) {
return input.followupContext; return input.followupContext;
} }
const recoveredCounterparty =
counterpartyHint ??
input.toNonEmptyString(proofBundles.valueBundle?.counterparty) ??
input.toNonEmptyString(proofBundles.documentBundle?.counterparty);
return { return {
...(input.followupContext ?? {}), ...(input.followupContext ?? {}),
previous_intent: input.toNonEmptyString(input.followupContext?.previous_intent) ?? "business_overview",
target_intent: input.toNonEmptyString(input.followupContext?.target_intent) ?? "business_overview",
previous_discovery_pilot_scope:
input.toNonEmptyString(input.followupContext?.previous_discovery_pilot_scope) ??
"business_overview_route_template_v1",
previous_discovery_loop_selected_chain_id:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_selected_chain_id) ?? "business_overview",
previous_discovery_loop_asked_domain_family:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_domain_family) ?? "business_overview",
previous_discovery_loop_asked_action_family:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_asked_action_family) ?? "broad_evaluation",
previous_discovery_loop_metadata_scope_hint:
input.toNonEmptyString(input.followupContext?.previous_discovery_loop_metadata_scope_hint) ?? recoveredCounterparty ?? undefined,
previous_anchor_type: input.toNonEmptyString(input.followupContext?.previous_anchor_type) ?? (recoveredCounterparty ? "counterparty" : undefined),
previous_anchor_value: input.toNonEmptyString(input.followupContext?.previous_anchor_value) ?? recoveredCounterparty ?? undefined,
previous_discovery_bidirectional_value_flow: previous_discovery_bidirectional_value_flow:
currentValueBundle ?? proofBundles.valueBundle ?? undefined, currentValueBundle ?? proofBundles.valueBundle ?? undefined,
previous_discovery_document_summary: currentDocumentBundle ?? proofBundles.documentBundle ?? undefined previous_discovery_document_summary: currentDocumentBundle ?? proofBundles.documentBundle ?? undefined
@ -904,6 +924,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({ const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({
followupContext: discoveryFollowupContext, followupContext: discoveryFollowupContext,
userMessage: input.userMessage, userMessage: input.userMessage,
predecomposeContract,
sessionItems: input.sessionItems, sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString toNonEmptyString: input.toNonEmptyString
}); });

View File

@ -1137,7 +1137,7 @@ function buildCompactBusinessOverviewReply(
if (plainOrganizationClarificationSelection && (incomingAmount || outgoingAmount || netAmount)) { if (plainOrganizationClarificationSelection && (incomingAmount || outgoingAmount || netAmount)) {
lines.push( lines.push(
`Коротко: по компании ${organizationScope} ${period} подтвержден company-level денежный срез: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.` `по компании ${organizationScope} ${period} подтвержден company-level денежный срез: входящие ${incomingAmount ?? "0 руб."}, исходящие ${outgoingAmount ?? "0 руб."}, операционное нетто ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
); );
if (previousCounterpartySummary) { if (previousCounterpartySummary) {
lines.push(previousCounterpartySummary.line); lines.push(previousCounterpartySummary.line);
@ -1403,8 +1403,16 @@ function buildCompactBusinessOverviewReply(
) { ) {
const subject = organizationScope ?? "компания"; const subject = organizationScope ?? "компания";
const periodWithoutPrefix = period.replace(/^за\s+/iu, ""); const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
const executiveFacts = [
incomingAmount ? `входящие ${incomingAmount}` : null,
outgoingAmount ? `исходящие ${outgoingAmount}` : null,
netAmount ? `${netDirection} ${sentenceAmount(netAmount) ?? netAmount}` : null
].filter((value): value is string => Boolean(value));
lines.push( lines.push(
`Коротко: по ограниченному проверенному 1С-срезу ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж; это не аудиторское заключение и не подтвержденная чистая прибыль.` `По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, не аудиторское заключение и не подтвержденная чистая прибыль.`
);
lines.push(
"Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж."
); );
lines.push("Что видно в ограниченном денежном срезе:"); lines.push("Что видно в ограниченном денежном срезе:");
if (incomingAmount) { if (incomingAmount) {

View File

@ -2779,11 +2779,14 @@ export function buildAssistantMcpDiscoveryTurnInput(
businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty ? [] : entityCandidates, businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty ? [] : entityCandidates,
business_overview_separate_entity_candidates: businessOverviewSeparateEntityCandidates, business_overview_separate_entity_candidates: businessOverviewSeparateEntityCandidates,
previous_counterparty_value_flow_bundle: previous_counterparty_value_flow_bundle:
businessOverviewSignal && followupSeed.previousBidirectionalValueFlow (businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty) &&
followupSeed.previousBidirectionalValueFlow
? followupSeed.previousBidirectionalValueFlow ? followupSeed.previousBidirectionalValueFlow
: undefined, : undefined,
previous_counterparty_document_bundle: previous_counterparty_document_bundle:
businessOverviewSignal && followupSeed.previousDocumentSummary ? followupSeed.previousDocumentSummary : undefined, (businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty) && followupSeed.previousDocumentSummary
? followupSeed.previousDocumentSummary
: undefined,
metadata_ambiguity_entity_sets: metadata_ambiguity_entity_sets:
metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0 metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
? followupSeed.metadataAmbiguityEntitySets ? followupSeed.metadataAmbiguityEntitySets

View File

@ -555,9 +555,13 @@ export function createAssistantTransitionPolicy(deps) {
if (!text) { if (!text) {
return null; return null;
} }
const match = text.match( const boundaryMatch = text.match(
/Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu /Отдельно\s+по\s+контрагенту\s+([^:\n]+):\s*подтверждено\s+получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^,\n]+?руб\.?),\s*расчетное\s+нетто\s+в\s+нашу\s+сторону\s+([^.\n]+?руб\.?)/iu
); );
const directMatch = text.match(
/по\s+контрагенту\s+([^.\n]+?)\s+в\s+проверенном\s+окне[\s\S]{0,160}?получили\s+([^,\n]+?руб\.?),\s*заплатили\s+([^;\n]+?руб\.?);\s*расчетное\s+нетто\s+в\s+нашу\s+сторону:?\s*([^.\n]+?руб\.?)/iu
);
const match = boundaryMatch ?? directMatch;
if (!match?.[1] || !match?.[2] || !match?.[3] || !match?.[4]) { if (!match?.[1] || !match?.[2] || !match?.[3] || !match?.[4]) {
return null; return null;
} }

View File

@ -2139,8 +2139,11 @@ describe("address compose stage utf8 headers", () => {
expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Коротко: подтвержденный НДС к уплате за налоговый период по организации ООО Альтернатива Плюс"); expect(reply.text).toContain("Коротко: подтвержденный НДС к уплате за налоговый период по организации ООО Альтернатива Плюс");
expect(reply.text).toContain("Расчет сделан по подтвержденным строкам книг продаж и покупок.");
expect(reply.text).toContain("- Организация: ООО Альтернатива Плюс."); expect(reply.text).toContain("- Организация: ООО Альтернатива Плюс.");
expect(reply.text).toContain("50.000,00 ₽"); expect(reply.text).toContain("50.000,00 ₽");
expect(reply.text).toContain("не полный налоговый вердикт");
expect(reply.text).toContain("корректировки");
expect(reply.semantics?.result_mode).toBe("confirmed_balance"); expect(reply.semantics?.result_mode).toBe("confirmed_balance");
expect(reply.semantics?.balance_confirmed).toBe(true); expect(reply.semantics?.balance_confirmed).toBe(true);
}); });

View File

@ -484,6 +484,77 @@ describe("assistant address orchestration runtime adapter", () => {
); );
}); });
it("recovers business overview proof bundles for a plain organization clarification", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "Alt Plus",
sessionItems: [
{
role: "assistant",
text:
"Отдельно по контрагенту Group SVK: " +
"подтверждено получили 20 653 490 руб., " +
"заплатили 2 129 651 руб., " +
"расчетное нетто в нашу сторону 18 523 839 руб. " +
"Основа: проверенные входящие платежи и исходящие платежи и документы по цепочке: найдено 19."
}
],
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "Alt Plus",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: "Alt Plus" }
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({ followupContext: null })),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation"
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
await buildAssistantAddressOrchestrationRuntime(input);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
followupContext: expect.objectContaining({
previous_discovery_loop_selected_chain_id: "business_overview",
previous_discovery_loop_metadata_scope_hint: "Group SVK",
previous_discovery_bidirectional_value_flow: expect.objectContaining({
counterparty: "Group SVK",
net_amount_human_ru: "18 523 839 руб."
}),
previous_discovery_document_summary: expect.objectContaining({
counterparty: "Group SVK",
document_count: 19
})
})
})
);
});
it("recovers business overview proof bundles from navigation comparison state", async () => { it("recovers business overview proof bundles from navigation comparison state", async () => {
const valueFlowBundle = { const valueFlowBundle = {
counterparty: "Group SVK", counterparty: "Group SVK",

View File

@ -1546,9 +1546,12 @@ describe("assistant MCP discovery response candidate", () => {
); );
const firstLine = candidate.reply_text?.split("\n")[0] ?? ""; const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
expect(firstLine).toContain("по ограниченному проверенному 1С-срезу"); expect(firstLine).toContain("По подтвержденным строкам 1С");
expect(firstLine).toContain("входящие 157 192 981,43 руб.");
expect(firstLine).toContain("исходящие 35 439 044,74 руб.");
expect(firstLine).toContain("не аудиторское заключение"); expect(firstLine).toContain("не аудиторское заключение");
expect(firstLine).toContain("не подтвержденная чистая прибыль"); expect(firstLine).toContain("не подтвержденная чистая прибыль");
expect(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("НДС"); expect(candidate.reply_text).toContain("НДС");