ARCH: довести planner-selected entity chains и ambiguity follow-up

This commit is contained in:
dctouch 2026-04-22 17:32:24 +03:00
parent bc54cd9628
commit bd58ab490f
19 changed files with 1193 additions and 65 deletions

View File

@ -1,5 +1,7 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.readAssistantMcpDiscoveryEntityResolutionStatus = readAssistantMcpDiscoveryEntityResolutionStatus;
exports.readAssistantMcpDiscoveryEntityAmbiguityCandidates = readAssistantMcpDiscoveryEntityAmbiguityCandidates;
exports.readAssistantMcpDiscoveryEntityCandidates = readAssistantMcpDiscoveryEntityCandidates; exports.readAssistantMcpDiscoveryEntityCandidates = readAssistantMcpDiscoveryEntityCandidates;
exports.readAssistantMcpDiscoveryPilotScope = readAssistantMcpDiscoveryPilotScope; exports.readAssistantMcpDiscoveryPilotScope = readAssistantMcpDiscoveryPilotScope;
exports.readAssistantMcpDiscoveryMetadataRouteFamily = readAssistantMcpDiscoveryMetadataRouteFamily; exports.readAssistantMcpDiscoveryMetadataRouteFamily = readAssistantMcpDiscoveryMetadataRouteFamily;
@ -97,6 +99,16 @@ function readAssistantMcpDiscoveryDerivedEntityResolution(debug) {
const pilot = toRecordObject(bridge?.pilot); const pilot = toRecordObject(bridge?.pilot);
return toRecordObject(pilot?.derived_entity_resolution); return toRecordObject(pilot?.derived_entity_resolution);
} }
function readAssistantMcpDiscoveryEntityResolutionStatus(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.resolution_status);
}
function readAssistantMcpDiscoveryEntityAmbiguityCandidates(debug, toNonEmptyString = fallbackToNonEmptyString) {
const values = readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.ambiguity_candidates;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
}
function collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString = fallbackToNonEmptyString) { function collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString = fallbackToNonEmptyString) {
const result = []; const result = [];
const resolution = readAssistantMcpDiscoveryDerivedEntityResolution(debug); const resolution = readAssistantMcpDiscoveryDerivedEntityResolution(debug);

View File

@ -27,6 +27,12 @@ function uniqueStrings(values) {
} }
return result; return result;
} }
function formatNamedChoiceList(values) {
return uniqueStrings(values)
.slice(0, 6)
.map((value, index) => `${index + 1}. ${value}`)
.join("; ");
}
function isInternalMechanicsLine(value) { function isInternalMechanicsLine(value) {
const text = value.toLowerCase(); const text = value.toLowerCase();
return (text.includes("primitive") || return (text.includes("primitive") ||
@ -273,6 +279,10 @@ function headlineFor(mode, pilot) {
} }
function nextStepFor(mode, pilot) { function nextStepFor(mode, pilot) {
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") { if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
const ambiguityCandidates = pilot.derived_entity_resolution?.ambiguity_candidates ?? [];
if (ambiguityCandidates.length > 0) {
return `Уточните, какой именно контрагент нужен: ${formatNamedChoiceList(ambiguityCandidates)}. Можно ответить названием или номером варианта.`;
}
return "Уточните точное название контрагента или добавьте ИНН, и я продолжу уже по нужной сущности в 1С."; return "Уточните точное название контрагента или добавьте ИНН, и я продолжу уже по нужной сущности в 1С.";
} }
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") { if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
@ -449,7 +459,7 @@ function derivedEntityResolutionInferenceLine(pilot) {
return "Сейчас подтверждено только заземление сущности по каталогу 1С; документы, движения и денежные показатели по ней еще не проверялись."; return "Сейчас подтверждено только заземление сущности по каталогу 1С; документы, движения и денежные показатели по ней еще не проверялись.";
} }
if (resolution.resolution_status === "ambiguous" && resolution.ambiguity_candidates.length > 0) { if (resolution.resolution_status === "ambiguous" && resolution.ambiguity_candidates.length > 0) {
return `В checked catalog slice есть несколько близких кандидатов: ${resolution.ambiguity_candidates.join(", ")}. Без уточнения нельзя честно выбрать одного контрагента для следующего шага.`; return `В каталоге 1С нашлось несколько близких кандидатов: ${formatNamedChoiceList(resolution.ambiguity_candidates)}. Без уточнения нельзя честно выбрать одного контрагента для следующего шага.`;
} }
return null; return null;
} }

View File

@ -655,6 +655,46 @@ function summarizeEntityResolutionRows(result) {
} }
return `${result.fetched_rows} MCP catalog rows fetched for entity search`; return `${result.fetched_rows} MCP catalog rows fetched for entity search`;
} }
function entityResolutionFollowupStepLimitation() {
return "Entity-resolution could not continue because the checked catalog search step did not return a confirmed slice";
}
function buildEntityResolutionResolveProbeResult(input) {
if (!input.resolution) {
return {
primitive_id: "resolve_entity_reference",
status: "ok",
rows_received: input.queryResult.fetched_rows,
rows_matched: 0,
limitation: null
};
}
if (input.resolution.resolution_status === "resolved") {
return {
primitive_id: "resolve_entity_reference",
status: "ok",
rows_received: input.queryResult.fetched_rows,
rows_matched: 1,
limitation: null
};
}
return {
primitive_id: "resolve_entity_reference",
status: "ok",
rows_received: input.queryResult.fetched_rows,
rows_matched: 0,
limitation: null
};
}
function buildEntityResolutionCoverageProbeResult(input) {
const resolved = input.resolution?.resolution_status === "resolved";
return {
primitive_id: "probe_coverage",
status: "ok",
rows_received: 1,
rows_matched: resolved ? 1 : 0,
limitation: null
};
}
function metadataRowText(row, keys) { function metadataRowText(row, keys) {
for (const key of keys) { for (const key of keys) {
const text = toNonEmptyString(row[key]); const text = toNonEmptyString(row[key]);
@ -1644,28 +1684,82 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
reason_codes: reasonCodes reason_codes: reasonCodes
}; };
} }
let derivedEntityResolution = null;
for (const step of dryRun.execution_steps) { for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "search_business_entity") { if (step.primitive_id === "search_business_entity") {
skippedPrimitives.push(step.primitive_id); queryResult = await runtimeDeps.executeAddressMcpQuery({
probeResults.push(skippedProbeResult(step, "pilot_only_executes_search_business_entity")); query: ENTITY_RESOLUTION_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT)),
limit: ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT
});
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_search_business_entity_mcp_error");
}
else {
pushReason(reasonCodes, "pilot_search_business_entity_mcp_executed");
derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
}
continue; continue;
} }
queryResult = await runtimeDeps.executeAddressMcpQuery({ if (step.primitive_id === "resolve_entity_reference") {
query: ENTITY_RESOLUTION_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT)), if (!queryResult || queryResult.error) {
limit: ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT skippedPrimitives.push(step.primitive_id);
}); probeResults.push(skippedProbeResult(step, entityResolutionFollowupStepLimitation()));
pushUnique(executedPrimitives, step.primitive_id); continue;
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult)); }
if (queryResult.error) { if (!derivedEntityResolution) {
pushUnique(queryLimitations, queryResult.error); derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
pushReason(reasonCodes, "pilot_search_business_entity_mcp_error"); }
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(buildEntityResolutionResolveProbeResult({
queryResult,
resolution: derivedEntityResolution
}));
if (derivedEntityResolution?.resolution_status === "resolved") {
pushReason(reasonCodes, "pilot_resolve_entity_reference_from_catalog_rows");
}
else if (derivedEntityResolution?.resolution_status === "ambiguous") {
pushReason(reasonCodes, "pilot_resolve_entity_reference_requires_clarification");
}
else {
pushReason(reasonCodes, "pilot_resolve_entity_reference_not_confirmed");
}
continue;
} }
else { if (step.primitive_id === "probe_coverage") {
pushReason(reasonCodes, "pilot_search_business_entity_mcp_executed"); if (!queryResult || queryResult.error) {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, entityResolutionFollowupStepLimitation()));
continue;
}
if (!derivedEntityResolution) {
derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
}
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(buildEntityResolutionCoverageProbeResult({
resolution: derivedEntityResolution
}));
pushReason(reasonCodes, "pilot_probe_coverage_executed_for_entity_resolution");
if (derivedEntityResolution?.resolution_status === "resolved") {
pushReason(reasonCodes, "pilot_entity_resolution_grounding_stable_for_downstream_probe");
}
else if (derivedEntityResolution?.resolution_status === "ambiguous") {
pushReason(reasonCodes, "pilot_entity_resolution_coverage_requires_clarification");
}
else {
pushReason(reasonCodes, "pilot_entity_resolution_coverage_not_confirmed");
}
continue;
} }
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_entity_resolution_step_not_implemented"));
} }
const sourceRowsSummary = queryResult ? summarizeEntityResolutionRows(queryResult) : null; const sourceRowsSummary = queryResult ? summarizeEntityResolutionRows(queryResult) : null;
const derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity); if (!derivedEntityResolution && queryResult && !queryResult.error) {
derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
}
if (derivedEntityResolution?.resolution_status === "resolved") { if (derivedEntityResolution?.resolution_status === "resolved") {
pushReason(reasonCodes, "pilot_derived_entity_resolution_from_catalog_rows"); pushReason(reasonCodes, "pilot_derived_entity_resolution_from_catalog_rows");
} }
@ -1683,7 +1777,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
unknownFacts: buildEntityResolutionUnknownFacts(derivedEntityResolution, requestedEntity), unknownFacts: buildEntityResolutionUnknownFacts(derivedEntityResolution, requestedEntity),
sourceRowsSummary, sourceRowsSummary,
queryLimitations, queryLimitations,
recommendedNextProbe: "resolve_entity_reference" recommendedNextProbe: null
}); });
return { return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION, schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,

View File

@ -138,11 +138,63 @@ function readDiscoveryTurnMeaning(entryPoint) {
const turnInput = toRecordObject(entryPoint?.turn_input); const turnInput = toRecordObject(entryPoint?.turn_input);
return toRecordObject(turnInput?.turn_meaning_ref); return toRecordObject(turnInput?.turn_meaning_ref);
} }
function readTruthAnswerShape(input) {
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
if (directShape) {
return directShape;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
return toRecordObject(truthAnswerPolicy?.answer_shape);
}
function hasEffectivelyFactualAddressReply(input) {
if (toNonEmptyString(input.currentReplyType) === "factual") {
return true;
}
const truthAnswerShape = readTruthAnswerShape(input);
return toNonEmptyString(truthAnswerShape?.reply_type) === "factual";
}
function readStateTransitionReasonCodes(input) {
const directTransition = toRecordObject(input.addressRuntimeMeta?.assistant_state_transition_v1);
const fallbackTransition = toRecordObject(input.addressRuntimeMeta?.state_transition_contract);
const stateTransition = directTransition ?? fallbackTransition;
if (!stateTransition || !Array.isArray(stateTransition.reason_codes)) {
return [];
}
return stateTransition.reason_codes
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item));
}
function hasRuntimeAdjustedExactReply(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
const hasFullConfirmedTruth = truthGateStatus === "full_confirmed" ||
sourceTruthGateStatus === "full_confirmed" ||
(coverageStatus === "full" && groundingStatus === "grounded");
if (!hasFullConfirmedTruth) {
return false;
}
const truthAnswerShape = readTruthAnswerShape(input);
const capabilityContractId = toNonEmptyString(truthAnswerShape?.capability_contract_id);
if (!capabilityContractId) {
return false;
}
return readStateTransitionReasonCodes(input).some((reason) => /^intent_adjusted_to_.+_followup_context$/i.test(reason));
}
function hasAlignedFactualAddressReply(input, entryPoint) { function hasAlignedFactualAddressReply(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false; return false;
} }
if (toNonEmptyString(input.currentReplyType) !== "factual") { if (!hasEffectivelyFactualAddressReply(input)) {
return false; return false;
} }
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent); const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
@ -152,7 +204,10 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false; return false;
} }
if (toNonEmptyString(input.currentReplyType) !== "factual") { if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasRuntimeAdjustedExactReply(input, entryPoint)) {
return false; return false;
} }
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent); const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
@ -166,7 +221,7 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint) {
return !isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning); return !isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning);
} }
function hasMatchedFactualAddressContinuationTarget(input, entryPoint) { function hasMatchedFactualAddressContinuationTarget(input, entryPoint) {
if (toNonEmptyString(input.currentReplyType) !== "factual") { if (!hasEffectivelyFactualAddressReply(input)) {
return false; return false;
} }
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) { if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
@ -182,7 +237,7 @@ function hasFullConfirmedFactualAddressReply(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false; return false;
} }
if (toNonEmptyString(input.currentReplyType) !== "factual") { if (!hasEffectivelyFactualAddressReply(input)) {
return false; return false;
} }
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) { if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
@ -214,6 +269,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint); const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint); const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint); const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
if (!entryPoint) { if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point"); pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
} }
@ -241,6 +297,9 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (fullConfirmedFactualAddressReply) { if (fullConfirmedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply"); pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
} }
if (runtimeAdjustedExactReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning");
}
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") { if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"); pushReason(reasonCodes, "mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate");
} }
@ -261,6 +320,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
!alignedFactualAddressReply && !alignedFactualAddressReply &&
!matchedFactualAddressContinuationTarget && !matchedFactualAddressContinuationTarget &&
!fullConfirmedFactualAddressReply && !fullConfirmedFactualAddressReply &&
!runtimeAdjustedExactReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") && !(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) && ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime && candidate.eligible_for_future_hot_runtime &&

