Архитектура: ввести answer-inspection follow-up contract и закрыть phase15 replay

This commit is contained in:
2026-04-18 23:04:37 +03:00
parent c605fae3a3
commit 1d48f01841
16 changed files with 575 additions and 13 deletions
@@ -75,6 +75,36 @@ function buildAddressMemoryRecapReply(input) {
}
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
}
function buildSelectedObjectAnswerInspectionReply(input) {
const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
? input.addressDebug.extracted_filters
: null;
const item = input.toNonEmptyString(extractedFilters?.item) ??
(String(input.addressDebug?.anchor_type ?? "") === "item"
? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ??
input.toNonEmptyString(input.addressDebug?.anchor_value_raw)
: null);
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
const itemLabel = item ?? "эта позиция";
if (detectedIntent === "inventory_sale_trace_for_item") {
return [
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция, по которой мы смотрели продажу.`,
"В предыдущем ответе я показывал документы выбытия по этой позиции. Покупатель в доступных данных отдельно не выделен, поэтому назвать контрагента-покупателя я там не мог.",
"Если хочешь, следующим шагом могу отдельно проверить, можно ли вытащить покупателя по связанным документам реализации."
].join(" ");
}
if (detectedIntent === "inventory_purchase_provenance_for_item" ||
detectedIntent === "inventory_purchase_documents_for_item") {
return [
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция / номенклатура.`,
"В предыдущем ответе речь шла о закупке этой позиции: я перечислял поставщиков или закупочные документы по ней, а не называл саму позицию контрагентом."
].join(" ");
}
return [
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а выбранный объект разбора.`,
"Я сейчас уточняю именно смысл предыдущего grounded-ответа по этой позиции, а не запускаю новый адресный поиск."
].join(" ");
}
async function runAssistantLivingChatRuntime(input) {
const userMessage = String(input.userMessage ?? "");
const organizationAuthority = (0, assistantContinuityPolicy_1.resolveAssistantOrganizationAuthority)({
@@ -114,6 +144,7 @@ async function runAssistantLivingChatRuntime(input) {
const contextualMemoryRecapFollowup = memoryRecapContext.contextualMemoryRecapFollowup;
const lastGroundedInventoryAddressDebug = memoryRecapContext.lastGroundedInventoryAddressDebug;
const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug;
const contextualAnswerInspectionFollowup = String(input.modeDecision?.reason ?? "") === "answer_inspection_followup_detected";
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
chatText = input.buildAssistantSafetyRefusalReply();
livingChatSource = "deterministic_safety_refusal";
@@ -178,6 +209,13 @@ async function runAssistantLivingChatRuntime(input) {
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_memory_recap_contract";
}
else if (contextualAnswerInspectionFollowup) {
chatText = buildSelectedObjectAnswerInspectionReply({
addressDebug: continuitySnapshot.lastGroundedItemAddressDebug ?? continuitySnapshot.lastGroundedAddressDebug,
toNonEmptyString: input.toNonEmptyString
});
livingChatSource = "deterministic_answer_inspection_contract";
}
else if (capabilityMetaQuery) {
chatText = input.buildAssistantCapabilityContractReply(userMessage);
livingChatSource = "deterministic_capability_contract";
@@ -96,6 +96,29 @@ function createAssistantLivingModePolicy(deps) {
hasDataRetrievalRequestSignal(sample) ||
hasStrongDataIntentSignal(sample));
}
function hasAnswerInspectionFollowupSignal(userMessage) {
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
const samples = [rawText, repairedText]
.filter((item) => item.length > 0)
.map((item) => item.replace(/ё/g, "е"));
if (samples.length === 0) {
return false;
}
const hasInspectionCue = samples.some((sample) => /(?:ошибк|неверн|не так|перепутал|у тебя написано|ты написал|это ошибка|правильно ли|корректн)/iu.test(sample));
if (!hasInspectionCue) {
return false;
}
const hasAnswerObjectCue = samples.some((sample) => /(?:контрагент|покупател|поставщик|поле|ответ|позици|номенклат|документ)/iu.test(sample));
if (!hasAnswerObjectCue) {
return false;
}
const hasFreshRetrievalAction = samples.some((sample) => /(?:покажи|выведи|найди|список|проверь по базе|подними документы|достань|рассчитай|посчитай|сколько|кто\s+нам|кто\s+у\s+нас)/iu.test(sample));
if (hasFreshRetrievalAction) {
return false;
}
return true;
}
function hasConversationMemoryRecallFollowupSignal(userMessage) {
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
@@ -348,6 +371,7 @@ function createAssistantLivingModePolicy(deps) {
hasDataRetrievalRequestSignal,
hasOrganizationFactLookupSignal,
hasMetaAnswerFollowupSignal,
hasAnswerInspectionFollowupSignal,
hasConversationMemoryRecallFollowupSignal,
hasHistoricalCapabilityFollowupSignal,
hasOrganizationFactFollowupSignal,
@@ -27,14 +27,16 @@ function createAssistantMetaFollowupPolicy(deps) {
return {
dataScopeMetaQuery: false,
capabilityMetaQuery: false,
metaAnswerFollowupSignal: false
metaAnswerFollowupSignal: false,
answerInspectionFollowupSignal: false
};
}
return {
dataScopeMetaQuery: hasSignalAcrossSamples(samples, deps.hasAssistantDataScopeMetaQuestionSignal),
capabilityMetaQuery: hasSignalAcrossSamples(samples, deps.shouldHandleAsAssistantCapabilityMetaQuery) ||
hasImplicitHistoricalCapabilityMetaSignal(samples),
metaAnswerFollowupSignal: hasSignalAcrossSamples(samples, deps.hasMetaAnswerFollowupSignal)
metaAnswerFollowupSignal: hasSignalAcrossSamples(samples, deps.hasMetaAnswerFollowupSignal),
answerInspectionFollowupSignal: hasSignalAcrossSamples(samples, deps.hasAnswerInspectionFollowupSignal)
};
}
function resolveHardMetaMode(input) {
@@ -60,9 +62,18 @@ function createAssistantMetaFollowupPolicy(deps) {
(!input.llmContractIntent || String(input.llmContractIntent) === "unknown") &&
String(input.llmContractMode ?? "") !== "address_query");
}
function isAnswerInspectionFollowupOverGroundedAnswer(input) {
return Boolean(input.followupContext &&
input.hasPriorAddressAnswerContext &&
input.answerInspectionFollowupSignal &&
!input.dataScopeMetaQuery &&
!input.capabilityMetaQuery &&
!input.aggregateBusinessAnalyticsSignal);
}
return {
resolveMetaSignalSet,
resolveHardMetaMode,
isMetaFollowupOverGroundedAnswer
isMetaFollowupOverGroundedAnswer,
isAnswerInspectionFollowupOverGroundedAnswer
};
}
@@ -47,7 +47,7 @@ function shouldBypassStrictDeepInvestigationCueForAddressIntent(intent) {
return Boolean(intent && ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS.has(intent));
}
function createAssistantRoutePolicy(deps) {
const { repairAddressMojibake, findLastGroundedAddressAnswerDebug, findLastOrganizationClarificationAddressDebug, mergeKnownOrganizations, normalizeOrganizationScopeValue, resolveOrganizationSelectionFromMessage, resolveMetaSignalSet, resolveHardMetaMode, isMetaFollowupOverGroundedAnswer, hasDataRetrievalRequestSignal, hasOrganizationFactLookupSignal, hasOrganizationFactFollowupSignal, hasAggregateBusinessAnalyticsSignal, hasStandaloneAddressTopicSignal, hasOpenContractsAddressSignal, detectAddressQuestionMode, resolveAddressIntent, toNonEmptyString, hasStrictDeepInvestigationCue, hasStrongDataIntentSignal, hasAccountingSignal, hasDangerOrCoercionSignal, hasAddressFollowupContextSignal, hasShortDebtMirrorFollowupSignal, isInventorySelectedObjectIntent, hasShortInventoryObjectFollowupSignal, isGroundedInventoryContextDebug, resolveRouteMemorySignals, findLastAddressAssistantItem, resolveAddressToolGateDecision, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, hasDirectDeepAnalysisSignal, compactWhitespace, hasDeepSessionContinuationSignal, resolveLivingAssistantModeDecision, resolveProviderExecutionState } = deps;
const { repairAddressMojibake, findLastGroundedAddressAnswerDebug, findLastOrganizationClarificationAddressDebug, mergeKnownOrganizations, normalizeOrganizationScopeValue, resolveOrganizationSelectionFromMessage, resolveMetaSignalSet, resolveHardMetaMode, isMetaFollowupOverGroundedAnswer, isAnswerInspectionFollowupOverGroundedAnswer, hasDataRetrievalRequestSignal, hasOrganizationFactLookupSignal, hasOrganizationFactFollowupSignal, hasAggregateBusinessAnalyticsSignal, hasStandaloneAddressTopicSignal, hasOpenContractsAddressSignal, detectAddressQuestionMode, resolveAddressIntent, toNonEmptyString, hasStrictDeepInvestigationCue, hasStrongDataIntentSignal, hasAccountingSignal, hasDangerOrCoercionSignal, hasAddressFollowupContextSignal, hasShortDebtMirrorFollowupSignal, isInventorySelectedObjectIntent, hasShortInventoryObjectFollowupSignal, isGroundedInventoryContextDebug, resolveRouteMemorySignals, findLastAddressAssistantItem, resolveAddressToolGateDecision, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, hasDirectDeepAnalysisSignal, compactWhitespace, hasDeepSessionContinuationSignal, resolveLivingAssistantModeDecision, resolveProviderExecutionState } = deps;
function hasInventoryRootRestatementFollowupSignal(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
if (!normalized) {
@@ -580,6 +580,7 @@ function createAssistantRoutePolicy(deps) {
};
}
const metaAnswerFollowupSignal = metaSignals.metaAnswerFollowupSignal;
const answerInspectionFollowupSignal = metaSignals.answerInspectionFollowupSignal;
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
llmPreDecomposeMeta?.applied &&
llmContractMode === "address_query") ||
@@ -707,6 +708,23 @@ function createAssistantRoutePolicy(deps) {
followupContext,
hasPriorAddressAnswerContext,
metaAnswerFollowupSignal,
answerInspectionFollowupSignal,
vatEvaluativeFollowupSignal,
dataScopeMetaQuery,
capabilityMetaQuery,
aggregateBusinessAnalyticsSignal,
dataRetrievalSignal,
strongDataSignal,
resolvedMode: resolvedModeDetection.mode,
resolvedIntent: resolvedIntentResolution.intent,
llmContractIntent,
llmContractMode
});
const answerInspectionFollowupOverGroundedAnswer = isAnswerInspectionFollowupOverGroundedAnswer({
followupContext,
hasPriorAddressAnswerContext,
metaAnswerFollowupSignal,
answerInspectionFollowupSignal,
vatEvaluativeFollowupSignal,
dataScopeMetaQuery,
capabilityMetaQuery,
@@ -762,6 +780,11 @@ function createAssistantRoutePolicy(deps) {
toolGateDecision = "skip_address_lane";
toolGateReason = "meta_followup_over_grounded_answer";
}
if (answerInspectionFollowupOverGroundedAnswer) {
runAddressLane = false;
toolGateDecision = "skip_address_lane";
toolGateReason = "answer_inspection_followup_over_grounded_answer";
}
let livingDecision = resolveLivingAssistantModeDecision({
userMessage: rawUserMessage,
addressLaneTriggered: runAddressLane,
@@ -802,6 +825,12 @@ function createAssistantRoutePolicy(deps) {
reason: "meta_followup_over_grounded_answer"
};
}
if (answerInspectionFollowupOverGroundedAnswer) {
livingDecision = {
mode: "chat",
reason: "answer_inspection_followup_detected"
};
}
return {
runAddressLane,
toolGateDecision,
@@ -834,6 +863,7 @@ function createAssistantRoutePolicy(deps) {
deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep,
aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep,
deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep,
answer_inspection_followup_over_grounded_answer: answerInspectionFollowupOverGroundedAnswer,
final_decision: {
run_address_lane: runAddressLane,
tool_gate_decision: toolGateDecision,
+3 -1
View File
@@ -4223,7 +4223,8 @@ const assistantLivingModePolicy = (0, assistantLivingModePolicy_1.createAssistan
const assistantMetaFollowupPolicy = (0, assistantMetaFollowupPolicy_1.createAssistantMetaFollowupPolicy)({
hasAssistantDataScopeMetaQuestionSignal: assistantLivingModePolicy.hasAssistantDataScopeMetaQuestionSignal,
shouldHandleAsAssistantCapabilityMetaQuery: assistantLivingModePolicy.shouldHandleAsAssistantCapabilityMetaQuery,
hasMetaAnswerFollowupSignal: assistantLivingModePolicy.hasMetaAnswerFollowupSignal
hasMetaAnswerFollowupSignal: assistantLivingModePolicy.hasMetaAnswerFollowupSignal,
hasAnswerInspectionFollowupSignal: assistantLivingModePolicy.hasAnswerInspectionFollowupSignal
});
const assistantMemoryRecapPolicy = (0, assistantMemoryRecapPolicy_1.createAssistantMemoryRecapPolicy)({
hasHistoricalCapabilityFollowupSignal: assistantLivingModePolicy.hasHistoricalCapabilityFollowupSignal,
@@ -4240,6 +4241,7 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli
resolveMetaSignalSet: assistantMetaFollowupPolicy.resolveMetaSignalSet,
resolveHardMetaMode: assistantMetaFollowupPolicy.resolveHardMetaMode,
isMetaFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isMetaFollowupOverGroundedAnswer,
isAnswerInspectionFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isAnswerInspectionFollowupOverGroundedAnswer,
hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal,
hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: assistantLivingModePolicy.hasOrganizationFactFollowupSignal,
@@ -162,6 +162,45 @@ function buildAddressMemoryRecapReply(input: {
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
}
function buildSelectedObjectAnswerInspectionReply(input: {
addressDebug: Record<string, unknown> | null;
toNonEmptyString: (value: unknown) => string | null;
}): string {
const extractedFilters =
input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
? (input.addressDebug.extracted_filters as Record<string, unknown>)
: null;
const item =
input.toNonEmptyString(extractedFilters?.item) ??
(String(input.addressDebug?.anchor_type ?? "") === "item"
? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ??
input.toNonEmptyString(input.addressDebug?.anchor_value_raw)
: null);
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
const itemLabel = item ?? "эта позиция";
if (detectedIntent === "inventory_sale_trace_for_item") {
return [
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция, по которой мы смотрели продажу.`,
"В предыдущем ответе я показывал документы выбытия по этой позиции. Покупатель в доступных данных отдельно не выделен, поэтому назвать контрагента-покупателя я там не мог.",
"Если хочешь, следующим шагом могу отдельно проверить, можно ли вытащить покупателя по связанным документам реализации."
].join(" ");
}
if (detectedIntent === "inventory_purchase_provenance_for_item" ||
detectedIntent === "inventory_purchase_documents_for_item") {
return [
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция / номенклатура.`,
"В предыдущем ответе речь шла о закупке этой позиции: я перечислял поставщиков или закупочные документы по ней, а не называл саму позицию контрагентом."
].join(" ");
}
return [
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а выбранный объект разбора.`,
"Я сейчас уточняю именно смысл предыдущего grounded-ответа по этой позиции, а не запускаю новый адресный поиск."
].join(" ");
}
export async function runAssistantLivingChatRuntime(
input: AssistantLivingChatRuntimeInput
): Promise<AssistantLivingChatRuntimeOutput> {
@@ -205,6 +244,8 @@ export async function runAssistantLivingChatRuntime(
const contextualMemoryRecapFollowup = memoryRecapContext.contextualMemoryRecapFollowup;
const lastGroundedInventoryAddressDebug = memoryRecapContext.lastGroundedInventoryAddressDebug;
const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug;
const contextualAnswerInspectionFollowup =
String(input.modeDecision?.reason ?? "") === "answer_inspection_followup_detected";
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
chatText = input.buildAssistantSafetyRefusalReply();
@@ -266,6 +307,12 @@ export async function runAssistantLivingChatRuntime(
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_memory_recap_contract";
} else if (contextualAnswerInspectionFollowup) {
chatText = buildSelectedObjectAnswerInspectionReply({
addressDebug: continuitySnapshot.lastGroundedItemAddressDebug ?? continuitySnapshot.lastGroundedAddressDebug,
toNonEmptyString: input.toNonEmptyString
});
livingChatSource = "deterministic_answer_inspection_contract";
} else if (capabilityMetaQuery) {
chatText = input.buildAssistantCapabilityContractReply(userMessage);
livingChatSource = "deterministic_capability_contract";
@@ -38,6 +38,7 @@ export interface AssistantLivingModePolicy {
hasDataRetrievalRequestSignal: (text: unknown) => boolean;
hasOrganizationFactLookupSignal: (text: unknown) => boolean;
hasMetaAnswerFollowupSignal: (text: unknown) => boolean;
hasAnswerInspectionFollowupSignal: (text: unknown) => boolean;
hasConversationMemoryRecallFollowupSignal: (text: unknown) => boolean;
hasHistoricalCapabilityFollowupSignal: (text: unknown) => boolean;
hasOrganizationFactFollowupSignal: (userMessage: unknown, items: unknown[]) => boolean;
@@ -160,6 +161,30 @@ export function createAssistantLivingModePolicy(deps: AssistantLivingModePolicyD
hasStrongDataIntentSignal(sample));
}
function hasAnswerInspectionFollowupSignal(userMessage) {
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
const samples = [rawText, repairedText]
.filter((item) => item.length > 0)
.map((item) => item.replace(/ё/g, "е"));
if (samples.length === 0) {
return false;
}
const hasInspectionCue = samples.some((sample) => /(?:ошибк|неверн|не так|перепутал|у тебя написано|ты написал|это ошибка|правильно ли|корректн)/iu.test(sample));
if (!hasInspectionCue) {
return false;
}
const hasAnswerObjectCue = samples.some((sample) => /(?:контрагент|покупател|поставщик|поле|ответ|позици|номенклат|документ)/iu.test(sample));
if (!hasAnswerObjectCue) {
return false;
}
const hasFreshRetrievalAction = samples.some((sample) => /(?:покажи|выведи|найди|список|проверь по базе|подними документы|достань|рассчитай|посчитай|сколько|кто\s+нам|кто\s+у\s+нас)/iu.test(sample));
if (hasFreshRetrievalAction) {
return false;
}
return true;
}
function hasConversationMemoryRecallFollowupSignal(userMessage) {
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
@@ -421,6 +446,7 @@ export function createAssistantLivingModePolicy(deps: AssistantLivingModePolicyD
hasDataRetrievalRequestSignal,
hasOrganizationFactLookupSignal,
hasMetaAnswerFollowupSignal,
hasAnswerInspectionFollowupSignal,
hasConversationMemoryRecallFollowupSignal,
hasHistoricalCapabilityFollowupSignal,
hasOrganizationFactFollowupSignal,
@@ -11,6 +11,7 @@ export interface ResolveAssistantMetaFollowupOverGroundedAnswerInput {
followupContext?: unknown;
hasPriorAddressAnswerContext?: boolean;
metaAnswerFollowupSignal?: boolean;
answerInspectionFollowupSignal?: boolean;
vatEvaluativeFollowupSignal?: boolean;
dataScopeMetaQuery?: boolean;
capabilityMetaQuery?: boolean;
@@ -33,12 +34,14 @@ export interface AssistantMetaSignalSet {
dataScopeMetaQuery: boolean;
capabilityMetaQuery: boolean;
metaAnswerFollowupSignal: boolean;
answerInspectionFollowupSignal: boolean;
}
export interface AssistantMetaFollowupPolicyDeps {
hasAssistantDataScopeMetaQuestionSignal: (text: unknown) => boolean;
shouldHandleAsAssistantCapabilityMetaQuery: (text: unknown) => boolean;
hasMetaAnswerFollowupSignal: (text: unknown) => boolean;
hasAnswerInspectionFollowupSignal: (text: unknown) => boolean;
}
function collectMessageSamples(input: ResolveAssistantMetaSignalSetInput): string[] {
@@ -83,7 +86,8 @@ export function createAssistantMetaFollowupPolicy(
return {
dataScopeMetaQuery: false,
capabilityMetaQuery: false,
metaAnswerFollowupSignal: false
metaAnswerFollowupSignal: false,
answerInspectionFollowupSignal: false
};
}
return {
@@ -97,6 +101,10 @@ export function createAssistantMetaFollowupPolicy(
metaAnswerFollowupSignal: hasSignalAcrossSamples(
samples,
deps.hasMetaAnswerFollowupSignal
),
answerInspectionFollowupSignal: hasSignalAcrossSamples(
samples,
deps.hasAnswerInspectionFollowupSignal
)
};
}
@@ -132,9 +140,23 @@ export function createAssistantMetaFollowupPolicy(
);
}
function isAnswerInspectionFollowupOverGroundedAnswer(
input: ResolveAssistantMetaFollowupOverGroundedAnswerInput
): boolean {
return Boolean(
input.followupContext &&
input.hasPriorAddressAnswerContext &&
input.answerInspectionFollowupSignal &&
!input.dataScopeMetaQuery &&
!input.capabilityMetaQuery &&
!input.aggregateBusinessAnalyticsSignal
);
}
return {
resolveMetaSignalSet,
resolveHardMetaMode,
isMetaFollowupOverGroundedAnswer
isMetaFollowupOverGroundedAnswer,
isAnswerInspectionFollowupOverGroundedAnswer
};
}
@@ -57,6 +57,7 @@ export function createAssistantRoutePolicy(deps) {
resolveMetaSignalSet,
resolveHardMetaMode,
isMetaFollowupOverGroundedAnswer,
isAnswerInspectionFollowupOverGroundedAnswer,
hasDataRetrievalRequestSignal,
hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal,
@@ -619,6 +620,7 @@ export function createAssistantRoutePolicy(deps) {
};
}
const metaAnswerFollowupSignal = metaSignals.metaAnswerFollowupSignal;
const answerInspectionFollowupSignal = metaSignals.answerInspectionFollowupSignal;
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
llmPreDecomposeMeta?.applied &&
llmContractMode === "address_query") ||
@@ -746,6 +748,23 @@ export function createAssistantRoutePolicy(deps) {
followupContext,
hasPriorAddressAnswerContext,
metaAnswerFollowupSignal,
answerInspectionFollowupSignal,
vatEvaluativeFollowupSignal,
dataScopeMetaQuery,
capabilityMetaQuery,
aggregateBusinessAnalyticsSignal,
dataRetrievalSignal,
strongDataSignal,
resolvedMode: resolvedModeDetection.mode,
resolvedIntent: resolvedIntentResolution.intent,
llmContractIntent,
llmContractMode
});
const answerInspectionFollowupOverGroundedAnswer = isAnswerInspectionFollowupOverGroundedAnswer({
followupContext,
hasPriorAddressAnswerContext,
metaAnswerFollowupSignal,
answerInspectionFollowupSignal,
vatEvaluativeFollowupSignal,
dataScopeMetaQuery,
capabilityMetaQuery,
@@ -801,6 +820,11 @@ export function createAssistantRoutePolicy(deps) {
toolGateDecision = "skip_address_lane";
toolGateReason = "meta_followup_over_grounded_answer";
}
if (answerInspectionFollowupOverGroundedAnswer) {
runAddressLane = false;
toolGateDecision = "skip_address_lane";
toolGateReason = "answer_inspection_followup_over_grounded_answer";
}
let livingDecision = resolveLivingAssistantModeDecision({
userMessage: rawUserMessage,
addressLaneTriggered: runAddressLane,
@@ -841,6 +865,12 @@ export function createAssistantRoutePolicy(deps) {
reason: "meta_followup_over_grounded_answer"
};
}
if (answerInspectionFollowupOverGroundedAnswer) {
livingDecision = {
mode: "chat",
reason: "answer_inspection_followup_detected"
};
}
return {
runAddressLane,
toolGateDecision,
@@ -873,6 +903,7 @@ export function createAssistantRoutePolicy(deps) {
deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep,
aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep,
deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep,
answer_inspection_followup_over_grounded_answer: answerInspectionFollowupOverGroundedAnswer,
final_decision: {
run_address_lane: runAddressLane,
tool_gate_decision: toolGateDecision,
@@ -4180,7 +4180,8 @@ const assistantLivingModePolicy = (0, assistantLivingModePolicy_1.createAssistan
const assistantMetaFollowupPolicy = (0, assistantMetaFollowupPolicy_1.createAssistantMetaFollowupPolicy)({
hasAssistantDataScopeMetaQuestionSignal: assistantLivingModePolicy.hasAssistantDataScopeMetaQuestionSignal,
shouldHandleAsAssistantCapabilityMetaQuery: assistantLivingModePolicy.shouldHandleAsAssistantCapabilityMetaQuery,
hasMetaAnswerFollowupSignal: assistantLivingModePolicy.hasMetaAnswerFollowupSignal
hasMetaAnswerFollowupSignal: assistantLivingModePolicy.hasMetaAnswerFollowupSignal,
hasAnswerInspectionFollowupSignal: assistantLivingModePolicy.hasAnswerInspectionFollowupSignal
});
const assistantMemoryRecapPolicy = (0, assistantMemoryRecapPolicy_1.createAssistantMemoryRecapPolicy)({
hasHistoricalCapabilityFollowupSignal: assistantLivingModePolicy.hasHistoricalCapabilityFollowupSignal,
@@ -4197,6 +4198,7 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli
resolveMetaSignalSet: assistantMetaFollowupPolicy.resolveMetaSignalSet,
resolveHardMetaMode: assistantMetaFollowupPolicy.resolveHardMetaMode,
isMetaFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isMetaFollowupOverGroundedAnswer,
isAnswerInspectionFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isAnswerInspectionFollowupOverGroundedAnswer,
hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal,
hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: assistantLivingModePolicy.hasOrganizationFactFollowupSignal,
@@ -255,4 +255,38 @@ describe("assistant living chat runtime adapter", () => {
expect(output.debug?.living_chat_continuity_active_organization).toBe("ООО Альтернатива Плюс");
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("builds deterministic answer inspection reply over grounded selected-object sale trace", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage: "у тебя написано кто контрагент: рабочая станция - это ошибка?",
modeDecision: { mode: "chat", reason: "answer_inspection_followup_detected" },
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "inventory_sale_trace_for_item",
extracted_filters: {
item: "Рабочая станция универсального специалиста",
organization: "ООО Альтернатива Плюс",
as_of_date: "2016-03-31"
}
}
}
],
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("не контрагент");
expect(output.chatText).toContain("Рабочая станция универсального специалиста");
expect(output.chatText).toContain("Покупатель");
expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
});
@@ -50,6 +50,12 @@ describe("assistantLivingModePolicy", () => {
expect(policy.hasConversationMemoryRecallFollowupSignal("а что мы уже выяснили по этой позиции?")).toBe(true);
});
it("detects answer inspection wording for previous answer correction", () => {
const policy = buildPolicy();
expect(policy.hasAnswerInspectionFollowupSignal("у тебя написано кто контрагент: рабочая станция - это ошибка?")).toBe(true);
});
it("routes casual small-talk to chat mode", () => {
const policy = buildPolicy();
@@ -3,11 +3,13 @@ import { createAssistantMetaFollowupPolicy } from "../src/services/assistantMeta
const policy = createAssistantMetaFollowupPolicy({
hasAssistantDataScopeMetaQuestionSignal: (text: unknown) =>
/по какой компании|какая база/i.test(String(text ?? "")),
/по какой компании|какая база/i.test(String(text ?? "")),
shouldHandleAsAssistantCapabilityMetaQuery: (text: unknown) =>
/что ты можешь|что ты умеешь/i.test(String(text ?? "")),
/что ты можешь|что ты умеешь/i.test(String(text ?? "")),
hasMetaAnswerFollowupSignal: (text: unknown) =>
/это норм|что думаешь/i.test(String(text ?? ""))
/это норм|что думаешь/i.test(String(text ?? "")),
hasAnswerInspectionFollowupSignal: (text: unknown) =>
/это ошибка|у тебя написано кто контрагент/i.test(String(text ?? ""))
});
describe("assistantMetaFollowupPolicy", () => {
@@ -15,13 +17,14 @@ describe("assistantMetaFollowupPolicy", () => {
const signals = policy.resolveMetaSignalSet({
rawUserMessage: "",
repairedRawUserMessage: "",
effectiveAddressUserMessage: "по какой компании мы можем работать?",
effectiveAddressUserMessage: "по какой компании мы можем работать?",
repairedEffectiveAddressUserMessage: ""
});
expect(signals.dataScopeMetaQuery).toBe(true);
expect(signals.capabilityMetaQuery).toBe(false);
expect(signals.metaAnswerFollowupSignal).toBe(false);
expect(signals.answerInspectionFollowupSignal).toBe(false);
});
it("treats historical capability phrasing as capability meta follow-up", () => {
@@ -72,4 +75,26 @@ describe("assistantMetaFollowupPolicy", () => {
expect(detected).toBe(true);
});
it("detects answer inspection follow-up over grounded answer", () => {
const signals = policy.resolveMetaSignalSet({
rawUserMessage: "у тебя написано кто контрагент: рабочая станция - это ошибка?",
repairedRawUserMessage: "",
effectiveAddressUserMessage: "",
repairedEffectiveAddressUserMessage: ""
});
expect(signals.answerInspectionFollowupSignal).toBe(true);
const detected = policy.isAnswerInspectionFollowupOverGroundedAnswer({
followupContext: { previous_intent: "inventory_sale_trace_for_item", previous_anchor_type: "item" },
hasPriorAddressAnswerContext: true,
answerInspectionFollowupSignal: true,
dataScopeMetaQuery: false,
capabilityMetaQuery: false,
aggregateBusinessAnalyticsSignal: false
});
expect(detected).toBe(true);
});
});
@@ -47,7 +47,8 @@ function buildPolicy(overrides: Record<string, unknown> = {}) {
return {
dataScopeMetaQuery: /по какой компании|какая база|по каким конторам/i.test(samples),
capabilityMetaQuery: /что ты можешь|что ты умеешь/i.test(samples),
metaAnswerFollowupSignal: /это норм|что думаешь/i.test(samples)
metaAnswerFollowupSignal: /это норм|что думаешь/i.test(samples),
answerInspectionFollowupSignal: /это ошибка|у тебя написано кто контрагент/i.test(samples)
};
},
resolveHardMetaMode: (input: {
@@ -61,6 +62,7 @@ function buildPolicy(overrides: Record<string, unknown> = {}) {
? "capability"
: null,
isMetaFollowupOverGroundedAnswer: () => false,
isAnswerInspectionFollowupOverGroundedAnswer: () => false,
hasDataRetrievalRequestSignal: () => false,
hasOrganizationFactLookupSignal: () => false,
hasOrganizationFactFollowupSignal: () => false,
@@ -236,6 +238,32 @@ describe("assistantRoutePolicy", () => {
expect(decision.livingReason).toBe("memory_recap_followup_detected");
});
it("routes answer inspection follow-up over grounded selected-object answer to chat", () => {
const policy = buildPolicy({
findLastGroundedAddressAnswerDebug: () => ({ execution_lane: "address_query" }),
resolveAddressToolGateDecision: () => ({
runAddressLane: true,
decision: "run_address_lane",
reason: "address_mode_classifier_detected"
}),
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
isAnswerInspectionFollowupOverGroundedAnswer: () => true
});
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: "у тебя написано кто контрагент: рабочая станция - это ошибка?",
effectiveAddressUserMessage: "у тебя написано кто контрагент: рабочая станция - это ошибка?",
followupContext: { previous_intent: "inventory_sale_trace_for_item", previous_anchor_type: "item" },
llmPreDecomposeMeta: null,
useMock: false
});
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateReason).toBe("answer_inspection_followup_over_grounded_answer");
expect(decision.livingMode).toBe("chat");
expect(decision.livingReason).toBe("answer_inspection_followup_detected");
});
it("routes organization fact lookup away from address lane even with follow-up context", () => {
const policy = buildPolicy({
hasDataRetrievalRequestSignal: () => true,