From 7ef788fa50bb46b450309199d17b5b66d79234c2 Mon Sep 17 00:00:00 2001 From: dctouch Date: Tue, 21 Apr 2026 23:01:12 +0300 Subject: [PATCH] =?UTF-8?q?ARCH:=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20metadata=20grounding=20=D1=81=20document=20MCP=20lane?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/assistantContinuityPolicy.js | 20 +++ .../assistantMcpDiscoveryAnswerAdapter.js | 10 ++ .../assistantMcpDiscoveryPilotExecutor.js | 127 +++++++++++++++- .../assistantMcpDiscoveryResponseCandidate.js | 16 ++ .../assistantMcpDiscoveryTurnInputAdapter.js | 96 ++++++++---- .../services/assistantTransitionPolicy.js | 6 + .../src/services/assistantContinuityPolicy.ts | 31 ++++ .../assistantMcpDiscoveryAnswerAdapter.ts | 11 ++ .../assistantMcpDiscoveryPilotExecutor.ts | 138 +++++++++++++++++- .../assistantMcpDiscoveryResponseCandidate.ts | 16 ++ .../assistantMcpDiscoveryTurnInputAdapter.ts | 78 ++++++++-- .../src/services/assistantTransitionPolicy.ts | 17 +++ ...assistantMcpDiscoveryAnswerAdapter.test.ts | 27 ++++ ...assistantMcpDiscoveryPilotExecutor.test.ts | 43 +++++- ...stantMcpDiscoveryResponseCandidate.test.ts | 27 ++++ ...assistantMcpDiscoveryRuntimeBridge.test.ts | 9 +- ...istantMcpDiscoveryTurnInputAdapter.test.ts | 33 +++++ .../tests/assistantTransitionPolicy.test.ts | 55 +++++++ 18 files changed, 706 insertions(+), 54 deletions(-) diff --git a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js index 7b4d4a9..dbb238c 100644 --- a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js @@ -1,6 +1,9 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.readAssistantMcpDiscoveryPilotScope = readAssistantMcpDiscoveryPilotScope; +exports.readAssistantMcpDiscoveryMetadataRouteFamily = readAssistantMcpDiscoveryMetadataRouteFamily; +exports.readAssistantMcpDiscoveryMetadataSelectedEntitySet = readAssistantMcpDiscoveryMetadataSelectedEntitySet; +exports.readAssistantMcpDiscoveryMetadataAmbiguityDetected = readAssistantMcpDiscoveryMetadataAmbiguityDetected; exports.formatIsoDateForReply = formatIsoDateForReply; exports.readAddressDebugFilters = readAddressDebugFilters; exports.readAddressDebugItem = readAddressDebugItem; @@ -75,15 +78,32 @@ function readAssistantMcpDiscoveryActionFamily(debug, toNonEmptyString = fallbac function readAssistantMcpDiscoveryBridge(debug) { return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge); } +function readAssistantMcpDiscoveryDerivedMetadataSurface(debug) { + const bridge = readAssistantMcpDiscoveryBridge(debug); + const pilot = toRecordObject(bridge?.pilot); + return toRecordObject(pilot?.derived_metadata_surface); +} function readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString = fallbackToNonEmptyString) { const bridge = readAssistantMcpDiscoveryBridge(debug); const pilot = toRecordObject(bridge?.pilot); return toNonEmptyString(pilot?.pilot_scope); } +function readAssistantMcpDiscoveryMetadataRouteFamily(debug, toNonEmptyString = fallbackToNonEmptyString) { + return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.downstream_route_family); +} +function readAssistantMcpDiscoveryMetadataSelectedEntitySet(debug, toNonEmptyString = fallbackToNonEmptyString) { + return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.selected_entity_set); +} +function readAssistantMcpDiscoveryMetadataAmbiguityDetected(debug) { + return readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_detected === true; +} function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(pilotScope, actionFamily) { if (pilotScope === "counterparty_lifecycle_query_documents_v1") { return "counterparty_activity_lifecycle"; } + if (pilotScope === "counterparty_document_evidence_query_documents_v1") { + return "list_documents_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 f779584..aaa5b12 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js @@ -64,6 +64,9 @@ function isValueFlowPilot(pilot) { pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1" || pilot.pilot_scope === "counterparty_bidirectional_value_flow_query_movements_v1"); } +function isDocumentPilot(pilot) { + return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1"; +} function isMetadataPilot(pilot) { return pilot.pilot_scope === "metadata_inspection_v1"; } @@ -109,6 +112,9 @@ function headlineFor(mode, pilot) { } return "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк."; } + if (isDocumentPilot(pilot) && mode === "confirmed_with_bounded_inference") { + return "По данным 1С найдены строки документов; ответ ограничен проверенным периодом и найденными строками."; + } if (mode === "confirmed_with_bounded_inference") { return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк."; } @@ -158,6 +164,10 @@ function buildMustNotClaim(pilot) { claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it."); claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows."); } + if (isDocumentPilot(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 (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 a7b233b..ca1a7cb 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js @@ -107,6 +107,15 @@ function isLifecyclePilotEligible(planner) { return (planner.proposed_primitives.includes("query_documents") && (combined.includes("lifecycle") || combined.includes("activity") || combined.includes("duration") || combined.includes("age"))); } +function isDocumentEvidencePilotEligible(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 combined = `${domain} ${action} ${unsupported}`; + return (planner.proposed_primitives.includes("query_documents") && + (combined.includes("document") || combined.includes("list_documents"))); +} function isValueFlowPilotEligible(planner) { const meaning = planner.discovery_plan.turn_meaning_ref; const domain = String(meaning?.asked_domain_family ?? "").toLowerCase(); @@ -379,6 +388,15 @@ function summarizeLifecycleRows(result) { } return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`; } +function summarizeDocumentRows(result) { + if (result.error) { + return null; + } + if (result.fetched_rows <= 0) { + return "0 MCP document rows fetched"; + } + return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched document scope`; +} function summarizeValueFlowRows(result) { if (result.error) { return null; @@ -953,6 +971,16 @@ function buildLifecycleConfirmedFacts(result, counterparty) { : "1C activity rows were found for the requested counterparty scope" ]; } +function buildDocumentConfirmedFacts(result, counterparty) { + if (result.error || result.matched_rows <= 0) { + return []; + } + return [ + counterparty + ? `1C document rows were found for counterparty ${counterparty}` + : "1C document rows were found for the requested scope" + ]; +} function buildValueFlowConfirmedFacts(result, counterparty, direction) { if (result.error || result.matched_rows <= 0) { return []; @@ -991,6 +1019,12 @@ function buildLifecycleInferredFacts(result) { } return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"]; } +function buildDocumentInferredFacts(result) { + if (result.error || result.fetched_rows <= 0) { + return []; + } + return ["Counterparty document evidence is limited to confirmed 1C document rows in the checked scope"]; +} function buildValueFlowInferredFacts(derived) { if (!derived) { return []; @@ -1026,6 +1060,13 @@ function buildBidirectionalValueFlowInferredFacts(derived) { function buildLifecycleUnknownFacts() { return ["Legal registration date is not proven by this MCP discovery pilot"]; } +function buildDocumentUnknownFacts(periodScope) { + return [ + periodScope + ? "Full document history outside the checked period is not proven by this MCP discovery pilot" + : "Full document history is not proven without an explicit checked period" + ]; +} function buildValueFlowUnknownFacts(periodScope, direction, derived) { const unknownFacts = []; if (derived?.coverage_limited_by_probe_limit) { @@ -1068,6 +1109,9 @@ function pilotScopeForPlanner(planner) { if (isValueFlowPilotEligible(planner)) { return valueFlowPilotProfile(planner).scope; } + if (isDocumentEvidencePilotEligible(planner)) { + return "counterparty_document_evidence_query_documents_v1"; + } return "counterparty_lifecycle_query_documents_v1"; } async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { @@ -1129,9 +1173,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { }; } const metadataPilotEligible = isMetadataPilotEligible(planner); + const documentPilotEligible = isDocumentEvidencePilotEligible(planner); const lifecyclePilotEligible = isLifecyclePilotEligible(planner); const valueFlowPilotEligible = isValueFlowPilotEligible(planner); - if (!metadataPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) { + if (!metadataPilotEligible && !documentPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) { pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution"); for (const step of dryRun.execution_steps) { skippedPrimitives.push(step.primitive_id); @@ -1221,6 +1266,86 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { reason_codes: reasonCodes }; } + if (documentPilotEligible) { + let queryResult = null; + const filters = buildLifecycleFilters(planner); + const selection = (0, addressRecipeCatalog_1.selectAddressRecipe)("list_documents_by_counterparty", filters); + if (!selection.selected_recipe) { + pushReason(reasonCodes, "pilot_document_recipe_not_available"); + const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Document-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_document_evidence_query_documents_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: ["Document-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_documents") { + skippedPrimitives.push(step.primitive_id); + probeResults.push(skippedProbeResult(step, "pilot_only_executes_query_documents")); + 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_documents_mcp_error"); + } + else { + pushReason(reasonCodes, "pilot_query_documents_mcp_executed"); + } + } + const sourceRowsSummary = queryResult ? summarizeDocumentRows(queryResult) : null; + const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({ + plan: planner.discovery_plan, + probeResults, + confirmedFacts: queryResult ? buildDocumentConfirmedFacts(queryResult, counterparty) : [], + inferredFacts: queryResult ? buildDocumentInferredFacts(queryResult) : [], + unknownFacts: buildDocumentUnknownFacts(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_document_evidence_query_documents_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/assistantMcpDiscoveryResponseCandidate.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js index f2f3a53..6cc5b14 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js @@ -75,6 +75,13 @@ function localizeLine(value) { if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) { return "В 1С найдены строки денежных движений по запрошенному контрагентскому контуру."; } + const documentRowsMatch = value.match(/^1C document rows were found for counterparty\s+(.+)$/i); + if (documentRowsMatch) { + return `В 1С найдены строки документов по контрагенту ${documentRowsMatch[1]}.`; + } + if (/^1C document 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]}.`; @@ -97,6 +104,9 @@ function localizeLine(value) { if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) { return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С."; } + if (/^Counterparty document evidence is limited to confirmed 1C document 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С."; } @@ -171,6 +181,12 @@ function localizeLine(value) { if (/^Full all-time turnover is not proven without an explicit checked period$/i.test(value)) { return "Полный оборот за все время без явно проверенного периода не подтвержден."; } + if (/^Full document history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { + return "Полный исторический срез документов вне проверенного периода этим поиском не подтвержден."; + } + if (/^Full document 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 5585908..b881a3a 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -211,7 +211,10 @@ function collectFollowupDiscoverySeed(followupContext) { counterparty, discoveryEntity: discoveryEntities[0] ?? null, organization, - dateScope + dateScope, + metadataRouteFamily: toNonEmptyString(followupContext?.previous_discovery_metadata_route_family), + metadataSelectedEntitySet: toNonEmptyString(followupContext?.previous_discovery_metadata_selected_entity_set), + metadataAmbiguityDetected: followupContext?.previous_discovery_metadata_ambiguity_detected === true }; } function hasLifecycleSignal(text) { @@ -239,6 +242,9 @@ function hasMetadataSignal(text) { function hasMetadataObjectHint(text) { return /(?:\u0440\u0435\u0433\u0438\u0441\u0442\u0440(?:\u044b)?|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442(?:\u044b)?|\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a(?:\u0438)?|\u043f\u043e\u043b(?:\u0435|\u044f)|registers?|documents?|catalogs?|fields?)/iu.test(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 metadataActionFromRawText(text) { if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) { return "inspect_fields"; @@ -338,9 +344,29 @@ function buildAssistantMcpDiscoveryTurnInput(input) { !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataObjectHint(rawText)); - const seededDomain = followupDiscoverySeedApplicable || metadataFollowupSeedApplicable ? followupSeed.domain : null; - const seededAction = followupDiscoverySeedApplicable || metadataFollowupSeedApplicable ? followupSeed.action : null; - const seededUnsupported = followupDiscoverySeedApplicable || metadataFollowupSeedApplicable ? followupSeed.unsupported : null; + const metadataGroundedDocumentFollowupApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" && + followupSeed.metadataRouteFamily === "document_evidence" && + !followupSeed.metadataAmbiguityDetected && + followupSeed.counterparty && + !rawLifecycleSignal && + !rawValueFlowSignal && + hasDocumentEvidenceFollowupSignal(rawText)); + const effectiveMetadataFollowupSeedApplicable = metadataFollowupSeedApplicable && !metadataGroundedDocumentFollowupApplicable; + const seededDomain = metadataGroundedDocumentFollowupApplicable + ? "documents" + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? followupSeed.domain + : null; + const seededAction = metadataGroundedDocumentFollowupApplicable + ? "list_documents" + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? followupSeed.action + : null; + const seededUnsupported = metadataGroundedDocumentFollowupApplicable + ? "document_evidence" + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? followupSeed.unsupported + : null; const lifecycleSignal = rawLifecycleSignal || seededDomain === "counterparty_lifecycle"; const bidirectionalValueFlowSignal = !lifecycleSignal && (rawBidirectionalValueFlowSignal || seededAction === "net_value_flow"); @@ -354,7 +380,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) { unsupported: unsupported ?? seededUnsupported, lifecycleSignal, valueFlowSignal, - metadataSignal: rawMetadataSignal || metadataFollowupSeedApplicable + metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable }); const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates); pushUnique(entityCandidates, predecomposeEntities.counterparty); @@ -375,9 +401,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) { ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" - : rawMetadataSignal || metadataFollowupSeedApplicable - ? "metadata" - : rawDomain ?? seededDomain, + : metadataGroundedDocumentFollowupApplicable + ? "documents" + : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable + ? "metadata" + : rawDomain ?? seededDomain, asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal @@ -386,9 +414,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) { : payoutSignal ? "payout" : rawAction ?? seededAction ?? "turnover" - : rawMetadataSignal || metadataFollowupSeedApplicable - ? metadataActionFromRawText(rawText) ?? seededAction - : rawAction ?? seededAction, + : metadataGroundedDocumentFollowupApplicable + ? "list_documents" + : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable + ? metadataActionFromRawText(rawText) ?? seededAction + : rawAction ?? seededAction, asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis, explicit_entity_candidates: entityCandidates, explicit_organization_scope: explicitOrganizationScope, @@ -402,17 +432,20 @@ function buildAssistantMcpDiscoveryTurnInput(input) { : payoutSignal ? "counterparty_payouts_or_outflow" : seededUnsupported ?? "counterparty_value_or_turnover" - : rawMetadataSignal || metadataFollowupSeedApplicable - ? "1c_metadata_surface" - : followupDiscoverySeedApplicable - ? seededUnsupported - : null), + : metadataGroundedDocumentFollowupApplicable + ? "document_evidence" + : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable + ? "1c_metadata_surface" + : followupDiscoverySeedApplicable + ? seededUnsupported + : null), stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal || + metadataGroundedDocumentFollowupApplicable || rawMetadataSignal || - metadataFollowupSeedApplicable || + effectiveMetadataFollowupSeedApplicable || followupDiscoverySeedApplicable) }; const cleanTurnMeaning = {}; @@ -444,25 +477,29 @@ function buildAssistantMcpDiscoveryTurnInput(input) { unsupported: unsupported ?? seededUnsupported, lifecycleSignal, valueFlowSignal, - metadataSignal: rawMetadataSignal || metadataFollowupSeedApplicable, + metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable, semanticDataNeed, explicitIntentCandidate, - followupDiscoverySeedApplicable: followupDiscoverySeedApplicable || metadataFollowupSeedApplicable + followupDiscoverySeedApplicable: followupDiscoverySeedApplicable || + effectiveMetadataFollowupSeedApplicable || + metadataGroundedDocumentFollowupApplicable }); const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0; const sourceSignal = assistantTurnMeaning ? "assistant_turn_meaning" - : followupDiscoverySeedApplicable || metadataFollowupSeedApplicable + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable ? "followup_context" - : predecomposeContract - ? "predecompose_contract" - : lifecycleSignal - ? "raw_text" - : valueFlowSignal + : metadataGroundedDocumentFollowupApplicable + ? "followup_context" + : predecomposeContract + ? "predecompose_contract" + : lifecycleSignal ? "raw_text" - : rawMetadataSignal || metadataFollowupSeedApplicable + : valueFlowSignal ? "raw_text" - : "none"; + : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable + ? "raw_text" + : "none"; if (lifecycleSignal) { pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected"); } @@ -484,9 +521,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) { if (followupDiscoverySeedApplicable) { pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context"); } - if (metadataFollowupSeedApplicable) { + if (effectiveMetadataFollowupSeedApplicable) { pushReason(reasonCodes, "mcp_discovery_metadata_seeded_from_followup_context"); } + if (metadataGroundedDocumentFollowupApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_grounded_document_followup"); + } if (unsupported) { pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn"); } diff --git a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js index 3827532..db0b698 100644 --- a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js @@ -463,6 +463,9 @@ function createAssistantTransitionPolicy(deps) { } const sourceIntent = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryPilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(carryoverSourceDebug, deps.toNonEmptyString); + const sourceDiscoveryMetadataRouteFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataRouteFamily)(carryoverSourceDebug, deps.toNonEmptyString); + const sourceDiscoveryMetadataSelectedEntitySet = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataSelectedEntitySet)(carryoverSourceDebug, deps.toNonEmptyString); + const sourceDiscoveryMetadataAmbiguityDetected = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityDetected)(carryoverSourceDebug); const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); const llmSelectedObjectScopeDetected = llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true; const resolvedPrimaryIntent = deps.resolveAddressIntent(deps.repairAddressMojibake(String(userMessage ?? ""))).intent; @@ -693,6 +696,9 @@ function createAssistantTransitionPolicy(deps) { previous_anchor_type: previousAnchorType ?? undefined, previous_anchor_value: previousAnchor, previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined, + previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined, + previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? undefined, + previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined, resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined, root_context_only: rootScopedPivot || undefined, root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined, diff --git a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts index fe3daea..5c3bc99 100644 --- a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts @@ -155,6 +155,14 @@ function readAssistantMcpDiscoveryBridge( return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge); } +function readAssistantMcpDiscoveryDerivedMetadataSurface( + debug: Record | null +): Record | null { + const bridge = readAssistantMcpDiscoveryBridge(debug); + const pilot = toRecordObject(bridge?.pilot); + return toRecordObject(pilot?.derived_metadata_surface); +} + export function readAssistantMcpDiscoveryPilotScope( debug: Record | null, toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString @@ -164,6 +172,26 @@ export function readAssistantMcpDiscoveryPilotScope( return toNonEmptyString(pilot?.pilot_scope); } +export function readAssistantMcpDiscoveryMetadataRouteFamily( + debug: Record | null, + toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString +): string | null { + return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.downstream_route_family); +} + +export function readAssistantMcpDiscoveryMetadataSelectedEntitySet( + debug: Record | null, + toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString +): string | null { + return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.selected_entity_set); +} + +export function readAssistantMcpDiscoveryMetadataAmbiguityDetected( + debug: Record | null +): boolean { + return readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_detected === true; +} + function mapAssistantMcpDiscoveryPilotScopeToAddressIntent( pilotScope: string | null, actionFamily: string | null @@ -171,6 +199,9 @@ function mapAssistantMcpDiscoveryPilotScopeToAddressIntent( if (pilotScope === "counterparty_lifecycle_query_documents_v1") { return "counterparty_activity_lifecycle"; } + if (pilotScope === "counterparty_document_evidence_query_documents_v1") { + return "list_documents_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 b822c2a..417260b 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts @@ -97,6 +97,10 @@ function isValueFlowPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): b ); } +function isDocumentPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean { + return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1"; +} + function isMetadataPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean { return pilot.pilot_scope === "metadata_inspection_v1"; } @@ -147,6 +151,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD } return "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк."; } + if (isDocumentPilot(pilot) && mode === "confirmed_with_bounded_inference") { + return "По данным 1С найдены строки документов; ответ ограничен проверенным периодом и найденными строками."; + } if (mode === "confirmed_with_bounded_inference") { return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк."; } @@ -198,6 +205,10 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it."); claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows."); } + if (isDocumentPilot(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 (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 c4d6618..a834c95 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_document_evidence_query_documents_v1" | "counterparty_lifecycle_query_documents_v1" | "counterparty_value_flow_query_movements_v1" | "counterparty_supplier_payout_query_movements_v1" @@ -291,6 +292,18 @@ function isLifecyclePilotEligible(planner: AssistantMcpDiscoveryPlannerContract) ); } +function isDocumentEvidencePilotEligible(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 combined = `${domain} ${action} ${unsupported}`; + return ( + planner.proposed_primitives.includes("query_documents") && + (combined.includes("document") || combined.includes("list_documents")) + ); +} + function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean { const meaning = planner.discovery_plan.turn_meaning_ref; const domain = String(meaning?.asked_domain_family ?? "").toLowerCase(); @@ -641,6 +654,16 @@ function summarizeLifecycleRows(result: AddressMcpQueryExecutorResult): string | return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched lifecycle scope`; } +function summarizeDocumentRows(result: AddressMcpQueryExecutorResult): string | null { + if (result.error) { + return null; + } + if (result.fetched_rows <= 0) { + return "0 MCP document rows fetched"; + } + return `${result.fetched_rows} MCP document rows fetched, ${result.matched_rows} matched document scope`; +} + function summarizeValueFlowRows(result: AssistantMcpDiscoveryCoverageAwareQueryResult): string | null { if (result.error) { return null; @@ -1310,6 +1333,17 @@ function buildLifecycleConfirmedFacts(result: AddressMcpQueryExecutorResult, cou ]; } +function buildDocumentConfirmedFacts(result: AddressMcpQueryExecutorResult, counterparty: string | null): string[] { + if (result.error || result.matched_rows <= 0) { + return []; + } + return [ + counterparty + ? `1C document rows were found for counterparty ${counterparty}` + : "1C document rows were found for the requested scope" + ]; +} + function buildValueFlowConfirmedFacts( result: AssistantMcpDiscoveryCoverageAwareQueryResult, counterparty: string | null, @@ -1357,6 +1391,13 @@ function buildLifecycleInferredFacts(result: AddressMcpQueryExecutorResult): str return ["Business activity duration may be inferred from first and latest confirmed 1C activity rows"]; } +function buildDocumentInferredFacts(result: AddressMcpQueryExecutorResult): string[] { + if (result.error || result.fetched_rows <= 0) { + return []; + } + return ["Counterparty document evidence is limited to confirmed 1C document rows in the checked scope"]; +} + function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueFlow | null): string[] { if (!derived) { return []; @@ -1400,6 +1441,14 @@ function buildLifecycleUnknownFacts(): string[] { return ["Legal registration date is not proven by this MCP discovery pilot"]; } +function buildDocumentUnknownFacts(periodScope: string | null): string[] { + return [ + periodScope + ? "Full document history outside the checked period is not proven by this MCP discovery pilot" + : "Full document history is not proven without an explicit checked period" + ]; +} + function buildValueFlowUnknownFacts( periodScope: string | null, direction: AssistantMcpDiscoveryDerivedValueFlow["value_flow_direction"], @@ -1465,6 +1514,9 @@ function pilotScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): As if (isValueFlowPilotEligible(planner)) { return valueFlowPilotProfile(planner).scope; } + if (isDocumentEvidencePilotEligible(planner)) { + return "counterparty_document_evidence_query_documents_v1"; + } return "counterparty_lifecycle_query_documents_v1"; } @@ -1533,10 +1585,11 @@ export async function executeAssistantMcpDiscoveryPilot( } const metadataPilotEligible = isMetadataPilotEligible(planner); + const documentPilotEligible = isDocumentEvidencePilotEligible(planner); const lifecyclePilotEligible = isLifecyclePilotEligible(planner); const valueFlowPilotEligible = isValueFlowPilotEligible(planner); - if (!metadataPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) { + if (!metadataPilotEligible && !documentPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) { pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution"); for (const step of dryRun.execution_steps) { skippedPrimitives.push(step.primitive_id); @@ -1631,6 +1684,89 @@ export async function executeAssistantMcpDiscoveryPilot( }; } + if (documentPilotEligible) { + let queryResult: AddressMcpQueryExecutorResult | null = null; + const filters = buildLifecycleFilters(planner); + const selection = selectAddressRecipe("list_documents_by_counterparty", filters); + if (!selection.selected_recipe) { + pushReason(reasonCodes, "pilot_document_recipe_not_available"); + const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "Document-evidence recipe is not available"); + return { + schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, + policy_owner: "assistantMcpDiscoveryPilotExecutor", + pilot_status: "unsupported", + pilot_scope: "counterparty_document_evidence_query_documents_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: ["Document-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_documents") { + skippedPrimitives.push(step.primitive_id); + probeResults.push(skippedProbeResult(step, "pilot_only_executes_query_documents")); + 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_documents_mcp_error"); + } else { + pushReason(reasonCodes, "pilot_query_documents_mcp_executed"); + } + } + + const sourceRowsSummary = queryResult ? summarizeDocumentRows(queryResult) : null; + const evidence = resolveAssistantMcpDiscoveryEvidence({ + plan: planner.discovery_plan, + probeResults, + confirmedFacts: queryResult ? buildDocumentConfirmedFacts(queryResult, counterparty) : [], + inferredFacts: queryResult ? buildDocumentInferredFacts(queryResult) : [], + unknownFacts: buildDocumentUnknownFacts(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_document_evidence_query_documents_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/assistantMcpDiscoveryResponseCandidate.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts index 51f4bb1..f99cca8 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts @@ -105,6 +105,13 @@ function localizeLine(value: string): string { if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) { return "В 1С найдены строки денежных движений по запрошенному контрагентскому контуру."; } + const documentRowsMatch = value.match(/^1C document rows were found for counterparty\s+(.+)$/i); + if (documentRowsMatch) { + return `В 1С найдены строки документов по контрагенту ${documentRowsMatch[1]}.`; + } + if (/^1C document 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]}.`; @@ -131,6 +138,9 @@ function localizeLine(value: string): string { if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) { return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С."; } + if (/^Counterparty document evidence is limited to confirmed 1C document 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С."; } @@ -210,6 +220,12 @@ function localizeLine(value: string): string { if (/^Full all-time turnover is not proven without an explicit checked period$/i.test(value)) { return "Полный оборот за все время без явно проверенного периода не подтвержден."; } + if (/^Full document history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { + return "Полный исторический срез документов вне проверенного периода этим поиском не подтвержден."; + } + if (/^Full document 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 c8f47a4..a4245e3 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -248,6 +248,9 @@ function collectFollowupDiscoverySeed(followupContext: Record | discoveryEntity: string | null; organization: string | null; dateScope: string | null; + metadataRouteFamily: string | null; + metadataSelectedEntitySet: string | null; + metadataAmbiguityDetected: boolean; } { const previousFilters = toRecordObject(followupContext?.previous_filters); const rootFilters = toRecordObject(followupContext?.root_filters); @@ -282,7 +285,10 @@ function collectFollowupDiscoverySeed(followupContext: Record | counterparty, discoveryEntity: discoveryEntities[0] ?? null, organization, - dateScope + dateScope, + metadataRouteFamily: toNonEmptyString(followupContext?.previous_discovery_metadata_route_family), + metadataSelectedEntitySet: toNonEmptyString(followupContext?.previous_discovery_metadata_selected_entity_set), + metadataAmbiguityDetected: followupContext?.previous_discovery_metadata_ambiguity_detected === true }; } @@ -338,6 +344,12 @@ function hasMetadataObjectHint(text: string): boolean { ); } +function hasDocumentEvidenceFollowupSignal(text: string): boolean { + 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 metadataActionFromRawText(text: string): string { if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) { return "inspect_fields"; @@ -465,9 +477,32 @@ export function buildAssistantMcpDiscoveryTurnInput( !rawValueFlowSignal && hasMetadataObjectHint(rawText) ); - const seededDomain = followupDiscoverySeedApplicable || metadataFollowupSeedApplicable ? followupSeed.domain : null; - const seededAction = followupDiscoverySeedApplicable || metadataFollowupSeedApplicable ? followupSeed.action : null; - const seededUnsupported = followupDiscoverySeedApplicable || metadataFollowupSeedApplicable ? followupSeed.unsupported : null; + const metadataGroundedDocumentFollowupApplicable = Boolean( + followupSeed.pilotScope === "metadata_inspection_v1" && + followupSeed.metadataRouteFamily === "document_evidence" && + !followupSeed.metadataAmbiguityDetected && + followupSeed.counterparty && + !rawLifecycleSignal && + !rawValueFlowSignal && + hasDocumentEvidenceFollowupSignal(rawText) + ); + const effectiveMetadataFollowupSeedApplicable = + metadataFollowupSeedApplicable && !metadataGroundedDocumentFollowupApplicable; + const seededDomain = metadataGroundedDocumentFollowupApplicable + ? "documents" + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? followupSeed.domain + : null; + const seededAction = metadataGroundedDocumentFollowupApplicable + ? "list_documents" + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? followupSeed.action + : null; + const seededUnsupported = metadataGroundedDocumentFollowupApplicable + ? "document_evidence" + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? followupSeed.unsupported + : null; const lifecycleSignal = rawLifecycleSignal || seededDomain === "counterparty_lifecycle"; const bidirectionalValueFlowSignal = @@ -485,7 +520,7 @@ export function buildAssistantMcpDiscoveryTurnInput( unsupported: unsupported ?? seededUnsupported, lifecycleSignal, valueFlowSignal, - metadataSignal: rawMetadataSignal || metadataFollowupSeedApplicable + metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable }); const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates); pushUnique(entityCandidates, predecomposeEntities.counterparty); @@ -509,7 +544,9 @@ export function buildAssistantMcpDiscoveryTurnInput( ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" - : rawMetadataSignal || metadataFollowupSeedApplicable + : metadataGroundedDocumentFollowupApplicable + ? "documents" + : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable ? "metadata" : rawDomain ?? seededDomain, asked_action_family: lifecycleSignal @@ -520,7 +557,9 @@ export function buildAssistantMcpDiscoveryTurnInput( : payoutSignal ? "payout" : rawAction ?? seededAction ?? "turnover" - : rawMetadataSignal || metadataFollowupSeedApplicable + : metadataGroundedDocumentFollowupApplicable + ? "list_documents" + : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable ? metadataActionFromRawText(rawText) ?? seededAction : rawAction ?? seededAction, asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis, @@ -537,7 +576,9 @@ export function buildAssistantMcpDiscoveryTurnInput( : payoutSignal ? "counterparty_payouts_or_outflow" : seededUnsupported ?? "counterparty_value_or_turnover" - : rawMetadataSignal || metadataFollowupSeedApplicable + : metadataGroundedDocumentFollowupApplicable + ? "document_evidence" + : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable ? "1c_metadata_surface" : followupDiscoverySeedApplicable ? seededUnsupported @@ -547,8 +588,9 @@ export function buildAssistantMcpDiscoveryTurnInput( unsupported || lifecycleSignal || valueFlowSignal || + metadataGroundedDocumentFollowupApplicable || rawMetadataSignal || - metadataFollowupSeedApplicable || + effectiveMetadataFollowupSeedApplicable || followupDiscoverySeedApplicable ) }; @@ -583,15 +625,20 @@ export function buildAssistantMcpDiscoveryTurnInput( unsupported: unsupported ?? seededUnsupported, lifecycleSignal, valueFlowSignal, - metadataSignal: rawMetadataSignal || metadataFollowupSeedApplicable, + metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable, semanticDataNeed, explicitIntentCandidate, - followupDiscoverySeedApplicable: followupDiscoverySeedApplicable || metadataFollowupSeedApplicable + followupDiscoverySeedApplicable: + followupDiscoverySeedApplicable || + effectiveMetadataFollowupSeedApplicable || + metadataGroundedDocumentFollowupApplicable }); const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0; const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning ? "assistant_turn_meaning" - : followupDiscoverySeedApplicable || metadataFollowupSeedApplicable + : followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable + ? "followup_context" + : metadataGroundedDocumentFollowupApplicable ? "followup_context" : predecomposeContract ? "predecompose_contract" @@ -599,7 +646,7 @@ export function buildAssistantMcpDiscoveryTurnInput( ? "raw_text" : valueFlowSignal ? "raw_text" - : rawMetadataSignal || metadataFollowupSeedApplicable + : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable ? "raw_text" : "none"; @@ -624,9 +671,12 @@ export function buildAssistantMcpDiscoveryTurnInput( if (followupDiscoverySeedApplicable) { pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context"); } - if (metadataFollowupSeedApplicable) { + if (effectiveMetadataFollowupSeedApplicable) { pushReason(reasonCodes, "mcp_discovery_metadata_seeded_from_followup_context"); } + if (metadataGroundedDocumentFollowupApplicable) { + pushReason(reasonCodes, "mcp_discovery_metadata_grounded_document_followup"); + } if (unsupported) { pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn"); } diff --git a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts index 3011aac..3d3c03b 100644 --- a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts @@ -10,6 +10,9 @@ import { readAddressDebugIntent, readAddressDebugFilters, readAddressDebugItem, + readAssistantMcpDiscoveryMetadataAmbiguityDetected, + readAssistantMcpDiscoveryMetadataRouteFamily, + readAssistantMcpDiscoveryMetadataSelectedEntitySet, readAddressDebugTemporalScope, readAssistantMcpDiscoveryPilotScope, resolveOrganizationClarificationContinuation, @@ -607,6 +610,17 @@ export function createAssistantTransitionPolicy(deps) { } const sourceIntent = readAddressDebugIntent(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryPilotScope = readAssistantMcpDiscoveryPilotScope(carryoverSourceDebug, deps.toNonEmptyString); + const sourceDiscoveryMetadataRouteFamily = readAssistantMcpDiscoveryMetadataRouteFamily( + carryoverSourceDebug, + deps.toNonEmptyString + ); + const sourceDiscoveryMetadataSelectedEntitySet = readAssistantMcpDiscoveryMetadataSelectedEntitySet( + carryoverSourceDebug, + deps.toNonEmptyString + ); + const sourceDiscoveryMetadataAmbiguityDetected = readAssistantMcpDiscoveryMetadataAmbiguityDetected( + carryoverSourceDebug + ); const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); const llmSelectedObjectScopeDetected = llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true; @@ -939,6 +953,9 @@ export function createAssistantTransitionPolicy(deps) { previous_anchor_type: previousAnchorType ?? undefined, previous_anchor_value: previousAnchor, previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined, + previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined, + previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? undefined, + previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined, resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined, root_context_only: rootScopedPivot || undefined, root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined, diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts index 5611c01..6675450 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts @@ -89,6 +89,33 @@ describe("assistant MCP discovery answer adapter", () => { expect(draft.must_not_claim).toContain("Do not claim a confirmed business fact when confirmed_facts is empty."); }); + it("turns generic document evidence into a bounded document answer draft", async () => { + const planner = planAssistantMcpDiscovery({ + turnMeaning: { + asked_domain_family: "documents", + asked_action_family: "list_documents", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020", + unsupported_but_understood_family: "document_evidence" + } + }); + const pilot = await executeAssistantMcpDiscoveryPilot( + planner, + buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Doc1" }]) + ); + + const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); + + expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); + expect(draft.headline).toContain("документ"); + expect(draft.confirmed_lines).toContain("1C document rows were found for counterparty SVK"); + expect(draft.inference_lines).toContain( + "Counterparty document evidence is limited to confirmed 1C document rows in the checked scope" + ); + expect(draft.unknown_lines).toContain("Full document history outside the checked period is not proven by this MCP discovery pilot"); + expect(draft.must_not_claim).toContain("Do not claim full document history outside the checked period."); + }); + 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 948ced9..cb10ec5 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts @@ -103,6 +103,36 @@ describe("assistant MCP discovery pilot executor", () => { expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled(); }); + it("executes generic document evidence through query_documents", async () => { + const planner = planAssistantMcpDiscovery({ + turnMeaning: { + asked_domain_family: "documents", + asked_action_family: "list_documents", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020", + unsupported_but_understood_family: "document_evidence" + } + }); + const deps = buildDeps([ + { Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Doc1" }, + { Period: "2020-03-20T00:00:00", Counterparty: "SVK", Registrar: "Doc2" } + ]); + + const result = await executeAssistantMcpDiscoveryPilot(planner, deps); + + expect(result.pilot_status).toBe("executed"); + expect(result.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1"); + expect(result.executed_primitives).toEqual(["query_documents"]); + expect(result.evidence.confirmed_facts).toContain("1C document rows were found for counterparty SVK"); + expect(result.evidence.inferred_facts).toContain( + "Counterparty document evidence is limited to confirmed 1C document rows in the checked scope" + ); + expect(result.evidence.unknown_facts).toContain( + "Full document history outside the checked period is not proven by this MCP discovery pilot" + ); + expect(result.source_rows_summary).toBe("2 MCP document rows fetched, 2 matched document scope"); + }); + it("executes inspect_1c_metadata and derives a confirmed metadata surface", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { @@ -535,7 +565,7 @@ describe("assistant MCP discovery pilot executor", () => { expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14); }); - it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => { + it("executes document-ready plans through the dedicated document pilot", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_documents", @@ -547,11 +577,12 @@ describe("assistant MCP discovery pilot executor", () => { const result = await executeAssistantMcpDiscoveryPilot(planner, deps); - expect(result.pilot_status).toBe("unsupported"); - expect(result.mcp_execution_performed).toBe(false); - expect(result.skipped_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]); - expect(result.reason_codes).toContain("pilot_scope_unsupported_for_live_execution"); - expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled(); + expect(result.pilot_status).toBe("executed"); + expect(result.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1"); + expect(result.mcp_execution_performed).toBe(true); + expect(result.executed_primitives).toEqual(["query_documents"]); + expect(result.reason_codes).toContain("pilot_query_documents_mcp_executed"); + expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1); }); it("records MCP errors as limitations without converting them into facts", async () => { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts index 8693e7e..ca5e7ec 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts @@ -209,6 +209,33 @@ describe("assistant MCP discovery response candidate", () => { expect(candidate.reply_text).not.toContain("broad probe hit the row limit"); }); + it("localizes document 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 document rows were found for counterparty SVK"], + inference_lines: ["Counterparty document evidence is limited to confirmed 1C document rows in the checked scope"], + unknown_lines: ["Full document 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 document rows were found"); + }); + it("localizes metadata evidence without leaking raw MCP wording", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({ diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts index 4bb2769..652df60 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts @@ -51,7 +51,7 @@ describe("assistant MCP discovery runtime bridge", () => { expect(result.answer_draft.next_step_line).toContain("Уточните контрагента"); }); - it("keeps unsupported ready plans outside the hot answer path", async () => { + it("keeps document-ready plans bounded when the pilot finds no confirmed rows", async () => { const result = await runAssistantMcpDiscoveryRuntimeBridge({ turnMeaning: { asked_domain_family: "document", @@ -61,11 +61,12 @@ describe("assistant MCP discovery runtime bridge", () => { deps: buildDeps([]) }); - expect(result.bridge_status).toBe("unsupported"); + expect(result.bridge_status).toBe("checked_sources_only"); expect(result.hot_runtime_wired).toBe(false); - expect(result.pilot.mcp_execution_performed).toBe(false); + expect(result.pilot.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1"); + expect(result.pilot.mcp_execution_performed).toBe(true); expect(result.business_fact_answer_allowed).toBe(false); - expect(result.reason_codes).toContain("runtime_bridge_status_unsupported"); + expect(result.reason_codes).toContain("runtime_bridge_status_checked_sources_only"); }); it("preserves the answer adapter boundary against internal mechanics leakage", async () => { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts index 6d91a8e..cb57329 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts @@ -231,6 +231,39 @@ 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 document evidence when the next lane is explicit", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "then documents", + 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_document_followup"); + expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_followup_context"); + }); + it("switches the checked year on a short payout follow-up while keeping prior discovery counterparty", () => { const result = buildAssistantMcpDiscoveryTurnInput({ userMessage: "а теперь за 2021?", diff --git a/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts b/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts index 8598044..0be5645 100644 --- a/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts @@ -1114,6 +1114,61 @@ describe("assistantTransitionPolicy", () => { period_to: "2020-12-31" }); }); + it("carries grounded metadata downstream route hints into followup context", () => { + const policy = buildPolicy({ + findLastAddressAssistantItem: () => null, + hasAddressFollowupContextSignal: () => true + }); + + const carryover = policy.resolveAddressFollowupCarryoverContext( + "then documents", + [ + { + role: "assistant", + text: "Metadata surface confirmed.", + debug: { + execution_lane: "living_chat", + mcp_discovery_response_applied: true, + assistant_mcp_discovery_entry_point_v1: { + schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", + entry_status: "bridge_executed", + turn_input: { + turn_meaning_ref: { + asked_domain_family: "metadata", + asked_action_family: "inspect_documents", + explicit_entity_candidates: ["SVK"], + explicit_date_scope: "2020" + } + }, + bridge: { + bridge_status: "answer_draft_ready", + business_fact_answer_allowed: true, + pilot: { + pilot_scope: "metadata_inspection_v1", + derived_metadata_surface: { + selected_entity_set: "Документ", + downstream_route_family: "document_evidence", + ambiguity_detected: false + } + }, + answer_draft: { + answer_mode: "confirmed_with_bounded_inference" + } + } + } + } + } + ], + null, + null, + null + ); + + expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("metadata_inspection_v1"); + expect(carryover?.followupContext?.previous_discovery_metadata_route_family).toBe("document_evidence"); + expect(carryover?.followupContext?.previous_discovery_metadata_selected_entity_set).toBe("Документ"); + expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBeUndefined(); + }); it("switches to VAT tax-period intent while preserving carried period filters", () => { const policy = buildPolicy({ findLastAddressAssistantItem: () => ({