View File

@ -241,12 +241,15 @@ function collectFollowupDiscoverySeed(followupContext) {
? mapPilotScopeToFollowupMeaning(pilotScope) ? mapPilotScopeToFollowupMeaning(pilotScope)
: mapAddressIntentToFollowupMeaning(previousIntent); : mapAddressIntentToFollowupMeaning(previousIntent);
const discoveryEntities = collectEntityCandidates(followupContext?.previous_discovery_entity_candidates); const discoveryEntities = collectEntityCandidates(followupContext?.previous_discovery_entity_candidates);
const entityResolutionStatus = toNonEmptyString(followupContext?.previous_discovery_entity_resolution_status);
const entityResolutionAmbiguityCandidates = collectEntityCandidates(followupContext?.previous_discovery_entity_ambiguity_candidates);
const ambiguityBlocksImplicitGrounding = pilotScope === "entity_resolution_search_v1" && entityResolutionStatus === "ambiguous";
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"
? toNonEmptyString(followupContext?.previous_anchor_value) ? toNonEmptyString(followupContext?.previous_anchor_value)
: null) ?? : null) ??
(discoveryEntities[0] ?? null); (ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null);
const organization = toNonEmptyString(previousFilters?.organization) ?? const organization = toNonEmptyString(previousFilters?.organization) ??
toNonEmptyString(rootFilters?.organization) ?? toNonEmptyString(rootFilters?.organization) ??
(toNonEmptyString(followupContext?.previous_anchor_type) === "organization" (toNonEmptyString(followupContext?.previous_anchor_type) === "organization"
@ -260,7 +263,9 @@ function collectFollowupDiscoverySeed(followupContext) {
action: mapped.action, action: mapped.action,
unsupported: mapped.unsupported, unsupported: mapped.unsupported,
counterparty, counterparty,
discoveryEntity: discoveryEntities[0] ?? null, discoveryEntity: ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null,
entityResolutionStatus,
entityResolutionAmbiguityCandidates,
organization, organization,
dateScope, dateScope,
metadataRouteFamily: toNonEmptyString(followupContext?.previous_discovery_metadata_route_family), metadataRouteFamily: toNonEmptyString(followupContext?.previous_discovery_metadata_route_family),
@ -350,6 +355,93 @@ function rawEntityResolutionCandidate(text) {
} }
return null; return null;
} }
function resolveEntityResolutionAmbiguityChoice(text, candidates) {
const normalizedText = canonicalizeEntityResolutionCandidate(text);
if (!normalizedText || candidates.length <= 0) {
return null;
}
const exactMatch = candidates.find((candidate) => canonicalizeEntityResolutionCandidate(candidate) === normalizedText);
if (exactMatch) {
return exactMatch;
}
const includedMatches = candidates.filter((candidate) => {
const normalizedCandidate = canonicalizeEntityResolutionCandidate(candidate);
return normalizedCandidate.length > 0 && normalizedText.includes(normalizedCandidate);
});
if (includedMatches.length === 1) {
return includedMatches[0];
}
const narrowedMatches = candidates.filter((candidate) => {
const normalizedCandidate = canonicalizeEntityResolutionCandidate(candidate);
return normalizedText.length >= 4 && normalizedCandidate.includes(normalizedText);
});
if (narrowedMatches.length === 1) {
return narrowedMatches[0];
}
const normalizedLowerText = compactLower(text);
const ordinalMatchers = [
{
index: 0,
keywords: ["первый"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|№)?\s*1(?:$|[^\p{L}\p{N}])/iu,
/^\s*1\s*$/u
]
},
{
index: 1,
keywords: ["второй"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|№)?\s*2(?:$|[^\p{L}\p{N}])/iu,
/^\s*2\s*$/u
]
},
{
index: 2,
keywords: ["третий"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|№)?\s*3(?:$|[^\p{L}\p{N}])/iu,
/^\s*3\s*$/u
]
},
{
index: 3,
keywords: ["четвертый", "четвёртый"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|№)?\s*4(?:$|[^\p{L}\p{N}])/iu,
/^\s*4\s*$/u
]
},
{
index: 4,
keywords: ["пятый"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|№)?\s*5(?:$|[^\p{L}\p{N}])/iu,
/^\s*5\s*$/u
]
},
{
index: 5,
keywords: ["шестой"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|№)?\s*6(?:$|[^\p{L}\p{N}])/iu,
/^\s*6\s*$/u
]
}
];
for (const matcher of ordinalMatchers) {
if (matcher.index < candidates.length &&
(matcher.keywords.some((keyword) => normalizedLowerText.includes(keyword)) ||
matcher.numericPatterns.some((pattern) => pattern.test(normalizedLowerText)))) {
return candidates[matcher.index];
}
}
if (candidates.length > 0 &&
(normalizedLowerText.includes("последний") || normalizedLowerText.includes("крайний"))) {
return candidates[candidates.length - 1] ?? null;
}
return null;
}
function metadataActionFromRawText(text) { function metadataActionFromRawText(text) {
if (/(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|objects?)/iu.test(text)) { if (/(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|objects?)/iu.test(text)) {
return "inspect_surface"; return "inspect_surface";
@ -468,7 +560,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const rawDateScope = collectDateScopeFromRawText(rawText); const rawDateScope = collectDateScopeFromRawText(rawText);
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null; const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null; const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
const entityResolutionSignal = rawEntityResolutionSignal || Boolean(rawEntityCandidate); const entityResolutionClarificationCandidate = followupSeed.pilotScope === "entity_resolution_search_v1" &&
followupSeed.entityResolutionStatus === "ambiguous"
? resolveEntityResolutionAmbiguityChoice(rawEntitySourceText, followupSeed.entityResolutionAmbiguityCandidates)
: null;
const entityResolutionSignal = rawEntityResolutionSignal || Boolean(rawEntityCandidate) || Boolean(entityResolutionClarificationCandidate);
const metadataDocumentHintSignal = hasDocumentEvidenceFollowupSignal(rawText) || hasPronounDocumentEvidenceFollowupSignal(rawText); const metadataDocumentHintSignal = hasDocumentEvidenceFollowupSignal(rawText) || hasPronounDocumentEvidenceFollowupSignal(rawText);
const metadataMovementHintSignal = hasMovementEvidenceFollowupSignal(rawText) || hasPronounMovementEvidenceFollowupSignal(rawText); const metadataMovementHintSignal = hasMovementEvidenceFollowupSignal(rawText) || hasPronounMovementEvidenceFollowupSignal(rawText);
const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family); const rawDomain = toNonEmptyString(assistantTurnMeaning?.asked_domain_family);
@ -521,12 +617,26 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
!rawValueFlowSignal && !rawValueFlowSignal &&
!rawMetadataSignal && !rawMetadataSignal &&
metadataDocumentHintSignal); metadataDocumentHintSignal);
const entityResolutionClarifiedDocumentFollowupApplicable = Boolean(followupSeed.pilotScope === "entity_resolution_search_v1" &&
followupSeed.entityResolutionStatus === "ambiguous" &&
entityResolutionClarificationCandidate &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
metadataDocumentHintSignal);
const entityResolutionGroundedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "entity_resolution_search_v1" && const entityResolutionGroundedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "entity_resolution_search_v1" &&
(followupSeed.counterparty || followupSeed.discoveryEntity) && (followupSeed.counterparty || followupSeed.discoveryEntity) &&
!rawLifecycleSignal && !rawLifecycleSignal &&
!rawValueFlowSignal && !rawValueFlowSignal &&
!rawMetadataSignal && !rawMetadataSignal &&
metadataMovementHintSignal); metadataMovementHintSignal);
const entityResolutionClarifiedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "entity_resolution_search_v1" &&
followupSeed.entityResolutionStatus === "ambiguous" &&
entityResolutionClarificationCandidate &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
metadataMovementHintSignal);
const groundedValueFlowFollowupApplicable = Boolean(rawValueFlowSignal && const groundedValueFlowFollowupApplicable = Boolean(rawValueFlowSignal &&
!rawLifecycleSignal && !rawLifecycleSignal &&
!rawMetadataSignal && !rawMetadataSignal &&
@ -607,6 +717,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const metadataGroundedDocumentLaneApplicable = metadataGroundedDocumentFollowupApplicable || const metadataGroundedDocumentLaneApplicable = metadataGroundedDocumentFollowupApplicable ||
metadataAmbiguityResolvedDocumentFollowupApplicable || metadataAmbiguityResolvedDocumentFollowupApplicable ||
entityResolutionGroundedDocumentFollowupApplicable || entityResolutionGroundedDocumentFollowupApplicable ||
entityResolutionClarifiedDocumentFollowupApplicable ||
valueFlowGroundedDocumentFollowupApplicable || valueFlowGroundedDocumentFollowupApplicable ||
movementEvidenceGroundedDocumentFollowupApplicable || movementEvidenceGroundedDocumentFollowupApplicable ||
(metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") || (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") ||
@ -614,6 +725,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const metadataGroundedMovementLaneApplicable = metadataGroundedMovementFollowupApplicable || const metadataGroundedMovementLaneApplicable = metadataGroundedMovementFollowupApplicable ||
metadataAmbiguityResolvedMovementFollowupApplicable || metadataAmbiguityResolvedMovementFollowupApplicable ||
entityResolutionGroundedMovementFollowupApplicable || entityResolutionGroundedMovementFollowupApplicable ||
entityResolutionClarifiedMovementFollowupApplicable ||
valueFlowGroundedMovementFollowupApplicable || valueFlowGroundedMovementFollowupApplicable ||
documentEvidenceGroundedMovementFollowupApplicable || documentEvidenceGroundedMovementFollowupApplicable ||
(metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") || (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") ||
@ -667,11 +779,14 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
lifecycleSignal, lifecycleSignal,
valueFlowSignal, valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable, metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
entityResolutionSignal entityResolutionSignal: entityResolutionSignal &&
!metadataGroundedDocumentLaneApplicable &&
!metadataGroundedMovementLaneApplicable
}); });
const groundedFollowupEntity = followupSeed.counterparty ?? followupSeed.discoveryEntity; const groundedFollowupEntity = followupSeed.counterparty ?? followupSeed.discoveryEntity;
const entityCandidates = entityResolutionSignal ? [] : []; const entityCandidates = entityResolutionSignal ? [] : [];
if (entityResolutionSignal) { if (entityResolutionSignal) {
pushNormalizedEntityResolutionCandidate(entityCandidates, entityResolutionClarificationCandidate);
pushNormalizedEntityResolutionCandidate(entityCandidates, rawEntityCandidate); pushNormalizedEntityResolutionCandidate(entityCandidates, rawEntityCandidate);
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) { for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate); pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
@ -814,12 +929,14 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
semanticDataNeed, semanticDataNeed,
explicitIntentCandidate, explicitIntentCandidate,
followupDiscoverySeedApplicable: followupDiscoverySeedApplicable || followupDiscoverySeedApplicable: followupDiscoverySeedApplicable ||
Boolean(entityResolutionClarificationCandidate) ||
effectiveMetadataFollowupSeedApplicable || effectiveMetadataFollowupSeedApplicable ||
metadataAmbiguityLaneClarificationApplicable || metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable || metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable || metadataGroundedDocumentLaneApplicable ||
groundedValueFlowFollowupApplicable, groundedValueFlowFollowupApplicable,
forceDiscoveryOverExplicitIntent: metadataAmbiguityLaneClarificationApplicable || forceDiscoveryOverExplicitIntent: Boolean(entityResolutionClarificationCandidate) ||
metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable || metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable || metadataGroundedDocumentLaneApplicable ||
groundedValueFlowFollowupApplicable groundedValueFlowFollowupApplicable
@ -827,7 +944,10 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
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 || effectiveMetadataFollowupSeedApplicable || metadataAmbiguityLaneClarificationApplicable : followupDiscoverySeedApplicable ||
Boolean(entityResolutionClarificationCandidate) ||
effectiveMetadataFollowupSeedApplicable ||
metadataAmbiguityLaneClarificationApplicable
? "followup_context" ? "followup_context"
: metadataGroundedMovementLaneApplicable : metadataGroundedMovementLaneApplicable
? "followup_context" ? "followup_context"
@ -862,6 +982,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (rawEntityCandidate) { if (rawEntityCandidate) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_from_raw_entity_search"); pushReason(reasonCodes, "mcp_discovery_entity_scope_from_raw_entity_search");
} }
if (entityResolutionClarificationCandidate) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarification_candidate_selected");
}
if (payoutSignal) { if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected"); pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
} }
@ -892,9 +1015,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (entityResolutionGroundedDocumentFollowupApplicable) { if (entityResolutionGroundedDocumentFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_document_followup"); pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_document_followup");
} }
if (entityResolutionClarifiedDocumentFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarified_document_followup");
}
if (entityResolutionGroundedMovementFollowupApplicable) { if (entityResolutionGroundedMovementFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_movement_followup"); pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_movement_followup");
} }
if (entityResolutionClarifiedMovementFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarified_movement_followup");
}
if (valueFlowGroundedDocumentFollowupApplicable) { if (valueFlowGroundedDocumentFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_value_flow_grounded_document_followup"); pushReason(reasonCodes, "mcp_discovery_value_flow_grounded_document_followup");
} }

