ARCH: замкнуть multi-hop ranking clarification loop

This commit is contained in:
2026-04-23 12:20:39 +03:00
parent e4cab85dd9
commit e18b0922ad
14 changed files with 363 additions and 20 deletions
@@ -193,12 +193,24 @@ function dryRunMissingAxis(pilot, axis) {
}
return pilot.dry_run.execution_steps.some((step) => step.missing_axis_options.some((option) => option.includes(axis)));
}
function queryPlanClarificationGaps(pilot) {
const values = pilot.evidence.query_plan.clarification_gaps;
return Array.isArray(values) ? uniqueStrings(values) : [];
}
function clarificationGapMissing(pilot, axis) {
const gaps = queryPlanClarificationGaps(pilot);
if (gaps.length > 0) {
return gaps.includes(axis);
}
return dryRunMissingAxis(pilot, axis);
}
function clarificationNeedRu(pilot) {
const needsPeriod = clarificationGapMissing(pilot, "period");
const organizationScopedOpenTotal = pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
if (organizationScopedOpenTotal) {
if (organizationScopedOpenTotal && !needsPeriod) {
return {
subject: "\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e",
verb: "\u043d\u0443\u0436\u043d\u043e"
@@ -206,8 +218,7 @@ function clarificationNeedRu(pilot) {
}
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
const hasAccount = dryRunHasAxis(pilot, "account");
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = !hasCounterparty && !hasAccount && dryRunMissingAxis(pilot, "organization");
const needsOrganization = !hasCounterparty && !hasAccount && clarificationGapMissing(pilot, "organization");
if (needsPeriod && needsOrganization) {
return { subject: "проверяемый период и организацию", verb: "нужно" };
}
@@ -224,8 +235,8 @@ function clarificationNextStepLine(pilot, laneLabel) {
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
const needsPeriod = clarificationGapMissing(pilot, "period");
const needsOrganization = clarificationGapMissing(pilot, "organization");
const scopeSuffix = laneScopeSuffix(pilot);
if (organizationScopedOpenTotal && !needsPeriod) {
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
@@ -677,6 +677,7 @@ function planAssistantMcpDiscovery(input) {
turnMeaning: input.turnMeaning,
proposedPrimitives: recipe.primitives,
requiredAxes: recipe.axes,
clarificationGaps: dataNeedGraph?.clarification_gaps ?? [],
maxProbeCount: budgetOverride.maxProbeCount
});
const review = (0, assistantMcpCatalogIndex_1.reviewAssistantMcpDiscoveryPlanAgainstCatalog)(plan);
@@ -134,6 +134,7 @@ function buildAssistantMcpDiscoveryPlan(input) {
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed);
const turnMeaning = normalizeTurnMeaning(input.turnMeaning);
const requiredAxes = toStringList(input.requiredAxes);
const clarificationGaps = toStringList(input.clarificationGaps);
const proposed = toStringList(input.proposedPrimitives);
const reasonCodes = [];
const allowedPrimitives = [];
@@ -194,6 +195,7 @@ function buildAssistantMcpDiscoveryPlan(input) {
allowed_primitives: allowedPrimitives,
rejected_primitives: rejectedPrimitives,
required_axes: requiredAxes,
clarification_gaps: clarificationGaps,
execution_budget: {
max_probe_count: clampInteger(input.maxProbeCount, DEFAULT_DISCOVERY_BUDGET.max_probe_count, 1, MAX_PROBE_COUNT),
max_rows_per_probe: clampInteger(input.maxRowsPerProbe, DEFAULT_DISCOVERY_BUDGET.max_rows_per_probe, 1, MAX_ROWS_PER_PROBE)
@@ -84,6 +84,7 @@ function entityCandidatesFromPlanner(planner) {
return uniqueStrings(values);
}
function buildLoopState(planner, pilot, bridgeStatus) {
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
@@ -94,7 +95,7 @@ function buildLoopState(planner, pilot, bridgeStatus) {
asked_action_family: planner.discovery_plan.turn_meaning_ref?.asked_action_family ?? null,
unsupported_but_understood_family: planner.discovery_plan.turn_meaning_ref?.unsupported_but_understood_family ?? null,
ranking_need: planner.data_need_graph?.ranking_need ?? planner.discovery_plan.turn_meaning_ref?.seeded_ranking_need ?? null,
pending_axes: flattenAxes(pilot, "missing_axis_options"),
pending_axes: plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options"),
provided_axes: flattenAxes(pilot, "provided_axes"),
explicit_entity_candidates: entityCandidatesFromPlanner(planner),
explicit_organization_scope: planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope ?? null,
@@ -647,6 +647,15 @@ function collectDateScopeFromRawText(text) {
}
return null;
}
function currentIsoDate() {
return new Date().toISOString().slice(0, 10);
}
function hasRelativeCurrentDateHint(text) {
return /(?:\bсегодня\b|\bна\s+сегодня\b|\bсегодняшн(?:ий|его|ем)\b|\btoday\b|\bas\s+of\s+today\b|\bcurrent\s+date\b)/iu.test(text);
}
function isImplicitCurrentDateScope(value) {
return Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value) && value === currentIsoDate());
}
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)) {
@@ -717,6 +726,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText);
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
const rawDateScope = collectDateScopeFromRawText(rawText);
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
@@ -1023,6 +1033,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
explicitOrganizationScopeSignal ||
organizationClarificationFollowupApplicable ||
followupSeed.organization);
const openScopeValueFlowWithoutResolvedCounterparty = Boolean(valueFlowSignal && !normalizedPredecomposeCounterparty && !followupSeed.counterparty);
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);
@@ -1037,14 +1048,33 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
}
}
}
const clarificationLoopStillNeedsPeriod = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopPendingAxes.includes("period"));
const currentTurnCarriesExplicitPeriod = Boolean(explicitDateScopeLiteralDetected ||
rawDateScope ||
relativeCurrentDateHintDetected ||
(predecomposeDateScope && !isImplicitCurrentDateScope(predecomposeDateScope)));
const suppressImplicitCurrentDateScope = Boolean(!currentTurnCarriesExplicitPeriod &&
(clarificationLoopStillNeedsPeriod ||
openScopeValueFlowWithoutResolvedCounterparty ||
(valueFlowOrganizationStaysScope && (Boolean(followupSeed.rankingNeed) || bidirectionalValueFlowSignal))));
const normalizedPredecomposeDateScope = suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(predecomposeDateScope) ? null : predecomposeDateScope;
const normalizedAssistantTurnMeaningDateScope = suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope)
? null
: assistantTurnMeaningDateScope;
const normalizedFollowupDateScope = suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(followupSeed.dateScope)
? null
: followupSeed.dateScope;
const explicitDateScope = rawAllTimeScopeSignal
? null
: assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
: normalizedAssistantTurnMeaningDateScope ??
normalizedPredecomposeDateScope ??
rawDateScope ??
normalizedFollowupDateScope;
const followupDateScopeApplied = Boolean(!rawAllTimeScopeSignal &&
!assistantTurnMeaningDateScope &&
!predecomposeDateScope &&
!normalizedAssistantTurnMeaningDateScope &&
!normalizedPredecomposeDateScope &&
!rawDateScope &&
followupSeed.dateScope);
normalizedFollowupDateScope);
const clarificationLoopSeedApplied = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId);
const turnMeaning = {
asked_domain_family: lifecycleSignal