ARCH: продолжить metadata continuity для MCP discovery

This commit is contained in:
dctouch 2026-04-21 22:14:12 +03:00
parent 561b4ea45c
commit d66e2bfb01
9 changed files with 348 additions and 26 deletions

View File

@ -112,6 +112,29 @@ function localizeLine(value) {
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) { if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С."; return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
} }
const metadataSurfaceMatch = value.match(/^Confirmed 1C metadata surface(?: for scope "([^"]+)")?: (\d+) rows and (\d+) matching objects$/i);
if (metadataSurfaceMatch) {
const scopePart = metadataSurfaceMatch[1] ? ` по области "${metadataSurfaceMatch[1]}"` : "";
return `В 1С подтверждена metadata-поверхность${scopePart}: ${metadataSurfaceMatch[2]} строк metadata-ответа и ${metadataSurfaceMatch[3]} совпавших объекта(ов).`;
}
const metadataObjectSetsMatch = value.match(/^Available metadata object sets: (.+)$/i);
if (metadataObjectSetsMatch) {
return `Доступные типы metadata-объектов: ${metadataObjectSetsMatch[1]}.`;
}
const metadataFieldsMatch = value.match(/^Available metadata fields\/sections: (.+)$/i);
if (metadataFieldsMatch) {
return `Доступные metadata-поля/секции: ${metadataFieldsMatch[1]}.`;
}
if (/^Detailed metadata fields were not returned by this MCP metadata probe$/i.test(value)) {
return "Эта MCP-проверка metadata не вернула детальный список полей.";
}
const noMatchingMetadataScopeMatch = value.match(/^No matching 1C metadata objects were confirmed for scope "([^"]+)"$/i);
if (noMatchingMetadataScopeMatch) {
return `В 1С не подтверждены metadata-объекты по области "${noMatchingMetadataScopeMatch[1]}".`;
}
if (/^No matching 1C metadata objects were confirmed by this MCP metadata probe$/i.test(value)) {
return "В 1С эта MCP-проверка не подтвердила подходящих metadata-объектов.";
}
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) { if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
return "Юридическая дата регистрации этим поиском не подтверждена."; return "Юридическая дата регистрации этим поиском не подтверждена.";
} }

View File