View File

@ -503,7 +503,9 @@ function createAssistantTransitionPolicy(deps) {
const sourceDiscoveryMetadataSelectedEntitySet = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataSelectedEntitySet)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryMetadataSelectedEntitySet = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataSelectedEntitySet)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryMetadataAmbiguityDetected = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityDetected)(carryoverSourceDebug); const sourceDiscoveryMetadataAmbiguityDetected = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityDetected)(carryoverSourceDebug);
const sourceDiscoveryMetadataAmbiguityEntitySets = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryMetadataAmbiguityEntitySets = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryEntityResolutionStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityResolutionStatus)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryEntityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityCandidates)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryEntityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryEntityAmbiguityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityAmbiguityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected = llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true; const llmSelectedObjectScopeDetected = llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
const resolvedPrimaryIntent = deps.resolveAddressIntent(deps.repairAddressMojibake(String(userMessage ?? ""))).intent; const resolvedPrimaryIntent = deps.resolveAddressIntent(deps.repairAddressMojibake(String(userMessage ?? ""))).intent;
@ -738,7 +740,11 @@ function createAssistantTransitionPolicy(deps) {
previous_anchor_type: previousAnchorType ?? undefined, previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor, previous_anchor_value: previousAnchor,
previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined, previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined,
previous_discovery_entity_resolution_status: sourceDiscoveryEntityResolutionStatus ?? undefined,
previous_discovery_entity_candidates: sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined, previous_discovery_entity_candidates: sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined,
previous_discovery_entity_ambiguity_candidates: sourceDiscoveryEntityAmbiguityCandidates.length > 0
? sourceDiscoveryEntityAmbiguityCandidates
: undefined,
previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined, previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined,
previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? undefined, previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? undefined,
previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined, previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined,

View File

@ -40,6 +40,8 @@ export interface AddressFollowupContext {
| "unknown" | "unknown"
| null; | null;
previous_anchor_value?: string | null; previous_anchor_value?: string | null;
previous_discovery_entity_resolution_status?: "resolved" | "ambiguous" | "not_found" | null;
previous_discovery_entity_ambiguity_candidates?: string[];
resolved_counterparty_from_display?: boolean; resolved_counterparty_from_display?: boolean;
root_intent?: AddressIntent; root_intent?: AddressIntent;
root_filters?: AddressFilterSet; root_filters?: AddressFilterSet;

View File

@ -182,6 +182,24 @@ function readAssistantMcpDiscoveryDerivedEntityResolution(
return toRecordObject(pilot?.derived_entity_resolution); return toRecordObject(pilot?.derived_entity_resolution);
} }
export function readAssistantMcpDiscoveryEntityResolutionStatus(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.resolution_status);
}
export function readAssistantMcpDiscoveryEntityAmbiguityCandidates(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string[] {
const values = readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.ambiguity_candidates;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
}
function collectAssistantMcpDiscoveryEntityCandidates( function collectAssistantMcpDiscoveryEntityCandidates(
debug: Record<string, unknown> | null, debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString

View File

@ -52,6 +52,13 @@ function uniqueStrings(values: string[]): string[] {
return result; return result;
} }
function formatNamedChoiceList(values: string[]): string {
return uniqueStrings(values)
.slice(0, 6)
.map((value, index) => `${index + 1}. ${value}`)
.join("; ");
}
function isInternalMechanicsLine(value: string): boolean { function isInternalMechanicsLine(value: string): boolean {
const text = value.toLowerCase(); const text = value.toLowerCase();
return ( return (
@ -344,6 +351,12 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") { if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
const ambiguityCandidates = pilot.derived_entity_resolution?.ambiguity_candidates ?? [];
if (ambiguityCandidates.length > 0) {
return `Уточните, какой именно контрагент нужен: ${formatNamedChoiceList(
ambiguityCandidates
)}. Можно ответить названием или номером варианта.`;
}
return "Уточните точное название контрагента или добавьте ИНН, и я продолжу уже по нужной сущности в 1С."; return "Уточните точное название контрагента или добавьте ИНН, и я продолжу уже по нужной сущности в 1С.";
} }
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") { if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
@ -536,7 +549,9 @@ function derivedEntityResolutionInferenceLine(pilot: AssistantMcpDiscoveryPilotE
return "Сейчас подтверждено только заземление сущности по каталогу 1С; документы, движения и денежные показатели по ней еще не проверялись."; return "Сейчас подтверждено только заземление сущности по каталогу 1С; документы, движения и денежные показатели по ней еще не проверялись.";
} }
if (resolution.resolution_status === "ambiguous" && resolution.ambiguity_candidates.length > 0) { if (resolution.resolution_status === "ambiguous" && resolution.ambiguity_candidates.length > 0) {
return `В checked catalog slice есть несколько близких кандидатов: ${resolution.ambiguity_candidates.join(", ")}. Без уточнения нельзя честно выбрать одного контрагента для следующего шага.`; return `В каталоге 1С нашлось несколько близких кандидатов: ${formatNamedChoiceList(
resolution.ambiguity_candidates
)}. Без уточнения нельзя честно выбрать одного контрагента для следующего шага.`;
} }
return null; return null;
} }

View File

@ -974,6 +974,54 @@ function summarizeEntityResolutionRows(result: AddressMcpQueryExecutorResult): s
return `${result.fetched_rows} MCP catalog rows fetched for entity search`; return `${result.fetched_rows} MCP catalog rows fetched for entity search`;
} }
function entityResolutionFollowupStepLimitation(): string {
return "Entity-resolution could not continue because the checked catalog search step did not return a confirmed slice";
}
function buildEntityResolutionResolveProbeResult(input: {
queryResult: AddressMcpQueryExecutorResult;
resolution: AssistantMcpDiscoveryDerivedEntityResolution | null;
}): AssistantMcpDiscoveryProbeResult {
if (!input.resolution) {
return {
primitive_id: "resolve_entity_reference",
status: "ok",
rows_received: input.queryResult.fetched_rows,
rows_matched: 0,
limitation: null
};
}
if (input.resolution.resolution_status === "resolved") {
return {
primitive_id: "resolve_entity_reference",
status: "ok",
rows_received: input.queryResult.fetched_rows,
rows_matched: 1,
limitation: null
};
}
return {
primitive_id: "resolve_entity_reference",
status: "ok",
rows_received: input.queryResult.fetched_rows,
rows_matched: 0,
limitation: null
};
}
function buildEntityResolutionCoverageProbeResult(input: {
resolution: AssistantMcpDiscoveryDerivedEntityResolution | null;
}): AssistantMcpDiscoveryProbeResult {
const resolved = input.resolution?.resolution_status === "resolved";
return {
primitive_id: "probe_coverage",
status: "ok",
rows_received: 1,
rows_matched: resolved ? 1 : 0,
limitation: null
};
}
function metadataRowText(row: Record<string, unknown>, keys: string[]): string | null { function metadataRowText(row: Record<string, unknown>, keys: string[]): string | null {
for (const key of keys) { for (const key of keys) {
const text = toNonEmptyString(row[key]); const text = toNonEmptyString(row[key]);
@ -2153,31 +2201,88 @@ export async function executeAssistantMcpDiscoveryPilot(
}; };
} }
let derivedEntityResolution: AssistantMcpDiscoveryDerivedEntityResolution | null = null;
for (const step of dryRun.execution_steps) { for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "search_business_entity") { if (step.primitive_id === "search_business_entity") {
skippedPrimitives.push(step.primitive_id); queryResult = await runtimeDeps.executeAddressMcpQuery({
probeResults.push(skippedProbeResult(step, "pilot_only_executes_search_business_entity")); query: ENTITY_RESOLUTION_COUNTERPARTY_QUERY_TEMPLATE.replaceAll(
"__LIMIT__",
String(ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT)
),
limit: ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT
});
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult));
if (queryResult.error) {
pushUnique(queryLimitations, queryResult.error);
pushReason(reasonCodes, "pilot_search_business_entity_mcp_error");
} else {
pushReason(reasonCodes, "pilot_search_business_entity_mcp_executed");
derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
}
continue; continue;
} }
queryResult = await runtimeDeps.executeAddressMcpQuery({
query: ENTITY_RESOLUTION_COUNTERPARTY_QUERY_TEMPLATE.replaceAll( if (step.primitive_id === "resolve_entity_reference") {
"__LIMIT__", if (!queryResult || queryResult.error) {
String(ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT) skippedPrimitives.push(step.primitive_id);
), probeResults.push(skippedProbeResult(step, entityResolutionFollowupStepLimitation()));
limit: ENTITY_RESOLUTION_COUNTERPARTY_LOOKUP_LIMIT continue;
}); }
pushUnique(executedPrimitives, step.primitive_id); if (!derivedEntityResolution) {
probeResults.push(queryResultToProbeResult(step.primitive_id, queryResult)); derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
if (queryResult.error) { }
pushUnique(queryLimitations, queryResult.error); pushUnique(executedPrimitives, step.primitive_id);
pushReason(reasonCodes, "pilot_search_business_entity_mcp_error"); probeResults.push(
} else { buildEntityResolutionResolveProbeResult({
pushReason(reasonCodes, "pilot_search_business_entity_mcp_executed"); queryResult,
resolution: derivedEntityResolution
})
);
if (derivedEntityResolution?.resolution_status === "resolved") {
pushReason(reasonCodes, "pilot_resolve_entity_reference_from_catalog_rows");
} else if (derivedEntityResolution?.resolution_status === "ambiguous") {
pushReason(reasonCodes, "pilot_resolve_entity_reference_requires_clarification");
} else {
pushReason(reasonCodes, "pilot_resolve_entity_reference_not_confirmed");
}
continue;
} }
if (step.primitive_id === "probe_coverage") {
if (!queryResult || queryResult.error) {
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, entityResolutionFollowupStepLimitation()));
continue;
}
if (!derivedEntityResolution) {
derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
}
pushUnique(executedPrimitives, step.primitive_id);
probeResults.push(
buildEntityResolutionCoverageProbeResult({
resolution: derivedEntityResolution
})
);
pushReason(reasonCodes, "pilot_probe_coverage_executed_for_entity_resolution");
if (derivedEntityResolution?.resolution_status === "resolved") {
pushReason(reasonCodes, "pilot_entity_resolution_grounding_stable_for_downstream_probe");
} else if (derivedEntityResolution?.resolution_status === "ambiguous") {
pushReason(reasonCodes, "pilot_entity_resolution_coverage_requires_clarification");
} else {
pushReason(reasonCodes, "pilot_entity_resolution_coverage_not_confirmed");
}
continue;
}
skippedPrimitives.push(step.primitive_id);
probeResults.push(skippedProbeResult(step, "pilot_entity_resolution_step_not_implemented"));
} }
const sourceRowsSummary = queryResult ? summarizeEntityResolutionRows(queryResult) : null; const sourceRowsSummary = queryResult ? summarizeEntityResolutionRows(queryResult) : null;
const derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity); if (!derivedEntityResolution && queryResult && !queryResult.error) {
derivedEntityResolution = deriveEntityResolution(queryResult, requestedEntity);
}
if (derivedEntityResolution?.resolution_status === "resolved") { if (derivedEntityResolution?.resolution_status === "resolved") {
pushReason(reasonCodes, "pilot_derived_entity_resolution_from_catalog_rows"); pushReason(reasonCodes, "pilot_derived_entity_resolution_from_catalog_rows");
} }
@ -2195,7 +2300,7 @@ export async function executeAssistantMcpDiscoveryPilot(
unknownFacts: buildEntityResolutionUnknownFacts(derivedEntityResolution, requestedEntity), unknownFacts: buildEntityResolutionUnknownFacts(derivedEntityResolution, requestedEntity),
sourceRowsSummary, sourceRowsSummary,
queryLimitations, queryLimitations,
recommendedNextProbe: "resolve_entity_reference" recommendedNextProbe: null
}); });
return { return {

View File

@ -214,6 +214,68 @@ function readDiscoveryTurnMeaning(
return toRecordObject(turnInput?.turn_meaning_ref); return toRecordObject(turnInput?.turn_meaning_ref);
} }
function readTruthAnswerShape(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): Record<string, unknown> | null {
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
if (directShape) {
return directShape;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
return toRecordObject(truthAnswerPolicy?.answer_shape);
}
function hasEffectivelyFactualAddressReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
if (toNonEmptyString(input.currentReplyType) === "factual") {
return true;
}
const truthAnswerShape = readTruthAnswerShape(input);
return toNonEmptyString(truthAnswerShape?.reply_type) === "factual";
}
function readStateTransitionReasonCodes(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): string[] {
const directTransition = toRecordObject(input.addressRuntimeMeta?.assistant_state_transition_v1);
const fallbackTransition = toRecordObject(input.addressRuntimeMeta?.state_transition_contract);
const stateTransition = directTransition ?? fallbackTransition;
if (!stateTransition || !Array.isArray(stateTransition.reason_codes)) {
return [];
}
return stateTransition.reason_codes
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item));
}
function hasRuntimeAdjustedExactReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
const hasFullConfirmedTruth =
truthGateStatus === "full_confirmed" ||
sourceTruthGateStatus === "full_confirmed" ||
(coverageStatus === "full" && groundingStatus === "grounded");
if (!hasFullConfirmedTruth) {
return false;
}
const truthAnswerShape = readTruthAnswerShape(input);
const capabilityContractId = toNonEmptyString(truthAnswerShape?.capability_contract_id);
if (!capabilityContractId) {
return false;
}
return readStateTransitionReasonCodes(input).some(
(reason) => /^intent_adjusted_to_.+_followup_context$/i.test(reason)
);
}
function hasAlignedFactualAddressReply( function hasAlignedFactualAddressReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput, input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
@ -221,7 +283,7 @@ function hasAlignedFactualAddressReply(
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false; return false;
} }
if (toNonEmptyString(input.currentReplyType) !== "factual") { if (!hasEffectivelyFactualAddressReply(input)) {
return false; return false;
} }
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent); const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
@ -235,7 +297,10 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false; return false;
} }
if (toNonEmptyString(input.currentReplyType) !== "factual") { if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasRuntimeAdjustedExactReply(input, entryPoint)) {
return false; return false;
} }
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent); const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
@ -253,7 +318,7 @@ function hasMatchedFactualAddressContinuationTarget(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput, input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean { ): boolean {
if (toNonEmptyString(input.currentReplyType) !== "factual") { if (!hasEffectivelyFactualAddressReply(input)) {
return false; return false;
} }
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) { if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
@ -274,7 +339,7 @@ function hasFullConfirmedFactualAddressReply(
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) { if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false; return false;
} }
if (toNonEmptyString(input.currentReplyType) !== "factual") { if (!hasEffectivelyFactualAddressReply(input)) {
return false; return false;
} }
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) { if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
@ -310,6 +375,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint); const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint); const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint); const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
if (!entryPoint) { if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point"); pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
@ -338,6 +404,12 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (fullConfirmedFactualAddressReply) { if (fullConfirmedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply"); pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
} }
if (runtimeAdjustedExactReply) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning"
);
}
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") { if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
pushReason( pushReason(
reasonCodes, reasonCodes,
@ -363,6 +435,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
!alignedFactualAddressReply && !alignedFactualAddressReply &&
!matchedFactualAddressContinuationTarget && !matchedFactualAddressContinuationTarget &&
!fullConfirmedFactualAddressReply && !fullConfirmedFactualAddressReply &&
!runtimeAdjustedExactReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") && !(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) && ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime && candidate.eligible_for_future_hot_runtime &&

View File

@ -306,6 +306,8 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
unsupported: string | null; unsupported: string | null;
counterparty: string | null; counterparty: string | null;
discoveryEntity: string | null; discoveryEntity: string | null;
entityResolutionStatus: string | null;
entityResolutionAmbiguityCandidates: string[];
organization: string | null; organization: string | null;
dateScope: string | null; dateScope: string | null;
metadataRouteFamily: string | null; metadataRouteFamily: string | null;
@ -323,13 +325,19 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
? mapPilotScopeToFollowupMeaning(pilotScope) ? mapPilotScopeToFollowupMeaning(pilotScope)
: mapAddressIntentToFollowupMeaning(previousIntent); : mapAddressIntentToFollowupMeaning(previousIntent);
const discoveryEntities = collectEntityCandidates(followupContext?.previous_discovery_entity_candidates); const discoveryEntities = collectEntityCandidates(followupContext?.previous_discovery_entity_candidates);
const entityResolutionStatus = toNonEmptyString(followupContext?.previous_discovery_entity_resolution_status);
const entityResolutionAmbiguityCandidates = collectEntityCandidates(
followupContext?.previous_discovery_entity_ambiguity_candidates
);
const ambiguityBlocksImplicitGrounding =
pilotScope === "entity_resolution_search_v1" && entityResolutionStatus === "ambiguous";
const counterparty = const counterparty =
toNonEmptyString(previousFilters?.counterparty) ?? toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty) ?? toNonEmptyString(rootFilters?.counterparty) ??
(toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty" (toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty"
? toNonEmptyString(followupContext?.previous_anchor_value) ? toNonEmptyString(followupContext?.previous_anchor_value)
: null) ?? : null) ??
(discoveryEntities[0] ?? null); (ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null);
const organization = const organization =
toNonEmptyString(previousFilters?.organization) ?? toNonEmptyString(previousFilters?.organization) ??
toNonEmptyString(rootFilters?.organization) ?? toNonEmptyString(rootFilters?.organization) ??
@ -345,7 +353,9 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
action: mapped.action, action: mapped.action,
unsupported: mapped.unsupported, unsupported: mapped.unsupported,
counterparty, counterparty,
discoveryEntity: discoveryEntities[0] ?? null, discoveryEntity: ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null,
entityResolutionStatus,
entityResolutionAmbiguityCandidates,
organization, organization,
dateScope, dateScope,
metadataRouteFamily: toNonEmptyString(followupContext?.previous_discovery_metadata_route_family), metadataRouteFamily: toNonEmptyString(followupContext?.previous_discovery_metadata_route_family),
@ -492,6 +502,105 @@ function rawEntityResolutionCandidate(text: string): string | null {
return null; return null;
} }
function resolveEntityResolutionAmbiguityChoice(text: string, candidates: string[]): string | null {
const normalizedText = canonicalizeEntityResolutionCandidate(text);
if (!normalizedText || candidates.length <= 0) {
return null;
}
const exactMatch = candidates.find(
(candidate) => canonicalizeEntityResolutionCandidate(candidate) === normalizedText
);
if (exactMatch) {
return exactMatch;
}
const includedMatches = candidates.filter((candidate) => {
const normalizedCandidate = canonicalizeEntityResolutionCandidate(candidate);
return normalizedCandidate.length > 0 && normalizedText.includes(normalizedCandidate);
});
if (includedMatches.length === 1) {
return includedMatches[0];
}
const narrowedMatches = candidates.filter((candidate) => {
const normalizedCandidate = canonicalizeEntityResolutionCandidate(candidate);
return normalizedText.length >= 4 && normalizedCandidate.includes(normalizedText);
});
if (narrowedMatches.length === 1) {
return narrowedMatches[0];
}
const normalizedLowerText = compactLower(text);
const ordinalMatchers: Array<{ index: number; keywords: string[]; numericPatterns: RegExp[] }> = [
{
index: 0,
keywords: ["первый"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|)?\s*1(?:$|[^\p{L}\p{N}])/iu,
/^\s*1\s*$/u
]
},
{
index: 1,
keywords: ["второй"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|)?\s*2(?:$|[^\p{L}\p{N}])/iu,
/^\s*2\s*$/u
]
},
{
index: 2,
keywords: ["третий"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|)?\s*3(?:$|[^\p{L}\p{N}])/iu,
/^\s*3\s*$/u
]
},
{
index: 3,
keywords: ["четвертый", "четвёртый"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|)?\s*4(?:$|[^\p{L}\p{N}])/iu,
/^\s*4\s*$/u
]
},
{
index: 4,
keywords: ["пятый"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|)?\s*5(?:$|[^\p{L}\p{N}])/iu,
/^\s*5\s*$/u
]
},
{
index: 5,
keywords: ["шестой"],
numericPatterns: [
/(?:^|[^\p{L}\p{N}])(?:вариант|номер|)?\s*6(?:$|[^\p{L}\p{N}])/iu,
/^\s*6\s*$/u
]
}
];
for (const matcher of ordinalMatchers) {
if (
matcher.index < candidates.length &&
(matcher.keywords.some((keyword) => normalizedLowerText.includes(keyword)) ||
matcher.numericPatterns.some((pattern) => pattern.test(normalizedLowerText)))
) {
return candidates[matcher.index];
}
}
if (
candidates.length > 0 &&
(normalizedLowerText.includes("последний") || normalizedLowerText.includes("крайний"))
) {
return candidates[candidates.length - 1] ?? null;
}
return null;
}
function metadataActionFromRawText(text: string): string { function metadataActionFromRawText(text: string): string {
if (/(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|objects?)/iu.test(text)) { if (/(?:\u043e\u0431\u044a\u0435\u043a\u0442(?:\u044b|\u0430|\u043e\u0432)?|objects?)/iu.test(text)) {
return "inspect_surface"; return "inspect_surface";
@ -642,7 +751,16 @@ export function buildAssistantMcpDiscoveryTurnInput(
const rawDateScope = collectDateScopeFromRawText(rawText); const rawDateScope = collectDateScopeFromRawText(rawText);
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null; const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null; const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
const entityResolutionSignal = rawEntityResolutionSignal || Boolean(rawEntityCandidate); const entityResolutionClarificationCandidate =
followupSeed.pilotScope === "entity_resolution_search_v1" &&
followupSeed.entityResolutionStatus === "ambiguous"
? resolveEntityResolutionAmbiguityChoice(
rawEntitySourceText,
followupSeed.entityResolutionAmbiguityCandidates
)
: null;
const entityResolutionSignal =
rawEntityResolutionSignal || Boolean(rawEntityCandidate) || Boolean(entityResolutionClarificationCandidate);
const metadataDocumentHintSignal = const metadataDocumentHintSignal =
hasDocumentEvidenceFollowupSignal(rawText) || hasPronounDocumentEvidenceFollowupSignal(rawText); hasDocumentEvidenceFollowupSignal(rawText) || hasPronounDocumentEvidenceFollowupSignal(rawText);
const metadataMovementHintSignal = const metadataMovementHintSignal =
@ -712,6 +830,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
!rawMetadataSignal && !rawMetadataSignal &&
metadataDocumentHintSignal metadataDocumentHintSignal
); );
const entityResolutionClarifiedDocumentFollowupApplicable = Boolean(
followupSeed.pilotScope === "entity_resolution_search_v1" &&
followupSeed.entityResolutionStatus === "ambiguous" &&
entityResolutionClarificationCandidate &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
metadataDocumentHintSignal
);
const entityResolutionGroundedMovementFollowupApplicable = Boolean( const entityResolutionGroundedMovementFollowupApplicable = Boolean(
followupSeed.pilotScope === "entity_resolution_search_v1" && followupSeed.pilotScope === "entity_resolution_search_v1" &&
(followupSeed.counterparty || followupSeed.discoveryEntity) && (followupSeed.counterparty || followupSeed.discoveryEntity) &&
@ -720,6 +847,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
!rawMetadataSignal && !rawMetadataSignal &&
metadataMovementHintSignal metadataMovementHintSignal
); );
const entityResolutionClarifiedMovementFollowupApplicable = Boolean(
followupSeed.pilotScope === "entity_resolution_search_v1" &&
followupSeed.entityResolutionStatus === "ambiguous" &&
entityResolutionClarificationCandidate &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
metadataMovementHintSignal
);
const groundedValueFlowFollowupApplicable = Boolean( const groundedValueFlowFollowupApplicable = Boolean(
rawValueFlowSignal && rawValueFlowSignal &&
!rawLifecycleSignal && !rawLifecycleSignal &&
@ -821,6 +957,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
metadataGroundedDocumentFollowupApplicable || metadataGroundedDocumentFollowupApplicable ||
metadataAmbiguityResolvedDocumentFollowupApplicable || metadataAmbiguityResolvedDocumentFollowupApplicable ||
entityResolutionGroundedDocumentFollowupApplicable || entityResolutionGroundedDocumentFollowupApplicable ||
entityResolutionClarifiedDocumentFollowupApplicable ||
valueFlowGroundedDocumentFollowupApplicable || valueFlowGroundedDocumentFollowupApplicable ||
movementEvidenceGroundedDocumentFollowupApplicable || movementEvidenceGroundedDocumentFollowupApplicable ||
(metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") || (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "document_evidence") ||
@ -829,6 +966,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
metadataGroundedMovementFollowupApplicable || metadataGroundedMovementFollowupApplicable ||
metadataAmbiguityResolvedMovementFollowupApplicable || metadataAmbiguityResolvedMovementFollowupApplicable ||
entityResolutionGroundedMovementFollowupApplicable || entityResolutionGroundedMovementFollowupApplicable ||
entityResolutionClarifiedMovementFollowupApplicable ||
valueFlowGroundedMovementFollowupApplicable || valueFlowGroundedMovementFollowupApplicable ||
documentEvidenceGroundedMovementFollowupApplicable || documentEvidenceGroundedMovementFollowupApplicable ||
(metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") || (metadataGroundedLaneContinuationApplicable && followupSeed.metadataRouteFamily === "movement_evidence") ||
@ -887,11 +1025,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
lifecycleSignal, lifecycleSignal,
valueFlowSignal, valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable, metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
entityResolutionSignal entityResolutionSignal:
entityResolutionSignal &&
!metadataGroundedDocumentLaneApplicable &&
!metadataGroundedMovementLaneApplicable
}); });
const groundedFollowupEntity = followupSeed.counterparty ?? followupSeed.discoveryEntity; const groundedFollowupEntity = followupSeed.counterparty ?? followupSeed.discoveryEntity;
const entityCandidates = entityResolutionSignal ? [] : []; const entityCandidates = entityResolutionSignal ? [] : [];
if (entityResolutionSignal) { if (entityResolutionSignal) {
pushNormalizedEntityResolutionCandidate(entityCandidates, entityResolutionClarificationCandidate);
pushNormalizedEntityResolutionCandidate(entityCandidates, rawEntityCandidate); pushNormalizedEntityResolutionCandidate(entityCandidates, rawEntityCandidate);
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) { for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate); pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
@ -1043,12 +1185,14 @@ export function buildAssistantMcpDiscoveryTurnInput(
explicitIntentCandidate, explicitIntentCandidate,
followupDiscoverySeedApplicable: followupDiscoverySeedApplicable:
followupDiscoverySeedApplicable || followupDiscoverySeedApplicable ||
Boolean(entityResolutionClarificationCandidate) ||
effectiveMetadataFollowupSeedApplicable || effectiveMetadataFollowupSeedApplicable ||
metadataAmbiguityLaneClarificationApplicable || metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable || metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable || metadataGroundedDocumentLaneApplicable ||
groundedValueFlowFollowupApplicable, groundedValueFlowFollowupApplicable,
forceDiscoveryOverExplicitIntent: forceDiscoveryOverExplicitIntent:
Boolean(entityResolutionClarificationCandidate) ||
metadataAmbiguityLaneClarificationApplicable || metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable || metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable || metadataGroundedDocumentLaneApplicable ||
@ -1057,7 +1201,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
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 || effectiveMetadataFollowupSeedApplicable || metadataAmbiguityLaneClarificationApplicable : followupDiscoverySeedApplicable ||
Boolean(entityResolutionClarificationCandidate) ||
effectiveMetadataFollowupSeedApplicable ||
metadataAmbiguityLaneClarificationApplicable
? "followup_context" ? "followup_context"
: metadataGroundedMovementLaneApplicable : metadataGroundedMovementLaneApplicable
? "followup_context" ? "followup_context"
@ -1093,6 +1240,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (rawEntityCandidate) { if (rawEntityCandidate) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_from_raw_entity_search"); pushReason(reasonCodes, "mcp_discovery_entity_scope_from_raw_entity_search");
} }
if (entityResolutionClarificationCandidate) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarification_candidate_selected");
}
if (payoutSignal) { if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected"); pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
} }
@ -1123,9 +1273,15 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (entityResolutionGroundedDocumentFollowupApplicable) { if (entityResolutionGroundedDocumentFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_document_followup"); pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_document_followup");
} }
if (entityResolutionClarifiedDocumentFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarified_document_followup");
}
if (entityResolutionGroundedMovementFollowupApplicable) { if (entityResolutionGroundedMovementFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_movement_followup"); pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_movement_followup");
} }
if (entityResolutionClarifiedMovementFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarified_movement_followup");
}
if (valueFlowGroundedDocumentFollowupApplicable) { if (valueFlowGroundedDocumentFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_value_flow_grounded_document_followup"); pushReason(reasonCodes, "mcp_discovery_value_flow_grounded_document_followup");
} }

