From 561b4ea45c891fa0780a49fb695a431252631d90 Mon Sep 17 00:00:00 2001 From: dctouch Date: Tue, 21 Apr 2026 22:04:23 +0300 Subject: [PATCH] =?UTF-8?q?ARCH:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=83=D1=81=D1=82=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=BD=D0=B0=20MCP=20bounded=20autonomy=20=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20metadata=20p?= =?UTF-8?q?ilot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...alog_authority_recovery_plan_2026-04-19.md | 16 + ..._bounded_autonomy_reset_plan_2026-04-21.md | 201 ++++++++++ .../assistantMcpDiscoveryAnswerAdapter.js | 32 +- .../assistantMcpDiscoveryPilotExecutor.js | 301 ++++++++++++++- .../services/assistantMcpDiscoveryPlanner.js | 18 +- .../assistantMcpDiscoveryTurnInputAdapter.js | 60 ++- .../assistantMcpDiscoveryAnswerAdapter.ts | 37 +- .../assistantMcpDiscoveryPilotExecutor.ts | 358 +++++++++++++++++- .../services/assistantMcpDiscoveryPlanner.ts | 20 +- .../assistantMcpDiscoveryTurnInputAdapter.ts | 64 +++- ...assistantMcpDiscoveryAnswerAdapter.test.ts | 41 ++ ...assistantMcpDiscoveryPilotExecutor.test.ts | 58 +++ .../assistantMcpDiscoveryPlanner.test.ts | 13 + ...stantMcpDiscoveryRuntimeEntryPoint.test.ts | 34 ++ ...istantMcpDiscoveryTurnInputAdapter.test.ts | 18 + 15 files changed, 1214 insertions(+), 57 deletions(-) create mode 100644 docs/ARCH/11 - architecture_turnaround/15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md diff --git a/docs/ARCH/11 - architecture_turnaround/14 - semantic_dialog_authority_recovery_plan_2026-04-19.md b/docs/ARCH/11 - architecture_turnaround/14 - semantic_dialog_authority_recovery_plan_2026-04-19.md index 6693deb..a79c890 100644 --- a/docs/ARCH/11 - architecture_turnaround/14 - semantic_dialog_authority_recovery_plan_2026-04-19.md +++ b/docs/ARCH/11 - architecture_turnaround/14 - semantic_dialog_authority_recovery_plan_2026-04-19.md @@ -1345,6 +1345,22 @@ Module progress: - Big Block 5 MCP Semantic Data Agent: `100%`. +## Reset Hand-Off - 2026-04-21 + +The progress updates above closed the first guarded MCP discovery pilot wave. + +They do **not** mean the strategic autonomy target is complete. + +From 2026-04-21 onward the mainline continues in: + +- [15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md](/x:/1C/NDC_1C/docs/ARCH/11%20-%20architecture_turnaround/15%20-%20mcp_bounded_autonomy_reset_plan_2026-04-21.md:1) + +That reset freezes the continuity/authority stabilization as sufficient and returns the project to the primary trajectory: + +- metadata-first self-navigation; +- entity/schema grounding; +- planner-selected MCP primitive chains instead of route-per-question hardcoding. + ## Execution Rule Do not implement this plan as: diff --git a/docs/ARCH/11 - architecture_turnaround/15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md b/docs/ARCH/11 - architecture_turnaround/15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md new file mode 100644 index 0000000..0ef04b7 --- /dev/null +++ b/docs/ARCH/11 - architecture_turnaround/15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md @@ -0,0 +1,201 @@ +# 15 - MCP Bounded Autonomy Reset Plan (2026-04-21) + +## Purpose + +This note resets the execution focus after the stabilization wave inside turnaround `11`. + +It does not cancel the continuity and authority work already done. + +It clarifies that the main project trajectory is not: + +- endless polishing of deterministic route arbitration; +- route-per-question hardcoding; +- a fake "agentic" mode that is actually another brittle prompt wrapper. + +The real trajectory is: + +- bounded assistant autonomy over reviewed MCP primitives; +- proof-first discovery of 1C evidence; +- gradual reduction of route hardcoding at the question level. + +## Why The Reset Is Necessary + +The previous wave repaired real defects: + +- stale carryover stopped beating the current question on critical contours; +- guarded MCP discovery answers stopped being overwritten by stale exact/lifecycle paths; +- broad business evaluation no longer breaks the return into the data contour. + +That work stays valid. + +But it was support work around the edge of the main target. + +The strategic target was always bigger: + +- the assistant should not need one deterministic route per business wording; +- the assistant should be able to orient itself inside 1C through a reviewed set of MCP primitives; +- the planner should decide which safe primitive chain to use for the current data need; +- the evidence gate should decide what may be stated to the user. + +So the reset is not "we were wrong". + +It is: + +- stabilization is now frozen as sufficient; +- the mainline returns to `MCP-first bounded autonomy`. + +## What Is Frozen + +The following is now baseline, not the mainline: + +- current-turn meaning authority; +- continuity subordinated to current explicit meaning; +- guarded discovery response replacement; +- broad-evaluation bridge that does not destroy the next data follow-up. + +These seams may still receive bug fixes if they block MCP-first execution. + +They are not the main feature track anymore. + +## North Star + +The target is not an unrestricted free agent. + +The target is a bounded planner over a reviewed primitive catalog: + +1. recognize the business data need; +2. pick allowed MCP primitives; +3. execute bounded probes against 1C; +4. aggregate evidence; +5. answer only within the evidence gate. + +In short: + +- move determinism from `route per user wording` +- to `catalog of safe primitives + proof workflow`. + +## Big Block A. Metadata-First Self-Navigation + +### Goal + +Teach the assistant to inspect the 1C schema surface before guessing a route. + +This is the first real step from pilot hardcoding toward self-navigation. + +### Scope + +- live execution of `inspect_1c_metadata`; +- metadata-aware planner path that cannot collapse into `query_documents`; +- machine-readable metadata evidence: + - available object sets; + - matching objects; + - available fields/sections when metadata returns them; + - known limitations; +- human-safe answer draft for metadata discovery. + +### Why This Block Comes First + +Without metadata-first inspection, every later autonomy step is blind: + +- entity resolution is guesswork; +- register/document choice is guesswork; +- long-tail discovery turns back into hidden route hardcoding. + +### Acceptance + +- raw metadata wording can bootstrap discovery input; +- planner keeps `inspect_1c_metadata` as the chosen primitive; +- pilot executes live metadata inspection through MCP; +- user-facing answer stays free of primitive/query/runtime garbage. + +## Big Block B. Entity And Schema Grounding + +### Goal + +Move from "I found some metadata" to "I can ground the user ask onto the right 1C surface". + +### Scope + +- search and resolve candidate entities through MCP instead of local tails only; +- bind metadata findings to probable document/register families; +- preserve ambiguity honestly when multiple surfaces compete; +- keep machine-readable grounding evidence for downstream probes. + +### Acceptance + +- assistant can say which schema surface it selected and why; +- ambiguity is surfaced as a bounded clarification, not silent route drift; +- chosen surface becomes reusable context for the next primitive. + +## Big Block C. Planner-Selected Primitive Chains + +### Goal + +Replace one-off pilot scopes with planner-selected safe chains. + +### Scope + +- chain primitives such as: + - `inspect_1c_metadata` + - `search_business_entity` + - `resolve_entity_reference` + - `query_documents` + - `query_movements` + - `aggregate_by_axis` + - `probe_coverage` + - `explain_evidence_basis` +- keep exact deterministic routes as fast-paths; +- use discovery as the general path for understood long-tail questions. + +### Acceptance + +- new long-tail questions become answerable without adding a dedicated route for each wording; +- the planner output explains the chosen primitive chain; +- the answer gate still blocks overclaiming. + +## Stage 1 Started Now + +The first block is no longer just planned. + +It has started in code in this pass. + +Implemented in this stage: + +- raw metadata wording now bootstraps discovery input; +- metadata planning stays on `inspect_1c_metadata` and no longer falls into `query_documents`; +- the pilot executor now has a live metadata inspection slice; +- the answer adapter can produce a user-safe metadata surface answer. + +This stage is intentionally narrow. + +It does **not** yet mean: + +- unrestricted Qwen3 navigation across arbitrary 1C contours; +- automatic multi-step schema-to-entity-to-query chaining; +- hot-runtime replacement of broad assistant behavior everywhere. + +It means the architecture now has the first real self-navigation primitive in production code. + +## Execution Rule + +From this point the project should prefer: + +- adding or strengthening reviewed MCP primitives; +- planner-selected evidence workflows; +- machine-readable grounding and proof contracts. + +And should avoid: + +- growing another layer of hidden route hardcoding for each new wording; +- long stabilization detours unless they protect an MCP-first invariant; +- fake autonomy that bypasses the evidence gate. + +## Bottom Line + +Turnaround `11` is no longer only about making the assistant feel less glitchy. + +The next move is larger: + +- make the assistant able to look into 1C through bounded MCP discovery, +- choose its path through reviewed primitives, +- and answer from proved evidence instead of memorized route scripts. diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js index 9f5d6a9..3d65e09 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js @@ -64,9 +64,15 @@ function isValueFlowPilot(pilot) { pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1" || pilot.pilot_scope === "counterparty_bidirectional_value_flow_query_movements_v1"); } +function isMetadataPilot(pilot) { + return pilot.pilot_scope === "metadata_inspection_v1"; +} function headlineFor(mode, pilot) { const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" || pilot.derived_value_flow?.aggregation_axis === "month"; + if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") { + return "По метаданным 1С найдена доступная схема для дальнейшего безопасного поиска."; + } if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") { return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду."; } @@ -124,6 +130,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 (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."); + } if (pilot.evidence.confirmed_facts.length === 0) { claims.push("Do not claim a confirmed business fact when confirmed_facts is empty."); } @@ -172,6 +182,23 @@ function derivedActivityInferenceLine(pilot) { "Это вывод по данным 1С, а не юридически подтвержденный возраст регистрации." ].join(" "); } +function derivedMetadataConfirmedLine(pilot) { + const surface = pilot.derived_metadata_surface; + if (!surface) { + return null; + } + const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : ""; + const entitySets = surface.available_entity_sets.length > 0 + ? ` Типы объектов: ${surface.available_entity_sets.join(", ")}.` + : ""; + const objects = surface.matched_objects.length > 0 + ? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.` + : ""; + const fields = surface.available_fields.length > 0 + ? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.` + : ""; + return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${fields}`.replace(/\s+/g, " ").trim(); +} function derivedValueFlowConfirmedLine(pilot) { const flow = pilot.derived_value_flow; if (!flow) { @@ -262,6 +289,7 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) { const inferenceLines = derivedInferenceLine ? [derivedInferenceLine] : pilot.evidence.inferred_facts; + const derivedMetadataLine = derivedMetadataConfirmedLine(pilot); const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot); const monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0 ? derivedBidirectionalValueFlowMonthlyLines(pilot) @@ -271,7 +299,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) { } const confirmedLines = derivedValueLine ? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines] - : pilot.evidence.confirmed_facts; + : derivedMetadataLine + ? [...pilot.evidence.confirmed_facts, derivedMetadataLine] + : pilot.evidence.confirmed_facts; return { schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryAnswerAdapter", diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js index 831c4c2..99beeb1 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js @@ -8,7 +8,8 @@ const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy"); const addressRecipeCatalog_1 = require("./addressRecipeCatalog"); exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION = "assistant_mcp_discovery_pilot_executor_v1"; const DEFAULT_DEPS = { - executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery + executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery, + executeAddressMcpMetadata: addressMcpClient_1.executeAddressMcpMetadata }; function toNonEmptyString(value) { if (value === null || value === undefined) { @@ -119,6 +120,55 @@ function isValueFlowPilotEligible(planner) { combined.includes("payout") || combined.includes("value"))); } +function isMetadataPilotEligible(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("inspect_1c_metadata") && + (combined.includes("metadata") || + combined.includes("schema") || + combined.includes("catalog") || + combined.includes("inspect_documents") || + combined.includes("inspect_registers") || + combined.includes("inspect_fields"))); +} +function metadataScopeForPlanner(planner) { + const entityCandidate = firstEntityCandidate(planner); + if (entityCandidate) { + return entityCandidate; + } + const meaning = planner.discovery_plan.turn_meaning_ref; + const combined = `${meaning?.asked_domain_family ?? ""} ${meaning?.asked_action_family ?? ""} ${meaning?.unsupported_but_understood_family ?? ""}` + .toLowerCase() + .trim(); + if (combined.includes("vat")) { + return "НДС"; + } + if (combined.includes("inventory")) { + return "склад"; + } + if (combined.includes("counterparty")) { + return "контрагент"; + } + return null; +} +function metadataTypesForPlanner(planner) { + const meaning = planner.discovery_plan.turn_meaning_ref; + const action = String(meaning?.asked_action_family ?? "").toLowerCase(); + if (action === "inspect_registers") { + return ["РегистрНакопления", "РегистрСведений"]; + } + if (action === "inspect_documents") { + return ["Документ"]; + } + if (action === "inspect_catalog") { + return ["Справочник"]; + } + return ["Документ", "РегистрНакопления", "РегистрСведений", "Справочник"]; +} function valueFlowPilotProfile(planner) { const meaning = planner.discovery_plan.turn_meaning_ref; const action = String(meaning?.asked_action_family ?? "").toLowerCase(); @@ -168,6 +218,15 @@ function queryResultToProbeResult(primitiveId, result) { limitation: result.error }; } +function metadataResultToProbeResult(primitiveId, result) { + return { + primitive_id: primitiveId, + status: result.error ? "error" : "ok", + rows_received: result.fetched_rows, + rows_matched: result.error ? 0 : result.rows.length, + limitation: result.error + }; +} function toCoverageAwareQueryResult(result, options = {}) { if (!result) { return null; @@ -332,6 +391,147 @@ function summarizeValueFlowRows(result) { } return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`; } +function summarizeMetadataRows(result) { + if (result.error) { + return null; + } + if (result.fetched_rows <= 0) { + return "0 MCP metadata rows fetched"; + } + return `${result.fetched_rows} MCP metadata rows fetched`; +} +function metadataRowText(row, keys) { + for (const key of keys) { + const text = toNonEmptyString(row[key]); + if (text) { + return text; + } + } + return null; +} +function metadataObjectName(row) { + return metadataRowText(row, [ + "ПолноеИмя", + "full_name", + "FullName", + "Имя", + "name", + "Name", + "presentation", + "Представление", + "synonym", + "Synonym" + ]); +} +function metadataEntitySet(row) { + return metadataRowText(row, [ + "ТипМетаданных", + "type", + "Type", + "meta_type", + "MetaType", + "ВидМетаданных", + "kind" + ]); +} +function metadataChildNames(value) { + if (!Array.isArray(value)) { + return []; + } + const result = []; + for (const item of value) { + if (!item || typeof item !== "object" || Array.isArray(item)) { + continue; + } + const record = item; + const fieldName = metadataRowText(record, ["Имя", "name", "Name", "full_name", "FullName"]); + if (fieldName) { + pushUnique(result, fieldName); + } + } + return result; +} +function metadataAvailableFields(rows) { + const result = []; + for (const row of rows) { + for (const field of metadataChildNames(row["Реквизиты"])) { + pushUnique(result, field); + } + for (const field of metadataChildNames(row["attributes"])) { + pushUnique(result, field); + } + for (const field of metadataChildNames(row["Attributes"])) { + pushUnique(result, field); + } + for (const field of metadataChildNames(row["Измерения"])) { + pushUnique(result, field); + } + for (const field of metadataChildNames(row["dimensions"])) { + pushUnique(result, field); + } + for (const field of metadataChildNames(row["Ресурсы"])) { + pushUnique(result, field); + } + for (const field of metadataChildNames(row["resources"])) { + pushUnique(result, field); + } + } + return result; +} +function deriveMetadataSurface(result, metadataScope, requestedMetaTypes) { + if (!result || result.error || result.rows.length <= 0) { + return null; + } + const matchedObjects = []; + const availableEntitySets = []; + for (const row of result.rows) { + const objectName = metadataObjectName(row); + if (objectName) { + pushUnique(matchedObjects, objectName); + } + const entitySet = metadataEntitySet(row); + if (entitySet) { + pushUnique(availableEntitySets, entitySet); + } + } + return { + metadata_scope: metadataScope, + requested_meta_types: requestedMetaTypes, + matched_rows: result.rows.length, + available_entity_sets: availableEntitySets, + matched_objects: matchedObjects, + available_fields: metadataAvailableFields(result.rows), + known_limitations: [], + inference_basis: "confirmed_1c_metadata_surface_rows" + }; +} +function buildMetadataConfirmedFacts(surface) { + if (!surface) { + return []; + } + const facts = []; + const scopeSuffix = surface.metadata_scope ? ` for ${surface.metadata_scope}` : ""; + facts.push(`Confirmed 1C metadata surface${scopeSuffix}: ${surface.matched_rows} rows and ${surface.matched_objects.length} matching objects`); + if (surface.available_entity_sets.length > 0) { + facts.push(`Available metadata object sets: ${surface.available_entity_sets.join(", ")}`); + } + if (surface.available_fields.length > 0) { + facts.push(`Available metadata fields/sections: ${surface.available_fields.slice(0, 12).join(", ")}`); + } + return facts; +} +function buildMetadataUnknownFacts(surface, metadataScope) { + if (surface) { + if (surface.available_fields.length > 0) { + return []; + } + return ["Detailed metadata fields were not returned by this MCP metadata probe"]; + } + if (metadataScope) { + return [`No matching 1C metadata objects were confirmed for scope "${metadataScope}"`]; + } + return ["No matching 1C metadata objects were confirmed by this MCP metadata probe"]; +} function rowDateValue(row) { const candidates = [ row["Период"], @@ -756,13 +956,27 @@ function buildEmptyEvidence(planner, dryRun, probeResults, reason) { recommendedNextProbe: dryRun.user_facing_fallback }); } +function pilotScopeForPlanner(planner) { + if (isMetadataPilotEligible(planner)) { + return "metadata_inspection_v1"; + } + if (isValueFlowPilotEligible(planner)) { + return valueFlowPilotProfile(planner).scope; + } + return "counterparty_lifecycle_query_documents_v1"; +} async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { + const runtimeDeps = { + ...DEFAULT_DEPS, + ...deps + }; const dryRun = (0, assistantMcpDiscoveryRuntimeAdapter_1.buildAssistantMcpDiscoveryRuntimeDryRun)(planner); const reasonCodes = [...dryRun.reason_codes]; const executedPrimitives = []; const skippedPrimitives = []; const probeResults = []; const queryLimitations = []; + const pilotScope = pilotScopeForPlanner(planner); if (dryRun.adapter_status === "blocked") { pushReason(reasonCodes, "pilot_blocked_before_mcp_execution"); const evidence = buildEmptyEvidence(planner, dryRun, probeResults, "MCP discovery pilot was blocked before execution"); @@ -770,7 +984,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryPilotExecutor", pilot_status: "blocked", - pilot_scope: "counterparty_lifecycle_query_documents_v1", + pilot_scope: pilotScope, dry_run: dryRun, mcp_execution_performed: false, executed_primitives: executedPrimitives, @@ -778,6 +992,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { 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, @@ -792,7 +1007,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryPilotExecutor", pilot_status: "skipped_needs_clarification", - pilot_scope: "counterparty_lifecycle_query_documents_v1", + pilot_scope: pilotScope, dry_run: dryRun, mcp_execution_performed: false, executed_primitives: executedPrimitives, @@ -800,6 +1015,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { 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, @@ -807,9 +1023,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { reason_codes: reasonCodes }; } + const metadataPilotEligible = isMetadataPilotEligible(planner); const lifecyclePilotEligible = isLifecyclePilotEligible(planner); const valueFlowPilotEligible = isValueFlowPilotEligible(planner); - if (!lifecyclePilotEligible && !valueFlowPilotEligible) { + if (!metadataPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) { pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution"); for (const step of dryRun.execution_steps) { skippedPrimitives.push(step.primitive_id); @@ -820,7 +1037,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryPilotExecutor", pilot_status: "unsupported", - pilot_scope: "counterparty_lifecycle_query_documents_v1", + pilot_scope: pilotScope, dry_run: dryRun, mcp_execution_performed: false, executed_primitives: executedPrimitives, @@ -828,6 +1045,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { 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, @@ -838,6 +1056,65 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { const counterparty = firstEntityCandidate(planner); const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope); const aggregationAxis = aggregationAxisForPlanner(planner); + if (metadataPilotEligible) { + let metadataResult = null; + const metadataScope = metadataScopeForPlanner(planner); + const requestedMetaTypes = metadataTypesForPlanner(planner); + for (const step of dryRun.execution_steps) { + if (step.primitive_id !== "inspect_1c_metadata") { + skippedPrimitives.push(step.primitive_id); + probeResults.push(skippedProbeResult(step, "pilot_metadata_uses_only_inspect_1c_metadata")); + continue; + } + metadataResult = await runtimeDeps.executeAddressMcpMetadata({ + meta_type: requestedMetaTypes, + name_mask: metadataScope ?? undefined, + limit: planner.discovery_plan.execution_budget.max_rows_per_probe + }); + pushUnique(executedPrimitives, step.primitive_id); + probeResults.push(metadataResultToProbeResult(step.primitive_id, metadataResult)); + if (metadataResult.error) { + pushUnique(queryLimitations, metadataResult.error); + pushReason(reasonCodes, "pilot_inspect_1c_metadata_mcp_error"); + } + else { + pushReason(reasonCodes, "pilot_inspect_1c_metadata_mcp_executed"); + } + } + const sourceRowsSummary = metadataResult ? summarizeMetadataRows(metadataResult) : null; + const derivedMetadataSurface = deriveMetadataSurface(metadataResult, metadataScope, requestedMetaTypes); + if (derivedMetadataSurface) { + pushReason(reasonCodes, "pilot_derived_metadata_surface_from_confirmed_rows"); + } + const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({ + plan: planner.discovery_plan, + probeResults, + confirmedFacts: buildMetadataConfirmedFacts(derivedMetadataSurface), + unknownFacts: buildMetadataUnknownFacts(derivedMetadataSurface, metadataScope), + sourceRowsSummary, + queryLimitations, + recommendedNextProbe: "inspect_1c_metadata" + }); + return { + schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, + policy_owner: "assistantMcpDiscoveryPilotExecutor", + pilot_status: "executed", + pilot_scope: "metadata_inspection_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: derivedMetadataSurface, + 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); @@ -862,6 +1139,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { 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, @@ -883,7 +1161,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { dateScope, maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count, maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe, - deps + deps: runtimeDeps }); const outgoingExecution = await executeCoverageAwareValueFlowQuery({ primitiveId: step.primitive_id, @@ -892,7 +1170,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { dateScope, maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count, maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe, - deps + deps: runtimeDeps }); incomingResult = incomingExecution.result; outgoingResult = outgoingExecution.result; @@ -953,6 +1231,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { probe_results: probeResults, evidence, source_rows_summary: sourceRowsSummary, + derived_metadata_surface: null, derived_activity_period: null, derived_value_flow: null, derived_bidirectional_value_flow: derivedBidirectionalValueFlow, @@ -977,6 +1256,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { 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, @@ -1000,7 +1280,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { dateScope, maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count, maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe, - deps + deps: runtimeDeps }); queryResult = execution.result; pushUnique(executedPrimitives, step.primitive_id); @@ -1048,6 +1328,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { probe_results: probeResults, evidence, source_rows_summary: sourceRowsSummary, + derived_metadata_surface: null, derived_activity_period: null, derived_value_flow: derivedValueFlow, derived_bidirectional_value_flow: null, @@ -1073,6 +1354,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { 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, @@ -1087,7 +1369,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { probeResults.push(skippedProbeResult(step, "pilot_only_executes_query_documents")); continue; } - queryResult = await deps.executeAddressMcpQuery({ + queryResult = await runtimeDeps.executeAddressMcpQuery({ query: recipePlan.query, limit: recipePlan.limit, account_scope: recipePlan.account_scope @@ -1129,6 +1411,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { probe_results: probeResults, evidence, source_rows_summary: sourceRowsSummary, + derived_metadata_surface: null, derived_activity_period: derivedActivityPeriod, derived_value_flow: null, derived_bidirectional_value_flow: null, diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js index df34f05..3d69371 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js @@ -98,15 +98,6 @@ function recipeFor(input) { : "planner_selected_value_flow_recipe" }; } - if (includesAny(combined, ["document", "documents"])) { - pushUnique(axes, "coverage_target"); - return { - semanticDataNeed: "document evidence", - primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"], - axes, - reason: "planner_selected_document_recipe" - }; - } if (includesAny(combined, ["lifecycle", "activity", "duration", "age"])) { pushUnique(axes, "document_date"); pushUnique(axes, "coverage_target"); @@ -127,6 +118,15 @@ function recipeFor(input) { reason: "planner_selected_metadata_recipe" }; } + if (includesAny(combined, ["document", "documents"])) { + pushUnique(axes, "coverage_target"); + return { + semanticDataNeed: "document evidence", + primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"], + axes, + reason: "planner_selected_document_recipe" + }; + } if (hasEntity(meaning)) { pushUnique(axes, "business_entity"); return { diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js index 59ec97a..c009258 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -220,6 +220,28 @@ function hasBidirectionalValueFlowSignal(text) { function hasMonthlyAggregationSignal(text) { return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text); } +function hasMetadataSignal(text) { + if (/(?:\u043c\u0435\u0442\u0430\u0434\u0430\u043d|schema|catalog|metadata\s+surface|\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440[\u0430\u044b]\s+1\u0441|\u0441\u0445\u0435\u043c[\u0430\u044b]\s+1\u0441)/iu.test(text)) { + return true; + } + 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) && + /(?:\u0435\u0441\u0442\u044c|\u0434\u043e\u0441\u0442\u0443\u043f\u043d|\u0432\s+1\u0441|available|exist)/iu.test(text)); +} +function metadataActionFromRawText(text) { + if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) { + return "inspect_fields"; + } + if (/(?:\u0440\u0435\u0433\u0438\u0441\u0442\u0440|register)/iu.test(text)) { + return "inspect_registers"; + } + if (/(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|document)/iu.test(text)) { + return "inspect_documents"; + } + if (/(?:\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a|directory|catalog)/iu.test(text)) { + return "inspect_catalog"; + } + return "inspect_catalog"; +} function hasExplicitDateScopeLiteral(text) { return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b)/iu.test(text); } @@ -240,6 +262,9 @@ function collectDateScopeFromRawText(text) { } function semanticNeedFor(input) { const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`); + if (input.metadataSignal || /(?:metadata|schema|catalog|inspect_(?:catalog|documents|registers|fields))/iu.test(combined)) { + return "1C metadata evidence"; + } if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) { return "counterparty lifecycle evidence"; } @@ -249,15 +274,15 @@ function semanticNeedFor(input) { if (/(?:document|documents|list_documents)/iu.test(combined)) { return "document evidence"; } - if (/(?:metadata|schema|catalog)/iu.test(combined)) { - return "1C metadata evidence"; - } return null; } function shouldRunDiscovery(input) { if (input.lifecycleSignal || input.unsupported) { return true; } + if (input.metadataSignal) { + return true; + } if (input.valueFlowSignal && !input.explicitIntentCandidate) { return true; } @@ -280,6 +305,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) { const rawLifecycleSignal = hasLifecycleSignal(rawText); const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText); const rawValueFlowSignal = !rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal); + const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText); const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText); const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText); const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText); @@ -311,7 +337,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) { action: rawAction ?? seededAction, unsupported: unsupported ?? seededUnsupported, lifecycleSignal, - valueFlowSignal + valueFlowSignal, + metadataSignal: rawMetadataSignal }); const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates); pushUnique(entityCandidates, predecomposeEntities.counterparty); @@ -329,7 +356,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) { ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" - : rawDomain ?? seededDomain, + : rawMetadataSignal + ? "metadata" + : rawDomain ?? seededDomain, asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal @@ -338,7 +367,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) { : payoutSignal ? "payout" : rawAction ?? seededAction ?? "turnover" - : rawAction ?? seededAction, + : rawMetadataSignal + ? metadataActionFromRawText(rawText) + : rawAction ?? seededAction, asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis, explicit_entity_candidates: entityCandidates, explicit_organization_scope: explicitOrganizationScope, @@ -352,13 +383,16 @@ function buildAssistantMcpDiscoveryTurnInput(input) { : payoutSignal ? "counterparty_payouts_or_outflow" : seededUnsupported ?? "counterparty_value_or_turnover" - : followupDiscoverySeedApplicable - ? seededUnsupported - : null), + : rawMetadataSignal + ? "1c_metadata_surface" + : followupDiscoverySeedApplicable + ? seededUnsupported + : null), stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || unsupported || lifecycleSignal || valueFlowSignal || + rawMetadataSignal || followupDiscoverySeedApplicable) }; const cleanTurnMeaning = {}; @@ -390,6 +424,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) { unsupported: unsupported ?? seededUnsupported, lifecycleSignal, valueFlowSignal, + metadataSignal: rawMetadataSignal, semanticDataNeed, explicitIntentCandidate, followupDiscoverySeedApplicable @@ -405,13 +440,18 @@ function buildAssistantMcpDiscoveryTurnInput(input) { ? "raw_text" : valueFlowSignal ? "raw_text" - : "none"; + : rawMetadataSignal + ? "raw_text" + : "none"; if (lifecycleSignal) { pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected"); } if (valueFlowSignal) { pushReason(reasonCodes, "mcp_discovery_value_flow_signal_detected"); } + if (rawMetadataSignal) { + pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected"); + } if (payoutSignal) { pushReason(reasonCodes, "mcp_discovery_payout_signal_detected"); } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts index b914db7..0c09124 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts @@ -97,10 +97,17 @@ function isValueFlowPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): b ); } +function isMetadataPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean { + return pilot.pilot_scope === "metadata_inspection_v1"; +} + function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string { const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" || pilot.derived_value_flow?.aggregation_axis === "month"; + if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") { + return "По метаданным 1С найдена доступная схема для дальнейшего безопасного поиска."; + } if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") { return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду."; } @@ -160,6 +167,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 (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."); + } if (pilot.evidence.confirmed_facts.length === 0) { claims.push("Do not claim a confirmed business fact when confirmed_facts is empty."); } @@ -213,6 +224,27 @@ function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecution ].join(" "); } +function derivedMetadataConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { + const surface = pilot.derived_metadata_surface; + if (!surface) { + return null; + } + const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : ""; + const entitySets = + surface.available_entity_sets.length > 0 + ? ` Типы объектов: ${surface.available_entity_sets.join(", ")}.` + : ""; + const objects = + surface.matched_objects.length > 0 + ? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.` + : ""; + const fields = + surface.available_fields.length > 0 + ? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.` + : ""; + return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${fields}`.replace(/\s+/g, " ").trim(); +} + function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { const flow = pilot.derived_value_flow; if (!flow) { @@ -318,6 +350,7 @@ export function buildAssistantMcpDiscoveryAnswerDraft( const inferenceLines = derivedInferenceLine ? [derivedInferenceLine] : pilot.evidence.inferred_facts; + const derivedMetadataLine = derivedMetadataConfirmedLine(pilot); const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot); const monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0 @@ -328,7 +361,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft( } const confirmedLines = derivedValueLine ? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines] - : pilot.evidence.confirmed_facts; + : derivedMetadataLine + ? [...pilot.evidence.confirmed_facts, derivedMetadataLine] + : pilot.evidence.confirmed_facts; return { schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION, diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts index a6ce78a..8851f3d 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts @@ -1,4 +1,5 @@ import { + executeAddressMcpMetadata, executeAddressMcpQuery, type AddressMcpMetadataRowsResult } from "./addressMcpClient"; @@ -26,7 +27,13 @@ export type AssistantMcpDiscoveryPilotStatus = | "unsupported"; export interface AssistantMcpDiscoveryPilotExecutorDeps { + executeAddressMcpQuery?: typeof executeAddressMcpQuery; + executeAddressMcpMetadata?: typeof executeAddressMcpMetadata; +} + +interface ResolvedAssistantMcpDiscoveryPilotExecutorDeps { executeAddressMcpQuery: typeof executeAddressMcpQuery; + executeAddressMcpMetadata: typeof executeAddressMcpMetadata; } export interface AssistantMcpDiscoveryDerivedActivityPeriod { @@ -109,6 +116,17 @@ export interface AssistantMcpDiscoveryDerivedBidirectionalValueFlow { inference_basis: "incoming_minus_outgoing_confirmed_1c_value_flow_rows"; } +export interface AssistantMcpDiscoveryDerivedMetadataSurface { + metadata_scope: string | null; + requested_meta_types: string[]; + matched_rows: number; + available_entity_sets: string[]; + matched_objects: string[]; + available_fields: string[]; + known_limitations: string[]; + inference_basis: "confirmed_1c_metadata_surface_rows"; +} + interface AssistantMcpDiscoveryCoverageAwareQueryResult extends AddressMcpQueryExecutorResult { coverage_limited_by_probe_limit: boolean; coverage_recovered_by_period_chunking: boolean; @@ -124,6 +142,7 @@ interface AssistantMcpDiscoveryCoverageAwareQueryExecution { } export type AssistantMcpDiscoveryPilotScope = + | "metadata_inspection_v1" | "counterparty_lifecycle_query_documents_v1" | "counterparty_value_flow_query_movements_v1" | "counterparty_supplier_payout_query_movements_v1" @@ -141,6 +160,7 @@ export interface AssistantMcpDiscoveryPilotExecutionContract { probe_results: AssistantMcpDiscoveryProbeResult[]; evidence: AssistantMcpDiscoveryEvidenceContract; source_rows_summary: string | null; + derived_metadata_surface: AssistantMcpDiscoveryDerivedMetadataSurface | null; derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null; derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null; derived_bidirectional_value_flow: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null; @@ -150,8 +170,9 @@ export interface AssistantMcpDiscoveryPilotExecutionContract { type AddressMcpQueryExecutorResult = Awaited>; -const DEFAULT_DEPS: AssistantMcpDiscoveryPilotExecutorDeps = { - executeAddressMcpQuery +const DEFAULT_DEPS: ResolvedAssistantMcpDiscoveryPilotExecutorDeps = { + executeAddressMcpQuery, + executeAddressMcpMetadata }; function toNonEmptyString(value: unknown): string | null { @@ -280,6 +301,60 @@ function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract) ); } +function isMetadataPilotEligible(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("inspect_1c_metadata") && + (combined.includes("metadata") || + combined.includes("schema") || + combined.includes("catalog") || + combined.includes("inspect_documents") || + combined.includes("inspect_registers") || + combined.includes("inspect_fields")) + ); +} + +function metadataScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string | null { + const entityCandidate = firstEntityCandidate(planner); + if (entityCandidate) { + return entityCandidate; + } + const meaning = planner.discovery_plan.turn_meaning_ref; + const combined = `${meaning?.asked_domain_family ?? ""} ${meaning?.asked_action_family ?? ""} ${meaning?.unsupported_but_understood_family ?? ""}` + .toLowerCase() + .trim(); + if (combined.includes("vat")) { + return "НДС"; + } + if (combined.includes("inventory")) { + return "склад"; + } + if (combined.includes("counterparty")) { + return "контрагент"; + } + return null; +} + +function metadataTypesForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string[] { + const meaning = planner.discovery_plan.turn_meaning_ref; + const action = String(meaning?.asked_action_family ?? "").toLowerCase(); + if (action === "inspect_registers") { + return ["РегистрНакопления", "РегистрСведений"]; + } + if (action === "inspect_documents") { + return ["Документ"]; + } + if (action === "inspect_catalog") { + return ["Справочник"]; + } + return ["Документ", "РегистрНакопления", "РегистрСведений", "Справочник"]; +} + interface ValueFlowPilotProfile { scope: Extract< AssistantMcpDiscoveryPilotScope, @@ -350,6 +425,19 @@ function queryResultToProbeResult( }; } +function metadataResultToProbeResult( + primitiveId: string, + result: AddressMcpMetadataRowsResult +): AssistantMcpDiscoveryProbeResult { + return { + primitive_id: primitiveId, + status: result.error ? "error" : "ok", + rows_received: result.fetched_rows, + rows_matched: result.error ? 0 : result.rows.length, + limitation: result.error + }; +} + function toCoverageAwareQueryResult( result: AddressMcpQueryExecutorResult | null, options: { @@ -428,7 +516,7 @@ async function executeCoverageAwareValueFlowQuery(input: { dateScope: string | null; maxProbeCount: number; maxRowsPerProbe: number; - deps: AssistantMcpDiscoveryPilotExecutorDeps; + deps: ResolvedAssistantMcpDiscoveryPilotExecutorDeps; }): Promise { const queryLimitations: string[] = []; const probeResults: AssistantMcpDiscoveryProbeResult[] = []; @@ -560,6 +648,167 @@ function summarizeValueFlowRows(result: AssistantMcpDiscoveryCoverageAwareQueryR return `${result.fetched_rows} MCP value-flow rows fetched, ${result.matched_rows} matched value-flow scope`; } +function summarizeMetadataRows(result: AddressMcpMetadataRowsResult): string | null { + if (result.error) { + return null; + } + if (result.fetched_rows <= 0) { + return "0 MCP metadata rows fetched"; + } + return `${result.fetched_rows} MCP metadata rows fetched`; +} + +function metadataRowText(row: Record, keys: string[]): string | null { + for (const key of keys) { + const text = toNonEmptyString(row[key]); + if (text) { + return text; + } + } + return null; +} + +function metadataObjectName(row: Record): string | null { + return metadataRowText(row, [ + "ПолноеИмя", + "full_name", + "FullName", + "Имя", + "name", + "Name", + "presentation", + "Представление", + "synonym", + "Synonym" + ]); +} + +function metadataEntitySet(row: Record): string | null { + return metadataRowText(row, [ + "ТипМетаданных", + "type", + "Type", + "meta_type", + "MetaType", + "ВидМетаданных", + "kind" + ]); +} + +function metadataChildNames(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + const result: string[] = []; + for (const item of value) { + if (!item || typeof item !== "object" || Array.isArray(item)) { + continue; + } + const record = item as Record; + const fieldName = metadataRowText(record, ["Имя", "name", "Name", "full_name", "FullName"]); + if (fieldName) { + pushUnique(result, fieldName); + } + } + return result; +} + +function metadataAvailableFields(rows: Array>): string[] { + const result: string[] = []; + for (const row of rows) { + for (const field of metadataChildNames(row["Реквизиты"])) { + pushUnique(result, field); + } + for (const field of metadataChildNames(row["attributes"])) { + pushUnique(result, field); + } + for (const field of metadataChildNames(row["Attributes"])) { + pushUnique(result, field); + } + for (const field of metadataChildNames(row["Измерения"])) { + pushUnique(result, field); + } + for (const field of metadataChildNames(row["dimensions"])) { + pushUnique(result, field); + } + for (const field of metadataChildNames(row["Ресурсы"])) { + pushUnique(result, field); + } + for (const field of metadataChildNames(row["resources"])) { + pushUnique(result, field); + } + } + return result; +} + +function deriveMetadataSurface( + result: AddressMcpMetadataRowsResult | null, + metadataScope: string | null, + requestedMetaTypes: string[] +): AssistantMcpDiscoveryDerivedMetadataSurface | null { + if (!result || result.error || result.rows.length <= 0) { + return null; + } + const matchedObjects: string[] = []; + const availableEntitySets: string[] = []; + for (const row of result.rows) { + const objectName = metadataObjectName(row); + if (objectName) { + pushUnique(matchedObjects, objectName); + } + const entitySet = metadataEntitySet(row); + if (entitySet) { + pushUnique(availableEntitySets, entitySet); + } + } + return { + metadata_scope: metadataScope, + requested_meta_types: requestedMetaTypes, + matched_rows: result.rows.length, + available_entity_sets: availableEntitySets, + matched_objects: matchedObjects, + available_fields: metadataAvailableFields(result.rows), + known_limitations: [], + inference_basis: "confirmed_1c_metadata_surface_rows" + }; +} + +function buildMetadataConfirmedFacts( + surface: AssistantMcpDiscoveryDerivedMetadataSurface | null +): string[] { + if (!surface) { + return []; + } + const facts: string[] = []; + const scopeSuffix = surface.metadata_scope ? ` for ${surface.metadata_scope}` : ""; + facts.push( + `Confirmed 1C metadata surface${scopeSuffix}: ${surface.matched_rows} rows and ${surface.matched_objects.length} matching objects` + ); + if (surface.available_entity_sets.length > 0) { + facts.push(`Available metadata object sets: ${surface.available_entity_sets.join(", ")}`); + } + if (surface.available_fields.length > 0) { + facts.push(`Available metadata fields/sections: ${surface.available_fields.slice(0, 12).join(", ")}`); + } + return facts; +} + +function buildMetadataUnknownFacts( + surface: AssistantMcpDiscoveryDerivedMetadataSurface | null, + metadataScope: string | null +): string[] { + if (surface) { + if (surface.available_fields.length > 0) { + return []; + } + return ["Detailed metadata fields were not returned by this MCP metadata probe"]; + } + if (metadataScope) { + return [`No matching 1C metadata objects were confirmed for scope "${metadataScope}"`]; + } + return ["No matching 1C metadata objects were confirmed by this MCP metadata probe"]; +} + function rowDateValue(row: Record): string | null { const candidates = [ row["Период"], @@ -1072,16 +1321,31 @@ function buildEmptyEvidence( }); } +function pilotScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): AssistantMcpDiscoveryPilotScope { + if (isMetadataPilotEligible(planner)) { + return "metadata_inspection_v1"; + } + if (isValueFlowPilotEligible(planner)) { + return valueFlowPilotProfile(planner).scope; + } + return "counterparty_lifecycle_query_documents_v1"; +} + export async function executeAssistantMcpDiscoveryPilot( planner: AssistantMcpDiscoveryPlannerContract, deps: AssistantMcpDiscoveryPilotExecutorDeps = DEFAULT_DEPS ): Promise { + const runtimeDeps: ResolvedAssistantMcpDiscoveryPilotExecutorDeps = { + ...DEFAULT_DEPS, + ...deps + }; const dryRun = buildAssistantMcpDiscoveryRuntimeDryRun(planner); const reasonCodes = [...dryRun.reason_codes]; const executedPrimitives: string[] = []; const skippedPrimitives: string[] = []; const probeResults: AssistantMcpDiscoveryProbeResult[] = []; const queryLimitations: string[] = []; + const pilotScope = pilotScopeForPlanner(planner); if (dryRun.adapter_status === "blocked") { pushReason(reasonCodes, "pilot_blocked_before_mcp_execution"); @@ -1090,7 +1354,7 @@ export async function executeAssistantMcpDiscoveryPilot( schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryPilotExecutor", pilot_status: "blocked", - pilot_scope: "counterparty_lifecycle_query_documents_v1", + pilot_scope: pilotScope, dry_run: dryRun, mcp_execution_performed: false, executed_primitives: executedPrimitives, @@ -1098,6 +1362,7 @@ export async function executeAssistantMcpDiscoveryPilot( 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, @@ -1113,7 +1378,7 @@ export async function executeAssistantMcpDiscoveryPilot( schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryPilotExecutor", pilot_status: "skipped_needs_clarification", - pilot_scope: "counterparty_lifecycle_query_documents_v1", + pilot_scope: pilotScope, dry_run: dryRun, mcp_execution_performed: false, executed_primitives: executedPrimitives, @@ -1121,6 +1386,7 @@ export async function executeAssistantMcpDiscoveryPilot( 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, @@ -1129,10 +1395,11 @@ export async function executeAssistantMcpDiscoveryPilot( }; } + const metadataPilotEligible = isMetadataPilotEligible(planner); const lifecyclePilotEligible = isLifecyclePilotEligible(planner); const valueFlowPilotEligible = isValueFlowPilotEligible(planner); - if (!lifecyclePilotEligible && !valueFlowPilotEligible) { + if (!metadataPilotEligible && !lifecyclePilotEligible && !valueFlowPilotEligible) { pushReason(reasonCodes, "pilot_scope_unsupported_for_live_execution"); for (const step of dryRun.execution_steps) { skippedPrimitives.push(step.primitive_id); @@ -1143,7 +1410,7 @@ export async function executeAssistantMcpDiscoveryPilot( schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryPilotExecutor", pilot_status: "unsupported", - pilot_scope: "counterparty_lifecycle_query_documents_v1", + pilot_scope: pilotScope, dry_run: dryRun, mcp_execution_performed: false, executed_primitives: executedPrimitives, @@ -1151,6 +1418,7 @@ export async function executeAssistantMcpDiscoveryPilot( 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, @@ -1163,6 +1431,68 @@ export async function executeAssistantMcpDiscoveryPilot( const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope); const aggregationAxis = aggregationAxisForPlanner(planner); + if (metadataPilotEligible) { + let metadataResult: AddressMcpMetadataRowsResult | null = null; + const metadataScope = metadataScopeForPlanner(planner); + const requestedMetaTypes = metadataTypesForPlanner(planner); + + for (const step of dryRun.execution_steps) { + if (step.primitive_id !== "inspect_1c_metadata") { + skippedPrimitives.push(step.primitive_id); + probeResults.push(skippedProbeResult(step, "pilot_metadata_uses_only_inspect_1c_metadata")); + continue; + } + metadataResult = await runtimeDeps.executeAddressMcpMetadata({ + meta_type: requestedMetaTypes, + name_mask: metadataScope ?? undefined, + limit: planner.discovery_plan.execution_budget.max_rows_per_probe + }); + pushUnique(executedPrimitives, step.primitive_id); + probeResults.push(metadataResultToProbeResult(step.primitive_id, metadataResult)); + if (metadataResult.error) { + pushUnique(queryLimitations, metadataResult.error); + pushReason(reasonCodes, "pilot_inspect_1c_metadata_mcp_error"); + } else { + pushReason(reasonCodes, "pilot_inspect_1c_metadata_mcp_executed"); + } + } + + const sourceRowsSummary = metadataResult ? summarizeMetadataRows(metadataResult) : null; + const derivedMetadataSurface = deriveMetadataSurface(metadataResult, metadataScope, requestedMetaTypes); + if (derivedMetadataSurface) { + pushReason(reasonCodes, "pilot_derived_metadata_surface_from_confirmed_rows"); + } + const evidence = resolveAssistantMcpDiscoveryEvidence({ + plan: planner.discovery_plan, + probeResults, + confirmedFacts: buildMetadataConfirmedFacts(derivedMetadataSurface), + unknownFacts: buildMetadataUnknownFacts(derivedMetadataSurface, metadataScope), + sourceRowsSummary, + queryLimitations, + recommendedNextProbe: "inspect_1c_metadata" + }); + + return { + schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, + policy_owner: "assistantMcpDiscoveryPilotExecutor", + pilot_status: "executed", + pilot_scope: "metadata_inspection_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: derivedMetadataSurface, + 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); @@ -1187,6 +1517,7 @@ export async function executeAssistantMcpDiscoveryPilot( 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, @@ -1213,7 +1544,7 @@ export async function executeAssistantMcpDiscoveryPilot( dateScope, maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count, maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe, - deps + deps: runtimeDeps }); const outgoingExecution = await executeCoverageAwareValueFlowQuery({ primitiveId: step.primitive_id, @@ -1222,7 +1553,7 @@ export async function executeAssistantMcpDiscoveryPilot( dateScope, maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count, maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe, - deps + deps: runtimeDeps }); incomingResult = incomingExecution.result; outgoingResult = outgoingExecution.result; @@ -1285,6 +1616,7 @@ export async function executeAssistantMcpDiscoveryPilot( probe_results: probeResults, evidence, source_rows_summary: sourceRowsSummary, + derived_metadata_surface: null, derived_activity_period: null, derived_value_flow: null, derived_bidirectional_value_flow: derivedBidirectionalValueFlow, @@ -1310,6 +1642,7 @@ export async function executeAssistantMcpDiscoveryPilot( 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, @@ -1337,7 +1670,7 @@ export async function executeAssistantMcpDiscoveryPilot( dateScope, maxProbeCount: planner.discovery_plan.execution_budget.max_probe_count, maxRowsPerProbe: planner.discovery_plan.execution_budget.max_rows_per_probe, - deps + deps: runtimeDeps }); queryResult = execution.result; pushUnique(executedPrimitives, step.primitive_id); @@ -1392,6 +1725,7 @@ export async function executeAssistantMcpDiscoveryPilot( probe_results: probeResults, evidence, source_rows_summary: sourceRowsSummary, + derived_metadata_surface: null, derived_activity_period: null, derived_value_flow: derivedValueFlow, derived_bidirectional_value_flow: null, @@ -1418,6 +1752,7 @@ export async function executeAssistantMcpDiscoveryPilot( 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, @@ -1433,7 +1768,7 @@ export async function executeAssistantMcpDiscoveryPilot( probeResults.push(skippedProbeResult(step, "pilot_only_executes_query_documents")); continue; } - queryResult = await deps.executeAddressMcpQuery({ + queryResult = await runtimeDeps.executeAddressMcpQuery({ query: recipePlan.query, limit: recipePlan.limit, account_scope: recipePlan.account_scope @@ -1476,6 +1811,7 @@ export async function executeAssistantMcpDiscoveryPilot( probe_results: probeResults, evidence, source_rows_summary: sourceRowsSummary, + derived_metadata_surface: null, derived_activity_period: derivedActivityPeriod, derived_value_flow: null, derived_bidirectional_value_flow: null, diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts index 389ddc8..f04a616 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts @@ -148,16 +148,6 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe { }; } - if (includesAny(combined, ["document", "documents"])) { - pushUnique(axes, "coverage_target"); - return { - semanticDataNeed: "document evidence", - primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"], - axes, - reason: "planner_selected_document_recipe" - }; - } - if (includesAny(combined, ["lifecycle", "activity", "duration", "age"])) { pushUnique(axes, "document_date"); pushUnique(axes, "coverage_target"); @@ -180,6 +170,16 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe { }; } + if (includesAny(combined, ["document", "documents"])) { + pushUnique(axes, "coverage_target"); + return { + semanticDataNeed: "document evidence", + primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"], + axes, + reason: "planner_selected_document_recipe" + }; + } + if (hasEntity(meaning)) { pushUnique(axes, "business_entity"); return { diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts index 8e78b19..e4ce9e8 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -306,6 +306,38 @@ function hasMonthlyAggregationSignal(text: string): boolean { ); } +function hasMetadataSignal(text: string): boolean { + if ( + /(?:\u043c\u0435\u0442\u0430\u0434\u0430\u043d|schema|catalog|metadata\s+surface|\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440[\u0430\u044b]\s+1\u0441|\u0441\u0445\u0435\u043c[\u0430\u044b]\s+1\u0441)/iu.test( + text + ) + ) { + return true; + } + 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 + ) && + /(?:\u0435\u0441\u0442\u044c|\u0434\u043e\u0441\u0442\u0443\u043f\u043d|\u0432\s+1\u0441|available|exist)/iu.test(text) + ); +} + +function metadataActionFromRawText(text: string): string { + if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) { + return "inspect_fields"; + } + if (/(?:\u0440\u0435\u0433\u0438\u0441\u0442\u0440|register)/iu.test(text)) { + return "inspect_registers"; + } + if (/(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|document)/iu.test(text)) { + return "inspect_documents"; + } + if (/(?:\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a|directory|catalog)/iu.test(text)) { + return "inspect_catalog"; + } + return "inspect_catalog"; +} + function hasExplicitDateScopeLiteral(text: string): boolean { return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b)/iu.test(text); } @@ -332,8 +364,12 @@ function semanticNeedFor(input: { unsupported: string | null; lifecycleSignal: boolean; valueFlowSignal: boolean; + metadataSignal: boolean; }): string | null { const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`); + if (input.metadataSignal || /(?:metadata|schema|catalog|inspect_(?:catalog|documents|registers|fields))/iu.test(combined)) { + return "1C metadata evidence"; + } if (input.lifecycleSignal || /(?:lifecycle|activity|duration|age)/iu.test(combined)) { return "counterparty lifecycle evidence"; } @@ -343,9 +379,6 @@ function semanticNeedFor(input: { if (/(?:document|documents|list_documents)/iu.test(combined)) { return "document evidence"; } - if (/(?:metadata|schema|catalog)/iu.test(combined)) { - return "1C metadata evidence"; - } return null; } @@ -353,6 +386,7 @@ function shouldRunDiscovery(input: { unsupported: string | null; lifecycleSignal: boolean; valueFlowSignal: boolean; + metadataSignal: boolean; semanticDataNeed: string | null; explicitIntentCandidate: string | null; followupDiscoverySeedApplicable: boolean; @@ -360,6 +394,9 @@ function shouldRunDiscovery(input: { if (input.lifecycleSignal || input.unsupported) { return true; } + if (input.metadataSignal) { + return true; + } if (input.valueFlowSignal && !input.explicitIntentCandidate) { return true; } @@ -386,6 +423,7 @@ export function buildAssistantMcpDiscoveryTurnInput( const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText); const rawValueFlowSignal = !rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal); + const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText); const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText); const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText); const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText); @@ -424,7 +462,8 @@ export function buildAssistantMcpDiscoveryTurnInput( action: rawAction ?? seededAction, unsupported: unsupported ?? seededUnsupported, lifecycleSignal, - valueFlowSignal + valueFlowSignal, + metadataSignal: rawMetadataSignal }); const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates); pushUnique(entityCandidates, predecomposeEntities.counterparty); @@ -445,7 +484,9 @@ export function buildAssistantMcpDiscoveryTurnInput( ? "counterparty_lifecycle" : valueFlowSignal ? "counterparty_value" - : rawDomain ?? seededDomain, + : rawMetadataSignal + ? "metadata" + : rawDomain ?? seededDomain, asked_action_family: lifecycleSignal ? "activity_duration" : valueFlowSignal @@ -454,7 +495,9 @@ export function buildAssistantMcpDiscoveryTurnInput( : payoutSignal ? "payout" : rawAction ?? seededAction ?? "turnover" - : rawAction ?? seededAction, + : rawMetadataSignal + ? metadataActionFromRawText(rawText) + : rawAction ?? seededAction, asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis, explicit_entity_candidates: entityCandidates, explicit_organization_scope: explicitOrganizationScope, @@ -469,6 +512,8 @@ export function buildAssistantMcpDiscoveryTurnInput( : payoutSignal ? "counterparty_payouts_or_outflow" : seededUnsupported ?? "counterparty_value_or_turnover" + : rawMetadataSignal + ? "1c_metadata_surface" : followupDiscoverySeedApplicable ? seededUnsupported : null), @@ -477,6 +522,7 @@ export function buildAssistantMcpDiscoveryTurnInput( unsupported || lifecycleSignal || valueFlowSignal || + rawMetadataSignal || followupDiscoverySeedApplicable ) }; @@ -511,6 +557,7 @@ export function buildAssistantMcpDiscoveryTurnInput( unsupported: unsupported ?? seededUnsupported, lifecycleSignal, valueFlowSignal, + metadataSignal: rawMetadataSignal, semanticDataNeed, explicitIntentCandidate, followupDiscoverySeedApplicable @@ -526,6 +573,8 @@ export function buildAssistantMcpDiscoveryTurnInput( ? "raw_text" : valueFlowSignal ? "raw_text" + : rawMetadataSignal + ? "raw_text" : "none"; if (lifecycleSignal) { @@ -534,6 +583,9 @@ export function buildAssistantMcpDiscoveryTurnInput( if (valueFlowSignal) { pushReason(reasonCodes, "mcp_discovery_value_flow_signal_detected"); } + if (rawMetadataSignal) { + pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected"); + } if (payoutSignal) { pushReason(reasonCodes, "mcp_discovery_payout_signal_detected"); } diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts index d1f7688..7e5e342 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts @@ -31,6 +31,17 @@ function buildSequentialDeps(results: Array<{ rows: Array>, error: string | null = null) { + return { + executeAddressMcpMetadata: vi.fn(async () => ({ + fetched_rows: error ? 0 : rows.length, + raw_rows: error ? [] : rows, + rows: error ? [] : rows, + error + })) + }; +} + describe("assistant MCP discovery answer adapter", () => { it("turns confirmed lifecycle evidence into a human-safe bounded answer draft", async () => { const planner = planAssistantMcpDiscovery({ @@ -96,6 +107,36 @@ describe("assistant MCP discovery answer adapter", () => { expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false."); }); + it("turns metadata surface evidence into a human-safe metadata answer draft", async () => { + const planner = planAssistantMcpDiscovery({ + turnMeaning: { + asked_domain_family: "metadata", + asked_action_family: "inspect_documents", + explicit_entity_candidates: ["НДС"] + } + }); + const pilot = await executeAssistantMcpDiscoveryPilot( + planner, + buildMetadataDeps([ + { + FullName: "Документ.СчетФактураВыданный", + MetaType: "Документ", + attributes: [{ Name: "Дата" }, { Name: "Организация" }] + } + ]) + ); + + const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); + const confirmedText = draft.confirmed_lines.join("\n"); + + expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); + expect(draft.headline).toContain("метаданным 1С"); + expect(confirmedText).toContain("Подтвержденная metadata-поверхность 1С"); + expect(confirmedText).toContain("Документ.СчетФактураВыданный"); + expect(confirmedText).toContain("Дата"); + expect(draft.must_not_claim).toContain("Do not present metadata surface as confirmed business data rows."); + }); + it("turns value-flow evidence into a bounded turnover answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts index 0f43bc9..e79b08c 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts @@ -30,6 +30,17 @@ function buildSequentialDeps(results: Array<{ rows: Array>, error: string | null = null) { + return { + executeAddressMcpMetadata: vi.fn(async () => ({ + fetched_rows: error ? 0 : rows.length, + raw_rows: error ? [] : rows, + rows: error ? [] : rows, + error + })) + }; +} + describe("assistant MCP discovery pilot executor", () => { it("executes only the lifecycle query_documents primitive through injected MCP deps", async () => { const planner = planAssistantMcpDiscovery({ @@ -92,6 +103,53 @@ describe("assistant MCP discovery pilot executor", () => { expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled(); }); + it("executes inspect_1c_metadata and derives a confirmed metadata surface", async () => { + const planner = planAssistantMcpDiscovery({ + turnMeaning: { + asked_domain_family: "metadata", + asked_action_family: "inspect_documents", + explicit_entity_candidates: ["НДС"] + } + }); + const deps = buildMetadataDeps([ + { + FullName: "Документ.СчетФактураВыданный", + MetaType: "Документ", + attributes: [{ Name: "Дата" }, { Name: "Организация" }] + }, + { + FullName: "Документ.СчетФактураПолученный", + MetaType: "Документ", + attributes: [{ Name: "Контрагент" }] + } + ]); + + const result = await executeAssistantMcpDiscoveryPilot(planner, deps); + + expect(result.pilot_status).toBe("executed"); + expect(result.pilot_scope).toBe("metadata_inspection_v1"); + expect(result.mcp_execution_performed).toBe(true); + expect(result.executed_primitives).toEqual(["inspect_1c_metadata"]); + expect(result.evidence.evidence_status).toBe("confirmed"); + expect(result.source_rows_summary).toBe("2 MCP metadata rows fetched"); + expect(result.derived_metadata_surface).toMatchObject({ + metadata_scope: "НДС", + requested_meta_types: ["Документ"], + matched_rows: 2, + available_entity_sets: ["Документ"], + matched_objects: ["Документ.СчетФактураВыданный", "Документ.СчетФактураПолученный"], + available_fields: ["Дата", "Организация", "Контрагент"], + inference_basis: "confirmed_1c_metadata_surface_rows" + }); + expect(result.reason_codes).toContain("pilot_inspect_1c_metadata_mcp_executed"); + expect(result.reason_codes).toContain("pilot_derived_metadata_surface_from_confirmed_rows"); + expect(deps.executeAddressMcpMetadata).toHaveBeenCalledTimes(1); + expect(deps.executeAddressMcpMetadata.mock.calls[0]?.[0]).toMatchObject({ + meta_type: ["Документ"], + name_mask: "НДС" + }); + }); + it("executes value-flow query_movements and derives a guarded turnover sum", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts index 99e33cc..8f1fd73 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts @@ -120,6 +120,19 @@ describe("assistant MCP discovery planner", () => { expect(result.catalog_review.evidence_floors.inspect_1c_metadata).toBe("source_summary"); }); + it("keeps metadata document inspection on inspect_1c_metadata instead of query_documents", () => { + const result = planAssistantMcpDiscovery({ + turnMeaning: { + asked_domain_family: "metadata", + asked_action_family: "inspect_documents" + } + }); + + expect(result.planner_status).toBe("ready_for_execution"); + expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]); + expect(result.proposed_primitives).not.toContain("query_documents"); + }); + it("does not mark an unclassified turn as executable without turn meaning context", () => { const result = planAssistantMcpDiscovery({}); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeEntryPoint.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeEntryPoint.test.ts index 761dba1..cf6274f 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeEntryPoint.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeEntryPoint.test.ts @@ -13,6 +13,17 @@ function buildDeps(rows: Array>, error: string | null = }; } +function buildMetadataDeps(rows: Array>, error: string | null = null) { + return { + executeAddressMcpMetadata: vi.fn(async () => ({ + fetched_rows: error ? 0 : rows.length, + raw_rows: error ? [] : rows, + rows: error ? [] : rows, + error + })) + }; +} + describe("assistant MCP discovery runtime entry point", () => { it("runs the bridge for discovery-eligible lifecycle turn context", async () => { const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({ @@ -78,4 +89,27 @@ describe("assistant MCP discovery runtime entry point", () => { expect(result.bridge?.hot_runtime_wired).toBe(false); expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn"); }); + + it("runs the bridge for raw metadata wording without an exact route owner", async () => { + const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({ + userMessage: "какие документы и поля есть в 1С по НДС?", + deps: buildMetadataDeps([ + { + FullName: "Документ.СчетФактураВыданный", + MetaType: "Документ", + attributes: [{ Name: "Дата" }] + } + ]) + }); + + expect(result.entry_status).toBe("bridge_executed"); + expect(result.discovery_attempted).toBe(true); + expect(result.turn_input.semantic_data_need).toBe("1C metadata evidence"); + expect(result.turn_input.turn_meaning_ref).toMatchObject({ + asked_domain_family: "metadata", + asked_action_family: "inspect_fields" + }); + expect(result.bridge?.pilot.pilot_scope).toBe("metadata_inspection_v1"); + expect(result.bridge?.answer_draft.headline).toContain("метаданным 1С"); + }); }); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts index aa522e6..d868de4 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts @@ -151,6 +151,24 @@ describe("assistant MCP discovery turn input adapter", () => { expect(result.reason_codes).toContain("mcp_discovery_monthly_aggregation_signal_detected"); }); + it("bootstraps metadata discovery from raw schema wording", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "какие регистры и поля есть в 1С по НДС?" + }); + + expect(result.adapter_status).toBe("ready"); + expect(result.should_run_discovery).toBe(true); + expect(result.source_signal).toBe("raw_text"); + expect(result.semantic_data_need).toBe("1C metadata evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "metadata", + asked_action_family: "inspect_fields", + unsupported_but_understood_family: "1c_metadata_surface", + stale_replay_forbidden: true + }); + expect(result.reason_codes).toContain("mcp_discovery_metadata_signal_detected"); + }); + it("seeds short monthly follow-up from prior bidirectional discovery context", () => { const result = buildAssistantMcpDiscoveryTurnInput({ userMessage: "а по месяцам?",