@ -141,6 +141,13 @@ function mapPilotScopeToFollowupMeaning(pilotScope) {
unsupported: "counterparty_bidirectional_value_flow_or_netting" unsupported: "counterparty_bidirectional_value_flow_or_netting"
}; };
} }
if (pilotScope === "metadata_inspection_v1") {
return {
domain: "metadata",
action: "inspect_catalog",
unsupported: "1c_metadata_surface"
};
}
return { return {
domain: null, domain: null,
action: null, action: null,
@ -183,6 +190,7 @@ function collectFollowupDiscoverySeed(followupContext) {
const mapped = mapPilotScopeToFollowupMeaning(pilotScope).domain !== null const mapped = mapPilotScopeToFollowupMeaning(pilotScope).domain !== null
? mapPilotScopeToFollowupMeaning(pilotScope) ? mapPilotScopeToFollowupMeaning(pilotScope)
: mapAddressIntentToFollowupMeaning(previousIntent); : mapAddressIntentToFollowupMeaning(previousIntent);
const discoveryEntities = collectEntityCandidates(followupContext?.previous_discovery_entity_candidates);
const counterparty = toNonEmptyString(previousFilters?.counterparty) ?? const counterparty = toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty) ?? toNonEmptyString(rootFilters?.counterparty) ??
(toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty" (toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty"
@ -201,6 +209,7 @@ function collectFollowupDiscoverySeed(followupContext) {
action: mapped.action, action: mapped.action,
unsupported: mapped.unsupported, unsupported: mapped.unsupported,
counterparty, counterparty,
discoveryEntity: discoveryEntities[0] ?? null,
organization, organization,
dateScope dateScope
}; };
@ -227,6 +236,9 @@ function hasMetadataSignal(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) && 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)); /(?:\u0435\u0441\u0442\u044c|\u0434\u043e\u0441\u0442\u0443\u043f\u043d|\u0432\s+1\u0441|available|exist)/iu.test(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 metadataActionFromRawText(text) { function metadataActionFromRawText(text) {
if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) { if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) {
return "inspect_fields"; return "inspect_fields";
@ -322,9 +334,13 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
!rawLifecycleSignal && !rawLifecycleSignal &&
!rawValueFlowSignal && !rawValueFlowSignal &&
(monthlyAggregationSignal || explicitDateScopeLiteralDetected || predecomposeDateScope)); (monthlyAggregationSignal || explicitDateScopeLiteralDetected || predecomposeDateScope));
const seededDomain = followupDiscoverySeedApplicable ? followupSeed.domain : null; const metadataFollowupSeedApplicable = Boolean(followupSeed.domain === "metadata" &&
const seededAction = followupDiscoverySeedApplicable ? followupSeed.action : null; !rawLifecycleSignal &&
const seededUnsupported = followupDiscoverySeedApplicable ? followupSeed.unsupported : null; !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 lifecycleSignal = rawLifecycleSignal || seededDomain === "counterparty_lifecycle"; const lifecycleSignal = rawLifecycleSignal || seededDomain === "counterparty_lifecycle";
const bidirectionalValueFlowSignal = !lifecycleSignal && const bidirectionalValueFlowSignal = !lifecycleSignal &&
(rawBidirectionalValueFlowSignal || seededAction === "net_value_flow"); (rawBidirectionalValueFlowSignal || seededAction === "net_value_flow");
@ -338,11 +354,14 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
unsupported: unsupported ?? seededUnsupported, unsupported: unsupported ?? seededUnsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal, valueFlowSignal,
metadataSignal: rawMetadataSignal metadataSignal: rawMetadataSignal || metadataFollowupSeedApplicable
}); });
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates); const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty); pushUnique(entityCandidates, predecomposeEntities.counterparty);
pushUnique(entityCandidates, followupSeed.counterparty); pushUnique(entityCandidates, followupSeed.counterparty);
if ((rawMetadataSignal || metadataFollowupSeedApplicable) && !followupSeed.counterparty) {
pushUnique(entityCandidates, followupSeed.discoveryEntity);
}
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) { if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
pushUnique(entityCandidates, predecomposeEntities.organization); pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization); pushUnique(entityCandidates, followupSeed.organization);
@ -356,7 +375,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "counterparty_lifecycle" ? "counterparty_lifecycle"
: valueFlowSignal : valueFlowSignal
? "counterparty_value" ? "counterparty_value"
: rawMetadataSignal : rawMetadataSignal || metadataFollowupSeedApplicable
? "metadata" ? "metadata"
: rawDomain ?? seededDomain, : rawDomain ?? seededDomain,
asked_action_family: lifecycleSignal asked_action_family: lifecycleSignal
@ -367,8 +386,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: payoutSignal : payoutSignal
? "payout" ? "payout"
: rawAction ?? seededAction ?? "turnover" : rawAction ?? seededAction ?? "turnover"
: rawMetadataSignal : rawMetadataSignal || metadataFollowupSeedApplicable
? metadataActionFromRawText(rawText) ? metadataActionFromRawText(rawText) ?? seededAction
: rawAction ?? seededAction, : rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis, asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
explicit_entity_candidates: entityCandidates, explicit_entity_candidates: entityCandidates,
@ -383,7 +402,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: payoutSignal : payoutSignal
? "counterparty_payouts_or_outflow" ? "counterparty_payouts_or_outflow"
: seededUnsupported ?? "counterparty_value_or_turnover" : seededUnsupported ?? "counterparty_value_or_turnover"
: rawMetadataSignal : rawMetadataSignal || metadataFollowupSeedApplicable
? "1c_metadata_surface" ? "1c_metadata_surface"
: followupDiscoverySeedApplicable : followupDiscoverySeedApplicable
? seededUnsupported ? seededUnsupported
@ -393,6 +412,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
lifecycleSignal || lifecycleSignal ||
valueFlowSignal || valueFlowSignal ||
rawMetadataSignal || rawMetadataSignal ||
metadataFollowupSeedApplicable ||
followupDiscoverySeedApplicable) followupDiscoverySeedApplicable)
}; };
const cleanTurnMeaning = {}; const cleanTurnMeaning = {};
@ -424,15 +444,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
unsupported: unsupported ?? seededUnsupported, unsupported: unsupported ?? seededUnsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal, valueFlowSignal,
metadataSignal: rawMetadataSignal, metadataSignal: rawMetadataSignal || metadataFollowupSeedApplicable,
semanticDataNeed, semanticDataNeed,
explicitIntentCandidate, explicitIntentCandidate,
followupDiscoverySeedApplicable followupDiscoverySeedApplicable: followupDiscoverySeedApplicable || metadataFollowupSeedApplicable
}); });
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0; const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
const sourceSignal = assistantTurnMeaning const sourceSignal = assistantTurnMeaning
? "assistant_turn_meaning" ? "assistant_turn_meaning"
: followupDiscoverySeedApplicable : followupDiscoverySeedApplicable || metadataFollowupSeedApplicable
? "followup_context" ? "followup_context"
: predecomposeContract : predecomposeContract
? "predecompose_contract" ? "predecompose_contract"
@ -440,7 +460,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "raw_text" ? "raw_text"
: valueFlowSignal : valueFlowSignal
? "raw_text" ? "raw_text"
: rawMetadataSignal : rawMetadataSignal || metadataFollowupSeedApplicable
? "raw_text" ? "raw_text"
: "none"; : "none";
if (lifecycleSignal) { if (lifecycleSignal) {
@ -464,6 +484,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (followupDiscoverySeedApplicable) { if (followupDiscoverySeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context"); pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context");
} }
if (metadataFollowupSeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_seeded_from_followup_context");
}
if (unsupported) { if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn"); pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
} }