View File

@ -13,6 +13,8 @@ import {
readAssistantMcpDiscoveryMetadataAmbiguityDetected, readAssistantMcpDiscoveryMetadataAmbiguityDetected,
readAssistantMcpDiscoveryMetadataAmbiguityEntitySets, readAssistantMcpDiscoveryMetadataAmbiguityEntitySets,
readAssistantMcpDiscoveryEntityCandidates, readAssistantMcpDiscoveryEntityCandidates,
readAssistantMcpDiscoveryEntityAmbiguityCandidates,
readAssistantMcpDiscoveryEntityResolutionStatus,
readAssistantMcpDiscoveryMetadataRouteFamily, readAssistantMcpDiscoveryMetadataRouteFamily,
readAssistantMcpDiscoveryMetadataSelectedEntitySet, readAssistantMcpDiscoveryMetadataSelectedEntitySet,
readAddressDebugTemporalScope, readAddressDebugTemporalScope,
@ -673,10 +675,18 @@ export function createAssistantTransitionPolicy(deps) {
carryoverSourceDebug, carryoverSourceDebug,
deps.toNonEmptyString deps.toNonEmptyString
); );
const sourceDiscoveryEntityResolutionStatus = readAssistantMcpDiscoveryEntityResolutionStatus(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryEntityCandidates = readAssistantMcpDiscoveryEntityCandidates( const sourceDiscoveryEntityCandidates = readAssistantMcpDiscoveryEntityCandidates(
carryoverSourceDebug, carryoverSourceDebug,
deps.toNonEmptyString deps.toNonEmptyString
); );
const sourceDiscoveryEntityAmbiguityCandidates = readAssistantMcpDiscoveryEntityAmbiguityCandidates(
carryoverSourceDebug,
deps.toNonEmptyString
);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent); const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected = const llmSelectedObjectScopeDetected =
llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true; llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
@ -1013,8 +1023,13 @@ export function createAssistantTransitionPolicy(deps) {
previous_anchor_type: previousAnchorType ?? undefined, previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor, previous_anchor_value: previousAnchor,
previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined, previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined,
previous_discovery_entity_resolution_status: sourceDiscoveryEntityResolutionStatus ?? undefined,
previous_discovery_entity_candidates: previous_discovery_entity_candidates:
sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined, sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined,
previous_discovery_entity_ambiguity_candidates:
sourceDiscoveryEntityAmbiguityCandidates.length > 0
? sourceDiscoveryEntityAmbiguityCandidates
: undefined,
previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined, previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined,
previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? undefined, previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? undefined,
previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined, previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined,

View File

@ -316,7 +316,12 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("несколько похожих контрагентов"); expect(draft.headline).toContain("несколько похожих контрагентов");
expect(draft.inference_lines.join("\n")).toContain("СВК-А"); expect(draft.inference_lines.join("\n")).toContain("СВК-А");
expect(draft.next_step_line).toContain("точное название контрагента"); expect(draft.inference_lines.join("\n")).toContain("1. СВК-А");
expect(draft.inference_lines.join("\n")).toContain("2. СВК-Б");
expect(draft.next_step_line).toContain("какой именно контрагент нужен");
expect(draft.next_step_line).toContain("1. СВК-А");
expect(draft.next_step_line).toContain("2. СВК-Б");
expect(draft.next_step_line).toContain("номером варианта");
}); });
it.skip("keeps entity search honest when no catalog candidate was confirmed", async () => { it.skip("keeps entity search honest when no catalog candidate was confirmed", async () => {

View File

@ -249,7 +249,7 @@ describe("assistant MCP discovery pilot executor", () => {
}); });
}); });
it.skip("executes entity-resolution search through the checked counterparty catalog slice", async () => { it("executes the full entity-resolution chain through the checked counterparty catalog slice", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
turnMeaning: { turnMeaning: {
asked_domain_family: "entity_resolution", asked_domain_family: "entity_resolution",
@ -268,8 +268,12 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.pilot_status).toBe("executed"); expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("entity_resolution_search_v1"); expect(result.pilot_scope).toBe("entity_resolution_search_v1");
expect(result.mcp_execution_performed).toBe(true); expect(result.mcp_execution_performed).toBe(true);
expect(result.executed_primitives).toEqual(["search_business_entity"]); expect(result.executed_primitives).toEqual([
expect(result.skipped_primitives).toEqual(["resolve_entity_reference", "probe_coverage"]); "search_business_entity",
"resolve_entity_reference",
"probe_coverage"
]);
expect(result.skipped_primitives).toEqual([]);
expect(result.derived_entity_resolution).toMatchObject({ expect(result.derived_entity_resolution).toMatchObject({
requested_entity: "Группа СВК", requested_entity: "Группа СВК",
resolution_status: "resolved", resolution_status: "resolved",
@ -279,20 +283,23 @@ describe("assistant MCP discovery pilot executor", () => {
inference_basis: "catalog_counterparty_search_rows" inference_basis: "catalog_counterparty_search_rows"
}); });
expect(result.evidence.confirmed_facts).toContain( expect(result.evidence.confirmed_facts).toContain(
"A matching 1C counterparty was found in the checked catalog slice: Группа СВК" "В проверенном каталожном срезе 1С найден контрагент: Группа СВК"
); );
expect(result.evidence.inferred_facts).toContain( expect(result.evidence.inferred_facts).toContain(
"Only catalog-level entity grounding was checked so far; no business rows were executed yet" "Пока проверено только заземление сущности по каталогу 1С; документы, движения и денежные показатели еще не проверялись"
); );
expect(result.evidence.unknown_facts).toContain( expect(result.evidence.unknown_facts).toContain(
"No business documents, movements, or turnovers were checked yet; only catalog grounding was attempted" "Документы, движения и денежные показатели по этому контрагенту еще не проверялись; пока был только каталожный поиск"
); );
expect(result.reason_codes).toContain("pilot_search_business_entity_mcp_executed"); expect(result.reason_codes).toContain("pilot_search_business_entity_mcp_executed");
expect(result.reason_codes).toContain("pilot_resolve_entity_reference_from_catalog_rows");
expect(result.reason_codes).toContain("pilot_probe_coverage_executed_for_entity_resolution");
expect(result.reason_codes).toContain("pilot_entity_resolution_grounding_stable_for_downstream_probe");
expect(result.reason_codes).toContain("pilot_derived_entity_resolution_from_catalog_rows"); expect(result.reason_codes).toContain("pilot_derived_entity_resolution_from_catalog_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1); expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1);
}); });
it.skip("keeps entity-resolution honest when several catalog candidates remain ambiguous", async () => { it("keeps entity-resolution honest when several catalog candidates remain ambiguous", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
turnMeaning: { turnMeaning: {
asked_domain_family: "entity_resolution", asked_domain_family: "entity_resolution",
@ -310,6 +317,12 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.pilot_status).toBe("executed"); expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("entity_resolution_search_v1"); expect(result.pilot_scope).toBe("entity_resolution_search_v1");
expect(result.executed_primitives).toEqual([
"search_business_entity",
"resolve_entity_reference",
"probe_coverage"
]);
expect(result.skipped_primitives).toEqual([]);
expect(result.derived_entity_resolution).toMatchObject({ expect(result.derived_entity_resolution).toMatchObject({
requested_entity: "СВК", requested_entity: "СВК",
resolution_status: "ambiguous", resolution_status: "ambiguous",
@ -319,8 +332,10 @@ describe("assistant MCP discovery pilot executor", () => {
}); });
expect(result.evidence.confirmed_facts).toEqual([]); expect(result.evidence.confirmed_facts).toEqual([]);
expect(result.evidence.unknown_facts).toContain( expect(result.evidence.unknown_facts).toContain(
"Exact 1C counterparty grounding remains ambiguous across: СВК-А, СВК-Б" "Точное заземление контрагента в 1С остается неоднозначным между вариантами: СВК-А, СВК-Б"
); );
expect(result.reason_codes).toContain("pilot_resolve_entity_reference_requires_clarification");
expect(result.reason_codes).toContain("pilot_probe_coverage_executed_for_entity_resolution");
expect(result.reason_codes).toContain("pilot_entity_resolution_ambiguity_requires_clarification"); expect(result.reason_codes).toContain("pilot_entity_resolution_ambiguity_requires_clarification");
}); });

View File

@ -281,6 +281,75 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply"); expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
}); });
it("keeps runtime-adjusted exact VAT follow-up replies over stale discovery turn meaning", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Коротко: подтвержденный НДС к уплате за май 2017 — 0,00 руб.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "vat_liability_confirmed_for_tax_period",
truth_gate_contract_status: "full_confirmed",
assistant_truth_answer_policy_v1: {
truth_gate: {
coverage_status: "full",
grounding_status: "grounded",
source_truth_gate_status: "full_confirmed"
},
answer_shape: {
reply_type: "factual",
capability_contract_id: "confirmed_vat_liability_for_tax_period"
}
},
assistant_state_transition_v1: {
reason_codes: [
"root_followup_continue_previous",
"intent_adjusted_to_vat_followup_context",
"period_from_from_followup_context",
"period_to_from_followup_context",
"confirmed_balance_exact_vat_tax_period_intent"
]
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "confirmed_snapshot",
explicit_entity_candidates: ["ООО Альтернатива Плюс"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2017-05-31",
unsupported_but_understood_family: "counterparty_value_or_turnover"
}
},
bridge: {
bridge_status: "checked_sources_only",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: false,
answer_draft: {
answer_mode: "checked_sources_only",
headline: "Я проверил доступный контур, но подтвержденного факта для ответа не получил.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: ["Полный объем входящих поступлений вне проверенного периода этим поиском не подтвержден."],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reply_text).toContain("подтвержденный НДС к уплате");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
});
it("keeps address lane answers when discovery was not requested for the current turn", () => { it("keeps address lane answers when discovery was not requested for the current turn", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({ const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "supported exact route answer", currentReply: "supported exact route answer",

View File

@ -112,4 +112,132 @@ describe("assistant MCP discovery runtime entry point", () => {
expect(result.bridge?.pilot.pilot_scope).toBe("metadata_inspection_v1"); expect(result.bridge?.pilot.pilot_scope).toBe("metadata_inspection_v1");
expect(result.bridge?.answer_draft.headline).toContain("метаданным 1С"); expect(result.bridge?.answer_draft.headline).toContain("метаданным 1С");
}); });
it("runs the bridge for raw entity-resolution wording and executes the full grounding chain", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "найди в 1С контрагента Группа СВК",
deps: buildDeps([
{ Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" },
{ Counterparty: "СВК Логистика", CounterpartyRef: "Ref-2" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.semantic_data_need).toBe("entity discovery evidence");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["Группа СВК"]
});
expect(result.bridge?.bridge_status).toBe("answer_draft_ready");
expect(result.bridge?.pilot.pilot_scope).toBe("entity_resolution_search_v1");
expect(result.bridge?.pilot.executed_primitives).toEqual([
"search_business_entity",
"resolve_entity_reference",
"probe_coverage"
]);
expect(result.bridge?.pilot.derived_entity_resolution).toMatchObject({
resolution_status: "resolved",
resolved_entity: "Группа СВК",
resolved_reference: "Ref-1"
});
expect(result.bridge?.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
});
it("runs the bridge again when the user clarifies one ambiguous entity-resolution candidate", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "СВК-А",
followupContext: {
previous_discovery_pilot_scope: "entity_resolution_search_v1",
previous_discovery_entity_resolution_status: "ambiguous",
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
},
deps: buildDeps([
{ Counterparty: "СВК-А", CounterpartyRef: "Ref-1" },
{ Counterparty: "СВК-Б", CounterpartyRef: "Ref-2" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.source_signal).toBe("followup_context");
expect(result.turn_input.semantic_data_need).toBe("entity discovery evidence");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["СВК-А"]
});
expect(result.bridge?.pilot.pilot_scope).toBe("entity_resolution_search_v1");
expect(result.bridge?.pilot.executed_primitives).toEqual([
"search_business_entity",
"resolve_entity_reference",
"probe_coverage"
]);
expect(result.bridge?.pilot.derived_entity_resolution).toMatchObject({
resolution_status: "resolved",
resolved_entity: "СВК-А",
resolved_reference: "Ref-1"
});
});
it("chains an ordinal ambiguity clarification directly into value-flow execution", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "второй вариант, сколько получили за 2020 год",
followupContext: {
previous_discovery_pilot_scope: "entity_resolution_search_v1",
previous_discovery_entity_resolution_status: "ambiguous",
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
},
deps: buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 1200, Counterparty: "СВК-Б" },
{ Period: "2020-02-20T00:00:00", Amount: 800, Counterparty: "СВК-Б" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.source_signal).toBe("followup_context");
expect(result.turn_input.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["СВК-Б"],
explicit_date_scope: "2020"
});
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
expect(result.bridge?.pilot.derived_value_flow).toMatchObject({
counterparty: "СВК-Б",
period_scope: "2020",
total_amount: 2000
});
});
it("chains an ordinal ambiguity clarification directly into document evidence execution", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "первый вариант, покажи документы за 2020 год",
followupContext: {
previous_discovery_pilot_scope: "entity_resolution_search_v1",
previous_discovery_entity_resolution_status: "ambiguous",
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
},
deps: buildDeps([{ Period: "2020-03-12T00:00:00", Counterparty: "СВК-А", Registrar: "Doc-1" }])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.source_signal).toBe("followup_context");
expect(result.turn_input.semantic_data_need).toBe("document evidence");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["СВК-А"],
explicit_date_scope: "2020"
});
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1");
expect(result.bridge?.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
});
}); });

