ARCH: удерживать mixed metadata ambiguity как явное clarification state
This commit is contained in:
parent
c0b3296953
commit
40cf71d118
|
|
@ -109,6 +109,13 @@ function isMetadataPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): bo
|
|||
return pilot.pilot_scope === "metadata_inspection_v1";
|
||||
}
|
||||
|
||||
function isMetadataLaneChoiceClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
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: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null
|
||||
): string | null {
|
||||
|
|
@ -167,6 +174,12 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
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С.";
|
||||
}
|
||||
|
|
@ -177,6 +190,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
}
|
||||
|
||||
function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
|
||||
return "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам.";
|
||||
}
|
||||
if (mode === "needs_clarification") {
|
||||
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,16 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
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");
|
||||
|
|
|
|||
|
|
@ -601,6 +601,19 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
!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 ||
|
||||
|
|
@ -613,23 +626,30 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
metadataAmbiguityCollapsedMovementLaneContinuationApplicable;
|
||||
const effectiveMetadataFollowupSeedApplicable =
|
||||
metadataFollowupSeedApplicable &&
|
||||
!metadataAmbiguityLaneClarificationApplicable &&
|
||||
!metadataGroundedDocumentLaneApplicable &&
|
||||
!metadataGroundedMovementLaneApplicable;
|
||||
const seededDomain = metadataGroundedDocumentLaneApplicable
|
||||
const seededDomain = metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "documents"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "movements"
|
||||
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
|
||||
? followupSeed.domain
|
||||
: null;
|
||||
const seededAction = metadataGroundedDocumentLaneApplicable
|
||||
const seededAction = metadataAmbiguityLaneClarificationApplicable
|
||||
? "resolve_next_lane"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "list_documents"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "list_movements"
|
||||
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
|
||||
? followupSeed.action
|
||||
: null;
|
||||
const seededUnsupported = metadataGroundedDocumentLaneApplicable
|
||||
const seededUnsupported = metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata_lane_choice_clarification"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "document_evidence"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "movement_evidence"
|
||||
|
|
@ -649,14 +669,16 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
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);
|
||||
|
|
@ -719,6 +741,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
? "movement_evidence"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "document_evidence"
|
||||
: metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata_lane_choice_clarification"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "1c_metadata_surface"
|
||||
: followupDiscoverySeedApplicable
|
||||
|
|
@ -731,6 +755,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
valueFlowSignal ||
|
||||
metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable ||
|
||||
rawMetadataSignal ||
|
||||
effectiveMetadataFollowupSeedApplicable ||
|
||||
followupDiscoverySeedApplicable
|
||||
|
|
@ -773,13 +798,14 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
followupDiscoverySeedApplicable:
|
||||
followupDiscoverySeedApplicable ||
|
||||
effectiveMetadataFollowupSeedApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable ||
|
||||
metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable
|
||||
});
|
||||
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
|
||||
const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning
|
||||
? "assistant_turn_meaning"
|
||||
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
|
||||
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable || metadataAmbiguityLaneClarificationApplicable
|
||||
? "followup_context"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "followup_context"
|
||||
|
|
@ -840,6 +866,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
|
|||
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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,6 +162,27 @@ describe("assistant MCP discovery answer adapter", () => {
|
|||
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
|
||||
});
|
||||
|
||||
it("asks for an explicit lane choice when mixed metadata ambiguity cannot continue on a neutral follow-up", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "resolve_next_lane",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "metadata_lane_choice_clarification"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.answer_mode).toBe("needs_clarification");
|
||||
expect(draft.headline).toContain("data-lane");
|
||||
expect(draft.next_step_line).toContain("по документам");
|
||||
expect(draft.next_step_line).toContain("по движениям/регистрам");
|
||||
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: {
|
||||
|
|
|
|||
|
|
@ -152,6 +152,26 @@ describe("assistant MCP discovery planner", () => {
|
|||
expect(result.proposed_primitives).not.toContain("query_documents");
|
||||
});
|
||||
|
||||
it("keeps metadata lane-choice clarification in needs_clarification without launching MCP primitives", () => {
|
||||
const result = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "resolve_next_lane",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "metadata_lane_choice_clarification"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.planner_status).toBe("needs_clarification");
|
||||
expect(result.semantic_data_need).toBe("metadata lane clarification");
|
||||
expect(result.proposed_primitives).toEqual([]);
|
||||
expect(result.required_axes).toEqual(["counterparty", "period", "lane_family_choice"]);
|
||||
expect(result.discovery_plan.plan_status).toBe("needs_clarification");
|
||||
expect(result.reason_codes).toContain("planner_selected_metadata_lane_clarification_recipe");
|
||||
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
|
||||
});
|
||||
|
||||
it("does not mark an unclassified turn as executable without turn meaning context", () => {
|
||||
const result = planAssistantMcpDiscovery({});
|
||||
|
||||
|
|
|
|||
|
|
@ -351,6 +351,34 @@ describe("assistant MCP discovery response candidate", () => {
|
|||
expect(candidate.eligible_for_future_hot_runtime).toBe(true);
|
||||
});
|
||||
|
||||
it("surfaces metadata lane-choice clarification as a user-facing clarification candidate", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
bridge: {
|
||||
bridge_status: "needs_clarification",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: false,
|
||||
requires_user_clarification: true,
|
||||
answer_draft: {
|
||||
answer_mode: "needs_clarification",
|
||||
headline: "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.",
|
||||
confirmed_lines: [],
|
||||
inference_lines: [],
|
||||
unknown_lines: [],
|
||||
limitation_lines: [],
|
||||
next_step_line: "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам."
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.candidate_status).toBe("clarification_candidate");
|
||||
expect(candidate.reply_type).toBe("clarification_required");
|
||||
expect(candidate.reply_text).toContain("data-lane");
|
||||
expect(candidate.reply_text).toContain("по документам");
|
||||
expect(candidate.reply_text).toContain("по движениям/регистрам");
|
||||
});
|
||||
|
||||
it("does not expose unsupported bridge output as a future hot candidate", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
|
|
|
|||
|
|
@ -570,6 +570,38 @@ describe("assistant MCP discovery turn input adapter", () => {
|
|||
expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_collapsed_to_movement_lane");
|
||||
});
|
||||
|
||||
it("requires an explicit lane choice on a generic downstream follow-up when metadata ambiguity stays mixed", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "давай дальше",
|
||||
followupContext: {
|
||||
previous_discovery_pilot_scope: "metadata_inspection_v1",
|
||||
previous_discovery_metadata_ambiguity_detected: true,
|
||||
previous_discovery_metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"],
|
||||
previous_filters: {
|
||||
counterparty: "SVK",
|
||||
period_from: "2020-01-01",
|
||||
period_to: "2020-12-31"
|
||||
},
|
||||
previous_anchor_type: "counterparty",
|
||||
previous_anchor_value: "SVK"
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.should_run_discovery).toBe(true);
|
||||
expect(result.source_signal).toBe("followup_context");
|
||||
expect(result.semantic_data_need).toBe("metadata lane clarification");
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "metadata",
|
||||
asked_action_family: "resolve_next_lane",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "metadata_lane_choice_clarification",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_requires_lane_choice");
|
||||
});
|
||||
|
||||
it("switches the checked year on a short payout follow-up while keeping prior discovery counterparty", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "а теперь за 2021?",
|
||||
|
|
|
|||
Loading…
Reference in New Issue