ARCH: сохранять metadata ambiguity через clarification follow-up

This commit is contained in:
dctouch 2026-04-22 10:05:03 +03:00
parent 40cf71d118
commit 007369a78a
9 changed files with 179 additions and 39 deletions

View File

@ -73,6 +73,10 @@ function isMovementPilot(pilot) {
function isMetadataPilot(pilot) {
return pilot.pilot_scope === "metadata_inspection_v1";
}
function isMetadataLaneChoiceClarification(pilot) {
return (pilot.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe"));
}
function metadataRouteFamilyLabelRu(routeFamily) {
if (routeFamily === "document_evidence") {
return "контур документов";
@ -127,6 +131,12 @@ function headlineFor(mode, pilot) {
if (mode === "bounded_inference_only") {
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
}
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.";
}
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам.";
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
@ -136,6 +146,9 @@ function headlineFor(mode, pilot) {
return "Я проверил доступный контур, но подтвержденного факта для ответа не получил.";
}
function nextStepFor(mode, pilot) {
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам.";
}
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}

View File

@ -82,6 +82,15 @@ function recipeFor(input) {
const axes = [];
const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning);
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
pushUnique(axes, "lane_family_choice");
return {
semanticDataNeed: "metadata lane clarification",
primitives: [],
axes,
reason: "planner_selected_metadata_lane_clarification_recipe"
};
}
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value", "net", "netting", "balance", "cashflow"])) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");

View File

@ -441,6 +441,17 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
!metadataDocumentHintSignal &&
!metadataMovementHintSignal &&
hasMetadataDownstreamContinuationSignal(rawText));
const metadataAmbiguityLaneClarificationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataAmbiguityDetected &&
!metadataAmbiguityCollapsesToDocumentLane(followupSeed.metadataAmbiguityEntitySets) &&
!metadataAmbiguityCollapsesToMovementLane(followupSeed.metadataAmbiguityEntitySets) &&
followupSeed.counterparty &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
!metadataDocumentHintSignal &&
!metadataMovementHintSignal &&
hasMetadataDownstreamContinuationSignal(rawText));
const metadataGroundedDocumentLaneApplicable = metadataGroundedDocumentFollowupApplicable ||
metadataAmbiguityResolvedDocumentFollowupApplicable ||
(metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") ||
@ -450,29 +461,36 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
(metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") ||
metadataAmbiguityCollapsedMovementLaneContinuationApplicable;
const effectiveMetadataFollowupSeedApplicable = metadataFollowupSeedApplicable &&
!metadataAmbiguityLaneClarificationApplicable &&
!metadataGroundedDocumentLaneApplicable &&
!metadataGroundedMovementLaneApplicable;
const seededDomain = metadataGroundedDocumentLaneApplicable
? "documents"
: metadataGroundedMovementLaneApplicable
? "movements"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.domain
: null;
const seededAction = metadataGroundedDocumentLaneApplicable
? "list_documents"
: metadataGroundedMovementLaneApplicable
? "list_movements"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.action
: null;
const seededUnsupported = metadataGroundedDocumentLaneApplicable
? "document_evidence"
: metadataGroundedMovementLaneApplicable
? "movement_evidence"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.unsupported
: null;
const seededDomain = metadataAmbiguityLaneClarificationApplicable
? "metadata"
: metadataGroundedDocumentLaneApplicable
? "documents"
: metadataGroundedMovementLaneApplicable
? "movements"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.domain
: null;
const seededAction = metadataAmbiguityLaneClarificationApplicable
? "resolve_next_lane"
: metadataGroundedDocumentLaneApplicable
? "list_documents"
: metadataGroundedMovementLaneApplicable
? "list_movements"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.action
: null;
const seededUnsupported = metadataAmbiguityLaneClarificationApplicable
? "metadata_lane_choice_clarification"
: metadataGroundedDocumentLaneApplicable
? "document_evidence"
: metadataGroundedMovementLaneApplicable
? "movement_evidence"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.unsupported
: null;
const lifecycleSignal = rawLifecycleSignal || seededDomain === "counterparty_lifecycle";
const bidirectionalValueFlowSignal = !lifecycleSignal &&
(rawBidirectionalValueFlowSignal || seededAction === "net_value_flow");
@ -482,14 +500,16 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const payoutSignal = valueFlowSignal &&
!bidirectionalValueFlowSignal &&
(rawPayoutSignal || seededAction === "payout");
const semanticDataNeed = semanticNeedFor({
domain: rawDomain ?? seededDomain,
action: rawAction ?? seededAction,
unsupported: unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
});
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
? "metadata lane clarification"
: semanticNeedFor({
domain: rawDomain ?? seededDomain,
action: rawAction ?? seededAction,
unsupported: unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
});
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty);
pushUnique(entityCandidates, followupSeed.counterparty);
@ -533,6 +553,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
explicit_entity_candidates: entityCandidates,
metadata_ambiguity_entity_sets: metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
? followupSeed.metadataAmbiguityEntitySets
: undefined,
explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: explicitDateScope,
unsupported_but_understood_family: unsupported ??
@ -548,17 +571,20 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "movement_evidence"
: metadataGroundedDocumentLaneApplicable
? "document_evidence"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "1c_metadata_surface"
: followupDiscoverySeedApplicable
? seededUnsupported
: null),
: metadataAmbiguityLaneClarificationApplicable
? "metadata_lane_choice_clarification"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "1c_metadata_surface"
: followupDiscoverySeedApplicable
? seededUnsupported
: null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden ||
unsupported ||
lifecycleSignal ||
valueFlowSignal ||
metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable ||
metadataAmbiguityLaneClarificationApplicable ||
rawMetadataSignal ||
effectiveMetadataFollowupSeedApplicable ||
followupDiscoverySeedApplicable)
@ -576,6 +602,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
}
if ((turnMeaning.metadata_ambiguity_entity_sets?.length ?? 0) > 0) {
cleanTurnMeaning.metadata_ambiguity_entity_sets = turnMeaning.metadata_ambiguity_entity_sets;
}
if (toNonEmptyString(turnMeaning.explicit_organization_scope)) {
cleanTurnMeaning.explicit_organization_scope = turnMeaning.explicit_organization_scope;
}
@ -597,13 +626,14 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
explicitIntentCandidate,
followupDiscoverySeedApplicable: followupDiscoverySeedApplicable ||
effectiveMetadataFollowupSeedApplicable ||
metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable
});
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
const sourceSignal = assistantTurnMeaning
? "assistant_turn_meaning"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable || metadataAmbiguityLaneClarificationApplicable
? "followup_context"
: metadataGroundedMovementLaneApplicable
? "followup_context"
@ -663,6 +693,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (metadataAmbiguityCollapsedMovementLaneContinuationApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_collapsed_to_movement_lane");
}
if (metadataAmbiguityLaneClarificationApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_requires_lane_choice");
}
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}

