АРЧ - Усилить root-frame возврат, memory recap и colloquial provenance follow-up
This commit is contained in:
+74
-8
@@ -2778,6 +2778,48 @@ function hasShortInventoryObjectFollowupSignal(userMessage) {
|
||||
(0, decomposeStage_1.hasInventorySaleFollowupCue)(sample) ||
|
||||
(0, decomposeStage_1.hasInventoryPurchaseToSaleChainFollowupCue)(sample));
|
||||
}
|
||||
function hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage = null) {
|
||||
const samples = [
|
||||
compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()),
|
||||
compactWhitespace(String(userMessage ?? "").toLowerCase()),
|
||||
compactWhitespace(repairAddressMojibake(String(alternateMessage ?? "")).toLowerCase()),
|
||||
compactWhitespace(String(alternateMessage ?? "").toLowerCase())
|
||||
].filter((item) => item.length > 0);
|
||||
if (samples.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return samples.some((sample) => /(?:ндс|vat|налог(?:и|ов|ом|у|ами|ах)?|налогов(?:ый|ого)?|tax(?:es)?|сч[её]т[\s-]?фактур|книга\s+покупок|книга\s+продаж|вычет)/iu.test(sample) ||
|
||||
/(?:амортиз|основн(?:ые|ых|ым)?\s+средств|fixed\s*asset|depreciat|\bос\b)/iu.test(sample) ||
|
||||
/(?:закрыти|месяц|затрат|рбп|period\s*close|month\s*close|allocation|residual|cost)/iu.test(sample) ||
|
||||
/(?:оплат|плат(?:е|ё)ж|аванс|зач(?:е|ё)т|выписк|statement|wire|settlement|payment|\b51(?:\.\d{1,2})?\b|\b60(?:\.\d{1,2})?\b|\b62(?:\.\d{1,2})?\b)/iu.test(sample));
|
||||
}
|
||||
function buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame) {
|
||||
const candidateFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
|
||||
? inventoryRootFrame.filters
|
||||
: previousFilters;
|
||||
const nextFilters = {};
|
||||
const organization = toNonEmptyString(candidateFilters?.organization) ?? toNonEmptyString(previousFilters?.organization);
|
||||
const warehouse = toNonEmptyString(candidateFilters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse);
|
||||
const asOfDate = toNonEmptyString(candidateFilters?.as_of_date) ?? toNonEmptyString(previousFilters?.as_of_date);
|
||||
const periodFrom = toNonEmptyString(candidateFilters?.period_from) ?? toNonEmptyString(previousFilters?.period_from);
|
||||
const periodTo = toNonEmptyString(candidateFilters?.period_to) ?? toNonEmptyString(previousFilters?.period_to);
|
||||
if (organization) {
|
||||
nextFilters.organization = organization;
|
||||
}
|
||||
if (warehouse) {
|
||||
nextFilters.warehouse = warehouse;
|
||||
}
|
||||
if (asOfDate) {
|
||||
nextFilters.as_of_date = asOfDate;
|
||||
}
|
||||
if (periodFrom) {
|
||||
nextFilters.period_from = periodFrom;
|
||||
}
|
||||
if (periodTo) {
|
||||
nextFilters.period_to = periodTo;
|
||||
}
|
||||
return nextFilters;
|
||||
}
|
||||
function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
|
||||
const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||
if (!normalized || countTokens(normalized) > 10) {
|
||||
@@ -2916,7 +2958,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
}
|
||||
};
|
||||
}
|
||||
const currentFrameKind = inventoryRootFrame
|
||||
let currentFrameKind = inventoryRootFrame
|
||||
? isInventoryDrilldownFrameIntent(sourceIntent)
|
||||
? "inventory_drilldown"
|
||||
: isInventoryRootFrameIntent(sourceIntent)
|
||||
@@ -2925,7 +2967,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
: null;
|
||||
let resolvedCounterpartyFromDisplay = false;
|
||||
const previousFiltersRaw = previousAddressDebug.extracted_filters;
|
||||
const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
|
||||
let previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
|
||||
? { ...previousFiltersRaw }
|
||||
: {};
|
||||
if (!toNonEmptyString(previousFilters.contract)) {
|
||||
@@ -2961,13 +3003,23 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
if (!toNonEmptyString(previousFilters.period_to) && toNonEmptyString(navigationDateScope?.period_to)) {
|
||||
previousFilters.period_to = toNonEmptyString(navigationDateScope?.period_to);
|
||||
}
|
||||
const rootContextOnlyPivot = Boolean((isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
||||
hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
||||
if (rootContextOnlyPivot) {
|
||||
previousIntent = null;
|
||||
previousAnchorType = null;
|
||||
previousAnchor = null;
|
||||
previousFilters = buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame);
|
||||
currentFrameKind = inventoryRootFrame ? "inventory_root" : currentFrameKind;
|
||||
followupSelectionMode = "carry_root_context";
|
||||
}
|
||||
const displayedEntityType = inferDisplayedEntityTypeFromIntent(sourceIntent);
|
||||
const displayedEntities = extractDisplayedAddressEntityCandidates(toNonEmptyString(previousAddressItem?.text) ?? "", displayedEntityType);
|
||||
const resolvedEntityFromFollowup = resolveDisplayedAddressEntityMention(userMessage, displayedEntities) ??
|
||||
(toNonEmptyString(alternateMessage)
|
||||
? resolveDisplayedAddressEntityMention(String(alternateMessage ?? ""), displayedEntities)
|
||||
: null);
|
||||
if (resolvedEntityFromFollowup) {
|
||||
if (resolvedEntityFromFollowup && !rootContextOnlyPivot) {
|
||||
if (resolvedEntityFromFollowup.entityType === "counterparty") {
|
||||
previousFilters.counterparty = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "counterparty";
|
||||
@@ -2988,7 +3040,8 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
followupSelectionMode = "carry_referenced_entity";
|
||||
}
|
||||
}
|
||||
if (!toNonEmptyString(previousFilters.item) &&
|
||||
if (!rootContextOnlyPivot &&
|
||||
!toNonEmptyString(previousFilters.item) &&
|
||||
navigationFocusObjectType === "item" &&
|
||||
navigationFocusObjectLabel &&
|
||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
@@ -3028,6 +3081,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
previous_anchor_type: previousAnchorType ?? undefined,
|
||||
previous_anchor_value: previousAnchor,
|
||||
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
|
||||
root_context_only: rootContextOnlyPivot || undefined,
|
||||
root_intent: inventoryRootFrame?.intent ?? undefined,
|
||||
root_filters: inventoryRootFrame?.filters ?? undefined,
|
||||
root_anchor_type: inventoryRootFrame?.anchorType ?? undefined,
|
||||
@@ -3047,10 +3101,13 @@ function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage,
|
||||
const hasFollowupContext = Boolean(carryoverMeta?.followupContext);
|
||||
const previousIntent = toNonEmptyString(carryoverMeta?.previousSourceIntent) ?? null;
|
||||
const selectionMode = toNonEmptyString(carryoverMeta?.followupSelectionMode) ?? null;
|
||||
const rootContextOnly = selectionMode === "carry_root_context";
|
||||
const explicitIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const targetIntent = selectionMode === "switch_to_suggested_intent"
|
||||
? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null
|
||||
: explicitIntent ?? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||||
: rootContextOnly
|
||||
? explicitIntent ?? null
|
||||
: explicitIntent ?? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||||
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
||||
const rewrittenByPredecompose = compactWhitespace(sourceMessage.toLowerCase()) !== compactWhitespace(canonicalMessage.toLowerCase());
|
||||
const hasExplicitIntent = Boolean(explicitIntent);
|
||||
@@ -3075,6 +3132,9 @@ function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage,
|
||||
if (selectionMode === "carry_referenced_entity" && explicitIntent && previousIntent && explicitIntent !== previousIntent) {
|
||||
reasons.push("operation_intent_from_current_message");
|
||||
}
|
||||
if (rootContextOnly) {
|
||||
reasons.push("root_context_only_carryover");
|
||||
}
|
||||
return {
|
||||
schema_version: "address_dialog_continuation_contract_v2",
|
||||
source_message: sourceMessage,
|
||||
@@ -4471,19 +4531,25 @@ function resolveAssistantOrchestrationDecision(input) {
|
||||
const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true;
|
||||
const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" ||
|
||||
semanticExtraction?.aggregation_profile === "management_profile";
|
||||
const rootContextOnlyFollowup = Boolean(followupContext && followupContext.root_context_only === true);
|
||||
const followupSemanticOverrideToDeepAllowed = Boolean(followupContext &&
|
||||
!supportedAddressIntentDetected &&
|
||||
(llmContractMode === "unsupported" ||
|
||||
(rootContextOnlyFollowup ||
|
||||
llmContractMode === "unsupported" ||
|
||||
semanticAggregateShapeDetected ||
|
||||
semanticDeepInvestigationHintDetected ||
|
||||
!semanticApplyCanonicalRecommended));
|
||||
const unsupportedIntentOrMode = (resolvedModeDetection.mode !== "address_query" && resolvedIntentResolution.intent === "unknown") ||
|
||||
llmContractMode === "unsupported";
|
||||
llmContractMode === "unsupported" ||
|
||||
(rootContextOnlyFollowup &&
|
||||
resolvedIntentResolution.intent === "unknown" &&
|
||||
(!llmContractIntent || llmContractIntent === "unknown"));
|
||||
const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
unsupportedIntentOrMode &&
|
||||
strongDataSignal &&
|
||||
(llmContractMode === "deep_analysis" ||
|
||||
(rootContextOnlyFollowup ||
|
||||
llmContractMode === "deep_analysis" ||
|
||||
!dataRetrievalSignal ||
|
||||
strictDeepInvestigationCueDetected ||
|
||||
semanticDeepInvestigationHintDetected ||
|
||||
|
||||
@@ -1177,6 +1177,11 @@ function trimInventoryItemArrowSuffix(rawValue: string): string {
|
||||
}
|
||||
|
||||
function isTemporalWarehousePhrase(candidate: string): boolean {
|
||||
const temporalZaPattern =
|
||||
/^(?:за)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu;
|
||||
if (temporalZaPattern.test(cleanupAnchorValue(candidate).toLowerCase().replace(/ё/g, "е").trim())) {
|
||||
return true;
|
||||
}
|
||||
const normalized = cleanupAnchorValue(candidate)
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
|
||||
@@ -91,6 +91,12 @@ function hasSameDateHint(text: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function hasSamePeriodHint(text: string): boolean {
|
||||
return /(?:на\s+тот\s+же\s+период|за\s+тот\s+же\s+период|тот\s+же\s+период(?:\s+рассмотрения)?|на\s+этот\s+же\s+период|за\s+этот\s+же\s+период|аналогичн\w+\s+текущ\w+\s+период\w+|same\s+period|same\s+range|same\s+window)/iu.test(
|
||||
String(text ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
function hasExplicitPeriodLiteral(text: string): boolean {
|
||||
return /(?:^|[^\d*×xх])((?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?)(?=$|[^\d*×xх])/iu.test(String(text ?? ""));
|
||||
}
|
||||
@@ -487,10 +493,19 @@ function shouldRestoreInventoryRootFrame(
|
||||
const previousIntent = followupContext.previous_intent;
|
||||
const comingFromInventoryDrilldown =
|
||||
currentFrameKind === "inventory_drilldown" || isInventoryDrilldownFrameIntent(previousIntent);
|
||||
if (!comingFromInventoryDrilldown) {
|
||||
const normalized = String(userMessage ?? "");
|
||||
const hasInventoryRootRestatementCue =
|
||||
/(?:склад|остат(?:ок|ки)|позици(?:я|и|ю)|товар(?:ы|ов)?|номенклатур)/iu.test(normalized) &&
|
||||
/(?:покажи|показать|выведи|раскрой|еще\s+раз|ещ[её]\s+раз|снова|опять|верни|вернись|повтори|тот\s+же|этот\s+же|same|again)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const canReenterInventoryRoot =
|
||||
comingFromInventoryDrilldown ||
|
||||
(currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) ||
|
||||
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
|
||||
if (!canReenterInventoryRoot) {
|
||||
return false;
|
||||
}
|
||||
const normalized = String(userMessage ?? "");
|
||||
if (
|
||||
hasSelectedObjectInventorySignal(normalized) ||
|
||||
hasInventorySupplierFollowupCue(normalized) ||
|
||||
@@ -508,6 +523,7 @@ function shouldRestoreInventoryRootFrame(
|
||||
const hasTemporalPatch =
|
||||
hasExplicitPeriodWindow(extractedFilters) ||
|
||||
Boolean(toNonEmptyString(extractedFilters.as_of_date)) ||
|
||||
hasSamePeriodHint(normalized) ||
|
||||
hasExplicitPeriodLiteral(normalized) ||
|
||||
Boolean(resolveRelativeMonthPeriodFromInventoryRoot(normalized, followupContext));
|
||||
return hasTemporalPatch;
|
||||
@@ -651,6 +667,7 @@ function mergeFollowupFilters(
|
||||
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
|
||||
const allTimeRequested = hasAllTimeHint(userMessage);
|
||||
const sameDateRequested = hasSameDateHint(userMessage);
|
||||
const samePeriodRequested = hasSamePeriodHint(userMessage);
|
||||
if (!toNonEmptyString(merged.organization) && previousOrganization) {
|
||||
merged.organization = previousOrganization;
|
||||
reasons.push("organization_from_followup_context");
|
||||
@@ -821,6 +838,24 @@ function mergeFollowupFilters(
|
||||
reasons.push("as_of_date_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (
|
||||
samePeriodRequested &&
|
||||
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date")
|
||||
) {
|
||||
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
|
||||
merged.period_from = previousPeriodFrom;
|
||||
reasons.push("period_from_from_followup_context");
|
||||
}
|
||||
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
|
||||
merged.period_to = previousPeriodTo;
|
||||
reasons.push("period_to_from_followup_context");
|
||||
}
|
||||
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
|
||||
if (inheritedAsOfDate && merged.as_of_date !== inheritedAsOfDate) {
|
||||
merged.as_of_date = inheritedAsOfDate;
|
||||
reasons.push("as_of_date_from_followup_context");
|
||||
}
|
||||
}
|
||||
if (
|
||||
!sameDateRequested &&
|
||||
(intent === "inventory_aging_by_purchase_date" || isInventoryLifecycleHistoryIntent(intent)) &&
|
||||
|
||||
@@ -99,6 +99,51 @@ function findLastGroundedInventoryAddressDebug(items: unknown[]): Record<string,
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLastAddressDebugWithItem(items: unknown[]): Record<string, unknown> | null {
|
||||
if (!Array.isArray(items)) {
|
||||
return null;
|
||||
}
|
||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||
const item = items[index] as { role?: string; debug?: Record<string, unknown> } | null;
|
||||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
const debug = item.debug;
|
||||
if (String(debug.execution_lane ?? "") !== "address_query") {
|
||||
continue;
|
||||
}
|
||||
const extractedFilters =
|
||||
debug.extracted_filters && typeof debug.extracted_filters === "object"
|
||||
? (debug.extracted_filters as Record<string, unknown>)
|
||||
: null;
|
||||
const itemLabel =
|
||||
String(extractedFilters?.item ?? "").trim() ||
|
||||
(String(debug.anchor_type ?? "") === "item"
|
||||
? String(debug.anchor_value_resolved ?? debug.anchor_value_raw ?? "").trim()
|
||||
: "");
|
||||
if (itemLabel) {
|
||||
return debug;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLastAddressDebug(items: unknown[]): Record<string, unknown> | null {
|
||||
if (!Array.isArray(items)) {
|
||||
return null;
|
||||
}
|
||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||
const item = items[index] as { role?: string; debug?: Record<string, unknown> } | null;
|
||||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||||
continue;
|
||||
}
|
||||
if (String(item.debug.execution_lane ?? "") === "address_query") {
|
||||
return item.debug;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildInventoryHistoryCapabilityFollowupReply(input: {
|
||||
organization: string | null;
|
||||
addressDebug: Record<string, unknown> | null;
|
||||
@@ -135,6 +180,55 @@ function buildInventoryHistoryCapabilityFollowupReply(input: {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function buildAddressMemoryRecapReply(input: {
|
||||
organization: string | null;
|
||||
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 rootFrameContext =
|
||||
input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object"
|
||||
? (input.addressDebug.address_root_frame_context 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 organization =
|
||||
input.organization ??
|
||||
input.toNonEmptyString(extractedFilters?.organization) ??
|
||||
input.toNonEmptyString(rootFrameContext?.organization);
|
||||
const scopedDate =
|
||||
formatIsoDateForReply(extractedFilters?.as_of_date) ??
|
||||
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
|
||||
formatIsoDateForReply(extractedFilters?.period_to);
|
||||
|
||||
if (item) {
|
||||
const datePart = scopedDate ? ` в срезе на ${scopedDate}` : "";
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
return [
|
||||
`Да, помню. Мы обсуждали позицию «${item}»${organizationPart}${datePart}.`,
|
||||
"Могу продолжить по ней без переписывания сущности: кто поставил, когда купили, по каким документам или кому продали."
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
if (organization || scopedDate) {
|
||||
const organizationPart = organization ? ` по компании «${organization}»` : "";
|
||||
const datePart = scopedDate ? ` на ${scopedDate}` : "";
|
||||
return [
|
||||
`Да, помню. Мы уже смотрели адресный контур${organizationPart}${datePart}.`,
|
||||
"Могу кратко напомнить контекст или сразу продолжить следующий шаг по этому же сценарию."
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
|
||||
}
|
||||
|
||||
export async function runAssistantLivingChatRuntime(
|
||||
input: AssistantLivingChatRuntimeInput
|
||||
): Promise<AssistantLivingChatRuntimeOutput> {
|
||||
@@ -157,9 +251,14 @@ export async function runAssistantLivingChatRuntime(
|
||||
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
|
||||
const contextualInventoryHistoryCapabilityFollowup =
|
||||
input.modeDecision?.reason === "inventory_history_capability_followup_detected";
|
||||
const contextualMemoryRecapFollowup =
|
||||
input.modeDecision?.reason === "memory_recap_followup_detected";
|
||||
const lastGroundedInventoryAddressDebug = contextualInventoryHistoryCapabilityFollowup
|
||||
? findLastGroundedInventoryAddressDebug(input.sessionItems)
|
||||
: null;
|
||||
const lastMemoryAddressDebug = contextualMemoryRecapFollowup
|
||||
? findLastAddressDebugWithItem(input.sessionItems) ?? findLastAddressDebug(input.sessionItems)
|
||||
: null;
|
||||
|
||||
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
||||
chatText = input.buildAssistantSafetyRefusalReply();
|
||||
@@ -211,6 +310,15 @@ export async function runAssistantLivingChatRuntime(
|
||||
});
|
||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||
livingChatSource = "deterministic_inventory_history_capability_contract";
|
||||
} else if (contextualMemoryRecapFollowup) {
|
||||
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
|
||||
chatText = buildAddressMemoryRecapReply({
|
||||
organization: scopedOrganization,
|
||||
addressDebug: lastMemoryAddressDebug,
|
||||
toNonEmptyString: input.toNonEmptyString
|
||||
});
|
||||
activeOrganization = scopedOrganization ?? activeOrganization;
|
||||
livingChatSource = "deterministic_memory_recap_contract";
|
||||
} else if (capabilityMetaQuery) {
|
||||
chatText = input.buildAssistantCapabilityContractReply();
|
||||
livingChatSource = "deterministic_capability_contract";
|
||||
|
||||
@@ -2466,6 +2466,32 @@ function isInventoryDrilldownFrameIntent(intent) {
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date";
|
||||
}
|
||||
function resolveAddressIntentFamily(intent) {
|
||||
const normalizedIntent = toNonEmptyString(intent);
|
||||
if (!normalizedIntent) {
|
||||
return null;
|
||||
}
|
||||
if (normalizedIntent.startsWith("inventory_")) {
|
||||
return "inventory";
|
||||
}
|
||||
if (normalizedIntent.startsWith("vat_")) {
|
||||
return "vat";
|
||||
}
|
||||
if (normalizedIntent === "account_balance_snapshot" || normalizedIntent === "documents_forming_balance") {
|
||||
return "balance";
|
||||
}
|
||||
if (normalizedIntent === "open_items_by_counterparty_or_contract" ||
|
||||
normalizedIntent === "list_documents_by_counterparty" ||
|
||||
normalizedIntent === "bank_operations_by_counterparty" ||
|
||||
normalizedIntent === "list_contracts_by_counterparty" ||
|
||||
normalizedIntent === "list_documents_by_contract" ||
|
||||
normalizedIntent === "bank_operations_by_contract" ||
|
||||
normalizedIntent === "receivables_confirmed_for_period" ||
|
||||
normalizedIntent === "payables_confirmed_for_period") {
|
||||
return "settlements";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function extractAddressCarryoverAnchor(addressDebug) {
|
||||
if (!isAddressLaneDebugPayload(addressDebug)) {
|
||||
return {
|
||||
@@ -2736,6 +2762,48 @@ function hasShortInventoryObjectFollowupSignal(userMessage) {
|
||||
(0, decomposeStage_1.hasInventorySaleFollowupCue)(sample) ||
|
||||
(0, decomposeStage_1.hasInventoryPurchaseToSaleChainFollowupCue)(sample));
|
||||
}
|
||||
function hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage = null) {
|
||||
const samples = [
|
||||
compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()),
|
||||
compactWhitespace(String(userMessage ?? "").toLowerCase()),
|
||||
compactWhitespace(repairAddressMojibake(String(alternateMessage ?? "")).toLowerCase()),
|
||||
compactWhitespace(String(alternateMessage ?? "").toLowerCase())
|
||||
].filter((item) => item.length > 0);
|
||||
if (samples.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return samples.some((sample) => /(?:ндс|vat|налог(?:и|ов|ом|у|ами|ах)?|налогов(?:ый|ого)?|tax(?:es)?|сч[её]т[\s-]?фактур|книга\s+покупок|книга\s+продаж|вычет)/iu.test(sample) ||
|
||||
/(?:амортиз|основн(?:ые|ых|ым)?\s+средств|fixed\s*asset|depreciat|\bос\b)/iu.test(sample) ||
|
||||
/(?:закрыти|месяц|затрат|рбп|period\s*close|month\s*close|allocation|residual|cost)/iu.test(sample) ||
|
||||
/(?:оплат|плат(?:е|ё)ж|аванс|зач(?:е|ё)т|выписк|statement|wire|settlement|payment|\b51(?:\.\d{1,2})?\b|\b60(?:\.\d{1,2})?\b|\b62(?:\.\d{1,2})?\b)/iu.test(sample));
|
||||
}
|
||||
function buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame) {
|
||||
const candidateFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
|
||||
? inventoryRootFrame.filters
|
||||
: previousFilters;
|
||||
const nextFilters = {};
|
||||
const organization = toNonEmptyString(candidateFilters?.organization) ?? toNonEmptyString(previousFilters?.organization);
|
||||
const warehouse = toNonEmptyString(candidateFilters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse);
|
||||
const asOfDate = toNonEmptyString(candidateFilters?.as_of_date) ?? toNonEmptyString(previousFilters?.as_of_date);
|
||||
const periodFrom = toNonEmptyString(candidateFilters?.period_from) ?? toNonEmptyString(previousFilters?.period_from);
|
||||
const periodTo = toNonEmptyString(candidateFilters?.period_to) ?? toNonEmptyString(previousFilters?.period_to);
|
||||
if (organization) {
|
||||
nextFilters.organization = organization;
|
||||
}
|
||||
if (warehouse) {
|
||||
nextFilters.warehouse = warehouse;
|
||||
}
|
||||
if (asOfDate) {
|
||||
nextFilters.as_of_date = asOfDate;
|
||||
}
|
||||
if (periodFrom) {
|
||||
nextFilters.period_from = periodFrom;
|
||||
}
|
||||
if (periodTo) {
|
||||
nextFilters.period_to = periodTo;
|
||||
}
|
||||
return nextFilters;
|
||||
}
|
||||
function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
|
||||
const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||||
if (!normalized || countTokens(normalized) > 10) {
|
||||
@@ -2793,6 +2861,18 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
|
||||
: false;
|
||||
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
|
||||
const hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
hasImplicitContinuationSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
hasFollowupMarker(userMessage) ||
|
||||
hasReferentialPointer(userMessage) ||
|
||||
(toNonEmptyString(alternateMessage)
|
||||
? hasFollowupMarker(String(alternateMessage ?? "")) || hasReferentialPointer(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
|
||||
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
||||
if (hasStandaloneAddressTopic &&
|
||||
@@ -2814,6 +2894,23 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
return null;
|
||||
}
|
||||
const sourceIntent = toNonEmptyString(previousAddressDebug.detected_intent);
|
||||
const llmExplicitIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const resolvedPrimaryIntent = (0, addressIntentResolver_1.resolveAddressIntent)(repairAddressMojibake(String(userMessage ?? ""))).intent;
|
||||
const resolvedAlternateIntent = toNonEmptyString(alternateMessage)
|
||||
? (0, addressIntentResolver_1.resolveAddressIntent)(repairAddressMojibake(String(alternateMessage ?? ""))).intent
|
||||
: null;
|
||||
const explicitIntent = llmExplicitIntent && llmExplicitIntent !== "unknown"
|
||||
? llmExplicitIntent
|
||||
: resolvedPrimaryIntent && resolvedPrimaryIntent !== "unknown"
|
||||
? resolvedPrimaryIntent
|
||||
: resolvedAlternateIntent && resolvedAlternateIntent !== "unknown"
|
||||
? resolvedAlternateIntent
|
||||
: null;
|
||||
const sourceIntentFamily = resolveAddressIntentFamily(sourceIntent);
|
||||
const explicitIntentFamily = resolveAddressIntentFamily(explicitIntent);
|
||||
if (sourceIntentFamily && explicitIntentFamily && sourceIntentFamily !== explicitIntentFamily && !hasStrongFollowupReference) {
|
||||
return null;
|
||||
}
|
||||
let previousIntent = sourceIntent;
|
||||
let followupSelectionMode = "carry_previous_intent";
|
||||
if (debtRoleSwapIntent) {
|
||||
@@ -2874,7 +2971,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
}
|
||||
};
|
||||
}
|
||||
const currentFrameKind = inventoryRootFrame
|
||||
let currentFrameKind = inventoryRootFrame
|
||||
? isInventoryDrilldownFrameIntent(sourceIntent)
|
||||
? "inventory_drilldown"
|
||||
: isInventoryRootFrameIntent(sourceIntent)
|
||||
@@ -2883,7 +2980,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
: null;
|
||||
let resolvedCounterpartyFromDisplay = false;
|
||||
const previousFiltersRaw = previousAddressDebug.extracted_filters;
|
||||
const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
|
||||
let previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
|
||||
? { ...previousFiltersRaw }
|
||||
: {};
|
||||
if (!toNonEmptyString(previousFilters.contract)) {
|
||||
@@ -2919,13 +3016,23 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
if (!toNonEmptyString(previousFilters.period_to) && toNonEmptyString(navigationDateScope?.period_to)) {
|
||||
previousFilters.period_to = toNonEmptyString(navigationDateScope?.period_to);
|
||||
}
|
||||
const rootContextOnlyPivot = Boolean((isInventorySelectedObjectIntent(sourceIntentHint) || currentFrameKind === "inventory_drilldown") &&
|
||||
hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
|
||||
if (rootContextOnlyPivot) {
|
||||
previousIntent = null;
|
||||
previousAnchorType = null;
|
||||
previousAnchor = null;
|
||||
previousFilters = buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame);
|
||||
currentFrameKind = inventoryRootFrame ? "inventory_root" : currentFrameKind;
|
||||
followupSelectionMode = "carry_root_context";
|
||||
}
|
||||
const displayedEntityType = inferDisplayedEntityTypeFromIntent(sourceIntent);
|
||||
const displayedEntities = extractDisplayedAddressEntityCandidates(toNonEmptyString(previousAddressItem?.text) ?? "", displayedEntityType);
|
||||
const resolvedEntityFromFollowup = resolveDisplayedAddressEntityMention(userMessage, displayedEntities) ??
|
||||
(toNonEmptyString(alternateMessage)
|
||||
? resolveDisplayedAddressEntityMention(String(alternateMessage ?? ""), displayedEntities)
|
||||
: null);
|
||||
if (resolvedEntityFromFollowup) {
|
||||
if (resolvedEntityFromFollowup && !rootContextOnlyPivot) {
|
||||
if (resolvedEntityFromFollowup.entityType === "counterparty") {
|
||||
previousFilters.counterparty = resolvedEntityFromFollowup.value;
|
||||
previousAnchorType = "counterparty";
|
||||
@@ -2946,7 +3053,8 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
followupSelectionMode = "carry_referenced_entity";
|
||||
}
|
||||
}
|
||||
if (!toNonEmptyString(previousFilters.item) &&
|
||||
if (!rootContextOnlyPivot &&
|
||||
!toNonEmptyString(previousFilters.item) &&
|
||||
navigationFocusObjectType === "item" &&
|
||||
navigationFocusObjectLabel &&
|
||||
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||||
@@ -2986,6 +3094,7 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
|
||||
previous_anchor_type: previousAnchorType ?? undefined,
|
||||
previous_anchor_value: previousAnchor,
|
||||
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
|
||||
root_context_only: rootContextOnlyPivot || undefined,
|
||||
root_intent: inventoryRootFrame?.intent ?? undefined,
|
||||
root_filters: inventoryRootFrame?.filters ?? undefined,
|
||||
root_anchor_type: inventoryRootFrame?.anchorType ?? undefined,
|
||||
@@ -3005,10 +3114,13 @@ function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage,
|
||||
const hasFollowupContext = Boolean(carryoverMeta?.followupContext);
|
||||
const previousIntent = toNonEmptyString(carryoverMeta?.previousSourceIntent) ?? null;
|
||||
const selectionMode = toNonEmptyString(carryoverMeta?.followupSelectionMode) ?? null;
|
||||
const rootContextOnly = selectionMode === "carry_root_context";
|
||||
const explicitIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||||
const targetIntent = selectionMode === "switch_to_suggested_intent"
|
||||
? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null
|
||||
: explicitIntent ?? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||||
: rootContextOnly
|
||||
? explicitIntent ?? null
|
||||
: explicitIntent ?? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||||
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
||||
const rewrittenByPredecompose = compactWhitespace(sourceMessage.toLowerCase()) !== compactWhitespace(canonicalMessage.toLowerCase());
|
||||
const hasExplicitIntent = Boolean(explicitIntent);
|
||||
@@ -3033,6 +3145,9 @@ function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage,
|
||||
if (selectionMode === "carry_referenced_entity" && explicitIntent && previousIntent && explicitIntent !== previousIntent) {
|
||||
reasons.push("operation_intent_from_current_message");
|
||||
}
|
||||
if (rootContextOnly) {
|
||||
reasons.push("root_context_only_carryover");
|
||||
}
|
||||
return {
|
||||
schema_version: "address_dialog_continuation_contract_v2",
|
||||
source_message: sourceMessage,
|
||||
@@ -4274,6 +4389,17 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
hasHistoricalCapabilityFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasHistoricalCapabilityFollowupSignal(repairedEffectiveAddressUserMessage)) &&
|
||||
isGroundedInventoryContextDebug(lastGroundedAddressDebug));
|
||||
const contextualMemoryRecapFollowupDetected = Boolean(!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
!dataRetrievalSignal &&
|
||||
!strongDataSignal &&
|
||||
!aggregateBusinessAnalyticsSignal &&
|
||||
(hasConversationMemoryRecallFollowupSignal(rawUserMessage) ||
|
||||
hasConversationMemoryRecallFollowupSignal(repairedRawUserMessage) ||
|
||||
hasConversationMemoryRecallFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasConversationMemoryRecallFollowupSignal(repairedEffectiveAddressUserMessage)) &&
|
||||
(lastGroundedAddressDebug ||
|
||||
findLastAddressAssistantItem(sessionItems)?.debug));
|
||||
const hardMetaMode = dataScopeMetaQuery
|
||||
? "data_scope"
|
||||
: capabilityMetaQuery && !dataRetrievalSignal
|
||||
@@ -4364,6 +4490,34 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
};
|
||||
}
|
||||
if (nonDomainQueryIndexed) {
|
||||
if (contextualMemoryRecapFollowupDetected) {
|
||||
return {
|
||||
runAddressLane: false,
|
||||
toolGateDecision: "skip_address_lane",
|
||||
toolGateReason: "memory_recap_followup_detected",
|
||||
livingMode: "chat",
|
||||
livingReason: "memory_recap_followup_detected",
|
||||
orchestrationContract: {
|
||||
schema_version: "assistant_orchestration_contract_v1",
|
||||
hard_meta_mode: "non_domain",
|
||||
address_mode: resolvedModeDetection.mode,
|
||||
address_mode_confidence: resolvedModeDetection.confidence,
|
||||
address_intent: resolvedIntentResolution.intent,
|
||||
address_intent_confidence: resolvedIntentResolution.confidence,
|
||||
strong_data_signal_detected: strongDataSignal,
|
||||
data_retrieval_signal_detected: dataRetrievalSignal,
|
||||
followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug),
|
||||
unsupported_address_intent_fallback_to_deep: false,
|
||||
final_decision: {
|
||||
run_address_lane: false,
|
||||
tool_gate_decision: "skip_address_lane",
|
||||
tool_gate_reason: "memory_recap_followup_detected",
|
||||
living_mode: "chat",
|
||||
living_reason: "memory_recap_followup_detected"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
runAddressLane: false,
|
||||
toolGateDecision: "skip_address_lane",
|
||||
@@ -4430,19 +4584,25 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true;
|
||||
const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" ||
|
||||
semanticExtraction?.aggregation_profile === "management_profile";
|
||||
const rootContextOnlyFollowup = Boolean(followupContext && followupContext.root_context_only === true);
|
||||
const followupSemanticOverrideToDeepAllowed = Boolean(followupContext &&
|
||||
!supportedAddressIntentDetected &&
|
||||
(llmContractMode === "unsupported" ||
|
||||
(rootContextOnlyFollowup ||
|
||||
llmContractMode === "unsupported" ||
|
||||
semanticAggregateShapeDetected ||
|
||||
semanticDeepInvestigationHintDetected ||
|
||||
!semanticApplyCanonicalRecommended));
|
||||
const unsupportedIntentOrMode = (resolvedModeDetection.mode !== "address_query" && resolvedIntentResolution.intent === "unknown") ||
|
||||
llmContractMode === "unsupported";
|
||||
llmContractMode === "unsupported" ||
|
||||
(rootContextOnlyFollowup &&
|
||||
resolvedIntentResolution.intent === "unknown" &&
|
||||
(!llmContractIntent || llmContractIntent === "unknown"));
|
||||
const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
unsupportedIntentOrMode &&
|
||||
strongDataSignal &&
|
||||
(llmContractMode === "deep_analysis" ||
|
||||
(rootContextOnlyFollowup ||
|
||||
llmContractMode === "deep_analysis" ||
|
||||
!dataRetrievalSignal ||
|
||||
strictDeepInvestigationCueDetected ||
|
||||
semanticDeepInvestigationHintDetected ||
|
||||
@@ -4462,6 +4622,9 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
const vatExplainFollowupSignal = Boolean(followupContext &&
|
||||
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
||||
/(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
|
||||
const vatEvaluativeFollowupSignal = Boolean(followupContext &&
|
||||
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
||||
/(?:^|\s)(?:это\s+)?много\s+или\s+мало(?:\?|$)|(?:^|\s)(?:это\s+)?нормально(?:\?|$)|(?:^|\s)(?:это\s+)?плохо(?:\?|$)|(?:^|\s)(?:это\s+)?хорошо(?:\?|$)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
|
||||
const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||||
!llmRuntimeUnavailableDetected &&
|
||||
(deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) &&
|
||||
@@ -4492,7 +4655,7 @@ export function resolveAssistantOrchestrationDecision(input) {
|
||||
const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent));
|
||||
const metaFollowupOverGroundedAnswer = Boolean(followupContext &&
|
||||
hasPriorAddressAnswerContext &&
|
||||
metaAnswerFollowupSignal &&
|
||||
(metaAnswerFollowupSignal || vatEvaluativeFollowupSignal) &&
|
||||
!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
!aggregateBusinessAnalyticsSignal &&
|
||||
@@ -4737,7 +4900,28 @@ function hasMetaAnswerFollowupSignal(userMessage) {
|
||||
sample.includes("по этому поводу") ||
|
||||
sample.includes("об этом") ||
|
||||
(sample.includes("это") && hasReferentialPointer(sample)));
|
||||
if (!(hasReflectionCue && hasTopicPointerCue)) {
|
||||
const hasEvaluationCue = samples.some((sample) => /\b(?:много|мало|нормально|хорошо|плохо|критично|перебор|слабо)\b/iu.test(sample));
|
||||
if (!((hasReflectionCue || hasEvaluationCue) &&
|
||||
(hasTopicPointerCue || (hasEvaluationCue && samples.some((sample) => /^(?:это|ну это)\b/iu.test(sample)))))) {
|
||||
return false;
|
||||
}
|
||||
return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) ||
|
||||
shouldHandleAsAssistantCapabilityMetaQuery(sample) ||
|
||||
hasDataRetrievalRequestSignal(sample) ||
|
||||
hasStrongDataIntentSignal(sample));
|
||||
}
|
||||
function hasConversationMemoryRecallFollowupSignal(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 hasMemoryCue = samples.some((sample) => /(?:помни(?:шь|те|м)?|remember|recall)/iu.test(sample));
|
||||
const hasDiscussionCue = samples.some((sample) => /(?:обсуждал[аи]?|говорил[аи]?|смотрел[аи]?|разбирал[аи]?|спрашивал[аи]?)/iu.test(sample));
|
||||
if (!hasMemoryCue || !hasDiscussionCue) {
|
||||
return false;
|
||||
}
|
||||
return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) ||
|
||||
|
||||
@@ -8,8 +8,11 @@ export function hasInventoryPurchaseStem(text: string): boolean {
|
||||
|
||||
export function hasInventorySupplierCue(text: string): boolean {
|
||||
const value = toText(text);
|
||||
if (/(?:купил(?:и|о)?\s+у\s+кого|куплен(?:о)?\s+у\s+кого|купил(?:и|о)?\s+от\s+кого|куплен(?:о)?\s+от\s+кого)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(
|
||||
/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|откуда\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runAddressDecomposeStage } from "../src/services/address_runtime/decomposeStage";
|
||||
|
||||
describe("inventory root frame regressions", () => {
|
||||
it("restores inventory root frame for restatement on the same period after foreign domain drift", () => {
|
||||
const result = runAddressDecomposeStage(
|
||||
"ладно ок. покажи мне еще раз позиции на складе на тот же период рассмотрения",
|
||||
{
|
||||
previous_intent: "customer_revenue_and_payments",
|
||||
previous_filters: {
|
||||
organization: "ООО \\Альтернатива Плюс\\"
|
||||
},
|
||||
previous_anchor_type: "organization",
|
||||
previous_anchor_value: "ООО \\Альтернатива Плюс\\",
|
||||
root_intent: "inventory_on_hand_as_of_date",
|
||||
root_filters: {
|
||||
organization: "ООО \\Альтернатива Плюс\\",
|
||||
period_from: "2022-02-01",
|
||||
period_to: "2022-02-28",
|
||||
as_of_date: "2022-02-28"
|
||||
},
|
||||
root_anchor_type: "organization",
|
||||
root_anchor_value: "ООО \\Альтернатива Плюс\\",
|
||||
current_frame_kind: "generic"
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
|
||||
expect(result?.intent.reasons).toContain("intent_restored_to_inventory_root_frame");
|
||||
expect(result?.filters.extracted_filters.organization).toBe("ООО \\Альтернатива Плюс\\");
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2022-02-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2022-02-28");
|
||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2022-02-28");
|
||||
expect(result?.filters.warnings).toContain("period_from_from_followup_context");
|
||||
expect(result?.filters.warnings).toContain("period_to_from_followup_context");
|
||||
});
|
||||
|
||||
it("promotes selected-object provenance slang with 'где мы взяли это' into inventory provenance", () => {
|
||||
const result = runAddressDecomposeStage(
|
||||
'По выбранному объекту "Зеркало для инвалидов поворотное травмобезопасное": где мы взяли это говнище?',
|
||||
{
|
||||
previous_intent: "inventory_on_hand_as_of_date",
|
||||
previous_filters: {
|
||||
organization: "ООО \\Альтернатива Плюс\\",
|
||||
warehouse: "Основной склад",
|
||||
period_from: "2022-02-01",
|
||||
period_to: "2022-02-28",
|
||||
as_of_date: "2022-02-28"
|
||||
},
|
||||
previous_anchor_type: "unknown",
|
||||
previous_anchor_value: null
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("inventory_purchase_provenance_for_item");
|
||||
expect(result?.filters.extracted_filters.item).toBe("Зеркало для инвалидов поворотное травмобезопасное");
|
||||
expect(result?.filters.extracted_filters.as_of_date).toBe("2022-02-28");
|
||||
expect(
|
||||
result?.baseReasons?.includes("intent_adjusted_to_inventory_followup_context") ||
|
||||
result?.intent.reasons.includes("inventory_selected_object_provenance_signal_detected")
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,19 @@ describe("inventory warehouse anchor extraction", () => {
|
||||
expect(filters.as_of_date).toBe("2019-03-31");
|
||||
expect(filters.warehouse).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not materialize 'за май' as warehouse in inventory balance phrasing from the run", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"проверить остатки по складу за май 2020 года",
|
||||
"inventory_on_hand_as_of_date"
|
||||
).extracted_filters;
|
||||
|
||||
expect(filters.period_from).toBe("2020-05-01");
|
||||
expect(filters.period_to).toBe("2020-05-31");
|
||||
expect(filters.as_of_date).toBe("2020-05-31");
|
||||
expect(filters.warehouse).toBeUndefined();
|
||||
});
|
||||
|
||||
it("treats 'у нас' as implicit self-scope instead of literal warehouse anchor", () => {
|
||||
const result = extractAddressFilters("что на складе у нас", "inventory_on_hand_as_of_date");
|
||||
|
||||
|
||||
@@ -2058,5 +2058,323 @@ describe("assistant address follow-up carryover", () => {
|
||||
expect(calls[1].options?.followupContext?.root_filters?.organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sanitizes selected-item carryover when inventory drilldown pivots into VAT follow-up", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const followupMessage = "\u0430 \u043d\u0434\u0441?";
|
||||
const itemLabel =
|
||||
"\u041a\u0440\u043e\u043c\u043a\u0430 \u0441 \u043a\u043b\u0435\u0435\u043c 33 \u0434\u0443\u0431 \u043d\u0438\u0430\u0433\u0430\u0440\u0430 137 \u043c";
|
||||
const organization = "\u041e\u041e\u041e \\\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441\\";
|
||||
const warehouse = "\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u043a\u043b\u0430\u0434";
|
||||
|
||||
const vatResult = buildAddressLaneResult({
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "vat_payable_confirmed_as_of_date",
|
||||
extracted_filters: {
|
||||
sort: "period_desc",
|
||||
period_from: "2021-03-01",
|
||||
period_to: "2021-03-31",
|
||||
as_of_date: "2021-03-31",
|
||||
organization
|
||||
},
|
||||
selected_recipe: "address_vat_payable_confirmed_as_of_date_v1",
|
||||
response_type: "FACTUAL_SUMMARY",
|
||||
requested_result_mode: "confirmed_balance",
|
||||
result_mode: "confirmed_balance",
|
||||
balance_confirmed: true,
|
||||
reasons: [
|
||||
"address_action_detected",
|
||||
"address_entity_detected",
|
||||
"address_followup_context_applied"
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
return vatResult;
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
||||
reply_type: "partial_coverage",
|
||||
debug: {}
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const sessionId = `asst-address-followup-inventory-vat-pivot-${Date.now()}`;
|
||||
sessions.appendItem(sessionId, {
|
||||
message_id: "msg-inventory-root-seed",
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: "inventory root seed",
|
||||
reply_type: "factual",
|
||||
created_at: "2026-04-15T14:01:00.000Z",
|
||||
trace_id: "address-root-seed",
|
||||
debug: {
|
||||
detected_mode: "address_query",
|
||||
detected_intent: "inventory_on_hand_as_of_date",
|
||||
extracted_filters: {
|
||||
as_of_date: "2021-03-31",
|
||||
period_from: "2021-03-01",
|
||||
period_to: "2021-03-31",
|
||||
organization,
|
||||
warehouse
|
||||
},
|
||||
selected_recipe: "address_inventory_on_hand_as_of_date_v1"
|
||||
}
|
||||
} as any);
|
||||
sessions.appendItem(sessionId, {
|
||||
message_id: "msg-inventory-sale-seed",
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: "inventory sale trace seed",
|
||||
reply_type: "factual",
|
||||
created_at: "2026-04-15T14:02:00.000Z",
|
||||
trace_id: "address-sale-seed",
|
||||
debug: {
|
||||
detected_mode: "address_query",
|
||||
detected_intent: "inventory_sale_trace_for_item",
|
||||
extracted_filters: {
|
||||
item: itemLabel,
|
||||
organization,
|
||||
as_of_date: "2021-03-31"
|
||||
},
|
||||
selected_recipe: "address_inventory_sale_trace_for_item_v1",
|
||||
anchor_type: "item",
|
||||
anchor_value_raw: itemLabel,
|
||||
anchor_value_resolved: itemLabel
|
||||
}
|
||||
} as any);
|
||||
|
||||
const second = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: followupMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
|
||||
expect(second.ok).toBe(true);
|
||||
expect(second.reply_type).toBe("factual");
|
||||
expect(second.debug?.detected_intent).toBe("vat_payable_confirmed_as_of_date");
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].message).toBe(followupMessage);
|
||||
expect(calls[0].options?.followupContext?.root_context_only).toBe(true);
|
||||
expect(calls[0].options?.followupContext?.previous_intent).toBeUndefined();
|
||||
expect(calls[0].options?.followupContext?.previous_anchor_type).toBeUndefined();
|
||||
expect(calls[0].options?.followupContext?.previous_anchor_value).toBeNull();
|
||||
expect(calls[0].options?.followupContext?.previous_filters?.item).toBeUndefined();
|
||||
expect(calls[0].options?.followupContext?.previous_filters?.organization).toBe(organization);
|
||||
expect(calls[0].options?.followupContext?.previous_filters?.warehouse).toBe(warehouse);
|
||||
expect(calls[0].options?.followupContext?.previous_filters?.as_of_date).toBe("2021-03-31");
|
||||
expect(calls[0].options?.followupContext?.previous_filters?.period_from).toBe("2021-03-01");
|
||||
expect(calls[0].options?.followupContext?.previous_filters?.period_to).toBe("2021-03-31");
|
||||
expect(calls[0].options?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
|
||||
expect(calls[0].options?.followupContext?.root_filters?.organization).toBe(organization);
|
||||
expect(calls[0].options?.followupContext?.root_filters?.as_of_date).toBe("2021-03-31");
|
||||
expect(calls[0].options?.followupContext?.root_filters?.period_from).toBe("2021-03-01");
|
||||
expect(calls[0].options?.followupContext?.root_filters?.period_to).toBe("2021-03-31");
|
||||
expect(calls[0].options?.followupContext?.current_frame_kind).toBe("inventory_root");
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats short supplier follow-up after sale trace as continuation of the active selected object", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const followupMessage = "а купили у кого";
|
||||
const saleTraceResult = {
|
||||
handled: true,
|
||||
reply_text: "По позиции Столешница 600*3050*26 дуб ниагара подтвержден покупатель: ООО \\Ромашка\\.",
|
||||
reply_type: "factual",
|
||||
response_type: "FACTUAL_LIST",
|
||||
debug: {
|
||||
detected_mode: "address_query",
|
||||
detected_intent: "inventory_sale_trace_for_item",
|
||||
detected_intent_confidence: "medium",
|
||||
extracted_filters: {
|
||||
item: "Столешница 600*3050*26 дуб ниагара",
|
||||
organization: "ООО \\Альтернатива Плюс\\",
|
||||
as_of_date: "2020-05-31"
|
||||
},
|
||||
selected_recipe: "address_inventory_sale_trace_for_item_v1",
|
||||
anchor_type: "item",
|
||||
anchor_value_raw: "Столешница 600*3050*26 дуб ниагара",
|
||||
anchor_value_resolved: "Столешница 600*3050*26 дуб ниагара",
|
||||
reasons: ["address_action_detected", "address_entity_detected"]
|
||||
}
|
||||
} as any;
|
||||
const provenanceResult = {
|
||||
handled: true,
|
||||
reply_text: "По позиции Столешница 600*3050*26 дуб ниагара подтвержден поставщик: Торговый дом \\Союз\\.",
|
||||
reply_type: "factual",
|
||||
response_type: "FACTUAL_SUMMARY",
|
||||
debug: {
|
||||
detected_mode: "address_query",
|
||||
detected_intent: "inventory_purchase_provenance_for_item",
|
||||
detected_intent_confidence: "medium",
|
||||
extracted_filters: {
|
||||
item: "Столешница 600*3050*26 дуб ниагара",
|
||||
organization: "ООО \\Альтернатива Плюс\\",
|
||||
as_of_date: "2020-05-31"
|
||||
},
|
||||
selected_recipe: "address_inventory_purchase_provenance_for_item_v1",
|
||||
reasons: ["address_action_detected", "address_entity_detected", "address_followup_context_applied"]
|
||||
}
|
||||
} as any;
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
if (message === followupMessage && options?.followupContext) {
|
||||
return provenanceResult;
|
||||
}
|
||||
return saleTraceResult;
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
||||
reply_type: "partial_coverage",
|
||||
debug: {}
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const sessionId = `asst-address-followup-sale-to-supplier-${Date.now()}`;
|
||||
sessions.appendItem(sessionId, {
|
||||
message_id: "msg-sale-trace-seed",
|
||||
session_id: sessionId,
|
||||
role: "assistant",
|
||||
text: saleTraceResult.reply_text,
|
||||
reply_type: saleTraceResult.reply_type,
|
||||
created_at: "2026-04-15T18:00:00.000Z",
|
||||
trace_id: "address-sale-seed",
|
||||
debug: saleTraceResult.debug
|
||||
} as any);
|
||||
|
||||
const second = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: followupMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
|
||||
expect(second.ok).toBe(true);
|
||||
expect(second.reply_type).toBe("factual");
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].message).toBe(followupMessage);
|
||||
expect(calls[0].options?.followupContext?.previous_intent).toBe("inventory_sale_trace_for_item");
|
||||
expect(calls[0].options?.followupContext?.previous_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
|
||||
expect(calls[0].options?.followupContext?.previous_filters?.organization).toBe("ООО \\Альтернатива Плюс\\");
|
||||
expect(calls[0].options?.followupContext?.previous_filters?.as_of_date).toBe("2020-05-31");
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not carry VAT previous_intent into a fresh inventory root query", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const firstMessage = "прогноз ндс на март 2020";
|
||||
const secondMessage = "остаток на складе за май 2020";
|
||||
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
if (message === firstMessage) {
|
||||
return {
|
||||
handled: true,
|
||||
reply_text: "Прогноз НДС на март 2020 собран.",
|
||||
reply_type: "factual",
|
||||
response_type: "FACTUAL_SUMMARY",
|
||||
debug: {
|
||||
detected_mode: "address_query",
|
||||
detected_intent: "vat_payable_forecast",
|
||||
detected_intent_confidence: "high",
|
||||
extracted_filters: {
|
||||
period_from: "2020-03-01",
|
||||
period_to: "2020-03-31"
|
||||
},
|
||||
selected_recipe: "address_vat_payable_forecast_v1"
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
handled: true,
|
||||
reply_text: "Нужно уточнить организацию.",
|
||||
reply_type: "partial_coverage",
|
||||
response_type: "LIMITED_WITH_REASON",
|
||||
debug: {
|
||||
detected_mode: "address_query",
|
||||
detected_intent: "inventory_on_hand_as_of_date",
|
||||
detected_intent_confidence: "high",
|
||||
extracted_filters: {
|
||||
period_from: "2020-05-01",
|
||||
period_to: "2020-05-31",
|
||||
as_of_date: "2020-05-31"
|
||||
},
|
||||
selected_recipe: null,
|
||||
limited_reason_category: "missing_anchor",
|
||||
reasons: ["organization_clarification_required", "multiple_known_organizations_detected"]
|
||||
}
|
||||
};
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
||||
reply_type: "partial_coverage",
|
||||
debug: {}
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const sessionId = `asst-address-followup-vat-inventory-${Date.now()}`;
|
||||
const first = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: firstMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
expect(first.ok).toBe(true);
|
||||
|
||||
const second = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: secondMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
|
||||
expect(second.ok).toBe(true);
|
||||
expect(calls.length).toBeGreaterThanOrEqual(2);
|
||||
const inventoryCalls = calls.slice(1);
|
||||
expect(inventoryCalls.every((call) => call.message === secondMessage)).toBe(true);
|
||||
expect(inventoryCalls.every((call) => call.options?.followupContext === undefined)).toBe(true);
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -133,4 +133,35 @@ describe("assistant living chat runtime adapter", () => {
|
||||
expect(output.debug?.living_chat_response_source).toBe("llm_chat_grounding_guard");
|
||||
expect(executeLlmChat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("builds deterministic memory recap for prior selected-object address context", async () => {
|
||||
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||
const input = buildRuntimeInput({
|
||||
userMessage: "а ты помнишь мы зеркало обсуждали?",
|
||||
modeDecision: { mode: "chat", reason: "memory_recap_followup_detected" },
|
||||
sessionItems: [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "address_query",
|
||||
detected_intent: "inventory_purchase_provenance_for_item",
|
||||
extracted_filters: {
|
||||
item: "Зеркало для инвалидов поворотное травмобезопасное",
|
||||
organization: "ООО Альтернатива Плюс",
|
||||
as_of_date: "2022-02-28"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
executeLlmChat
|
||||
});
|
||||
|
||||
const output = await runAssistantLivingChatRuntime(input);
|
||||
|
||||
expect(output.handled).toBe(true);
|
||||
expect(output.chatText).toContain("Зеркало для инвалидов поворотное травмобезопасное");
|
||||
expect(output.chatText).toContain("кто поставил");
|
||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract");
|
||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -730,6 +730,98 @@ describe("assistant orchestration contract", () => {
|
||||
expect(decision.livingReason).toBe("meta_followup_over_grounded_answer");
|
||||
});
|
||||
|
||||
it("routes evaluative VAT follow-up 'это много или мало' to contextual chat instead of replaying address lane", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "это много или мало?",
|
||||
effectiveAddressUserMessage: "это много или мало?",
|
||||
followupContext: {
|
||||
previous_intent: "vat_payable_forecast",
|
||||
previous_filters: {
|
||||
period_from: "2020-03-01",
|
||||
period_to: "2020-03-31"
|
||||
},
|
||||
previous_anchor_type: "unknown",
|
||||
previous_anchor_value: null
|
||||
},
|
||||
llmPreDecomposeMeta: {
|
||||
applied: false,
|
||||
reason: "normalized_fragment_rejected_semantic_guard",
|
||||
llmCanonicalCandidateDetected: true,
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
mode_confidence: "low",
|
||||
intent: "unknown",
|
||||
intent_confidence: "low"
|
||||
},
|
||||
semanticExtractionContract: {
|
||||
valid: false,
|
||||
apply_canonical_recommended: false
|
||||
}
|
||||
} as any,
|
||||
sessionItems: [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "address_query",
|
||||
answer_grounding_check: {
|
||||
status: "grounded"
|
||||
},
|
||||
detected_intent: "vat_payable_forecast"
|
||||
}
|
||||
}
|
||||
],
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(decision.runAddressLane).toBe(false);
|
||||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||||
expect(decision.toolGateReason).toBe("meta_followup_over_grounded_answer");
|
||||
expect(decision.livingMode).toBe("chat");
|
||||
expect(decision.livingReason).toBe("meta_followup_over_grounded_answer");
|
||||
});
|
||||
|
||||
it("routes memory recap follow-up over prior address context to deterministic chat instead of generic non-domain chat", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "а ты помнишь мы зеркало обсуждали?",
|
||||
effectiveAddressUserMessage: "а ты помнишь мы зеркало обсуждали?",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: {
|
||||
applied: false,
|
||||
reason: "normalized_fragment_rejected_semantic_guard",
|
||||
llmCanonicalCandidateDetected: false,
|
||||
predecomposeContract: {
|
||||
mode: "unsupported",
|
||||
mode_confidence: "low",
|
||||
intent: "unknown",
|
||||
intent_confidence: "low"
|
||||
}
|
||||
} as any,
|
||||
sessionItems: [
|
||||
{
|
||||
role: "assistant",
|
||||
debug: {
|
||||
execution_lane: "address_query",
|
||||
answer_grounding_check: {
|
||||
status: "grounded"
|
||||
},
|
||||
detected_intent: "inventory_purchase_provenance_for_item",
|
||||
extracted_filters: {
|
||||
item: "Зеркало для инвалидов поворотное травмобезопасное",
|
||||
as_of_date: "2022-02-28"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
useMock: false
|
||||
} as any);
|
||||
|
||||
expect(decision.runAddressLane).toBe(false);
|
||||
expect(decision.toolGateDecision).toBe("skip_address_lane");
|
||||
expect(decision.toolGateReason).toBe("memory_recap_followup_detected");
|
||||
expect(decision.livingMode).toBe("chat");
|
||||
expect(decision.livingReason).toBe("memory_recap_followup_detected");
|
||||
});
|
||||
|
||||
it("keeps documentary inventory chain verification in address lane for supported exact intent", () => {
|
||||
const question =
|
||||
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы";
|
||||
|
||||
Reference in New Issue
Block a user