diff --git a/llm_normalizer/backend/dist/services/address_runtime/composeStage.js b/llm_normalizer/backend/dist/services/address_runtime/composeStage.js index b2e861e..157c291 100644 --- a/llm_normalizer/backend/dist/services/address_runtime/composeStage.js +++ b/llm_normalizer/backend/dist/services/address_runtime/composeStage.js @@ -3159,7 +3159,7 @@ function composeFactualReplyBody(intent, rows, options = {}) { : `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`; const lines = [ directVatLine, - "Расчет сделан по книгам продаж и покупок.", + "Расчет сделан по подтвержденным строкам книг продаж и покупок.", "", "Что вошло в расчет:", ...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []), @@ -3169,6 +3169,7 @@ function composeFactualReplyBody(intent, rows, options = {}) { `- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`, `- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.` ]; + lines.push("", "Граница вывода: это подтвержденный баланс по найденным строкам книг продаж/покупок, но не полный налоговый вердикт и не сверка декларации; для финального вывода нужно отдельно проверить корректировки, применимость вычетов, закрытие периода и первичные документы."); if (rows.length === 0) { lines.push("", "За выбранный налоговый период не найдены строки книг продаж/покупок, поэтому подтвержденная сумма к уплате равна 0."); } diff --git a/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js index 211e960..4b34f48 100644 --- a/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js @@ -327,9 +327,6 @@ function parseBusinessOverviewProofBundlesFromTextV2(value, counterpartyHint, to return valueBundle || documentBundle ? { valueBundle, documentBundle } : null; } function findRecentBusinessOverviewProofBundles(input) { - if (!input.counterpartyHint) { - return null; - } for (let index = input.sessionItems.length - 1; index >= 0; index -= 1) { const item = toRecordObject(input.sessionItems[index]); if (input.toNonEmptyString(item?.role) !== "assistant") { @@ -344,7 +341,7 @@ function findRecentBusinessOverviewProofBundles(input) { const documentBundle = toRecordObject(turnMeaningRef.previous_counterparty_document_bundle); if (valueBundle || documentBundle) { const bundleCounterparty = turnMeaningCounterpartyName(turnMeaningRef, valueBundle, documentBundle, input.toNonEmptyString); - if (sameEntityHint(input.counterpartyHint, bundleCounterparty)) { + if (!input.counterpartyHint || sameEntityHint(input.counterpartyHint, bundleCounterparty)) { return { valueBundle, documentBundle }; } } @@ -356,7 +353,7 @@ function findRecentBusinessOverviewProofBundles(input) { } const parsedCounterparty = input.toNonEmptyString(parsedBundles.valueBundle?.counterparty) ?? input.toNonEmptyString(parsedBundles.documentBundle?.counterparty); - if (!sameEntityHint(input.counterpartyHint, parsedCounterparty)) { + if (input.counterpartyHint && !sameEntityHint(input.counterpartyHint, parsedCounterparty)) { continue; } return parsedBundles; @@ -381,7 +378,7 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input) { const entities = toRecordObject(input.predecomposeContract?.entities); const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization)); const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString); - if (!hasComparisonScopeProofCue(input.userMessage)) { + if (!hasComparisonScopeProofCue(input.userMessage) && !hasCurrentOrganizationSelection) { return input.followupContext; } if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) { @@ -416,10 +413,13 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(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; } - if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) { + if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) { return input.followupContext; } const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow); @@ -438,8 +438,21 @@ function mergeBusinessOverviewProofBundlesFromSessionItems(input) { if (!proofBundles) { return input.followupContext; } + const recoveredCounterparty = counterpartyHint ?? + input.toNonEmptyString(proofBundles.valueBundle?.counterparty) ?? + input.toNonEmptyString(proofBundles.documentBundle?.counterparty); return { ...(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_document_summary: currentDocumentBundle ?? proofBundles.documentBundle ?? undefined }; @@ -620,6 +633,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) { const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({ followupContext: discoveryFollowupContext, userMessage: input.userMessage, + predecomposeContract, sessionItems: input.sessionItems, toNonEmptyString: input.toNonEmptyString }); diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js index 52cfd88..126fbb7 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js @@ -933,7 +933,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) { return joinBusinessReplyLines(lines); } 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) { lines.push(previousCounterpartySummary.line); } @@ -1137,7 +1137,13 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) { (actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) { const subject = organizationScope ?? "компания"; 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("Что видно в ограниченном денежном срезе:"); if (incomingAmount) { lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`); diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js index 5b1b712..05befe1 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -2088,10 +2088,13 @@ function buildAssistantMcpDiscoveryTurnInput(input) { : undefined, explicit_entity_candidates: businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty ? [] : entityCandidates, 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 : 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 ? followupSeed.metadataAmbiguityEntitySets : undefined, diff --git a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js index fdd280c..400b919 100644 --- a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js @@ -409,7 +409,9 @@ function createAssistantTransitionPolicy(deps) { if (!text) { 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]) { return null; } diff --git a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts index dc07b22..dcb4ea3 100644 --- a/llm_normalizer/backend/src/services/address_runtime/composeStage.ts +++ b/llm_normalizer/backend/src/services/address_runtime/composeStage.ts @@ -4053,7 +4053,7 @@ function composeFactualReplyBody( const lines = [ directVatLine, - "Расчет сделан по книгам продаж и покупок.", + "Расчет сделан по подтвержденным строкам книг продаж и покупок.", "", "Что вошло в расчет:", ...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []), @@ -4063,6 +4063,10 @@ function composeFactualReplyBody( `- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`, `- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.` ]; + lines.push( + "", + "Граница вывода: это подтвержденный баланс по найденным строкам книг продаж/покупок, но не полный налоговый вердикт и не сверка декларации; для финального вывода нужно отдельно проверить корректировки, применимость вычетов, закрытие периода и первичные документы." + ); if (rows.length === 0) { lines.push( diff --git a/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts index c4a533a..cf2a454 100644 --- a/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts @@ -483,9 +483,6 @@ function findRecentBusinessOverviewProofBundles(input: { counterpartyHint: string | null; toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]; }): { valueBundle: Record | null; documentBundle: Record | null } | null { - if (!input.counterpartyHint) { - return null; - } for (let index = input.sessionItems.length - 1; index >= 0; index -= 1) { const item = toRecordObject(input.sessionItems[index]); if (input.toNonEmptyString(item?.role) !== "assistant") { @@ -505,7 +502,7 @@ function findRecentBusinessOverviewProofBundles(input: { documentBundle, input.toNonEmptyString ); - if (sameEntityHint(input.counterpartyHint, bundleCounterparty)) { + if (!input.counterpartyHint || sameEntityHint(input.counterpartyHint, bundleCounterparty)) { return { valueBundle, documentBundle }; } } @@ -519,7 +516,7 @@ function findRecentBusinessOverviewProofBundles(input: { const parsedCounterparty = input.toNonEmptyString(parsedBundles.valueBundle?.counterparty) ?? input.toNonEmptyString(parsedBundles.documentBundle?.counterparty); - if (!sameEntityHint(input.counterpartyHint, parsedCounterparty)) { + if (input.counterpartyHint && !sameEntityHint(input.counterpartyHint, parsedCounterparty)) { continue; } return parsedBundles; @@ -551,7 +548,7 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: { const entities = toRecordObject(input.predecomposeContract?.entities); const hasCurrentOrganizationSelection = Boolean(input.toNonEmptyString(entities?.organization)); const businessOverviewFollowup = isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString); - if (!hasComparisonScopeProofCue(input.userMessage)) { + if (!hasComparisonScopeProofCue(input.userMessage) && !hasCurrentOrganizationSelection) { return input.followupContext; } if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) { @@ -597,13 +594,17 @@ function mergeBusinessOverviewProofBundlesFromNavigationState(input: { function mergeBusinessOverviewProofBundlesFromSessionItems(input: { followupContext: Record | null; userMessage: string; + predecomposeContract: Record | null; sessionItems: unknown[]; toNonEmptyString: BuildAssistantAddressOrchestrationRuntimeInput["toNonEmptyString"]; }): Record | 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; } - if (!isBusinessOverviewDiscoveryFollowup(input.followupContext, input.toNonEmptyString)) { + if (!businessOverviewFollowup && !hasCurrentOrganizationSelection) { return input.followupContext; } const currentValueBundle = toRecordObject(input.followupContext?.previous_discovery_bidirectional_value_flow); @@ -622,8 +623,27 @@ function mergeBusinessOverviewProofBundlesFromSessionItems(input: { if (!proofBundles) { return input.followupContext; } + const recoveredCounterparty = + counterpartyHint ?? + input.toNonEmptyString(proofBundles.valueBundle?.counterparty) ?? + input.toNonEmptyString(proofBundles.documentBundle?.counterparty); return { ...(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_document_summary: currentDocumentBundle ?? proofBundles.documentBundle ?? undefined @@ -904,6 +924,7 @@ export async function buildAssistantAddressOrchestrationRuntime( const discoveryFollowupContextWithProofBundles = mergeBusinessOverviewProofBundlesFromSessionItems({ followupContext: discoveryFollowupContext, userMessage: input.userMessage, + predecomposeContract, sessionItems: input.sessionItems, toNonEmptyString: input.toNonEmptyString }); diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts index 3d6539e..0f761d1 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts @@ -1137,7 +1137,7 @@ function buildCompactBusinessOverviewReply( if (plainOrganizationClarificationSelection && (incomingAmount || outgoingAmount || netAmount)) { 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) { lines.push(previousCounterpartySummary.line); @@ -1403,8 +1403,16 @@ function buildCompactBusinessOverviewReply( ) { const subject = organizationScope ?? "компания"; 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( - `Коротко: по ограниченному проверенному 1С-срезу ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж; это не аудиторское заключение и не подтвержденная чистая прибыль.` + `По подтвержденным строкам 1С ${subject} за ${periodWithoutPrefix}: ${executiveFacts.length > 0 ? executiveFacts.join(", ") : "денежные метрики не подтверждены"}; это ограниченный проверенный срез, не аудиторское заключение и не подтвержденная чистая прибыль.` + ); + lines.push( + "Интерпретация: по этому срезу видны крупные контрактные денежные потоки и заметная зависимость от нескольких крупных контрагентов, а не равномерный поток мелких продаж." ); lines.push("Что видно в ограниченном денежном срезе:"); if (incomingAmount) { diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts index 88c600b..48cd495 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -2779,11 +2779,14 @@ export function buildAssistantMcpDiscoveryTurnInput( businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty ? [] : entityCandidates, business_overview_separate_entity_candidates: businessOverviewSeparateEntityCandidates, previous_counterparty_value_flow_bundle: - businessOverviewSignal && followupSeed.previousBidirectionalValueFlow + (businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty) && + followupSeed.previousBidirectionalValueFlow ? followupSeed.previousBidirectionalValueFlow : undefined, previous_counterparty_document_bundle: - businessOverviewSignal && followupSeed.previousDocumentSummary ? followupSeed.previousDocumentSummary : undefined, + (businessOverviewSignal || shouldPreserveBusinessOverviewSeparateCounterparty) && followupSeed.previousDocumentSummary + ? followupSeed.previousDocumentSummary + : undefined, metadata_ambiguity_entity_sets: metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0 ? followupSeed.metadataAmbiguityEntitySets diff --git a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts index 7999da6..cbcba2a 100644 --- a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts @@ -555,9 +555,13 @@ export function createAssistantTransitionPolicy(deps) { if (!text) { 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 ); + 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]) { return null; } diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index 30973ab..bfd1e10 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -2139,8 +2139,11 @@ describe("address compose stage utf8 headers", () => { expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.text).toContain("Коротко: подтвержденный НДС к уплате за налоговый период по организации ООО Альтернатива Плюс"); + expect(reply.text).toContain("Расчет сделан по подтвержденным строкам книг продаж и покупок."); expect(reply.text).toContain("- Организация: ООО Альтернатива Плюс."); 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?.balance_confirmed).toBe(true); }); diff --git a/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts index f1e2527..74fb8ba 100644 --- a/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts @@ -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 () => { const valueFlowBundle = { counterparty: "Group SVK", diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts index e09e1f8..0dab922 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts @@ -1546,9 +1546,12 @@ describe("assistant MCP discovery response candidate", () => { ); 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(candidate.reply_text).toContain("Интерпретация:"); expect(candidate.reply_text).toContain("Что видно в ограниченном денежном срезе"); expect(candidate.reply_text).toContain("Что не подтверждено в этом срезе"); expect(candidate.reply_text).toContain("НДС");