View File

@ -142,6 +142,17 @@ function readAssistantMcpDiscoveryTurnMeaning(
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string[] {
const values = readAssistantMcpDiscoveryTurnMeaning(debug)?.metadata_ambiguity_entity_sets;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
}
function readAssistantMcpDiscoveryActionFamily(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
@ -189,7 +200,10 @@ export function readAssistantMcpDiscoveryMetadataSelectedEntitySet(
export function readAssistantMcpDiscoveryMetadataAmbiguityDetected(
debug: Record<string, unknown> | null
): boolean {
return readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_detected === true;
return (
readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_detected === true ||
readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug).length > 0
);
}
export function readAssistantMcpDiscoveryMetadataAmbiguityEntitySets(
@ -197,10 +211,10 @@ export function readAssistantMcpDiscoveryMetadataAmbiguityEntitySets(
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string[] {
const values = readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_entity_sets;
if (!Array.isArray(values)) {
return [];
if (Array.isArray(values)) {
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
}
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
return readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug, toNonEmptyString);
}
function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(

View File

@ -1562,6 +1562,9 @@ function buildEmptyEvidence(
}
function pilotScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): AssistantMcpDiscoveryPilotScope {
if (planner.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe")) {
return "metadata_inspection_v1";
}
if (isMetadataPilotEligible(planner)) {
return "metadata_inspection_v1";
}

View File

@ -25,6 +25,7 @@ export interface AssistantMcpDiscoveryTurnMeaningRef {
asked_action_family?: string | null;
asked_aggregation_axis?: string | null;
explicit_entity_candidates?: string[];
metadata_ambiguity_entity_sets?: string[];
explicit_organization_scope?: string | null;
explicit_date_scope?: string | null;
meaning_confidence?: number | null;
@ -170,6 +171,7 @@ function normalizeTurnMeaning(
const dateScope = toNonEmptyString(value.explicit_date_scope);
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
const entities = toStringList(value.explicit_entity_candidates);
const metadataAmbiguityEntitySets = toStringList(value.metadata_ambiguity_entity_sets);
if (domain) {
result.asked_domain_family = domain;
}
@ -182,6 +184,9 @@ function normalizeTurnMeaning(
if (entities.length > 0) {
result.explicit_entity_candidates = entities;
}
if (metadataAmbiguityEntitySets.length > 0) {
result.metadata_ambiguity_entity_sets = metadataAmbiguityEntitySets;
}
if (organization) {
result.explicit_organization_scope = organization;
}

View File

@ -725,6 +725,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
: rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
explicit_entity_candidates: entityCandidates,
metadata_ambiguity_entity_sets:
metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
? followupSeed.metadataAmbiguityEntitySets
: undefined,
explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: explicitDateScope,
unsupported_but_understood_family:
@ -775,6 +779,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
}
if ((turnMeaning.metadata_ambiguity_entity_sets?.length ?? 0) > 0) {
cleanTurnMeaning.metadata_ambiguity_entity_sets = turnMeaning.metadata_ambiguity_entity_sets;
}
if (toNonEmptyString(turnMeaning.explicit_organization_scope)) {
cleanTurnMeaning.explicit_organization_scope = turnMeaning.explicit_organization_scope;
}

View File

@ -595,6 +595,7 @@ describe("assistant MCP discovery turn input adapter", () => {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["SVK"],
metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "metadata_lane_choice_clarification",
stale_replay_forbidden: true

View File

@ -1227,6 +1227,61 @@ describe("assistantTransitionPolicy", () => {
"РегистрНакопления"
]);
});
it("preserves metadata ambiguity choice sets through a clarification assistant turn", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "уточните: по документам или по движениям?",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["SVK"],
metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "metadata_lane_choice_clarification"
}
},
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "metadata_inspection_v1"
},
answer_draft: {
answer_mode: "needs_clarification"
}
}
}
}
}),
hasAddressFollowupContextSignal: () => true,
hasReferentialPointer: () => false,
resolveAddressIntent: () => ({ intent: "unknown" }),
resolveAddressIntentFamily: () => null,
resolveAssistantTurnMeaning: () => null
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"по движениям",
[{ kind: "assistant", text: "уточните: по документам или по движениям?" }],
"по движениям",
{ predecomposeContract: { intent: "unknown" } },
null
);
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("metadata_inspection_v1");
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBe(true);
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_entity_sets).toEqual([
"Документ",
"РегистрНакопления"
]);
});
it("switches to VAT tax-period intent while preserving carried period filters", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({