View File

@ -34,8 +34,30 @@ function periodPartForRecap(scopedDate) {
} }
return /^\d{2}\.\d{2}\.\d{4}$/.test(scopedDate) ? ` на ${scopedDate}` : ` за период ${scopedDate}`; return /^\d{2}\.\d{2}\.\d{4}$/.test(scopedDate) ? ` на ${scopedDate}` : ` за период ${scopedDate}`;
} }
function readDiscoveryMetadataScope(debug) {
const discoveryEntry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
const bridge = toRecordObject(discoveryEntry?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const surface = toRecordObject(pilot?.derived_metadata_surface);
const surfaceScope = toNonEmptyString(surface?.metadata_scope);
if (surfaceScope) {
return surfaceScope;
}
const turnInput = toRecordObject(discoveryEntry?.turn_input);
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
const entityCandidates = Array.isArray(turnMeaningRef?.explicit_entity_candidates)
? turnMeaningRef.explicit_entity_candidates
: [];
for (const candidate of entityCandidates) {
const text = toNonEmptyString(candidate);
if (text) {
return text;
}
}
return null;
}
function buildDiscoveryRecapFactLine(input) { function buildDiscoveryRecapFactLine(input) {
if (!input.debug || !input.counterparty) { if (!input.debug) {
return null; return null;
} }
const pilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(input.debug, toNonEmptyString); const pilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(input.debug, toNonEmptyString);
@ -43,6 +65,34 @@ function buildDiscoveryRecapFactLine(input) {
const bridge = toRecordObject(discoveryEntry?.bridge); const bridge = toRecordObject(discoveryEntry?.bridge);
const pilot = toRecordObject(bridge?.pilot); const pilot = toRecordObject(bridge?.pilot);
const periodPart = periodPartForRecap(input.scopedDate); const periodPart = periodPartForRecap(input.scopedDate);
if (pilotScope === "metadata_inspection_v1") {
const metadataScope = readDiscoveryMetadataScope(input.debug);
const surface = toRecordObject(pilot?.derived_metadata_surface);
const entitySets = Array.isArray(surface?.available_entity_sets)
? surface.available_entity_sets
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item))
: [];
const fields = Array.isArray(surface?.available_fields)
? surface.available_fields
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item))
: [];
const objects = Array.isArray(surface?.matched_objects)
? surface.matched_objects
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item))
: [];
const rows = Number(surface?.matched_rows ?? 0);
const scopePart = metadataScope ? ` по области «${metadataScope}»` : "";
const objectsPart = objects.length > 0 ? `, нашли объекты ${objects.slice(0, 4).join(", ")}` : "";
const entitySetsPart = entitySets.length > 0 ? `, видны типы ${entitySets.slice(0, 4).join(", ")}` : "";
const fieldsPart = fields.length > 0 ? `, доступны поля/секции ${fields.slice(0, 5).join(", ")}` : "";
return `смотрели metadata-поверхность 1С${scopePart}${periodPart}: ${rows} подтвержденных строк${objectsPart}${entitySetsPart}${fieldsPart}`.trim();
}
if (!input.counterparty) {
return null;
}
if (pilotScope === "counterparty_lifecycle_query_documents_v1") { if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
const activityPeriod = toRecordObject(pilot?.derived_activity_period); const activityPeriod = toRecordObject(pilot?.derived_activity_period);
const duration = toNonEmptyString(activityPeriod?.duration_human_ru); const duration = toNonEmptyString(activityPeriod?.duration_human_ru);

View File

@ -146,6 +146,31 @@ function localizeLine(value: string): string {
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) { if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С."; return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
} }
const metadataSurfaceMatch = value.match(
/^Confirmed 1C metadata surface(?: for scope "([^"]+)")?: (\d+) rows and (\d+) matching objects$/i
);
if (metadataSurfaceMatch) {
const scopePart = metadataSurfaceMatch[1] ? ` по области "${metadataSurfaceMatch[1]}"` : "";
return `В 1С подтверждена metadata-поверхность${scopePart}: ${metadataSurfaceMatch[2]} строк metadata-ответа и ${metadataSurfaceMatch[3]} совпавших объекта(ов).`;
}
const metadataObjectSetsMatch = value.match(/^Available metadata object sets: (.+)$/i);
if (metadataObjectSetsMatch) {
return `Доступные типы metadata-объектов: ${metadataObjectSetsMatch[1]}.`;
}
const metadataFieldsMatch = value.match(/^Available metadata fields\/sections: (.+)$/i);
if (metadataFieldsMatch) {
return `Доступные metadata-поля/секции: ${metadataFieldsMatch[1]}.`;
}
if (/^Detailed metadata fields were not returned by this MCP metadata probe$/i.test(value)) {
return "Эта MCP-проверка metadata не вернула детальный список полей.";
}
const noMatchingMetadataScopeMatch = value.match(/^No matching 1C metadata objects were confirmed for scope "([^"]+)"$/i);
if (noMatchingMetadataScopeMatch) {
return `В 1С не подтверждены metadata-объекты по области "${noMatchingMetadataScopeMatch[1]}".`;
}
if (/^No matching 1C metadata objects were confirmed by this MCP metadata probe$/i.test(value)) {
return "В 1С эта MCP-проверка не подтвердила подходящих metadata-объектов.";
}
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) { if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
return "Юридическая дата регистрации этим поиском не подтверждена."; return "Юридическая дата регистрации этим поиском не подтверждена.";
} }