View File

@ -261,6 +261,163 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_followup_context"); expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_followup_context");
}); });
it("restarts entity-resolution when the user clarifies one ambiguous candidate", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "СВК-А",
followupContext: {
previous_discovery_pilot_scope: "entity_resolution_search_v1",
previous_discovery_entity_resolution_status: "ambiguous",
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
}
});
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("entity discovery evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["СВК-А"],
unsupported_but_understood_family: "entity_resolution",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_entity_resolution_signal_detected");
expect(result.reason_codes).toContain("mcp_discovery_entity_resolution_clarification_candidate_selected");
});
it("restarts entity-resolution when the user clarifies an ambiguous candidate by ordinal choice", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "второй вариант",
followupContext: {
previous_discovery_pilot_scope: "entity_resolution_search_v1",
previous_discovery_entity_resolution_status: "ambiguous",
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
}
});
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("entity discovery evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["СВК-Б"],
unsupported_but_understood_family: "entity_resolution",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_entity_resolution_clarification_candidate_selected");
});
it("chains an ordinal ambiguity clarification directly into value-flow follow-up intent", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "второй вариант, сколько получили за 2020 год",
followupContext: {
previous_discovery_pilot_scope: "entity_resolution_search_v1",
previous_discovery_entity_resolution_status: "ambiguous",
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
}
});
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("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["СВК-Б"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_entity_resolution_clarification_candidate_selected");
expect(result.reason_codes).toContain("mcp_discovery_value_flow_signal_detected");
});
it("chains an ordinal ambiguity clarification directly into document evidence follow-up intent", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "второй вариант, покажи документы за 2020 год",
followupContext: {
previous_discovery_pilot_scope: "entity_resolution_search_v1",
previous_discovery_entity_resolution_status: "ambiguous",
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
}
});
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("document evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["СВК-Б"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "document_evidence",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_entity_resolution_clarification_candidate_selected");
expect(result.reason_codes).toContain("mcp_discovery_entity_resolution_clarified_document_followup");
});
it("chains an ordinal ambiguity clarification directly into movement evidence follow-up intent", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "первый вариант, покажи движения за 2020 год",
followupContext: {
previous_discovery_pilot_scope: "entity_resolution_search_v1",
previous_discovery_entity_resolution_status: "ambiguous",
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
}
});
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("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["СВК-А"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_entity_resolution_clarification_candidate_selected");
expect(result.reason_codes).toContain("mcp_discovery_entity_resolution_clarified_movement_followup");
});
it("does not silently ground value-flow follow-ups from an ambiguous entity-resolution answer", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "сколько получили по нему за 2020 год",
followupContext: {
previous_discovery_pilot_scope: "entity_resolution_search_v1",
previous_discovery_entity_resolution_status: "ambiguous",
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).not.toContain("mcp_discovery_grounded_value_flow_followup");
});
it("overrides a wrong exact document intent when a grounded document follow-up asks to switch into movements", () => { it("overrides a wrong exact document intent when a grounded document follow-up asks to switch into movements", () => {
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а теперь по нему движения за 2020 год", userMessage: "а теперь по нему движения за 2020 год",

View File

@ -1173,6 +1173,65 @@ describe("assistantTransitionPolicy", () => {
}); });
}); });
it("carries ambiguity candidates from entity-resolution discovery into followup context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
hasAddressFollowupContextSignal: () => true
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"СВК-А",
[
{
role: "assistant",
text: "По каталогу 1С нашлось несколько похожих контрагентов: СВК-А, СВК-Б.",
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: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["СВК"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "entity_resolution_search_v1",
derived_entity_resolution: {
requested_entity: "СВК",
resolution_status: "ambiguous",
resolved_entity: null,
ambiguity_candidates: ["СВК-А", "СВК-Б"]
}
},
answer_draft: {
answer_mode: "needs_clarification"
}
}
}
}
}
],
null,
null,
null
);
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("entity_resolution_search_v1");
expect(carryover?.followupContext?.previous_discovery_entity_resolution_status).toBe("ambiguous");
expect(carryover?.followupContext?.previous_discovery_entity_candidates).toEqual(["СВК", "СВК-А", "СВК-Б"]);
expect(carryover?.followupContext?.previous_discovery_entity_ambiguity_candidates).toEqual([
"СВК-А",
"СВК-Б"
]);
});
it("keeps exact payout carryover for a short net follow-up without restating counterparty or year", () => { it("keeps exact payout carryover for a short net follow-up without restating counterparty or year", () => {
const policy = buildPolicy({ const policy = buildPolicy({
findLastAddressAssistantItem: () => ({ findLastAddressAssistantItem: () => ({