diff --git a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js index dbb238c..29b2f24 100644 --- a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js @@ -104,6 +104,9 @@ function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(pilotScope, actionFam if (pilotScope === "counterparty_document_evidence_query_documents_v1") { return "list_documents_by_counterparty"; } + if (pilotScope === "counterparty_movement_evidence_query_movements_v1") { + return "bank_operations_by_counterparty"; + } if (pilotScope === "counterparty_supplier_payout_query_movements_v1") { return "supplier_payouts_profile"; } diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js index aaa5b12..8510814 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js @@ -67,6 +67,9 @@ function isValueFlowPilot(pilot) { function isDocumentPilot(pilot) { return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1"; } +function isMovementPilot(pilot) { + return pilot.pilot_scope === "counterparty_movement_evidence_query_movements_v1"; +} function isMetadataPilot(pilot) { return pilot.pilot_scope === "metadata_inspection_v1"; } @@ -85,6 +88,9 @@ function metadataRouteFamilyLabelRu(routeFamily) { function headlineFor(mode, pilot) { const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" || pilot.derived_value_flow?.aggregation_axis === "month"; + if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") { + return "РџРѕ данным 1РЎ найдены строки движений; ответ ограничен проверенным периодом Рё найденными строками."; + } if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") { if (pilot.derived_metadata_surface.ambiguity_detected) { return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно."; @@ -168,6 +174,10 @@ function buildMustNotClaim(pilot) { claims.push("Do not claim full document history outside the checked period."); claims.push("Do not present the confirmed document rows as a complete document universe."); } + if (isMovementPilot(pilot)) { + claims.push("Do not claim full movement history outside the checked period."); + claims.push("Do not present the confirmed movement rows as a complete movement universe."); + } if (isMetadataPilot(pilot)) { claims.push("Do not present metadata surface as confirmed business data rows."); claims.push("Do not claim a document/register exists outside the checked metadata probe results."); diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js index ca1a7cb..f3a1b3a 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js @@ -116,6 +116,20 @@ function isDocumentEvidencePilotEligible(planner) { return (planner.proposed_primitives.includes("query_documents") && (combined.includes("document") || combined.includes("list_documents"))); } +function isMovementEvidencePilotEligible(planner) { + const meaning = planner.discovery_plan.turn_meaning_ref; + const domain = String(meaning?.asked_domain_family ?? "").toLowerCase(); + const action = String(meaning?.asked_action_family ?? "").toLowerCase(); + const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase(); + const semanticNeed = String(planner.semantic_data_need ?? "").toLowerCase(); + const combined = `${domain} ${action} ${unsupported} ${semanticNeed}`; + return (planner.proposed_primitives.includes("query_movements") && + (combined.includes("movement") || + combined.includes("movements") || + combined.includes("bank_operations") || + combined.includes("movement_evidence") || + combined.includes("list_movements"))); +} function isValueFlowPilotEligible(planner) { const meaning = planner.discovery_plan.turn_meaning_ref; const domain = String(meaning?.asked_domain_family ?? "").toLowerCase(); @@ -397,6 +411,15 @@ function summarizeDocumentRows(result) { } return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched document scope`; } +function summarizeMovementRows(result) { + if (result.error) { + return null; + } + if (result.fetched_rows <= 0) { + return "0 MCP movement rows fetched"; + } + return `${result.fetched_rows} MCP movement rows fetched, ${result.matched_rows} matched movement scope`; +} function summarizeValueFlowRows(result) { if (result.error) { return null; @@ -981,6 +1004,16 @@ function buildDocumentConfirmedFacts(result, counterparty) { : "1C document rows were found for the requested scope" ]; } +function buildMovementConfirmedFacts(result, counterparty) { + if (result.error || result.matched_rows <= 0) { + return []; + } + return [ + counterparty + ? `1C movement rows were found for counterparty ${counterparty}` + : "1C movement rows were found for the requested scope" + ]; +} function buildValueFlowConfirmedFacts(result, counterparty, direction) { if (result.error || result.matched_rows <= 0) { return []; @@ -1025,6 +1058,12 @@ function buildDocumentInferredFacts(result) { } return ["Counterparty document evidence is limited to confirmed 1C document rows in the checked scope"]; } +function buildMovementInferredFacts(result) { + if (result.error || result.fetched_rows <= 0) { + return []; + } + return ["Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"]; +} function buildValueFlowInferredFacts(derived) { if (!derived) { return []; @@ -1067,6 +1106,13 @@ function buildDocumentUnknownFacts(periodScope) { : "Full document history is not proven without an explicit checked period" ]; } +function buildMovementUnknownFacts(periodScope) { + return [ + periodScope + ? "Full movement history outside the checked period is not proven by this MCP discovery pilot" + : "Full movement history is not proven without an explicit checked period" + ]; +} function buildValueFlowUnknownFacts(periodScope, direction, derived) { const unknownFacts = []; if (derived?.coverage_limited_by_probe_limit) { @@ -1106,6 +1152,9 @@ function pilotScopeForPlanner(planner) { if (isMetadataPilotEligible(planner)) { return "metadata_inspection_v1"; } + if (isMovementEvidencePilotEligible(planner)) { + return "counterparty_movement_evidence_query_movements_v1"; + } if (isValueFlowPilotEligible(planner)) { return valueFlowPilotProfile(planner).scope; } @@ -1174,9 +1223,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { } const metadataPilotEligible = isMetadataPilotEligible(planner); const documentPilotEligible = isDocumentEvidencePilotEligible(planner); + const movementPilotEligible = isMovementEvidencePilotEligible(planner); const lifecyclePilotEligible = isLifecyclePilotEligible(planner); const valueFlowPilotEligible = isValueFlowPilotEligible(planner); - if (!metadataPilotEligible && !documentPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) { + if (!metadataPilotEligible && !documentPilotEligible && !movementPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) { pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution"); for (const step of dryRun.execution_steps) { skippedPrimitives.push(step.primitive_id); @@ -1346,6 +1396,86 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { reason_codes: reasonCodes }; } + if (movementPilotEligible) { + let queryResult = null; + const filters = buildValueFlowFilters(planner); + const selection = (0, addressRecipeCatalog_1.selectAddressRecipe)("bank_operations_by_counterparty", filters); + if (!selection.selected_recipe) { + pushReason(reasonCodes, "pilot_movement_recipe_not_available"); + const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Movement-evidence recipe is not available"); + return { + schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, + policy_owner: "assistantMcpDiscoveryPilotExecutor", + pilot_status: "unsupported", + pilot_scope: "counterparty_movement_evidence_query_movements_v1", + dry_run: dryRun, + mcp_execution_performed: false, + executed_primitives: executedPrimitives, + skipped_primitives: skippedPrimitives, + probe_results: probeResults, + evidence, + source_rows_summary: null, + derived_metadata_surface: null, + derived_activity_period: null, + derived_value_flow: null, + derived_bidirectional_value_flow: null, + query_limitations: ["Movement-evidence recipe is not available"], + reason_codes: reasonCodes + }; + } + const recipePlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(selection.selected_recipe, filters); + for (const step of dryRun.execution_steps) { + if (step.primitive_id !== "query_movements") { + skippedPrimitives.push(step.primitive_id); + probeResults.push(skippedProbeResult(step, "pilot_only_executes_query_movements")); + continue; + } + queryResult = await runtimeDeps.executeAddressMcpQuery({ + query: recipePlan.query, + limit: recipePlan.limit, + account_scope: recipePlan.account_scope + }); + executedPrimitives.push(step.primitive_id); + probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult)); + if (queryResult.error) { + pushUnique(queryLimitations, queryResult.error); + pushReason(reasonCodes, "pilot_query_movements_mcp_error"); + } + else { + pushReason(reasonCodes, "pilot_query_movements_mcp_executed"); + } + } + const sourceRowsSummary = queryResult ? summarizeMovementRows(queryResult) : null; + const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({ + plan: planner.discovery_plan, + probeResults, + confirmedFacts: queryResult ? buildMovementConfirmedFacts(queryResult, counterparty) : [], + inferredFacts: queryResult ? buildMovementInferredFacts(queryResult) : [], + unknownFacts: buildMovementUnknownFacts(dateScope), + sourceRowsSummary, + queryLimitations, + recommendedNextProbe: "explain_evidence_basis" + }); + return { + schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, + policy_owner: "assistantMcpDiscoveryPilotExecutor", + pilot_status: "executed", + pilot_scope: "counterparty_movement_evidence_query_movements_v1", + dry_run: dryRun, + mcp_execution_performed: executedPrimitives.length > 0, + executed_primitives: executedPrimitives, + skipped_primitives: skippedPrimitives, + probe_results: probeResults, + evidence, + source_rows_summary: sourceRowsSummary, + derived_metadata_surface: null, + derived_activity_period: null, + derived_value_flow: null, + derived_bidirectional_value_flow: null, + query_limitations: queryLimitations, + reason_codes: reasonCodes + }; + } if (valueFlowPilotEligible) { let queryResult = null; const filters = buildValueFlowFilters(planner); diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js index 3d69371..28ee741 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js @@ -118,6 +118,15 @@ function recipeFor(input) { reason: "planner_selected_metadata_recipe" }; } + if (includesAny(combined, ["movement", "movements", "bank_operations", "movement_evidence", "list_movements"])) { + pushUnique(axes, "coverage_target"); + return { + semanticDataNeed: "movement evidence", + primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"], + axes, + reason: "planner_selected_movement_recipe" + }; + } if (includesAny(combined, ["document", "documents"])) { pushUnique(axes, "coverage_target"); return { diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js index 6cc5b14..903104f 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js @@ -82,6 +82,13 @@ function localizeLine(value) { if (/^1C document rows were found for the requested scope$/i.test(value)) { return "В 1С найдены строки документов по запрошенному контуру."; } + const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i); + if (movementRowsMatch) { + return `Р’ 1РЎ найдены строки движений РїРѕ контрагенту ${movementRowsMatch[1]}.`; + } + if (/^1C movement rows were found for the requested scope$/i.test(value)) { + return "Р’ 1РЎ найдены строки движений РїРѕ запрошенному контуру."; + } const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i); if (supplierPayoutMatch) { return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`; @@ -107,6 +114,9 @@ function localizeLine(value) { if (/^Counterparty document evidence is limited to confirmed 1C document rows in the checked scope$/i.test(value)) { return "Срез документов ограничен только подтвержденными строками документов в проверенном окне."; } + if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) { + return "Срез движений ограничен только подтвержденными строками движений РІ проверенном РѕРєРЅРµ."; + } if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) { return "Сумма рассчитана только по подтвержденным строкам денежных движений в 1С."; } @@ -187,6 +197,12 @@ function localizeLine(value) { if (/^Full document history is not proven without an explicit checked period$/i.test(value)) { return "Полный срез документов без явно проверенного периода не подтвержден."; } + if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { + return "Полный исторический срез движений РІРЅРµ проверенного периода этим РїРѕРёСЃРєРѕРј РЅРµ подтвержден."; + } + if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) { + return "Полный срез движений без СЏРІРЅРѕ проверенного периода РЅРµ подтвержден."; + } if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден."; } diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js index b881a3a..998aae5 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -120,6 +120,13 @@ function mapPilotScopeToFollowupMeaning(pilotScope) { unsupported: "counterparty_lifecycle" }; } + if (pilotScope === "counterparty_movement_evidence_query_movements_v1") { + return { + domain: "movements", + action: "list_movements", + unsupported: "movement_evidence" + }; + } if (pilotScope === "counterparty_supplier_payout_query_movements_v1") { return { domain: "counterparty_value", @@ -176,6 +183,13 @@ function mapAddressIntentToFollowupMeaning(intent) { unsupported: "counterparty_value_or_turnover" }; } + if (intent === "bank_operations_by_counterparty") { + return { + domain: "movements", + action: "list_movements", + unsupported: "movement_evidence" + }; + } return { domain: null, action: null, @@ -245,6 +259,12 @@ function hasMetadataObjectHint(text) { function hasDocumentEvidenceFollowupSignal(text) { return /(?:\u043f\u043e\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u0430\u043c|\u044b)?|\u0434\u0430\u0432\u0430\u0439\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0438\u0449\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|document(?:s)?\s+(?:then|next)?|(?:then|next)\s+documents?|go\s+to\s+documents?)/iu.test(text); } +function hasMovementEvidenceFollowupSignal(text) { + return /(?:\u043f\u043e\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f\u043c|\u0438\u044f)?|\u0434\u0430\u0432\u0430\u0439\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|\u0438\u0449\u0438\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a(?:\u0438\u0435|\u0438\u0439)\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|movement(?:s)?\s+(?:then|next)?|(?:then|next)\s+movements?|go\s+to\s+movements?)/iu.test(text); +} +function hasMetadataDownstreamContinuationSignal(text) { + return /(?:\u0434\u0430\u0432\u0430\u0439\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0438\u0434(?:\u0435|\u0451)\u043c\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u043f\u043e\u0448\u043b(?:\u0438|\u0451\u043c)\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0439|\u0438\u0449\u0438\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0438\u0449\u0438\s+\u0434\u0430\u043d\u043d\u044b\u0435|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u0430\u043d\u043d\u044b\u0435|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0441\u0442\u0440\u043e\u043a\u0438|\u0433\u043b\u0443\u0431\u0436\u0435|\u0447\u0442\u043e\s+\u0434\u0430\u043b\u044c\u0448\u0435|continue|go\s+ahead|go\s+deeper|look\s+deeper|drill\s+down|show\s+(?:data|rows))/iu.test(text); +} function metadataActionFromRawText(text) { if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) { return "inspect_fields"; @@ -289,6 +309,9 @@ function semanticNeedFor(input) { if (input.valueFlowSignal || /(?:turnover|revenue|payment|payout|value|net|netting|balance|cashflow)/iu.test(combined)) { return "counterparty value-flow evidence"; } + if (/(?:movement|movements|bank_operations|movement_evidence|list_movements)/iu.test(combined)) { + return "movement evidence"; + } if (/(?:document|documents|list_documents)/iu.test(combined)) { return "document evidence"; } @@ -351,22 +374,52 @@ function buildAssistantMcpDiscoveryTurnInput(input) { !rawLifecycleSignal && !rawValueFlowSignal && hasDocumentEvidenceFollowupSignal(rawText)); - const effectiveMetadataFollowupSeedApplicable = metadataFollowupSeedApplicable && !metadataGroundedDocumentFollowupApplicable; - const seededDomain = metadataGroundedDocumentFollowupApplicable + const metadataGroundedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" && + followupSeed.metadataRouteFamily === "movement_evidence" && + !followupSeed.metadataAmbiguityDetected && + followupSeed.counterparty && + !rawLifecycleSignal && + !rawValueFlowSignal && + hasMovementEvidenceFollowupSignal(rawText)); + const metadataGroundedLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" && + (followupSeed.metadataRouteFamily === "document_evidence" || + followupSeed.metadataRouteFamily === "movement_evidence") && + !followupSeed.metadataAmbiguityDetected && + followupSeed.counterparty && + !rawLifecycleSignal && + !rawValueFlowSignal && + !rawMetadataSignal && + !hasDocumentEvidenceFollowupSignal(rawText) && + !hasMovementEvidenceFollowupSignal(rawText) && + hasMetadataDownstreamContinuationSignal(rawText)); + const metadataGroundedDocumentLaneApplicable = metadataGroundedDocumentFollowupApplicable || + (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence"); + const metadataGroundedMovementLaneApplicable = metadataGroundedMovementFollowupApplicable || + (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence"); + const effectiveMetadataFollowupSeedApplicable = metadataFollowupSeedApplicable && + !metadataGroundedDocumentLaneApplicable && + !metadataGroundedMovementLaneApplicable; + const seededDomain = metadataGroundedDocumentLaneApplicable ? "documents" - : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable - ? followupSeed.domain - : null; - const seededAction = metadataGroundedDocumentFollowupApplicable + : metadataGroundedMovementLaneApplicable + ? "movements" + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? followupSeed.domain + : null; + const seededAction = metadataGroundedDocumentLaneApplicable ? "list_documents" - : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable - ? followupSeed.action - : null; - const seededUnsupported = metadataGroundedDocumentFollowupApplicable + : metadataGroundedMovementLaneApplicable + ? "list_movements" + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? followupSeed.action + : null; + const seededUnsupported = metadataGroundedDocumentLaneApplicable ? "document_evidence" - : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable - ? followupSeed.unsupported - : null; + : metadataGroundedMovementLaneApplicable + ? "movement_evidence" + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? followupSeed.unsupported + : null; const lifecycleSignal = rawLifecycleSignal || seededDomain === "counterparty_lifecycle"; const bidirectionalValueFlowSignal = !lifecycleSignal && (rawBidirectionalValueFlowSignal || seededAction === "net_value_flow"); @@ -401,11 +454,13 @@ function buildAssistantMcpDiscoveryTurnInput(input) { ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" - : metadataGroundedDocumentFollowupApplicable - ? "documents" - : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable - ? "metadata" - : rawDomain ?? seededDomain, + : metadataGroundedMovementLaneApplicable + ? "movements" + : metadataGroundedDocumentLaneApplicable + ? "documents" + : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable + ? "metadata" + : rawDomain ?? seededDomain, asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal @@ -414,11 +469,13 @@ function buildAssistantMcpDiscoveryTurnInput(input) { : payoutSignal ? "payout" : rawAction ?? seededAction ?? "turnover" - : metadataGroundedDocumentFollowupApplicable - ? "list_documents" - : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable - ? metadataActionFromRawText(rawText) ?? seededAction - : rawAction ?? seededAction, + : metadataGroundedMovementLaneApplicable + ? "list_movements" + : metadataGroundedDocumentLaneApplicable + ? "list_documents" + : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable + ? metadataActionFromRawText(rawText) ?? seededAction + : rawAction ?? seededAction, asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis, explicit_entity_candidates: entityCandidates, explicit_organization_scope: explicitOrganizationScope, @@ -432,18 +489,21 @@ function buildAssistantMcpDiscoveryTurnInput(input) { : payoutSignal ? "counterparty_payouts_or_outflow" : seededUnsupported ?? "counterparty_value_or_turnover" - : metadataGroundedDocumentFollowupApplicable - ? "document_evidence" - : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable - ? "1c_metadata_surface" - : followupDiscoverySeedApplicable - ? seededUnsupported - : null), + : metadataGroundedMovementLaneApplicable + ? "movement_evidence" + : metadataGroundedDocumentLaneApplicable + ? "document_evidence" + : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable + ? "1c_metadata_surface" + : followupDiscoverySeedApplicable + ? seededUnsupported + : null), stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal || - metadataGroundedDocumentFollowupApplicable || + metadataGroundedMovementLaneApplicable || + metadataGroundedDocumentLaneApplicable || rawMetadataSignal || effectiveMetadataFollowupSeedApplicable || followupDiscoverySeedApplicable) @@ -482,24 +542,27 @@ function buildAssistantMcpDiscoveryTurnInput(input) { explicitIntentCandidate, followupDiscoverySeedApplicable: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable || - metadataGroundedDocumentFollowupApplicable + metadataGroundedMovementLaneApplicable || + metadataGroundedDocumentLaneApplicable }); const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0; const sourceSignal = assistantTurnMeaning ? "assistant_turn_meaning" : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable ? "followup_context" - : metadataGroundedDocumentFollowupApplicable + : metadataGroundedMovementLaneApplicable ? "followup_context" - : predecomposeContract - ? "predecompose_contract" - : lifecycleSignal - ? "raw_text" - : valueFlowSignal + : metadataGroundedDocumentLaneApplicable + ? "followup_context" + : predecomposeContract + ? "predecompose_contract" + : lifecycleSignal ? "raw_text" - : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable + : valueFlowSignal ? "raw_text" - : "none"; + : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable + ? "raw_text" + : "none"; if (lifecycleSignal) { pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected"); } @@ -527,6 +590,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) { if (metadataGroundedDocumentFollowupApplicable) { pushReason(reasonCodes, "mcp_discovery_metadata_grounded_document_followup"); } + if (metadataGroundedMovementFollowupApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_grounded_movement_followup"); + } + if (metadataGroundedLaneContinuationApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_grounded_lane_continuation"); + } if (unsupported) { pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn"); } diff --git a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts index 5c3bc99..01971e1 100644 --- a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts @@ -202,6 +202,9 @@ function mapAssistantMcpDiscoveryPilotScopeToAddressIntent( if (pilotScope === "counterparty_document_evidence_query_documents_v1") { return "list_documents_by_counterparty"; } + if (pilotScope === "counterparty_movement_evidence_query_movements_v1") { + return "bank_operations_by_counterparty"; + } if (pilotScope === "counterparty_supplier_payout_query_movements_v1") { return "supplier_payouts_profile"; } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts index 417260b..023407c 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts @@ -101,6 +101,10 @@ function isDocumentPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): bo return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1"; } +function isMovementPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean { + return pilot.pilot_scope === "counterparty_movement_evidence_query_movements_v1"; +} + function isMetadataPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean { return pilot.pilot_scope === "metadata_inspection_v1"; } @@ -124,6 +128,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" || pilot.derived_value_flow?.aggregation_axis === "month"; + if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") { + return "РџРѕ данным 1РЎ найдены строки движений; ответ ограничен проверенным периодом Рё найденными строками."; + } if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") { if (pilot.derived_metadata_surface.ambiguity_detected) { return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно."; @@ -209,6 +216,10 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): claims.push("Do not claim full document history outside the checked period."); claims.push("Do not present the confirmed document rows as a complete document universe."); } + if (isMovementPilot(pilot)) { + claims.push("Do not claim full movement history outside the checked period."); + claims.push("Do not present the confirmed movement rows as a complete movement universe."); + } if (isMetadataPilot(pilot)) { claims.push("Do not present metadata surface as confirmed business data rows."); claims.push("Do not claim a document/register exists outside the checked metadata probe results."); diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts index a834c95..008676f 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts @@ -149,6 +149,7 @@ interface AssistantMcpDiscoveryCoverageAwareQueryExecution { export type AssistantMcpDiscoveryPilotScope = | "metadata_inspection_v1" + | "counterparty_movement_evidence_query_movements_v1" | "counterparty_document_evidence_query_documents_v1" | "counterparty_lifecycle_query_documents_v1" | "counterparty_value_flow_query_movements_v1" @@ -304,6 +305,23 @@ function isDocumentEvidencePilotEligible(planner: AssistantMcpDiscoveryPlannerCo ); } +function isMovementEvidencePilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean { + const meaning = planner.discovery_plan.turn_meaning_ref; + const domain = String(meaning?.asked_domain_family ?? "").toLowerCase(); + const action = String(meaning?.asked_action_family ?? "").toLowerCase(); + const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase(); + const semanticNeed = String(planner.semantic_data_need ?? "").toLowerCase(); + const combined = `${domain} ${action} ${unsupported} ${semanticNeed}`; + return ( + planner.proposed_primitives.includes("query_movements") && + (combined.includes("movement") || + combined.includes("movements") || + combined.includes("bank_operations") || + combined.includes("movement_evidence") || + combined.includes("list_movements")) + ); +} + function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean { const meaning = planner.discovery_plan.turn_meaning_ref; const domain = String(meaning?.asked_domain_family ?? "").toLowerCase(); @@ -664,6 +682,16 @@ function summarizeDocumentRows(result: AddressMcpQueryExecutorResult): string | return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched document scope`; } +function summarizeMovementRows(result: AddressMcpQueryExecutorResult): string | null { + if (result.error) { + return null; + } + if (result.fetched_rows <= 0) { + return "0 MCP movement rows fetched"; + } + return `${result.fetched_rows} MCP movement rows fetched, ${result.matched_rows} matched movement scope`; +} + function summarizeValueFlowRows(result: AssistantMcpDiscoveryCoverageAwareQueryResult): string | null { if (result.error) { return null; @@ -1344,6 +1372,17 @@ function buildDocumentConfirmedFacts(result: AddressMcpQueryExecutorResult, coun ]; } +function buildMovementConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] { + if (result.error || result.matched_rows <= 0) { + return []; + } + return [ + counterparty + ? `1C movement rows were found for counterparty ${counterparty}` + : "1C movement rows were found for the requested scope" + ]; +} + function buildValueFlowConfirmedFacts( result: AssistantMcpDiscoveryCoverageAwareQueryResult, counterparty: string | null, @@ -1398,6 +1437,13 @@ function buildDocumentInferredFacts(result: AddressMcpQueryExecutorResult): stri return ["Counterparty document evidence is limited to confirmed 1C document rows in the checked scope"]; } +function buildMovementInferredFacts(result: AddressMcpQueryExecutorResult): string[] { + if (result.error || result.fetched_rows <= 0) { + return []; + } + return ["Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"]; +} + function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueFlow | null): string[] { if (!derived) { return []; @@ -1449,6 +1495,14 @@ function buildDocumentUnknownFacts(periodScope: string | null): string[] { ]; } +function buildMovementUnknownFacts(periodScope: string | null): string[] { + return [ + periodScope + ? "Full movement history outside the checked period is not proven by this MCP discovery pilot" + : "Full movement history is not proven without an explicit checked period" + ]; +} + function buildValueFlowUnknownFacts( periodScope: string | null, direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"], @@ -1511,6 +1565,9 @@ function pilotScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): As if (isMetadataPilotEligible(planner)) { return "metadata_inspection_v1"; } + if (isMovementEvidencePilotEligible(planner)) { + return "counterparty_movement_evidence_query_movements_v1"; + } if (isValueFlowPilotEligible(planner)) { return valueFlowPilotProfile(planner).scope; } @@ -1586,10 +1643,11 @@ export async function executeAssistantMcpDiscoveryPilot( const metadataPilotEligible = isMetadataPilotEligible(planner); const documentPilotEligible = isDocumentEvidencePilotEligible(planner); + const movementPilotEligible = isMovementEvidencePilotEligible(planner); const lifecyclePilotEligible = isLifecyclePilotEligible(planner); const valueFlowPilotEligible = isValueFlowPilotEligible(planner); - if (!metadataPilotEligible && !documentPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) { + if (!metadataPilotEligible && !documentPilotEligible && !movementPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) { pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution"); for (const step of dryRun.execution_steps) { skippedPrimitives.push(step.primitive_id); @@ -1767,6 +1825,89 @@ export async function executeAssistantMcpDiscoveryPilot( }; } + if (movementPilotEligible) { + let queryResult: AddressMcpQueryExecutorResult | null = null; + const filters = buildValueFlowFilters(planner); + const selection = selectAddressRecipe("bank_operations_by_counterparty", filters); + if (!selection.selected_recipe) { + pushReason(reasonCodes, "pilot_movement_recipe_not_available"); + const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Movement-evidence recipe is not available"); + return { + schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, + policy_owner: "assistantMcpDiscoveryPilotExecutor", + pilot_status: "unsupported", + pilot_scope: "counterparty_movement_evidence_query_movements_v1", + dry_run: dryRun, + mcp_execution_performed: false, + executed_primitives: executedPrimitives, + skipped_primitives: skippedPrimitives, + probe_results: probeResults, + evidence, + source_rows_summary: null, + derived_metadata_surface: null, + derived_activity_period: null, + derived_value_flow: null, + derived_bidirectional_value_flow: null, + query_limitations: ["Movement-evidence recipe is not available"], + reason_codes: reasonCodes + }; + } + + const recipePlan = buildAddressRecipePlan(selection.selected_recipe, filters); + for (const step of dryRun.execution_steps) { + if (step.primitive_id !== "query_movements") { + skippedPrimitives.push(step.primitive_id); + probeResults.push(skippedProbeResult(step, "pilot_only_executes_query_movements")); + continue; + } + queryResult = await runtimeDeps.executeAddressMcpQuery({ + query: recipePlan.query, + limit: recipePlan.limit, + account_scope: recipePlan.account_scope + }); + executedPrimitives.push(step.primitive_id); + probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult)); + if (queryResult.error) { + pushUnique(queryLimitations, queryResult.error); + pushReason(reasonCodes, "pilot_query_movements_mcp_error"); + } else { + pushReason(reasonCodes, "pilot_query_movements_mcp_executed"); + } + } + + const sourceRowsSummary = queryResult ? summarizeMovementRows(queryResult) : null; + const evidence = resolveAssistantMcpDiscoveryEvidence({ + plan: planner.discovery_plan, + probeResults, + confirmedFacts: queryResult ? buildMovementConfirmedFacts(queryResult, counterparty) : [], + inferredFacts: queryResult ? buildMovementInferredFacts(queryResult) : [], + unknownFacts: buildMovementUnknownFacts(dateScope), + sourceRowsSummary, + queryLimitations, + recommendedNextProbe: "explain_evidence_basis" + }); + + return { + schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, + policy_owner: "assistantMcpDiscoveryPilotExecutor", + pilot_status: "executed", + pilot_scope: "counterparty_movement_evidence_query_movements_v1", + dry_run: dryRun, + mcp_execution_performed: executedPrimitives.length > 0, + executed_primitives: executedPrimitives, + skipped_primitives: skippedPrimitives, + probe_results: probeResults, + evidence, + source_rows_summary: sourceRowsSummary, + derived_metadata_surface: null, + derived_activity_period: null, + derived_value_flow: null, + derived_bidirectional_value_flow: null, + query_limitations: queryLimitations, + reason_codes: reasonCodes + }; + } + if (valueFlowPilotEligible) { let queryResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null = null; const filters = buildValueFlowFilters(planner); diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts index f04a616..2c7f850 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts @@ -170,6 +170,16 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe { }; } + if (includesAny(combined, ["movement", "movements", "bank_operations", "movement_evidence", "list_movements"])) { + pushUnique(axes, "coverage_target"); + return { + semanticDataNeed: "movement evidence", + primitives: ["resolve_entity_reference", "query_movements", "probe_coverage"], + axes, + reason: "planner_selected_movement_recipe" + }; + } + if (includesAny(combined, ["document", "documents"])) { pushUnique(axes, "coverage_target"); return { diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts index f99cca8..0411d87 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts @@ -112,6 +112,13 @@ function localizeLine(value: string): string { if (/^1C document rows were found for the requested scope$/i.test(value)) { return "В 1С найдены строки документов по запрошенному контуру."; } + const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i); + if (movementRowsMatch) { + return `Р’ 1РЎ найдены строки движений РїРѕ контрагенту ${movementRowsMatch[1]}.`; + } + if (/^1C movement rows were found for the requested scope$/i.test(value)) { + return "Р’ 1РЎ найдены строки движений РїРѕ запрошенному контуру."; + } const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i); if (supplierPayoutMatch) { return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`; @@ -141,6 +148,9 @@ function localizeLine(value: string): string { if (/^Counterparty document evidence is limited to confirmed 1C document rows in the checked scope$/i.test(value)) { return "Срез документов ограничен только подтвержденными строками документов в проверенном окне."; } + if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) { + return "Срез движений ограничен только подтвержденными строками движений РІ проверенном РѕРєРЅРµ."; + } if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) { return "Сумма рассчитана только по подтвержденным строкам денежных движений в 1С."; } @@ -226,6 +236,12 @@ function localizeLine(value: string): string { if (/^Full document history is not proven without an explicit checked period$/i.test(value)) { return "Полный срез документов без явно проверенного периода не подтвержден."; } + if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { + return "Полный исторический срез движений РІРЅРµ проверенного периода этим РїРѕРёСЃРєРѕРј РЅРµ подтвержден."; + } + if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) { + return "Полный срез движений без СЏРІРЅРѕ проверенного периода РЅРµ подтвержден."; + } if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден."; } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts index a4245e3..250d757 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -169,6 +169,13 @@ function mapPilotScopeToFollowupMeaning( unsupported: "counterparty_lifecycle" }; } + if (pilotScope === "counterparty_movement_evidence_query_movements_v1") { + return { + domain: "movements", + action: "list_movements", + unsupported: "movement_evidence" + }; + } if (pilotScope === "counterparty_supplier_payout_query_movements_v1") { return { domain: "counterparty_value", @@ -232,6 +239,13 @@ function mapAddressIntentToFollowupMeaning( unsupported: "counterparty_value_or_turnover" }; } + if (intent === "bank_operations_by_counterparty") { + return { + domain: "movements", + action: "list_movements", + unsupported: "movement_evidence" + }; + } return { domain: null, action: null, @@ -350,6 +364,18 @@ function hasDocumentEvidenceFollowupSignal(text: string): boolean { ); } +function hasMovementEvidenceFollowupSignal(text: string): boolean { + return /(?:\u043f\u043e\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f\u043c|\u0438\u044f)?|\u0434\u0430\u0432\u0430\u0439\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|\u0438\u0449\u0438\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a(?:\u0438\u0435|\u0438\u0439)\s+\u0434\u0432\u0438\u0436\u0435\u043d(?:\u0438\u044f|\u0438\u0435)|movement(?:s)?\s+(?:then|next)?|(?:then|next)\s+movements?|go\s+to\s+movements?)/iu.test( + text + ); +} + +function hasMetadataDownstreamContinuationSignal(text: string): boolean { + return /(?:\u0434\u0430\u0432\u0430\u0439\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0438\u0434(?:\u0435|\u0451)\u043c\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u043f\u043e\u0448\u043b(?:\u0438|\u0451\u043c)\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0439|\u0438\u0449\u0438\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0438\u0449\u0438\s+\u0434\u0430\u043d\u043d\u044b\u0435|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0434\u0430\u043d\u043d\u044b\u0435|\u043f\u043e\u043a\u0430\u0436\u0438\s+\u0441\u0442\u0440\u043e\u043a\u0438|\u0433\u043b\u0443\u0431\u0436\u0435|\u0447\u0442\u043e\s+\u0434\u0430\u043b\u044c\u0448\u0435|continue|go\s+ahead|go\s+deeper|look\s+deeper|drill\s+down|show\s+(?:data|rows))/iu.test( + text + ); +} + function metadataActionFromRawText(text: string): string { if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) { return "inspect_fields"; @@ -404,6 +430,9 @@ function semanticNeedFor(input: { if (input.valueFlowSignal || /(?:turnover|revenue|payment|payout|value|net|netting|balance|cashflow)/iu.test(combined)) { return "counterparty value-flow evidence"; } + if (/(?:movement|movements|bank_operations|movement_evidence|list_movements)/iu.test(combined)) { + return "movement evidence"; + } if (/(?:document|documents|list_documents)/iu.test(combined)) { return "document evidence"; } @@ -486,20 +515,56 @@ export function buildAssistantMcpDiscoveryTurnInput( !rawValueFlowSignal && hasDocumentEvidenceFollowupSignal(rawText) ); + const metadataGroundedMovementFollowupApplicable = Boolean( + followupSeed.pilotScope === "metadata_inspection_v1" && + followupSeed.metadataRouteFamily === "movement_evidence" && + !followupSeed.metadataAmbiguityDetected && + followupSeed.counterparty && + !rawLifecycleSignal && + !rawValueFlowSignal && + hasMovementEvidenceFollowupSignal(rawText) + ); + const metadataGroundedLaneContinuationApplicable = Boolean( + followupSeed.pilotScope === "metadata_inspection_v1" && + (followupSeed.metadataRouteFamily === "document_evidence" || + followupSeed.metadataRouteFamily === "movement_evidence") && + !followupSeed.metadataAmbiguityDetected && + followupSeed.counterparty && + !rawLifecycleSignal && + !rawValueFlowSignal && + !rawMetadataSignal && + !hasDocumentEvidenceFollowupSignal(rawText) && + !hasMovementEvidenceFollowupSignal(rawText) && + hasMetadataDownstreamContinuationSignal(rawText) + ); + const metadataGroundedDocumentLaneApplicable = + metadataGroundedDocumentFollowupApplicable || + (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence"); + const metadataGroundedMovementLaneApplicable = + metadataGroundedMovementFollowupApplicable || + (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence"); const effectiveMetadataFollowupSeedApplicable = - metadataFollowupSeedApplicable && !metadataGroundedDocumentFollowupApplicable; - const seededDomain = metadataGroundedDocumentFollowupApplicable + metadataFollowupSeedApplicable && + !metadataGroundedDocumentLaneApplicable && + !metadataGroundedMovementLaneApplicable; + const seededDomain = metadataGroundedDocumentLaneApplicable ? "documents" + : metadataGroundedMovementLaneApplicable + ? "movements" : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable ? followupSeed.domain : null; - const seededAction = metadataGroundedDocumentFollowupApplicable + const seededAction = metadataGroundedDocumentLaneApplicable ? "list_documents" + : metadataGroundedMovementLaneApplicable + ? "list_movements" : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable ? followupSeed.action : null; - const seededUnsupported = metadataGroundedDocumentFollowupApplicable + const seededUnsupported = metadataGroundedDocumentLaneApplicable ? "document_evidence" + : metadataGroundedMovementLaneApplicable + ? "movement_evidence" : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable ? followupSeed.unsupported : null; @@ -543,9 +608,11 @@ export function buildAssistantMcpDiscoveryTurnInput( lifecycleSignal ? "counterparty_lifecycle" : valueFlowSignal - ? "counterparty_value" - : metadataGroundedDocumentFollowupApplicable - ? "documents" + ? "counterparty_value" + : metadataGroundedMovementLaneApplicable + ? "movements" + : metadataGroundedDocumentLaneApplicable + ? "documents" : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable ? "metadata" : rawDomain ?? seededDomain, @@ -557,7 +624,9 @@ export function buildAssistantMcpDiscoveryTurnInput( : payoutSignal ? "payout" : rawAction ?? seededAction ?? "turnover" - : metadataGroundedDocumentFollowupApplicable + : metadataGroundedMovementLaneApplicable + ? "list_movements" + : metadataGroundedDocumentLaneApplicable ? "list_documents" : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable ? metadataActionFromRawText(rawText) ?? seededAction @@ -576,7 +645,9 @@ export function buildAssistantMcpDiscoveryTurnInput( : payoutSignal ? "counterparty_payouts_or_outflow" : seededUnsupported ?? "counterparty_value_or_turnover" - : metadataGroundedDocumentFollowupApplicable + : metadataGroundedMovementLaneApplicable + ? "movement_evidence" + : metadataGroundedDocumentLaneApplicable ? "document_evidence" : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable ? "1c_metadata_surface" @@ -588,7 +659,8 @@ export function buildAssistantMcpDiscoveryTurnInput( unsupported || lifecycleSignal || valueFlowSignal || - metadataGroundedDocumentFollowupApplicable || + metadataGroundedMovementLaneApplicable || + metadataGroundedDocumentLaneApplicable || rawMetadataSignal || effectiveMetadataFollowupSeedApplicable || followupDiscoverySeedApplicable @@ -631,14 +703,17 @@ export function buildAssistantMcpDiscoveryTurnInput( followupDiscoverySeedApplicable: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable || - metadataGroundedDocumentFollowupApplicable + metadataGroundedMovementLaneApplicable || + metadataGroundedDocumentLaneApplicable }); const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0; const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning ? "assistant_turn_meaning" : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable ? "followup_context" - : metadataGroundedDocumentFollowupApplicable + : metadataGroundedMovementLaneApplicable + ? "followup_context" + : metadataGroundedDocumentLaneApplicable ? "followup_context" : predecomposeContract ? "predecompose_contract" @@ -677,6 +752,12 @@ export function buildAssistantMcpDiscoveryTurnInput( if (metadataGroundedDocumentFollowupApplicable) { pushReason(reasonCodes, "mcp_discovery_metadata_grounded_document_followup"); } + if (metadataGroundedMovementFollowupApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_grounded_movement_followup"); + } + if (metadataGroundedLaneContinuationApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_grounded_lane_continuation"); + } if (unsupported) { pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn"); } diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts index 6675450..f09881f 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts @@ -116,6 +116,34 @@ describe("assistant MCP discovery answer adapter", () => { expect(draft.must_not_claim).toContain("Do not claim full document history outside the checked period."); }); + it("turns generic movement evidence into a bounded movement answer draft", async () => { + const planner = planAssistantMcpDiscovery({ + turnMeaning: { + asked_domain_family: "movements", + asked_action_family: "list_movements", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020", + unsupported_but_understood_family: "movement_evidence" + } + }); + const pilot = await executeAssistantMcpDiscoveryPilot( + planner, + buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Move1" }]) + ); + + const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); + + expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); + expect(draft.headline).toContain("движений"); + expect(draft.confirmed_lines).toContain("1C movement rows were found for counterparty SVK"); + expect(draft.inference_lines).toContain( + "Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope" + ); + expect(draft.unknown_lines).toContain("Full movement history outside the checked period is not proven by this MCP discovery pilot"); + expect(draft.must_not_claim).toContain("Do not claim full movement history outside the checked period."); + expect(draft.must_not_claim).toContain("Do not present the confirmed movement rows as a complete movement universe."); + }); + it("asks for clarification when discovery did not execute due to missing scope", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts index cb10ec5..07150c2 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts @@ -133,6 +133,44 @@ describe("assistant MCP discovery pilot executor", () => { expect(result.source_rows_summary).toBe("2 MCP document rows fetched, 2 matched document scope"); }); + it("executes generic movement evidence through query_movements without deriving turnover totals", async () => { + const planner = planAssistantMcpDiscovery({ + turnMeaning: { + asked_domain_family: "movements", + asked_action_family: "list_movements", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020", + unsupported_but_understood_family: "movement_evidence" + } + }); + const deps = buildDeps([ + { Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK", Registrar: "Move1" }, + { Period: "2020-03-20T00:00:00", Amount: "900,25", Counterparty: "SVK", Registrar: "Move2" } + ]); + + const result = await executeAssistantMcpDiscoveryPilot(planner, deps); + + expect(result.pilot_status).toBe("executed"); + expect(result.pilot_scope).toBe("counterparty_movement_evidence_query_movements_v1"); + expect(result.executed_primitives).toEqual(["query_movements"]); + expect(result.derived_value_flow).toBeNull(); + expect(result.derived_bidirectional_value_flow).toBeNull(); + expect(result.evidence.confirmed_facts).toContain("1C movement rows were found for counterparty SVK"); + expect(result.evidence.inferred_facts).toContain( + "Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope" + ); + expect(result.evidence.unknown_facts).toContain( + "Full movement history outside the checked period is not proven by this MCP discovery pilot" + ); + expect(result.source_rows_summary).toBe("2 MCP movement rows fetched, 2 matched movement scope"); + + expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1); + const call = deps.executeAddressMcpQuery.mock.calls[0]?.[0]; + expect(String(call?.query ?? "")).toContain("Документ.СписаниеСРасчетногоСчета"); + expect(String(call?.query ?? "")).toContain("Документ.ПоступлениеНаРасчетныйСчет"); + expect(call?.limit).toBeGreaterThan(0); + }); + it("executes inspect_1c_metadata and derives a confirmed metadata surface", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts index 8f1fd73..bb605c5 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts @@ -87,6 +87,25 @@ describe("assistant MCP discovery planner", () => { expect(result.required_axes).toEqual(["counterparty", "coverage_target"]); }); + it("builds a movement discovery plan without aggregating value-flow totals", () => { + const result = planAssistantMcpDiscovery({ + turnMeaning: { + asked_domain_family: "movements", + asked_action_family: "list_movements", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020", + unsupported_but_understood_family: "movement_evidence" + } + }); + + expect(result.planner_status).toBe("ready_for_execution"); + expect(result.semantic_data_need).toBe("movement evidence"); + expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]); + expect(result.proposed_primitives).not.toContain("aggregate_by_axis"); + expect(result.required_axes).toEqual(["counterparty", "period", "coverage_target"]); + expect(result.reason_codes).toContain("planner_selected_movement_recipe"); + }); + it("builds an inference-safe lifecycle plan with evidence explanation", () => { const result = planAssistantMcpDiscovery({ turnMeaning: { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts index ca5e7ec..606088e 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts @@ -236,6 +236,33 @@ describe("assistant MCP discovery response candidate", () => { expect(candidate.reply_text).not.toContain("1C document rows were found"); }); + it("localizes movement evidence without leaking raw English facts", () => { + const candidate = buildAssistantMcpDiscoveryResponseCandidate( + entryPoint({ + bridge: { + bridge_status: "answer_draft_ready", + user_facing_response_allowed: true, + business_fact_answer_allowed: true, + requires_user_clarification: false, + answer_draft: { + answer_mode: "confirmed_with_bounded_inference", + headline: "РџРѕ данным 1РЎ найдены строки движений; ответ ограничен проверенным периодом Рё найденными строками.", + confirmed_lines: ["1C movement rows were found for counterparty SVK"], + inference_lines: ["Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"], + unknown_lines: ["Full movement history outside the checked period is not proven by this MCP discovery pilot"], + limitation_lines: [], + next_step_line: null + } + } + }) + ); + + expect(candidate.reply_text).toContain("Р’ 1РЎ найдены строки движений РїРѕ контрагенту SVK."); + expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений"); + expect(candidate.reply_text).toContain("Полный исторический срез движений РІРЅРµ проверенного периода этим РїРѕРёСЃРєРѕРј РЅРµ подтвержден."); + expect(candidate.reply_text).not.toContain("1C movement rows were found"); + }); + it("localizes metadata evidence without leaking raw MCP wording", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts index cb57329..372b92e 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts @@ -264,6 +264,103 @@ describe("assistant MCP discovery turn input adapter", () => { expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_followup_context"); }); + it("pivots grounded metadata follow-up into movement evidence when the next lane is explicit", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "then movements", + followupContext: { + previous_discovery_pilot_scope: "metadata_inspection_v1", + previous_discovery_metadata_route_family: "movement_evidence", + previous_discovery_metadata_selected_entity_set: "РегистрНакопления", + previous_filters: { + counterparty: "SVK", + period_from: "2020-01-01", + period_to: "2020-12-31" + }, + previous_anchor_type: "counterparty", + previous_anchor_value: "SVK" + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.source_signal).toBe("followup_context"); + expect(result.semantic_data_need).toBe("movement evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "movements", + asked_action_family: "list_movements", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020", + unsupported_but_understood_family: "movement_evidence", + stale_replay_forbidden: true + }); + expect(result.reason_codes).toContain("mcp_discovery_metadata_grounded_movement_followup"); + expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_followup_context"); + }); + + it("continues from grounded metadata into document evidence on a generic downstream follow-up", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "давай дальше", + followupContext: { + previous_discovery_pilot_scope: "metadata_inspection_v1", + previous_discovery_metadata_route_family: "document_evidence", + previous_discovery_metadata_selected_entity_set: "Документ", + previous_filters: { + counterparty: "SVK", + period_from: "2020-01-01", + period_to: "2020-12-31" + }, + previous_anchor_type: "counterparty", + previous_anchor_value: "SVK" + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.source_signal).toBe("followup_context"); + expect(result.semantic_data_need).toBe("document evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "documents", + asked_action_family: "list_documents", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020", + unsupported_but_understood_family: "document_evidence", + stale_replay_forbidden: true + }); + expect(result.reason_codes).toContain("mcp_discovery_metadata_grounded_lane_continuation"); + }); + + it("continues from grounded metadata into movement evidence on a generic downstream follow-up", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "continue with data", + followupContext: { + previous_discovery_pilot_scope: "metadata_inspection_v1", + previous_discovery_metadata_route_family: "movement_evidence", + previous_discovery_metadata_selected_entity_set: "РегистрНакопления", + previous_filters: { + counterparty: "SVK", + period_from: "2020-01-01", + period_to: "2020-12-31" + }, + previous_anchor_type: "counterparty", + previous_anchor_value: "SVK" + } + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.source_signal).toBe("followup_context"); + expect(result.semantic_data_need).toBe("movement evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "movements", + asked_action_family: "list_movements", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020", + unsupported_but_understood_family: "movement_evidence", + stale_replay_forbidden: true + }); + expect(result.reason_codes).toContain("mcp_discovery_metadata_grounded_lane_continuation"); + }); + it("switches the checked year on a short payout follow-up while keeping prior discovery counterparty", () => { const result = buildAssistantMcpDiscoveryTurnInput({ userMessage: "а теперь за 2021?",