View File

@ -190,6 +190,13 @@ function mapPilotScopeToFollowupMeaning(
unsupported: "counterparty_bidirectional_value_flow_or_netting" unsupported: "counterparty_bidirectional_value_flow_or_netting"
}; };
} }
if (pilotScope === "metadata_inspection_v1") {
return {
domain: "metadata",
action: "inspect_catalog",
unsupported: "1c_metadata_surface"
};
}
return { return {
domain: null, domain: null,
action: null, action: null,
@ -238,6 +245,7 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
action: string | null; action: string | null;
unsupported: string | null; unsupported: string | null;
counterparty: string | null; counterparty: string | null;
discoveryEntity: string | null;
organization: string | null; organization: string | null;
dateScope: string | null; dateScope: string | null;
} { } {
@ -250,6 +258,7 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
mapPilotScopeToFollowupMeaning(pilotScope).domain !== null mapPilotScopeToFollowupMeaning(pilotScope).domain !== null
? mapPilotScopeToFollowupMeaning(pilotScope) ? mapPilotScopeToFollowupMeaning(pilotScope)
: mapAddressIntentToFollowupMeaning(previousIntent); : mapAddressIntentToFollowupMeaning(previousIntent);
const discoveryEntities = collectEntityCandidates(followupContext?.previous_discovery_entity_candidates);
const counterparty = const counterparty =
toNonEmptyString(previousFilters?.counterparty) ?? toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty) ?? toNonEmptyString(rootFilters?.counterparty) ??
@ -271,6 +280,7 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
action: mapped.action, action: mapped.action,
unsupported: mapped.unsupported, unsupported: mapped.unsupported,
counterparty, counterparty,
discoveryEntity: discoveryEntities[0] ?? null,
organization, organization,
dateScope dateScope
}; };
@ -322,6 +332,12 @@ function hasMetadataSignal(text: string): boolean {
); );
} }
function hasMetadataObjectHint(text: string): boolean {
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 metadataActionFromRawText(text: string): string { function metadataActionFromRawText(text: string): string {
if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) { if (/(?:\u043f\u043e\u043b(?:\u0435|\u044f)|field)/iu.test(text)) {
return "inspect_fields"; return "inspect_fields";
@ -443,9 +459,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
!rawValueFlowSignal && !rawValueFlowSignal &&
(monthlyAggregationSignal || explicitDateScopeLiteralDetected || predecomposeDateScope) (monthlyAggregationSignal || explicitDateScopeLiteralDetected || predecomposeDateScope)
); );
const seededDomain = followupDiscoverySeedApplicable ? followupSeed.domain : null; const metadataFollowupSeedApplicable = Boolean(
const seededAction = followupDiscoverySeedApplicable ? followupSeed.action : null; followupSeed.domain === "metadata" &&
const seededUnsupported = followupDiscoverySeedApplicable ? followupSeed.unsupported : null; !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 lifecycleSignal = const lifecycleSignal =
rawLifecycleSignal || seededDomain === "counterparty_lifecycle"; rawLifecycleSignal || seededDomain === "counterparty_lifecycle";
const bidirectionalValueFlowSignal = const bidirectionalValueFlowSignal =
@ -463,11 +485,14 @@ export function buildAssistantMcpDiscoveryTurnInput(
unsupported: unsupported ?? seededUnsupported, unsupported: unsupported ?? seededUnsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal, valueFlowSignal,
metadataSignal: rawMetadataSignal metadataSignal: rawMetadataSignal || metadataFollowupSeedApplicable
}); });
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates); const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty); pushUnique(entityCandidates, predecomposeEntities.counterparty);
pushUnique(entityCandidates, followupSeed.counterparty); pushUnique(entityCandidates, followupSeed.counterparty);
if ((rawMetadataSignal || metadataFollowupSeedApplicable) && !followupSeed.counterparty) {
pushUnique(entityCandidates, followupSeed.discoveryEntity);
}
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) { if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
pushUnique(entityCandidates, predecomposeEntities.organization); pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization); pushUnique(entityCandidates, followupSeed.organization);
@ -484,7 +509,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "counterparty_lifecycle" ? "counterparty_lifecycle"
: valueFlowSignal : valueFlowSignal
? "counterparty_value" ? "counterparty_value"
: rawMetadataSignal : rawMetadataSignal || metadataFollowupSeedApplicable
? "metadata" ? "metadata"
: rawDomain ?? seededDomain, : rawDomain ?? seededDomain,
asked_action_family: lifecycleSignal asked_action_family: lifecycleSignal
@ -495,8 +520,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
: payoutSignal : payoutSignal
? "payout" ? "payout"
: rawAction ?? seededAction ?? "turnover" : rawAction ?? seededAction ?? "turnover"
: rawMetadataSignal : rawMetadataSignal || metadataFollowupSeedApplicable
? metadataActionFromRawText(rawText) ? metadataActionFromRawText(rawText) ?? seededAction
: rawAction ?? seededAction, : rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis, asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
explicit_entity_candidates: entityCandidates, explicit_entity_candidates: entityCandidates,
@ -512,7 +537,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
: payoutSignal : payoutSignal
? "counterparty_payouts_or_outflow" ? "counterparty_payouts_or_outflow"
: seededUnsupported ?? "counterparty_value_or_turnover" : seededUnsupported ?? "counterparty_value_or_turnover"
: rawMetadataSignal : rawMetadataSignal || metadataFollowupSeedApplicable
? "1c_metadata_surface" ? "1c_metadata_surface"
: followupDiscoverySeedApplicable : followupDiscoverySeedApplicable
? seededUnsupported ? seededUnsupported
@ -523,6 +548,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
lifecycleSignal || lifecycleSignal ||
valueFlowSignal || valueFlowSignal ||
rawMetadataSignal || rawMetadataSignal ||
metadataFollowupSeedApplicable ||
followupDiscoverySeedApplicable followupDiscoverySeedApplicable
) )
}; };
@ -557,15 +583,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
unsupported: unsupported ?? seededUnsupported, unsupported: unsupported ?? seededUnsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal, valueFlowSignal,
metadataSignal: rawMetadataSignal, metadataSignal: rawMetadataSignal || metadataFollowupSeedApplicable,
semanticDataNeed, semanticDataNeed,
explicitIntentCandidate, explicitIntentCandidate,
followupDiscoverySeedApplicable followupDiscoverySeedApplicable: followupDiscoverySeedApplicable || metadataFollowupSeedApplicable
}); });
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0; const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning
? "assistant_turn_meaning" ? "assistant_turn_meaning"
: followupDiscoverySeedApplicable : followupDiscoverySeedApplicable || metadataFollowupSeedApplicable
? "followup_context" ? "followup_context"
: predecomposeContract : predecomposeContract
? "predecompose_contract" ? "predecompose_contract"
@ -573,7 +599,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "raw_text" ? "raw_text"
: valueFlowSignal : valueFlowSignal
? "raw_text" ? "raw_text"
: rawMetadataSignal : rawMetadataSignal || metadataFollowupSeedApplicable
? "raw_text" ? "raw_text"
: "none"; : "none";
@ -598,6 +624,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (followupDiscoverySeedApplicable) { if (followupDiscoverySeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context"); pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context");
} }
if (metadataFollowupSeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_seeded_from_followup_context");
}
if (unsupported) { if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn"); pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
} }

View File

@ -77,12 +77,35 @@ function periodPartForRecap(scopedDate: string | null): string {
return /^\d{2}\.\d{2}\.\d{4}$/.test(scopedDate) ? ` на ${scopedDate}` : ` за период ${scopedDate}`; return /^\d{2}\.\d{2}\.\d{4}$/.test(scopedDate) ? ` на ${scopedDate}` : ` за период ${scopedDate}`;
} }
function readDiscoveryMetadataScope(debug: Record<string, unknown> | null): string | null {
const discoveryEntry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
const bridge = toRecordObject(discoveryEntry?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const surface = toRecordObject(pilot?.derived_metadata_surface);
const surfaceScope = toNonEmptyString(surface?.metadata_scope);
if (surfaceScope) {
return surfaceScope;
}
const turnInput = toRecordObject(discoveryEntry?.turn_input);
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
const entityCandidates = Array.isArray(turnMeaningRef?.explicit_entity_candidates)
? turnMeaningRef.explicit_entity_candidates
: [];
for (const candidate of entityCandidates) {
const text = toNonEmptyString(candidate);
if (text) {
return text;
}
}
return null;
}
function buildDiscoveryRecapFactLine(input: { function buildDiscoveryRecapFactLine(input: {
debug: Record<string, unknown> | null; debug: Record<string, unknown> | null;
counterparty: string | null; counterparty: string | null;
scopedDate: string | null; scopedDate: string | null;
}): string | null { }): string | null {
if (!input.debug || !input.counterparty) { if (!input.debug) {
return null; return null;
} }
const pilotScope = readAssistantMcpDiscoveryPilotScope(input.debug, toNonEmptyString); const pilotScope = readAssistantMcpDiscoveryPilotScope(input.debug, toNonEmptyString);
@ -90,6 +113,36 @@ function buildDiscoveryRecapFactLine(input: {
const bridge = toRecordObject(discoveryEntry?.bridge); const bridge = toRecordObject(discoveryEntry?.bridge);
const pilot = toRecordObject(bridge?.pilot); const pilot = toRecordObject(bridge?.pilot);
const periodPart = periodPartForRecap(input.scopedDate); const periodPart = periodPartForRecap(input.scopedDate);
if (pilotScope === "metadata_inspection_v1") {
const metadataScope = readDiscoveryMetadataScope(input.debug);
const surface = toRecordObject(pilot?.derived_metadata_surface);
const entitySets = Array.isArray(surface?.available_entity_sets)
? surface.available_entity_sets
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item))
: [];
const fields = Array.isArray(surface?.available_fields)
? surface.available_fields
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item))
: [];
const objects = Array.isArray(surface?.matched_objects)
? surface.matched_objects
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item))
: [];
const rows = Number(surface?.matched_rows ?? 0);
const scopePart = metadataScope ? ` по области «${metadataScope}»` : "";
const objectsPart = objects.length > 0 ? `, нашли объекты ${objects.slice(0, 4).join(", ")}` : "";
const entitySetsPart =
entitySets.length > 0 ? `, видны типы ${entitySets.slice(0, 4).join(", ")}` : "";
const fieldsPart =
fields.length > 0 ? `, доступны поля/секции ${fields.slice(0, 5).join(", ")}` : "";
return `смотрели metadata-поверхность 1С${scopePart}${periodPart}: ${rows} подтвержденных строк${objectsPart}${entitySetsPart}${fieldsPart}`.trim();
}
if (!input.counterparty) {
return null;
}
if (pilotScope === "counterparty_lifecycle_query_documents_v1") { if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
const activityPeriod = toRecordObject(pilot?.derived_activity_period); const activityPeriod = toRecordObject(pilot?.derived_activity_period);
const duration = toNonEmptyString(activityPeriod?.duration_human_ru); const duration = toNonEmptyString(activityPeriod?.duration_human_ru);

View File

@ -209,6 +209,39 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("broad probe hit the row limit"); expect(candidate.reply_text).not.toContain("broad probe hit the row limit");
}); });
it("localizes metadata evidence without leaking raw MCP wording", () => {
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С найдена подтвержденная metadata-поверхность.",
confirmed_lines: [
'Confirmed 1C metadata surface for scope "НДС": 7 rows and 3 matching objects',
"Available metadata object sets: accumulation_register, document",
"Available metadata fields/sections: amount, vat_rate, organization"
],
inference_lines: [],
unknown_lines: ['No matching 1C metadata objects were confirmed for scope "Прибыль"'],
limitation_lines: ["Detailed metadata fields were not returned by this MCP metadata probe"],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain('В 1С подтверждена metadata-поверхность по области "НДС"');
expect(candidate.reply_text).toContain("Доступные типы metadata-объектов");
expect(candidate.reply_text).toContain("Доступные metadata-поля/секции");
expect(candidate.reply_text).toContain('В 1С не подтверждены metadata-объекты по области "Прибыль"');
expect(candidate.reply_text).toContain("Эта MCP-проверка metadata не вернула детальный список полей");
expect(candidate.reply_text).not.toContain("Confirmed 1C metadata surface");
});
it("returns not applicable when discovery was skipped for an exact supported route", () => { it("returns not applicable when discovery was skipped for an exact supported route", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate({ const candidate = buildAssistantMcpDiscoveryResponseCandidate({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1", schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",

View File

@ -203,6 +203,34 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_date_scope_from_followup_context"); expect(result.reason_codes).toContain("mcp_discovery_date_scope_from_followup_context");
}); });
it("seeds short metadata follow-up from prior metadata discovery context", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а по регистрам?",
followupContext: {
previous_discovery_pilot_scope: "metadata_inspection_v1",
previous_filters: {
counterparty: "НДС"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "НДС"
}
});
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("1C metadata evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "metadata",
asked_action_family: "inspect_registers",
explicit_entity_candidates: ["НДС"],
unsupported_but_understood_family: "1c_metadata_surface",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_metadata_seeded_from_followup_context");
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", () => { it("switches the checked year on a short payout follow-up while keeping prior discovery counterparty", () => {
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а теперь за 2021?", userMessage: "а теперь за 2021?",

View File

@ -386,6 +386,64 @@ describe("assistantMemoryRecapPolicy", () => {
expect(reply).toContain("43 763 351,53 руб."); expect(reply).toContain("43 763 351,53 руб.");
}); });
it("builds deterministic recap summary from grounded MCP metadata discovery context", () => {
const sessionItems = [
{
role: "assistant",
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: {
explicit_entity_candidates: ["НДС"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "metadata_inspection_v1",
derived_metadata_surface: {
metadata_scope: "НДС",
matched_rows: 7,
matched_objects: ["РегистрНакопления.НДСПокупок"],
available_entity_sets: ["accumulation_register", "document"],
available_fields: ["amount", "vat_rate", "organization"]
}
}
}
}
}
}
];
const context = resolveAssistantLivingChatMemoryContext({
modeDecisionReason: "memory_recap_followup_detected",
sessionItems
});
const reply = buildAddressMemoryRecapReply({
organization: null,
addressDebug: context.lastMemoryAddressDebug,
sessionItems,
toNonEmptyString: (value: unknown) => {
const text = String(value ?? "").trim();
return text.length > 0 ? text : null;
}
});
expect(context.contextualMemoryRecapFollowup).toBe(true);
expect(reply).toContain("НДС");
expect(reply).toContain("metadata-поверхность 1С");
expect(reply).toContain("amount");
expect(reply).toContain("accumulation_register");
});
it("builds deterministic broad business evaluation summary from recent grounded organization facts", () => { it("builds deterministic broad business evaluation summary from recent grounded organization facts", () => {
const sessionItems = [ const sessionItems = [
{ {