АРЧ АП11 - Оркестрация: дожать агентный прогон по выбору компании и возрасту активности в 1С

This commit is contained in:
2026-04-17 16:40:08 +03:00
parent 7be037e225
commit cd0b78d1de
31 changed files with 1909 additions and 1187 deletions
@@ -283,6 +283,15 @@ const COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS = [
"разбей поставщиков на регуляр и разовые",
"кто новые в этом году",
"active customers",
"сколько лет активности в базе",
"сколько лет активности в 1с",
"сколько лет в базе 1с",
"какой первый платеж",
"какое первое поступление",
"когда была первая активность",
"когда была последняя активность",
"первая активность в базе",
"последняя активность в базе",
"customer activity list",
"counterparty lifecycle"
];
@@ -687,6 +696,11 @@ function hasCounterpartyActivityLifecycleSignal(text) {
if (hasCustomerRevenueAndPaymentsSignal(text) || hasSupplierPayoutsProfileSignal(text)) {
return false;
}
const hasActivityAgeCue = /(?:сколько\s+лет\s+активности|сколько\s+лет\s+в\s+базе|возраст\s+активности|перв(?:ая|ый|ое)\s+(?:активность|платеж|поступление|документ)|последн(?:яя|ий|ее)\s+активность|с\s+какого\s+года\s+актив)/iu.test(text);
const hasActivityAgeAnchor = /(?:компан|контрагент|организац|ооо|ао|зао|ип|по\s+[a-zа-я0-9"«»().,_-]{3,}|в\s+базе\s+1с|в\s+1с\s+базе)/iu.test(text);
if (hasActivityAgeCue && hasActivityAgeAnchor) {
return true;
}
const hasPaymentRiskLexeme = /(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|задерж(?:ива|к)|просроч|задолж|\bдолг(?:и|ов|а|у)?\b)/iu.test(text);
if (hasPaymentRiskLexeme) {
return false;
@@ -40,6 +40,7 @@ const ADDRESS_ACTION_TOKENS = [
"че по",
"чё по",
"остаток",
"остатки",
"скока",
"сколько",
"долг",
@@ -92,6 +93,7 @@ const ADDRESS_ENTITY_TOKENS = [
"доки",
"док",
"остаток",
"остатки",
"дебитор",
"кредитор",
"аванс",
+24 -1
View File
@@ -1394,6 +1394,17 @@ function isCounterpartyRiskIntent(intent) {
function sameNormalizedOrganizationScope(left, right) {
return (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(left ?? "") === (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(right ?? "");
}
function stripOrganizationLegalForm(value) {
return (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(value ?? "")
.replace(/(?:^|\s)(?:ооо|зао|оао|пао|ао|ип|llc|inc|ltd|corp|company|organization)(?=$|\s)/giu, " ")
.replace(/\s+/g, " ")
.trim();
}
function sameOrganizationEntityReference(left, right) {
const leftNorm = stripOrganizationLegalForm(left);
const rightNorm = stripOrganizationLegalForm(right);
return Boolean(leftNorm && rightNorm && leftNorm === rightNorm);
}
function applyPreExecutionOrganizationScopeGrounding(input) {
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(input.activeOrganization ?? null);
const candidateOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)([
@@ -2613,6 +2624,17 @@ class AddressQueryService {
receivablesConfirmedExecution?.executionFilters ??
vatPayableConfirmedExecution?.executionFilters ??
filters.extracted_filters;
if (intent.intent === "counterparty_activity_lifecycle" &&
typeof executionFilters.counterparty === "string" &&
sameOrganizationEntityReference(executionFilters.counterparty, executionFilters.organization ?? activeOrganization)) {
delete executionFilters.counterparty;
if (!filters.warnings.includes("counterparty_cleared_for_selected_organization_activity")) {
filters.warnings.push("counterparty_cleared_for_selected_organization_activity");
}
if (!baseReasons.includes("counterparty_cleared_for_selected_organization_activity")) {
baseReasons.push("counterparty_cleared_for_selected_organization_activity");
}
}
if (payablesConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)) {
if (!filters.warnings.includes("as_of_date_derived_for_confirmed_payables")) {
@@ -2694,7 +2716,7 @@ class AddressQueryService {
requestedResultMode,
filters: executionFilters
});
let anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, filters.extracted_filters);
let anchor = (0, resolveStage_1.resolvePrimaryAnchor)(intent.intent, executionFilters);
if ((0, addressCapabilityPolicy_1.isCapabilityRouteBlocked)(capabilityDecision)) {
return buildLimitedExecutionResult({
mode,
@@ -2727,6 +2749,7 @@ class AddressQueryService {
: typeof filterSet.counterparty === "string"
? filterSet.counterparty
: undefined,
organizationHint: typeof filterSet.organization === "string" ? filterSet.organization : activeOrganization ?? undefined,
accountHint: typeof options.accountHint === "string"
? options.accountHint
: typeof filterSet.account === "string"
@@ -440,6 +440,19 @@ __WHERE_OUT__
Регистратор
`;
const COUNTERPARTY_ACTIVITY_LIFECYCLE_QUERY_TEMPLATE = `
ВЫБРАТЬ
МИНИМУМ(БанкПоступление.Дата) КАК Период,
"CP_CUSTOMER_ACTIVITY_FIRST" КАК Регистратор,
"" КАК СчетДт,
"" КАК СчетКт,
КОЛИЧЕСТВО(*) КАК Сумма,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент
ИЗ
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
__WHERE_IN__
СГРУППИРОВАТЬ ПО
БанкПоступление.Контрагент
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
НАЧАЛОПЕРИОДА(БанкПоступление.Дата, ГОД) КАК Период,
"CP_CUSTOMER_ACTIVITY_YEAR" КАК Регистратор,
@@ -689,7 +702,7 @@ const BASE_RECIPES = [
intent: "counterparty_activity_lifecycle",
purpose: "Build active customer list for requested period/all-time using bank inflow docs",
required_filters: [],
optional_filters: ["period_from", "period_to", "organization", "limit", "sort"],
optional_filters: ["period_from", "period_to", "organization", "counterparty", "limit", "sort"],
default_limit: 200,
account_scope_mode: "preferred",
query_template: "counterparty_lifecycle_profile"
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.contractCandidatesFromRows = contractCandidatesFromRows;
exports.composeFactualReply = composeFactualReply;
exports.inferReplyType = inferReplyType;
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
function uniqueStrings(values) {
return Array.from(new Set(values
.map((item) => item.trim())
@@ -581,6 +582,39 @@ function extractCounterpartyName(row) {
}
return null;
}
function normalizeCounterpartyLookupText(value) {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/[^a-zа-я0-9]+/giu, " ")
.replace(/\s+/g, " ")
.trim();
}
function counterpartyLookupMatches(candidate, hint) {
const normalizedCandidate = normalizeCounterpartyLookupText(candidate);
const normalizedHint = normalizeCounterpartyLookupText(hint);
if (!normalizedCandidate || !normalizedHint) {
return false;
}
if (normalizedCandidate === normalizedHint) {
return true;
}
if (normalizedCandidate.includes(normalizedHint) || normalizedHint.includes(normalizedCandidate)) {
return true;
}
const hintTokens = normalizedHint.split(" ").filter((token) => token.length >= 3);
if (hintTokens.length === 0) {
return false;
}
return hintTokens.every((token) => normalizedCandidate.includes(token));
}
function hasCounterpartyActivityAgeQuestion(userMessage) {
const text = normalizeQuestionText(userMessage);
if (!text) {
return false;
}
return /(?:сколько\s+лет\s+активности|сколько\s+лет\s+в\s+базе|возраст\s+активности|перв(?:ая|ый|ое)\s+(?:активность|платеж|поступление|документ)|последн(?:яя|ий|ее)\s+активность|с\s+какого\s+года\s+актив)/iu.test(text);
}
function hasCounterpartyItemFlowQuestion(userMessage) {
const text = String(userMessage ?? "").trim().toLowerCase();
if (!text) {
@@ -2386,9 +2420,39 @@ function composeFactualReply(intent, rows, options = {}) {
};
}
if (intent === "counterparty_activity_lifecycle") {
const activityFirstRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY_FIRST");
const activityRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY");
const activityYearRows = rows.filter((row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY_YEAR");
const byCounterparty = new Map();
for (const row of activityFirstRows) {
const name = extractCounterpartyName(row);
if (!name) {
continue;
}
const opsCount = Math.max(0, Math.trunc(row.amount ?? 0));
const year = extractYearFromIso(row.period);
const current = byCounterparty.get(name);
if (!current) {
byCounterparty.set(name, {
name,
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: row.period,
years: new Set(year !== null ? [year] : [])
});
continue;
}
if (!current.firstObservedActivity || (row.period ?? "") < current.firstObservedActivity) {
current.firstObservedActivity = row.period;
}
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if (year !== null) {
current.years.add(year);
}
}
for (const row of activityYearRows) {
const name = extractCounterpartyName(row);
if (!name) {
@@ -2403,6 +2467,7 @@ function composeFactualReply(intent, rows, options = {}) {
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: null,
years: new Set(year !== null ? [year] : [])
});
continue;
@@ -2432,6 +2497,7 @@ function composeFactualReply(intent, rows, options = {}) {
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: row.period,
years: new Set(year !== null ? [year] : [])
});
continue;
@@ -2454,6 +2520,7 @@ function composeFactualReply(intent, rows, options = {}) {
const focus = detectCounterpartyLifecycleFocus(options.userMessage);
const requestedYear = extractRequestedYearFromQuestion(options.userMessage);
const longevityQuestion = hasCounterpartyLifecycleLongevityQuestion(options.userMessage);
const activityAgeQuestion = hasCounterpartyActivityAgeQuestion(options.userMessage);
const rankingLimit = detectRankingLimit(options.userMessage, 10);
const counterparties = counterpartiesRaw.sort((left, right) => {
if (longevityQuestion) {
@@ -2472,6 +2539,105 @@ function composeFactualReply(intent, rows, options = {}) {
: requestedYear
? `в ${requestedYear} году`
: "в выбранном периоде";
if (activityAgeQuestion) {
const focusedCounterparty = counterparties.find((item) => counterpartyLookupMatches(item.name, options.counterpartyHint)) ?? null;
if (focusedCounterparty) {
const firstObservedActivity = focusedCounterparty.firstObservedActivity ?? focusedCounterparty.firstPeriod;
const lastObservedActivity = focusedCounterparty.lastPeriod;
const firstTimestamp = toUtcDayTimestamp(firstObservedActivity);
const lastTimestamp = toUtcDayTimestamp(lastObservedActivity);
const observedDays = firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
: null;
const observedAgeLabel = observedDays !== null
? formatAgeYearsMonthsDays(observedDays)
: focusedCounterparty.years.size > 0
? `${focusedCounterparty.years.size} г.`
: null;
const directLine = observedAgeLabel && firstObservedActivity && lastObservedActivity
? `По активности в базе 1С контрагент ${focusedCounterparty.name} наблюдается минимум ${observedAgeLabel}.`
: `По активности в базе 1С контрагент ${focusedCounterparty.name} найден в подтвержденных движениях.`;
const lines = [directLine];
if (firstObservedActivity) {
lines.push(`Первая подтвержденная активность: ${formatDateRu(firstObservedActivity)}.`);
}
if (lastObservedActivity) {
lines.push(`Последняя подтвержденная активность: ${formatDateRu(lastObservedActivity)}.`);
}
lines.push(`Подтвержденных операций в агрегате: ${focusedCounterparty.opsCount}.`);
if (focusedCounterparty.years.size > 0) {
const years = Array.from(focusedCounterparty.years).sort((a, b) => a - b);
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
}
lines.push("Это возраст активности в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
const organizationHint = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint ?? null);
if (organizationHint && counterparties.length > 0) {
const organizationFirstObservedActivity = counterparties.reduce((earliest, item) => {
const candidate = item.firstObservedActivity ?? item.firstPeriod ?? null;
if (!candidate) {
return earliest;
}
if (!earliest || candidate < earliest) {
return candidate;
}
return earliest;
}, null);
const organizationLastObservedActivity = counterparties.reduce((latest, item) => {
const candidate = item.lastPeriod ?? item.firstPeriod ?? item.firstObservedActivity ?? null;
if (!candidate) {
return latest;
}
if (!latest || candidate > latest) {
return candidate;
}
return latest;
}, null);
const organizationYears = new Set();
let organizationOpsCount = 0;
for (const item of counterparties) {
organizationOpsCount += item.opsCount;
for (const year of item.years) {
organizationYears.add(year);
}
}
const firstTimestamp = toUtcDayTimestamp(organizationFirstObservedActivity);
const lastTimestamp = toUtcDayTimestamp(organizationLastObservedActivity);
const observedDays = firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
: null;
const observedAgeLabel = observedDays !== null
? formatAgeYearsMonthsDays(observedDays)
: organizationYears.size > 0
? `${organizationYears.size} г.`
: null;
const lines = [
observedAgeLabel && organizationFirstObservedActivity && organizationLastObservedActivity
? `По активности организации ${organizationHint} в базе 1С наблюдается минимум ${observedAgeLabel}.`
: `По активности организации ${organizationHint} в базе 1С найдены подтвержденные движения.`
];
if (organizationFirstObservedActivity) {
lines.push(`Первая подтвержденная активность: ${formatDateRu(organizationFirstObservedActivity)}.`);
}
if (organizationLastObservedActivity) {
lines.push(`Последняя подтвержденная активность: ${formatDateRu(organizationLastObservedActivity)}.`);
}
lines.push(`Подтвержденных операций в агрегате: ${organizationOpsCount}.`);
if (organizationYears.size > 0) {
const years = Array.from(organizationYears).sort((a, b) => a - b);
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
}
lines.push("Это возраст активности организации в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
}
const lines = longevityQuestion
? [
`Заказчиков с самым длинным горизонтом сотрудничества (по годам): ${counterparties.length}.`,
@@ -425,7 +425,7 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
/(?:покажи|показать|выведи|раскрой|еще\s+раз|ещ[её]\s+раз|снова|опять|верни|вернись|повтори|тот\s+же|этот\s+же|same|again)/iu.test(normalized);
const canReenterInventoryRoot = comingFromInventoryDrilldown ||
rootContextOnly ||
(currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) ||
(currentFrameKind === "inventory_root" && (hasSamePeriodHint(normalized) || hasInventoryRootRestatementCue)) ||
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
if (!canReenterInventoryRoot) {
return false;
@@ -537,7 +537,8 @@ function hasAddressFollowupContextSignal(text) {
return tokenCount <= 6;
}
function isValueCounterpartyIntent(intent) {
return (intent === "customer_revenue_and_payments" ||
return (intent === "counterparty_activity_lifecycle" ||
intent === "customer_revenue_and_payments" ||
intent === "supplier_payouts_profile" ||
intent === "contract_usage_and_value");
}
@@ -46,7 +46,19 @@ 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, 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, 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) {
return false;
}
if (!/(?:остатк(?:и|ов|ами|ах)?|на\s+складе|склад(?:е|у|ом)?)/iu.test(normalized)) {
return false;
}
const hasRequestCue = /(?:покажи|показать|выведи|список|какие|что\s+по|че\s+по|чё\s+по|тогда\s+покажи)/iu.test(normalized);
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
return hasRequestCue && hasTemporalCue;
}
function resolveAssistantOrchestrationDecision(input) {
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
@@ -212,6 +224,24 @@ function createAssistantRoutePolicy(deps) {
});
const contextualHistoricalCapabilityFollowupDetected = memorySignals.contextualHistoricalCapabilityFollowupDetected;
const contextualMemoryRecapFollowupDetected = memorySignals.contextualMemoryRecapFollowupDetected;
const organizationFactLookupDetected = hasOrganizationFactLookupSignal(rawUserMessage) ||
hasOrganizationFactLookupSignal(repairedRawUserMessage) ||
hasOrganizationFactLookupSignal(effectiveAddressUserMessage) ||
hasOrganizationFactLookupSignal(repairedEffectiveAddressUserMessage);
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
const rootContextOnlyFollowup = Boolean(followupContext && followupContext.root_context_only === true);
const inventoryRootRestatementFollowupDetected = Boolean(followupContext &&
(previousIntent === "inventory_on_hand_as_of_date" ||
previousIntent === "inventory_supplier_stock_overlap_as_of_date" ||
rootContextOnlyFollowup) &&
(hasInventoryRootRestatementFollowupSignal(rawUserMessage) ||
hasInventoryRootRestatementFollowupSignal(repairedRawUserMessage) ||
hasInventoryRootRestatementFollowupSignal(effectiveAddressUserMessage) ||
hasInventoryRootRestatementFollowupSignal(repairedEffectiveAddressUserMessage)));
const organizationFactBoundaryFollowupDetected = hasOrganizationFactFollowupSignal(rawUserMessage, sessionItems ?? []) ||
hasOrganizationFactFollowupSignal(repairedRawUserMessage, sessionItems ?? []) ||
hasOrganizationFactFollowupSignal(effectiveAddressUserMessage, sessionItems ?? []) ||
hasOrganizationFactFollowupSignal(repairedEffectiveAddressUserMessage, sessionItems ?? []);
const hardMetaMode = resolveHardMetaMode({
dataScopeMetaQuery,
capabilityMetaQuery,
@@ -333,6 +363,35 @@ function createAssistantRoutePolicy(deps) {
}
};
}
if (organizationFactLookupDetected || organizationFactBoundaryFollowupDetected) {
return {
runAddressLane: false,
toolGateDecision: "skip_address_lane",
toolGateReason: "organization_fact_lookup_signal_detected",
livingMode: "chat",
livingReason: "organization_fact_lookup_signal_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: null,
provider_execution: providerExecution,
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 || organizationFactBoundaryFollowupDetected),
unsupported_address_intent_fallback_to_deep: false,
final_decision: {
run_address_lane: false,
tool_gate_decision: "skip_address_lane",
tool_gate_reason: "organization_fact_lookup_signal_detected",
living_mode: "chat",
living_reason: "organization_fact_lookup_signal_detected"
}
}
};
}
if (nonDomainQueryIndexed) {
return {
runAddressLane: false,
@@ -382,7 +441,8 @@ function createAssistantRoutePolicy(deps) {
hasShortDebtMirrorFollowupSignal(rawUserMessage) ||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage));
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage) ||
inventoryRootRestatementFollowupDetected);
const supportedAddressIntentDetected = (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed) &&
Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) ||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
@@ -398,7 +458,6 @@ function createAssistantRoutePolicy(deps) {
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 &&
(rootContextOnlyFollowup ||
+15 -579
View File
@@ -2967,451 +2967,10 @@ function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourc
return /^(?:а|и|ну)?\s*по\s+[a-zа-яё0-9._-]{2,}(?:\s+[a-zа-яё0-9._-]{2,})?$/iu.test(normalized);
}
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null, addressNavigationState = null) {
const previousAddressItem = findLastAddressAssistantItem(items);
const previousAddressDebug = previousAddressItem?.debug ?? null;
const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(items);
const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates)
? mergeKnownOrganizations(lastOrganizationClarificationDebug.organization_candidates)
: [];
const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(userMessage, organizationClarificationCandidates) ??
(toNonEmptyString(alternateMessage)
? resolveOrganizationSelectionFromMessage(String(alternateMessage ?? ""), organizationClarificationCandidates)
: null);
const hasOrganizationClarificationContinuation = Boolean(lastOrganizationClarificationDebug && organizationClarificationSelection);
const followupOffer = previousAddressDebug ? buildAddressFollowupOffer(previousAddressDebug) : null;
const hasImplicitContinuationSignal = Boolean(previousAddressDebug) &&
Boolean(followupOffer?.enabled) &&
(isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
(toNonEmptyString(alternateMessage) ? isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta) : false));
const sourceIntentHint = toNonEmptyString(previousAddressDebug?.detected_intent);
const navigationFocusObjectHint = addressNavigationState &&
typeof addressNavigationState === "object" &&
addressNavigationState.session_context &&
typeof addressNavigationState.session_context === "object" &&
addressNavigationState.session_context.active_focus_object &&
typeof addressNavigationState.session_context.active_focus_object === "object"
? addressNavigationState.session_context.active_focus_object
: null;
const hasNavigationInventoryItemFocusHint = Boolean(toNonEmptyString(navigationFocusObjectHint?.label) &&
toNonEmptyString(navigationFocusObjectHint?.object_type) === "item" &&
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
isInventorySelectedObjectIntent(sourceIntentHint)));
let inventoryShortFollowupPrimary = (isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
hasShortInventoryObjectFollowupSignal(userMessage);
let inventoryShortFollowupAlternate = (isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) && toNonEmptyString(alternateMessage)
? hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""))
: false;
const debtRoleSwapPrimary = sourceIntentHint ? resolveDebtRoleSwapFollowupIntent(userMessage, sourceIntentHint) : null;
const debtRoleSwapAlternate = sourceIntentHint && toNonEmptyString(alternateMessage)
? resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
: null;
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
let hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
let hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
? hasAddressFollowupContextSignal(alternateMessage) || Boolean(debtRoleSwapAlternate) || inventoryShortFollowupAlternate
: false;
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
const hasAlternateIndexReferenceSignal = toNonEmptyString(alternateMessage)
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
: false;
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
const recentInventoryRootFrame = findRecentInventoryRootFrame(items);
const hasInventoryRootTemporalFollowupPrimary = hasInventoryRootTemporalFollowupSignal(userMessage, sourceIntentHint, Boolean(recentInventoryRootFrame));
const hasInventoryRootTemporalFollowupAlternate = toNonEmptyString(alternateMessage)
? hasInventoryRootTemporalFollowupSignal(String(alternateMessage ?? ""), sourceIntentHint, Boolean(recentInventoryRootFrame))
: false;
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation ||
hasImplicitContinuationSignal ||
inventoryShortFollowupPrimary ||
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
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 &&
!hasPrimaryFollowupSignal &&
!hasAlternateFollowupSignal &&
!hasInventoryRootTemporalFollowupPrimary &&
!hasInventoryRootTemporalFollowupAlternate &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
return null;
}
if (!hasPrimaryFollowupSignal &&
!hasAlternateFollowupSignal &&
!hasInventoryRootTemporalFollowupPrimary &&
!hasInventoryRootTemporalFollowupAlternate &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
return null;
}
if (!previousAddressDebug) {
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) {
previousIntent = debtRoleSwapIntent;
}
if (hasImplicitContinuationSignal) {
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
? toNonEmptyString(followupOffer.suggested_intents[0])
: null;
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent);
if (suggestedIntent && !keepPreviousIntent) {
previousIntent = suggestedIntent;
followupSelectionMode = "switch_to_suggested_intent";
}
}
let previousAnchorType = toNonEmptyString(previousAddressDebug.anchor_type);
let previousAnchor = toNonEmptyString(previousAddressDebug.anchor_value_resolved) ??
toNonEmptyString(previousAddressDebug.anchor_value_raw) ??
readAddressFilterString(previousAddressDebug, "item") ??
readAddressFilterString(previousAddressDebug, "counterparty") ??
readAddressFilterString(previousAddressDebug, "account") ??
readAddressFilterString(previousAddressDebug, "contract");
const navigationSessionContext = addressNavigationState && typeof addressNavigationState === "object"
? (addressNavigationState.session_context && typeof addressNavigationState.session_context === "object"
? addressNavigationState.session_context
: null)
: null;
const navigationDateScope = navigationSessionContext && typeof navigationSessionContext.date_scope === "object"
? navigationSessionContext.date_scope
: null;
const navigationOrganization = normalizeOrganizationScopeValue(navigationSessionContext?.organization_scope);
const navigationFocusObject = navigationSessionContext && typeof navigationSessionContext.active_focus_object === "object"
? navigationSessionContext.active_focus_object
: null;
const navigationFocusObjectType = toNonEmptyString(navigationFocusObject?.object_type);
const navigationFocusObjectLabel = toNonEmptyString(navigationFocusObject?.label);
const hasInventoryItemFocusCarryover = navigationFocusObjectType === "item" &&
navigationFocusObjectLabel &&
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
isInventorySelectedObjectIntent(sourceIntentHint));
if (!inventoryShortFollowupPrimary && hasInventoryItemFocusCarryover) {
inventoryShortFollowupPrimary = hasShortInventoryObjectFollowupSignal(userMessage);
}
if (!inventoryShortFollowupAlternate && hasInventoryItemFocusCarryover && toNonEmptyString(alternateMessage)) {
inventoryShortFollowupAlternate = hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""));
}
hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) ||
Boolean(debtRoleSwapPrimary) ||
inventoryShortFollowupPrimary ||
hasInventoryRootTemporalFollowupPrimary;
hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
? hasAddressFollowupContextSignal(alternateMessage) ||
Boolean(debtRoleSwapAlternate) ||
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupAlternate
: false;
hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation ||
hasImplicitContinuationSignal ||
inventoryShortFollowupPrimary ||
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
Boolean(debtRoleSwapIntent) ||
hasFollowupMarker(userMessage) ||
hasReferentialPointer(userMessage) ||
(toNonEmptyString(alternateMessage)
? hasFollowupMarker(String(alternateMessage ?? "")) || hasReferentialPointer(String(alternateMessage ?? ""))
: false);
const hasSelectedObjectInventorySignalPrimary = /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(userMessage ?? ""));
const hasSelectedObjectInventorySignalAlternate = toNonEmptyString(alternateMessage)
? /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(alternateMessage ?? ""))
: false;
let inventoryRootFrame = findRecentInventoryRootFrame(items);
if (inventoryRootFrame && navigationOrganization && !toNonEmptyString(inventoryRootFrame.filters?.organization)) {
inventoryRootFrame = {
...inventoryRootFrame,
filters: {
...(inventoryRootFrame.filters ?? {}),
organization: navigationOrganization
}
};
}
if (inventoryRootFrame && navigationDateScope) {
inventoryRootFrame = {
...inventoryRootFrame,
filters: {
...(inventoryRootFrame.filters ?? {}),
as_of_date: toNonEmptyString(inventoryRootFrame.filters?.as_of_date) ?? toNonEmptyString(navigationDateScope.as_of_date) ?? undefined,
period_from: toNonEmptyString(inventoryRootFrame.filters?.period_from) ?? toNonEmptyString(navigationDateScope.period_from) ?? undefined,
period_to: toNonEmptyString(inventoryRootFrame.filters?.period_to) ?? toNonEmptyString(navigationDateScope.period_to) ?? undefined
}
};
}
let currentFrameKind = inventoryRootFrame
? isInventoryDrilldownFrameIntent(sourceIntent)
? "inventory_drilldown"
: isInventoryRootFrameIntent(sourceIntent)
? "inventory_root"
: "generic"
: null;
let resolvedCounterpartyFromDisplay = false;
const previousFiltersRaw = previousAddressDebug.extracted_filters;
let previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
? { ...previousFiltersRaw }
: {};
const shouldBackfillHistoricalPartyAnchors = sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
sourceIntentHint === "bank_operations_by_counterparty" ||
sourceIntentHint === "list_documents_by_contract" ||
sourceIntentHint === "bank_operations_by_contract" ||
sourceIntentHint === "open_items_by_counterparty_or_contract";
if (shouldBackfillHistoricalPartyAnchors && !toNonEmptyString(previousFilters.contract)) {
const historicalContract = findRecentAddressFilterValue(items, "contract");
if (historicalContract) {
previousFilters.contract = historicalContract;
}
}
if (shouldBackfillHistoricalPartyAnchors && !toNonEmptyString(previousFilters.counterparty)) {
const historicalCounterparty = findRecentAddressFilterValue(items, "counterparty");
if (historicalCounterparty) {
previousFilters.counterparty = historicalCounterparty;
}
}
if (!toNonEmptyString(previousFilters.organization)) {
const historicalOrganization = findRecentAddressFilterValue(items, "organization");
if (historicalOrganization) {
previousFilters.organization = historicalOrganization;
}
}
if (!toNonEmptyString(previousFilters.organization) && navigationOrganization) {
previousFilters.organization = navigationOrganization;
}
if (!toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
previousFilters.organization = organizationClarificationSelection;
}
const shouldBackfillPreviousDateScopeFromNavigation = sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
sourceIntentHint === "inventory_sale_trace_for_item" ||
sourceIntentHint === "inventory_profitability_for_item" ||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
sourceIntentHint === "account_balance_snapshot" ||
sourceIntentHint === "documents_forming_balance";
if (shouldBackfillPreviousDateScopeFromNavigation &&
!toNonEmptyString(previousFilters.as_of_date) &&
toNonEmptyString(navigationDateScope?.as_of_date)) {
previousFilters.as_of_date = toNonEmptyString(navigationDateScope?.as_of_date);
}
if (shouldBackfillPreviousDateScopeFromNavigation &&
!toNonEmptyString(previousFilters.period_from) &&
toNonEmptyString(navigationDateScope?.period_from)) {
previousFilters.period_from = toNonEmptyString(navigationDateScope?.period_from);
}
if (shouldBackfillPreviousDateScopeFromNavigation &&
!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));
const inventoryRootTemporalPivot = Boolean(inventoryRootFrame &&
(isInventorySelectedObjectIntent(sourceIntentHint) ||
isInventoryRootFrameIntent(sourceIntentHint) ||
currentFrameKind === "inventory_drilldown" ||
currentFrameKind === "inventory_root") &&
(hasInventoryRootTemporalFollowupPrimary || hasInventoryRootTemporalFollowupAlternate) &&
!hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
const rootScopedPivot = rootContextOnlyPivot || inventoryRootTemporalPivot;
if (rootScopedPivot) {
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 && !rootScopedPivot) {
if (resolvedEntityFromFollowup.entityType === "counterparty") {
previousFilters.counterparty = resolvedEntityFromFollowup.value;
previousAnchorType = "counterparty";
previousAnchor = resolvedEntityFromFollowup.value;
resolvedCounterpartyFromDisplay = true;
}
else if (resolvedEntityFromFollowup.entityType === "contract") {
previousFilters.contract = resolvedEntityFromFollowup.value;
previousAnchorType = "contract";
previousAnchor = resolvedEntityFromFollowup.value;
}
else if (resolvedEntityFromFollowup.entityType === "item") {
previousFilters.item = resolvedEntityFromFollowup.value;
previousAnchorType = "item";
previousAnchor = resolvedEntityFromFollowup.value;
}
if (followupSelectionMode !== "switch_to_suggested_intent") {
followupSelectionMode = "carry_referenced_entity";
}
}
if (!rootScopedPivot &&
!toNonEmptyString(previousFilters.item) &&
navigationFocusObjectType === "item" &&
navigationFocusObjectLabel &&
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
sourceIntentHint === "inventory_sale_trace_for_item" ||
sourceIntentHint === "inventory_profitability_for_item" ||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
hasSelectedObjectInventorySignalPrimary ||
hasSelectedObjectInventorySignalAlternate)) {
previousFilters.item = navigationFocusObjectLabel;
if (!previousAnchor) {
previousAnchorType = "item";
previousAnchor = navigationFocusObjectLabel;
}
}
if (organizationClarificationSelection && !previousAnchor) {
previousAnchorType = "organization";
previousAnchor = organizationClarificationSelection;
}
if (inventoryRootFrame && organizationClarificationSelection && !toNonEmptyString(inventoryRootFrame.filters?.organization)) {
inventoryRootFrame = {
...inventoryRootFrame,
filters: {
...(inventoryRootFrame.filters ?? {}),
organization: organizationClarificationSelection
}
};
}
if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) {
return null;
}
const shouldAttachInventoryRootFrame = Boolean(inventoryRootFrame &&
(rootScopedPivot ||
isInventoryRootFrameIntent(sourceIntentHint) ||
isInventorySelectedObjectIntent(sourceIntentHint) ||
hasNavigationInventoryItemFocusHint ||
inventoryShortFollowupPrimary ||
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
hasSelectedObjectInventorySignalPrimary ||
hasSelectedObjectInventorySignalAlternate));
const carryoverTargetIntent = followupSelectionMode === "carry_root_context"
? inventoryRootFrame?.intent ?? explicitIntent ?? previousIntent ?? undefined
: explicitIntent ?? previousIntent ?? undefined;
return {
followupContext: {
previous_intent: previousIntent ?? undefined,
target_intent: carryoverTargetIntent,
previous_filters: previousFilters,
previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor,
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
root_context_only: rootScopedPivot || undefined,
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,
root_filters: shouldAttachInventoryRootFrame ? inventoryRootFrame?.filters ?? undefined : undefined,
root_anchor_type: shouldAttachInventoryRootFrame ? inventoryRootFrame?.anchorType ?? undefined : undefined,
root_anchor_value: shouldAttachInventoryRootFrame ? inventoryRootFrame?.anchorValue ?? undefined : undefined,
current_frame_kind: shouldAttachInventoryRootFrame ? currentFrameKind ?? undefined : undefined
},
previousAddressIntent: previousIntent,
previousAddressAnchor: previousAnchor,
previousSourceIntent: sourceIntent,
followupSelectionMode,
hasImplicitContinuationSignal
};
return assistantTransitionPolicy.resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage, llmPreDecomposeMeta, addressNavigationState);
}
function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta) {
const sourceMessage = String(userMessage ?? "");
const canonicalMessage = String(effectiveMessage ?? sourceMessage);
const hasFollowupContext = Boolean(carryoverMeta?.followupContext);
const previousIntent = toNonEmptyString(carryoverMeta?.previousSourceIntent) ?? null;
const selectionMode = toNonEmptyString(carryoverMeta?.followupSelectionMode) ?? null;
const rootContextOnly = selectionMode === "carry_root_context";
const explicitIntentRaw = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const explicitIntent = explicitIntentRaw === "unknown" ? null : explicitIntentRaw;
const rootIntent = toNonEmptyString(carryoverMeta?.followupContext?.root_intent) ?? null;
const targetIntent = selectionMode === "switch_to_suggested_intent"
? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null
: rootContextOnly
? rootIntent ?? explicitIntent ?? null
: explicitIntent ?? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
const rewrittenByPredecompose = compactWhitespace(sourceMessage.toLowerCase()) !== compactWhitespace(canonicalMessage.toLowerCase());
const hasExplicitIntent = Boolean(explicitIntent);
const decision = !hasFollowupContext
? "new_topic"
: selectionMode === "switch_to_suggested_intent"
? "switch_to_suggested"
: "continue_previous";
const reasons = [];
if (hasFollowupContext) {
reasons.push("followup_context_detected");
}
if (hasImplicitContinuationSignal) {
reasons.push("implicit_continuation_by_llm");
}
if (rewrittenByPredecompose) {
reasons.push("effective_message_rewritten_by_predecompose");
}
if (hasExplicitIntent) {
reasons.push("llm_contract_intent_available");
}
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,
effective_message: canonicalMessage,
decision,
decision_reasons: reasons,
followup_context_applied: hasFollowupContext,
previous_intent: previousIntent,
target_intent: targetIntent,
intent_selection_mode: selectionMode,
anchor_type: carryoverMeta?.followupContext?.previous_anchor_type ?? null,
anchor_value: carryoverMeta?.followupContext?.previous_anchor_value ?? null,
implicit_continuation_signal: hasImplicitContinuationSignal
};
return assistantTransitionPolicy.buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta);
}
function isRetryableAddressLimitedResult(addressLane) {
if (!addressLane || !addressLane.handled) {
@@ -4488,6 +4047,10 @@ function hasDataRetrievalRequestSignal(text) {
return assistantLivingModePolicy.hasDataRetrievalRequestSignal(text);
}
function hasOrganizationFactLookupSignal(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
if (/(?:активност[иь]\s+в\s+базе|в\s+базе\s+1с|в\s+1с\s+базе|перв(?:ая|ый|ое)\s+(?:операц|платеж|поступлен|списан|документ)|последн(?:яя|ий|ее)\s+активность|первая\s+активность|activity\s+in\s+1c|first\s+(?:payment|document|activity)|last\s+activity)/iu.test(normalized)) {
return false;
}
return assistantLivingModePolicy.hasOrganizationFactLookupSignal(text);
}
function findLastAssistantLivingChatDebug(items) {
@@ -4795,6 +4358,8 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli
resolveHardMetaMode: assistantMetaFollowupPolicy.resolveHardMetaMode,
isMetaFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isMetaFollowupOverGroundedAnswer,
hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal,
hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: assistantLivingModePolicy.hasOrganizationFactFollowupSignal,
hasAggregateBusinessAnalyticsSignal,
hasStandaloneAddressTopicSignal,
hasOpenContractsAddressSignal,
@@ -5504,154 +5069,25 @@ function applyLivingChatGroundingGuardFromPolicy(input) {
return assistantBoundaryPolicy.applyLivingChatGroundingGuard(input);
}
function buildAssistantDataScopeContractReply(scopeProbe = null) {
const channel = String(scopeProbe?.channel ?? config_1.ASSISTANT_MCP_CHANNEL ?? "default");
const organizations = Array.isArray(scopeProbe?.organizations) ? scopeProbe.organizations : [];
if (organizations.length === 1) {
return [
`Сейчас в активном MCP-канале \`${channel}\` доступна организация: ${organizations[0]}.`,
"Работаю в read-only режиме. Могу сразу показать по этой организации документы, операции, договоры или остатки."
].join(" ");
}
if (organizations.length > 1) {
const preview = organizations.slice(0, 10).join(", ");
return [
`Сейчас в активном MCP-канале \`${channel}\` доступны организации (${organizations.length}): ${preview}.`,
"Работаю в read-only режиме. Скажи, по какой организации смотреть документы/операции."
].join(" ");
}
if (scopeProbe?.status === "unresolved_with_error" && scopeProbe?.error) {
return [
`Не смог прочитать название организации из live MCP-канала \`${channel}\`: ${scopeProbe.error}.`,
"Работаю в read-only режиме и вижу только данные активного контура. Проверь подключение MCP/1С, после этого сразу назову контору."
].join(" ");
}
return [
`Работаю в read-only режиме и вижу только те данные, которые отдает текущий MCP-канал \`${channel}\`.`,
"Словарь компаний не зашит в код: рабочий контур определяется live-подключением.",
"Если подключено несколько баз, для автосписка нужен MCP-метод метаданных (перечень баз/организаций); без него можно анализировать только активный контур запросов."
].join(" ");
return buildAssistantDataScopeContractReplyFromPolicy(scopeProbe);
}
function buildAssistantDataScopeSelectionReply(organization) {
const selected = normalizeOrganizationScopeValue(organization) ?? String(organization ?? "").trim();
return [
`Отлично, фиксирую рабочую организацию: ${selected}.`,
"Дальше буду держать этот контур как активный, пока вы не переключите организацию."
].join(" ");
return buildAssistantDataScopeSelectionReplyFromPolicy(organization);
}
function buildAssistantOrganizationFactBoundaryReply(organization) {
const selected = normalizeOrganizationScopeValue(organization) ?? String(organization ?? "").trim();
if (selected) {
return [
`По организации ${selected} не буду называть дату/возраст без live-подтвержденного источника.`,
"Если нужно, запрошу факт из 1С и верну только подтвержденный ответ."
].join(" ");
}
return [
"Не буду называть дату/возраст организации без live-подтвержденного источника.",
"Сначала получу факт из 1С, потом дам точный ответ."
].join(" ");
return buildAssistantOrganizationFactBoundaryReplyFromPolicy(organization);
}
function buildAssistantOperationalBoundaryReply() {
return [
"Понимаю, что ситуация срочная.",
"Я не могу сам настраивать 1С или менять базу/конфигурацию.",
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/ИТ-админа."
].join(" ");
return buildAssistantOperationalBoundaryReplyFromPolicy();
}
function buildAssistantSafetyRefusalReply() {
return [
"Я не могу помогать с удалением базы или скрытием данных.",
"Если вам угрожает опасность, срочно звоните 112 и следуйте указаниям экстренных служб.",
"По 1С могу дать только безопасные диагностические рекомендации."
].join(" ");
}
function containsCjkChars(text) {
const source = String(text ?? "");
if (!source) {
return false;
}
return /[\u3400-\u9FFF\uF900-\uFAFF]/u.test(source);
}
function containsLetterLikeChars(text) {
const source = String(text ?? "");
if (!source) {
return false;
}
return /[A-Za-z\u0400-\u04FF]/u.test(source);
return buildAssistantSafetyRefusalReplyFromPolicy();
}
function applyLivingChatScriptGuard(chatText, userMessage) {
const source = String(chatText ?? "").trim();
if (!source) {
return {
text: "",
applied: false,
reason: null
};
}
if (!containsCjkChars(source) || containsCjkChars(userMessage)) {
return {
text: source,
applied: false,
reason: null
};
}
const stripped = source
.replace(/[\u3400-\u9FFF\uF900-\uFAFF]+/gu, "")
.replace(/[,。!?;:]/gu, "")
.replace(/\s{2,}/g, " ")
.replace(/\s+([,.!?;:])/g, "$1")
.trim();
if (stripped && containsLetterLikeChars(stripped)) {
return {
text: stripped,
applied: true,
reason: "unexpected_cjk_fragment_stripped"
};
}
return {
text: "Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.",
applied: true,
reason: "unexpected_cjk_fragment_fallback"
};
return applyLivingChatScriptGuardFromPolicy(chatText, userMessage);
}
function applyLivingChatGroundingGuard(input) {
const userMessage = String(input?.userMessage ?? "");
const chatText = String(input?.chatText ?? "").trim();
const organization = toNonEmptyString(input?.organization);
if (!chatText) {
return {
text: chatText,
applied: false,
reason: null
};
}
if (!hasOrganizationFactLookupSignal(userMessage)) {
return {
text: chatText,
applied: false,
reason: null
};
}
if (/(?:не\s+могу|не\s+вижу|после\s+проверки|live|подтвержден)/i.test(chatText)) {
return {
text: chatText,
applied: false,
reason: null
};
}
const hasSpecificUnverifiedFact = /(?:\b\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})\b|\b(?:19|20)\d{2}\b|\b\d+\s+лет\b)/i.test(chatText);
if (!hasSpecificUnverifiedFact) {
return {
text: chatText,
applied: false,
reason: null
};
}
return {
text: buildAssistantOrganizationFactBoundaryReply(organization),
applied: true,
reason: "organization_fact_without_live_source_blocked"
};
return applyLivingChatGroundingGuardFromPolicy(input);
}
function resolveLivingAssistantModeDecision(input) {
return assistantLivingModePolicy.resolveLivingAssistantModeDecision(input);
@@ -299,6 +299,15 @@ const COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS = [
"разбей поставщиков на регуляр и разовые",
"кто новые в этом году",
"active customers",
"сколько лет активности в базе",
"сколько лет активности в 1с",
"сколько лет в базе 1с",
"какой первый платеж",
"какое первое поступление",
"когда была первая активность",
"когда была последняя активность",
"первая активность в базе",
"последняя активность в базе",
"customer activity list",
"counterparty lifecycle"
];
@@ -787,6 +796,17 @@ function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
if (hasCustomerRevenueAndPaymentsSignal(text) || hasSupplierPayoutsProfileSignal(text)) {
return false;
}
const hasActivityAgeCue =
/(?:сколько\s+лет\s+активности|сколько\s+лет\s+в\s+базе|возраст\s+активности|перв(?:ая|ый|ое)\s+(?:активность|платеж|поступление|документ)|последн(?:яя|ий|ее)\s+активность|с\s+какого\s+года\s+актив)/iu.test(
text
);
const hasActivityAgeAnchor =
/(?:компан|контрагент|организац|ооо|ао|зао|ип|по\s+[a-zа-я0-9"«»().,_-]{3,}|в\s+базе\s+1с|в\s+1с\s+базе)/iu.test(
text
);
if (hasActivityAgeCue && hasActivityAgeAnchor) {
return true;
}
const hasPaymentRiskLexeme =
/(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|задерж(?:ива|к)|просроч|задолж|\bдолг(?:и|ов|а|у)?\b)/iu.test(
text
@@ -45,6 +45,7 @@ const ADDRESS_ACTION_TOKENS = [
"че по",
"чё по",
"остаток",
"остатки",
"скока",
"сколько",
"долг",
@@ -98,6 +99,7 @@ const ADDRESS_ENTITY_TOKENS = [
"доки",
"док",
"остаток",
"остатки",
"дебитор",
"кредитор",
"аванс",
@@ -1727,6 +1727,19 @@ function sameNormalizedOrganizationScope(left: string | null | undefined, right:
return normalizeOrganizationScopeSearchText(left ?? "") === normalizeOrganizationScopeSearchText(right ?? "");
}
function stripOrganizationLegalForm(value: string | null | undefined): string {
return normalizeOrganizationScopeSearchText(value ?? "")
.replace(/(?:^|\s)(?:ооо|зао|оао|пао|ао|ип|llc|inc|ltd|corp|company|organization)(?=$|\s)/giu, " ")
.replace(/\s+/g, " ")
.trim();
}
function sameOrganizationEntityReference(left: string | null | undefined, right: string | null | undefined): boolean {
const leftNorm = stripOrganizationLegalForm(left);
const rightNorm = stripOrganizationLegalForm(right);
return Boolean(leftNorm && rightNorm && leftNorm === rightNorm);
}
function applyPreExecutionOrganizationScopeGrounding(input: {
userMessage: string;
filters: AddressFilterSet;
@@ -3259,6 +3272,19 @@ export class AddressQueryService {
receivablesConfirmedExecution?.executionFilters ??
vatPayableConfirmedExecution?.executionFilters ??
filters.extracted_filters;
if (
intent.intent === "counterparty_activity_lifecycle" &&
typeof executionFilters.counterparty === "string" &&
sameOrganizationEntityReference(executionFilters.counterparty, executionFilters.organization ?? activeOrganization)
) {
delete executionFilters.counterparty;
if (!filters.warnings.includes("counterparty_cleared_for_selected_organization_activity")) {
filters.warnings.push("counterparty_cleared_for_selected_organization_activity");
}
if (!baseReasons.includes("counterparty_cleared_for_selected_organization_activity")) {
baseReasons.push("counterparty_cleared_for_selected_organization_activity");
}
}
if (
payablesConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
@@ -3354,7 +3380,7 @@ export class AddressQueryService {
requestedResultMode,
filters: executionFilters
});
let anchor = resolvePrimaryAnchor(intent.intent, filters.extracted_filters);
let anchor = resolvePrimaryAnchor(intent.intent, executionFilters);
if (isCapabilityRouteBlocked(capabilityDecision)) {
return buildLimitedExecutionResult({
mode,
@@ -3397,6 +3423,7 @@ export class AddressQueryService {
: typeof filterSet.counterparty === "string"
? filterSet.counterparty
: undefined,
organizationHint: typeof filterSet.organization === "string" ? filterSet.organization : activeOrganization ?? undefined,
accountHint:
typeof options.accountHint === "string"
? options.accountHint
@@ -456,6 +456,19 @@ __WHERE_OUT__
`;
const COUNTERPARTY_ACTIVITY_LIFECYCLE_QUERY_TEMPLATE = `
ВЫБРАТЬ
МИНИМУМ(БанкПоступление.Дата) КАК Период,
"CP_CUSTOMER_ACTIVITY_FIRST" КАК Регистратор,
"" КАК СчетДт,
"" КАК СчетКт,
КОЛИЧЕСТВО(*) КАК Сумма,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент
ИЗ
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
__WHERE_IN__
СГРУППИРОВАТЬ ПО
БанкПоступление.Контрагент
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
НАЧАЛОПЕРИОДА(БанкПоступление.Дата, ГОД) КАК Период,
"CP_CUSTOMER_ACTIVITY_YEAR" КАК Регистратор,
@@ -713,7 +726,7 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
intent: "counterparty_activity_lifecycle",
purpose: "Build active customer list for requested period/all-time using bank inflow docs",
required_filters: [],
optional_filters: ["period_from", "period_to", "organization", "limit", "sort"],
optional_filters: ["period_from", "period_to", "organization", "counterparty", "limit", "sort"],
default_limit: 200,
account_scope_mode: "preferred",
query_template: "counterparty_lifecycle_profile"
@@ -4,6 +4,7 @@ import type {
AddressResponseType,
AddressResultMode
} from "../../types/addressQuery";
import { normalizeOrganizationScopeValue } from "../assistantOrganizationMatcher";
export interface ComposeStageRow {
period: string | null;
@@ -42,6 +43,7 @@ interface ComposeFactualReplyOptions {
userMessage?: string;
itemHint?: string;
counterpartyHint?: string;
organizationHint?: string;
accountHint?: string;
periodFrom?: string;
periodTo?: string;
@@ -778,6 +780,44 @@ function extractCounterpartyName(row: ComposeStageRow): string | null {
return null;
}
function normalizeCounterpartyLookupText(value: string | null | undefined): string {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/[^a-zа-я0-9]+/giu, " ")
.replace(/\s+/g, " ")
.trim();
}
function counterpartyLookupMatches(candidate: string | null | undefined, hint: string | null | undefined): boolean {
const normalizedCandidate = normalizeCounterpartyLookupText(candidate);
const normalizedHint = normalizeCounterpartyLookupText(hint);
if (!normalizedCandidate || !normalizedHint) {
return false;
}
if (normalizedCandidate === normalizedHint) {
return true;
}
if (normalizedCandidate.includes(normalizedHint) || normalizedHint.includes(normalizedCandidate)) {
return true;
}
const hintTokens = normalizedHint.split(" ").filter((token) => token.length >= 3);
if (hintTokens.length === 0) {
return false;
}
return hintTokens.every((token) => normalizedCandidate.includes(token));
}
function hasCounterpartyActivityAgeQuestion(userMessage: string | null | undefined): boolean {
const text = normalizeQuestionText(userMessage);
if (!text) {
return false;
}
return /(?:сколько\s+лет\s+активности|сколько\s+лет\s+в\s+базе|возраст\s+активности|перв(?:ая|ый|ое)\s+(?:активность|платеж|поступление|документ)|последн(?:яя|ий|ее)\s+активность|с\s+какого\s+года\s+актив)/iu.test(
text
);
}
function hasCounterpartyItemFlowQuestion(userMessage: string | undefined): boolean {
const text = String(userMessage ?? "").trim().toLowerCase();
if (!text) {
@@ -3065,6 +3105,9 @@ export function composeFactualReply(
}
if (intent === "counterparty_activity_lifecycle") {
const activityFirstRows = rows.filter(
(row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY_FIRST"
);
const activityRows = rows.filter(
(row) => String(row.registrator ?? "").trim().toUpperCase() === "CP_CUSTOMER_ACTIVITY"
);
@@ -3073,9 +3116,46 @@ export function composeFactualReply(
);
const byCounterparty = new Map<
string,
{ name: string; opsCount: number; lastPeriod: string | null; firstPeriod: string | null; years: Set<number> }
{
name: string;
opsCount: number;
lastPeriod: string | null;
firstPeriod: string | null;
firstObservedActivity: string | null;
years: Set<number>;
}
>();
for (const row of activityFirstRows) {
const name = extractCounterpartyName(row);
if (!name) {
continue;
}
const opsCount = Math.max(0, Math.trunc(row.amount ?? 0));
const year = extractYearFromIso(row.period);
const current = byCounterparty.get(name);
if (!current) {
byCounterparty.set(name, {
name,
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: row.period,
years: new Set<number>(year !== null ? [year] : [])
});
continue;
}
if (!current.firstObservedActivity || (row.period ?? "") < current.firstObservedActivity) {
current.firstObservedActivity = row.period;
}
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if (year !== null) {
current.years.add(year);
}
}
for (const row of activityYearRows) {
const name = extractCounterpartyName(row);
if (!name) {
@@ -3090,6 +3170,7 @@ export function composeFactualReply(
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: null,
years: new Set<number>(year !== null ? [year] : [])
});
continue;
@@ -3120,6 +3201,7 @@ export function composeFactualReply(
opsCount,
lastPeriod: row.period,
firstPeriod: row.period,
firstObservedActivity: row.period,
years: new Set<number>(year !== null ? [year] : [])
});
continue;
@@ -3143,6 +3225,7 @@ export function composeFactualReply(
const focus = detectCounterpartyLifecycleFocus(options.userMessage);
const requestedYear = extractRequestedYearFromQuestion(options.userMessage);
const longevityQuestion = hasCounterpartyLifecycleLongevityQuestion(options.userMessage);
const activityAgeQuestion = hasCounterpartyActivityAgeQuestion(options.userMessage);
const rankingLimit = detectRankingLimit(options.userMessage, 10);
const counterparties = counterpartiesRaw.sort((left, right) => {
if (longevityQuestion) {
@@ -3163,6 +3246,112 @@ export function composeFactualReply(
? `в ${requestedYear} году`
: "в выбранном периоде";
if (activityAgeQuestion) {
const focusedCounterparty =
counterparties.find((item) => counterpartyLookupMatches(item.name, options.counterpartyHint)) ?? null;
if (focusedCounterparty) {
const firstObservedActivity = focusedCounterparty.firstObservedActivity ?? focusedCounterparty.firstPeriod;
const lastObservedActivity = focusedCounterparty.lastPeriod;
const firstTimestamp = toUtcDayTimestamp(firstObservedActivity);
const lastTimestamp = toUtcDayTimestamp(lastObservedActivity);
const observedDays =
firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
: null;
const observedAgeLabel =
observedDays !== null
? formatAgeYearsMonthsDays(observedDays)
: focusedCounterparty.years.size > 0
? `${focusedCounterparty.years.size} г.`
: null;
const directLine =
observedAgeLabel && firstObservedActivity && lastObservedActivity
? `По активности в базе 1С контрагент ${focusedCounterparty.name} наблюдается минимум ${observedAgeLabel}.`
: `По активности в базе 1С контрагент ${focusedCounterparty.name} найден в подтвержденных движениях.`;
const lines: string[] = [directLine];
if (firstObservedActivity) {
lines.push(`Первая подтвержденная активность: ${formatDateRu(firstObservedActivity)}.`);
}
if (lastObservedActivity) {
lines.push(`Последняя подтвержденная активность: ${formatDateRu(lastObservedActivity)}.`);
}
lines.push(`Подтвержденных операций в агрегате: ${focusedCounterparty.opsCount}.`);
if (focusedCounterparty.years.size > 0) {
const years = Array.from(focusedCounterparty.years).sort((a, b) => a - b);
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
}
lines.push("Это возраст активности в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
const organizationHint = normalizeOrganizationScopeValue(options.organizationHint ?? null);
if (organizationHint && counterparties.length > 0) {
const organizationFirstObservedActivity = counterparties.reduce<string | null>((earliest, item) => {
const candidate = item.firstObservedActivity ?? item.firstPeriod ?? null;
if (!candidate) {
return earliest;
}
if (!earliest || candidate < earliest) {
return candidate;
}
return earliest;
}, null);
const organizationLastObservedActivity = counterparties.reduce<string | null>((latest, item) => {
const candidate = item.lastPeriod ?? item.firstPeriod ?? item.firstObservedActivity ?? null;
if (!candidate) {
return latest;
}
if (!latest || candidate > latest) {
return candidate;
}
return latest;
}, null);
const organizationYears = new Set<number>();
let organizationOpsCount = 0;
for (const item of counterparties) {
organizationOpsCount += item.opsCount;
for (const year of item.years) {
organizationYears.add(year);
}
}
const firstTimestamp = toUtcDayTimestamp(organizationFirstObservedActivity);
const lastTimestamp = toUtcDayTimestamp(organizationLastObservedActivity);
const observedDays =
firstTimestamp !== null && lastTimestamp !== null && lastTimestamp >= firstTimestamp
? Math.floor((lastTimestamp - firstTimestamp) / 86_400_000)
: null;
const observedAgeLabel =
observedDays !== null
? formatAgeYearsMonthsDays(observedDays)
: organizationYears.size > 0
? `${organizationYears.size} г.`
: null;
const lines: string[] = [
observedAgeLabel && organizationFirstObservedActivity && organizationLastObservedActivity
? `По активности организации ${organizationHint} в базе 1С наблюдается минимум ${observedAgeLabel}.`
: `По активности организации ${organizationHint} в базе 1С найдены подтвержденные движения.`
];
if (organizationFirstObservedActivity) {
lines.push(`Первая подтвержденная активность: ${formatDateRu(organizationFirstObservedActivity)}.`);
}
if (organizationLastObservedActivity) {
lines.push(`Последняя подтвержденная активность: ${formatDateRu(organizationLastObservedActivity)}.`);
}
lines.push(`Подтвержденных операций в агрегате: ${organizationOpsCount}.`);
if (organizationYears.size > 0) {
const years = Array.from(organizationYears).sort((a, b) => a - b);
lines.push(`Годы с активностью в базе: ${years.join(", ")}.`);
}
lines.push("Это возраст активности организации в 1С по подтвержденным движениям, а не дата регистрации юрлица.");
return {
responseType: "FACTUAL_SUMMARY",
text: lines.join("\n")
};
}
}
const lines: string[] = longevityQuestion
? [
`Заказчиков с самым длинным горизонтом сотрудничества (по годам): ${counterparties.length}.`,
@@ -547,7 +547,7 @@ function shouldRestoreInventoryRootFrame(
const canReenterInventoryRoot =
comingFromInventoryDrilldown ||
rootContextOnly ||
(currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) ||
(currentFrameKind === "inventory_root" && (hasSamePeriodHint(normalized) || hasInventoryRootRestatementCue)) ||
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
if (!canReenterInventoryRoot) {
return false;
@@ -694,6 +694,7 @@ export function hasAddressFollowupContextSignal(text: string): boolean {
function isValueCounterpartyIntent(intent: AddressIntent): boolean {
return (
intent === "counterparty_activity_lifecycle" ||
intent === "customer_revenue_and_payments" ||
intent === "supplier_payouts_profile" ||
intent === "contract_usage_and_value"
@@ -54,6 +54,8 @@ export function createAssistantRoutePolicy(deps) {
resolveHardMetaMode,
isMetaFollowupOverGroundedAnswer,
hasDataRetrievalRequestSignal,
hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal,
hasAggregateBusinessAnalyticsSignal,
hasStandaloneAddressTopicSignal,
hasOpenContractsAddressSignal,
@@ -81,6 +83,18 @@ export function createAssistantRoutePolicy(deps) {
resolveLivingAssistantModeDecision,
resolveProviderExecutionState
} = deps;
function hasInventoryRootRestatementFollowupSignal(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
if (!normalized) {
return false;
}
if (!/(?:остатк(?:и|ов|ами|ах)?|на\s+складе|склад(?:е|у|ом)?)/iu.test(normalized)) {
return false;
}
const hasRequestCue = /(?:покажи|показать|выведи|список|какие|что\s+по|че\s+по|чё\s+по|тогда\s+покажи)/iu.test(normalized);
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
return hasRequestCue && hasTemporalCue;
}
function resolveAssistantOrchestrationDecision(input) {
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
@@ -246,6 +260,24 @@ export function createAssistantRoutePolicy(deps) {
});
const contextualHistoricalCapabilityFollowupDetected = memorySignals.contextualHistoricalCapabilityFollowupDetected;
const contextualMemoryRecapFollowupDetected = memorySignals.contextualMemoryRecapFollowupDetected;
const organizationFactLookupDetected = hasOrganizationFactLookupSignal(rawUserMessage) ||
hasOrganizationFactLookupSignal(repairedRawUserMessage) ||
hasOrganizationFactLookupSignal(effectiveAddressUserMessage) ||
hasOrganizationFactLookupSignal(repairedEffectiveAddressUserMessage);
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
const rootContextOnlyFollowup = Boolean(followupContext && followupContext.root_context_only === true);
const inventoryRootRestatementFollowupDetected = Boolean(followupContext &&
(previousIntent === "inventory_on_hand_as_of_date" ||
previousIntent === "inventory_supplier_stock_overlap_as_of_date" ||
rootContextOnlyFollowup) &&
(hasInventoryRootRestatementFollowupSignal(rawUserMessage) ||
hasInventoryRootRestatementFollowupSignal(repairedRawUserMessage) ||
hasInventoryRootRestatementFollowupSignal(effectiveAddressUserMessage) ||
hasInventoryRootRestatementFollowupSignal(repairedEffectiveAddressUserMessage)));
const organizationFactBoundaryFollowupDetected = hasOrganizationFactFollowupSignal(rawUserMessage, sessionItems ?? []) ||
hasOrganizationFactFollowupSignal(repairedRawUserMessage, sessionItems ?? []) ||
hasOrganizationFactFollowupSignal(effectiveAddressUserMessage, sessionItems ?? []) ||
hasOrganizationFactFollowupSignal(repairedEffectiveAddressUserMessage, sessionItems ?? []);
const hardMetaMode = resolveHardMetaMode({
dataScopeMetaQuery,
capabilityMetaQuery,
@@ -367,6 +399,35 @@ export function createAssistantRoutePolicy(deps) {
}
};
}
if (organizationFactLookupDetected || organizationFactBoundaryFollowupDetected) {
return {
runAddressLane: false,
toolGateDecision: "skip_address_lane",
toolGateReason: "organization_fact_lookup_signal_detected",
livingMode: "chat",
livingReason: "organization_fact_lookup_signal_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: null,
provider_execution: providerExecution,
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 || organizationFactBoundaryFollowupDetected),
unsupported_address_intent_fallback_to_deep: false,
final_decision: {
run_address_lane: false,
tool_gate_decision: "skip_address_lane",
tool_gate_reason: "organization_fact_lookup_signal_detected",
living_mode: "chat",
living_reason: "organization_fact_lookup_signal_detected"
}
}
};
}
if (nonDomainQueryIndexed) {
return {
runAddressLane: false,
@@ -416,7 +477,8 @@ export function createAssistantRoutePolicy(deps) {
hasShortDebtMirrorFollowupSignal(rawUserMessage) ||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage));
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage) ||
inventoryRootRestatementFollowupDetected);
const supportedAddressIntentDetected = (!strictDeepInvestigationCueDetected || strictDeepInvestigationBypassAllowed) &&
Boolean((resolvedIntentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(resolvedIntentResolution.intent)) ||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
@@ -432,7 +494,6 @@ export function createAssistantRoutePolicy(deps) {
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 &&
(rootContextOnlyFollowup ||
@@ -2925,452 +2925,10 @@ function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourc
return /^(?:а|и|ну)?\s*по\s+[a-zа-яё0-9._-]{2,}(?:\s+[a-zа-яё0-9._-]{2,})?$/iu.test(normalized);
}
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null, addressNavigationState = null) {
const previousAddressItem = findLastAddressAssistantItem(items);
const previousAddressDebug = previousAddressItem?.debug ?? null;
const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(items);
const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates)
? mergeKnownOrganizations(lastOrganizationClarificationDebug.organization_candidates)
: [];
const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(userMessage, organizationClarificationCandidates) ??
(toNonEmptyString(alternateMessage)
? resolveOrganizationSelectionFromMessage(String(alternateMessage ?? ""), organizationClarificationCandidates)
: null);
const hasOrganizationClarificationContinuation = Boolean(lastOrganizationClarificationDebug && organizationClarificationSelection);
const followupOffer = previousAddressDebug ? buildAddressFollowupOffer(previousAddressDebug) : null;
const hasImplicitContinuationSignal = Boolean(previousAddressDebug) &&
Boolean(followupOffer?.enabled) &&
(isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
(toNonEmptyString(alternateMessage) ? isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta) : false));
const sourceIntentHint = toNonEmptyString(previousAddressDebug?.detected_intent);
const navigationFocusObjectHint = addressNavigationState &&
typeof addressNavigationState === "object" &&
addressNavigationState.session_context &&
typeof addressNavigationState.session_context === "object" &&
addressNavigationState.session_context.active_focus_object &&
typeof addressNavigationState.session_context.active_focus_object === "object"
? addressNavigationState.session_context.active_focus_object
: null;
const hasNavigationInventoryItemFocusHint = Boolean(toNonEmptyString(navigationFocusObjectHint?.label) &&
toNonEmptyString(navigationFocusObjectHint?.object_type) === "item" &&
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
isInventorySelectedObjectIntent(sourceIntentHint)));
let inventoryShortFollowupPrimary = (isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
hasShortInventoryObjectFollowupSignal(userMessage);
let inventoryShortFollowupAlternate = (isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) && toNonEmptyString(alternateMessage)
? hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""))
: false;
const debtRoleSwapPrimary = sourceIntentHint ? resolveDebtRoleSwapFollowupIntent(userMessage, sourceIntentHint) : null;
const debtRoleSwapAlternate = sourceIntentHint && toNonEmptyString(alternateMessage)
? resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
: null;
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
let hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) || Boolean(debtRoleSwapPrimary) || inventoryShortFollowupPrimary;
let hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
? hasAddressFollowupContextSignal(alternateMessage) || Boolean(debtRoleSwapAlternate) || inventoryShortFollowupAlternate
: false;
const hasPrimaryIndexReferenceSignal = extractDisplayedEntityIndexMention(userMessage) !== null;
const hasAlternateIndexReferenceSignal = toNonEmptyString(alternateMessage)
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
: false;
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
const recentInventoryRootFrame = findRecentInventoryRootFrame(items);
const hasInventoryRootTemporalFollowupPrimary = hasInventoryRootTemporalFollowupSignal(userMessage, sourceIntentHint, Boolean(recentInventoryRootFrame));
const hasInventoryRootTemporalFollowupAlternate = toNonEmptyString(alternateMessage)
? hasInventoryRootTemporalFollowupSignal(String(alternateMessage ?? ""), sourceIntentHint, Boolean(recentInventoryRootFrame))
: false;
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation ||
hasImplicitContinuationSignal ||
inventoryShortFollowupPrimary ||
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
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 &&
!hasPrimaryFollowupSignal &&
!hasAlternateFollowupSignal &&
!hasInventoryRootTemporalFollowupPrimary &&
!hasInventoryRootTemporalFollowupAlternate &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
return null;
}
if (!hasPrimaryFollowupSignal &&
!hasAlternateFollowupSignal &&
!hasInventoryRootTemporalFollowupPrimary &&
!hasInventoryRootTemporalFollowupAlternate &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
return null;
}
if (!previousAddressDebug) {
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) {
previousIntent = debtRoleSwapIntent;
}
if (hasImplicitContinuationSignal) {
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
? toNonEmptyString(followupOffer.suggested_intents[0])
: null;
const keepPreviousIntent = shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent);
if (suggestedIntent && !keepPreviousIntent) {
previousIntent = suggestedIntent;
followupSelectionMode = "switch_to_suggested_intent";
}
}
let previousAnchorType = toNonEmptyString(previousAddressDebug.anchor_type);
let previousAnchor = toNonEmptyString(previousAddressDebug.anchor_value_resolved) ??
toNonEmptyString(previousAddressDebug.anchor_value_raw) ??
readAddressFilterString(previousAddressDebug, "item") ??
readAddressFilterString(previousAddressDebug, "counterparty") ??
readAddressFilterString(previousAddressDebug, "account") ??
readAddressFilterString(previousAddressDebug, "contract");
const navigationSessionContext = addressNavigationState && typeof addressNavigationState === "object"
? (addressNavigationState.session_context && typeof addressNavigationState.session_context === "object"
? addressNavigationState.session_context
: null)
: null;
const navigationDateScope = navigationSessionContext && typeof navigationSessionContext.date_scope === "object"
? navigationSessionContext.date_scope
: null;
const navigationOrganization = normalizeOrganizationScopeValue(navigationSessionContext?.organization_scope);
const navigationFocusObject = navigationSessionContext && typeof navigationSessionContext.active_focus_object === "object"
? navigationSessionContext.active_focus_object
: null;
const navigationFocusObjectType = toNonEmptyString(navigationFocusObject?.object_type);
const navigationFocusObjectLabel = toNonEmptyString(navigationFocusObject?.label);
const hasInventoryItemFocusCarryover = navigationFocusObjectType === "item" &&
navigationFocusObjectLabel &&
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
isInventorySelectedObjectIntent(sourceIntentHint));
if (!inventoryShortFollowupPrimary && hasInventoryItemFocusCarryover) {
inventoryShortFollowupPrimary = hasShortInventoryObjectFollowupSignal(userMessage);
}
if (!inventoryShortFollowupAlternate && hasInventoryItemFocusCarryover && toNonEmptyString(alternateMessage)) {
inventoryShortFollowupAlternate = hasShortInventoryObjectFollowupSignal(String(alternateMessage ?? ""));
}
hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage) ||
Boolean(debtRoleSwapPrimary) ||
inventoryShortFollowupPrimary ||
hasInventoryRootTemporalFollowupPrimary;
hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
? hasAddressFollowupContextSignal(alternateMessage) ||
Boolean(debtRoleSwapAlternate) ||
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupAlternate
: false;
hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation ||
hasImplicitContinuationSignal ||
inventoryShortFollowupPrimary ||
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
Boolean(debtRoleSwapIntent) ||
hasFollowupMarker(userMessage) ||
hasReferentialPointer(userMessage) ||
(toNonEmptyString(alternateMessage)
? hasFollowupMarker(String(alternateMessage ?? "")) || hasReferentialPointer(String(alternateMessage ?? ""))
: false);
const hasSelectedObjectInventorySignalPrimary = /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(userMessage ?? ""));
const hasSelectedObjectInventorySignalAlternate = toNonEmptyString(alternateMessage)
? /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(alternateMessage ?? ""))
: false;
let inventoryRootFrame = findRecentInventoryRootFrame(items);
if (inventoryRootFrame && navigationOrganization && !toNonEmptyString(inventoryRootFrame.filters?.organization)) {
inventoryRootFrame = {
...inventoryRootFrame,
filters: {
...(inventoryRootFrame.filters ?? {}),
organization: navigationOrganization
}
};
}
if (inventoryRootFrame && navigationDateScope) {
inventoryRootFrame = {
...inventoryRootFrame,
filters: {
...(inventoryRootFrame.filters ?? {}),
as_of_date: toNonEmptyString(inventoryRootFrame.filters?.as_of_date) ?? toNonEmptyString(navigationDateScope.as_of_date) ?? undefined,
period_from: toNonEmptyString(inventoryRootFrame.filters?.period_from) ?? toNonEmptyString(navigationDateScope.period_from) ?? undefined,
period_to: toNonEmptyString(inventoryRootFrame.filters?.period_to) ?? toNonEmptyString(navigationDateScope.period_to) ?? undefined
}
};
}
let currentFrameKind = inventoryRootFrame
? isInventoryDrilldownFrameIntent(sourceIntent)
? "inventory_drilldown"
: isInventoryRootFrameIntent(sourceIntent)
? "inventory_root"
: "generic"
: null;
let resolvedCounterpartyFromDisplay = false;
const previousFiltersRaw = previousAddressDebug.extracted_filters;
let previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
? { ...previousFiltersRaw }
: {};
const shouldBackfillHistoricalPartyAnchors =
sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
sourceIntentHint === "bank_operations_by_counterparty" ||
sourceIntentHint === "list_documents_by_contract" ||
sourceIntentHint === "bank_operations_by_contract" ||
sourceIntentHint === "open_items_by_counterparty_or_contract";
if (shouldBackfillHistoricalPartyAnchors && !toNonEmptyString(previousFilters.contract)) {
const historicalContract = findRecentAddressFilterValue(items, "contract");
if (historicalContract) {
previousFilters.contract = historicalContract;
}
}
if (shouldBackfillHistoricalPartyAnchors && !toNonEmptyString(previousFilters.counterparty)) {
const historicalCounterparty = findRecentAddressFilterValue(items, "counterparty");
if (historicalCounterparty) {
previousFilters.counterparty = historicalCounterparty;
}
}
if (!toNonEmptyString(previousFilters.organization)) {
const historicalOrganization = findRecentAddressFilterValue(items, "organization");
if (historicalOrganization) {
previousFilters.organization = historicalOrganization;
}
}
if (!toNonEmptyString(previousFilters.organization) && navigationOrganization) {
previousFilters.organization = navigationOrganization;
}
if (!toNonEmptyString(previousFilters.organization) && organizationClarificationSelection) {
previousFilters.organization = organizationClarificationSelection;
}
const shouldBackfillPreviousDateScopeFromNavigation = sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
sourceIntentHint === "inventory_sale_trace_for_item" ||
sourceIntentHint === "inventory_profitability_for_item" ||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
sourceIntentHint === "account_balance_snapshot" ||
sourceIntentHint === "documents_forming_balance";
if (shouldBackfillPreviousDateScopeFromNavigation &&
!toNonEmptyString(previousFilters.as_of_date) &&
toNonEmptyString(navigationDateScope?.as_of_date)) {
previousFilters.as_of_date = toNonEmptyString(navigationDateScope?.as_of_date);
}
if (shouldBackfillPreviousDateScopeFromNavigation &&
!toNonEmptyString(previousFilters.period_from) &&
toNonEmptyString(navigationDateScope?.period_from)) {
previousFilters.period_from = toNonEmptyString(navigationDateScope?.period_from);
}
if (shouldBackfillPreviousDateScopeFromNavigation &&
!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));
const inventoryRootTemporalPivot = Boolean(inventoryRootFrame &&
(isInventorySelectedObjectIntent(sourceIntentHint) ||
isInventoryRootFrameIntent(sourceIntentHint) ||
currentFrameKind === "inventory_drilldown" ||
currentFrameKind === "inventory_root") &&
(hasInventoryRootTemporalFollowupPrimary || hasInventoryRootTemporalFollowupAlternate) &&
!hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage));
const rootScopedPivot = rootContextOnlyPivot || inventoryRootTemporalPivot;
if (rootScopedPivot) {
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 && !rootScopedPivot) {
if (resolvedEntityFromFollowup.entityType === "counterparty") {
previousFilters.counterparty = resolvedEntityFromFollowup.value;
previousAnchorType = "counterparty";
previousAnchor = resolvedEntityFromFollowup.value;
resolvedCounterpartyFromDisplay = true;
}
else if (resolvedEntityFromFollowup.entityType === "contract") {
previousFilters.contract = resolvedEntityFromFollowup.value;
previousAnchorType = "contract";
previousAnchor = resolvedEntityFromFollowup.value;
}
else if (resolvedEntityFromFollowup.entityType === "item") {
previousFilters.item = resolvedEntityFromFollowup.value;
previousAnchorType = "item";
previousAnchor = resolvedEntityFromFollowup.value;
}
if (followupSelectionMode !== "switch_to_suggested_intent") {
followupSelectionMode = "carry_referenced_entity";
}
}
if (!rootScopedPivot &&
!toNonEmptyString(previousFilters.item) &&
navigationFocusObjectType === "item" &&
navigationFocusObjectLabel &&
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
sourceIntentHint === "inventory_sale_trace_for_item" ||
sourceIntentHint === "inventory_profitability_for_item" ||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
hasSelectedObjectInventorySignalPrimary ||
hasSelectedObjectInventorySignalAlternate)) {
previousFilters.item = navigationFocusObjectLabel;
if (!previousAnchor) {
previousAnchorType = "item";
previousAnchor = navigationFocusObjectLabel;
}
}
if (organizationClarificationSelection && !previousAnchor) {
previousAnchorType = "organization";
previousAnchor = organizationClarificationSelection;
}
if (inventoryRootFrame && organizationClarificationSelection && !toNonEmptyString(inventoryRootFrame.filters?.organization)) {
inventoryRootFrame = {
...inventoryRootFrame,
filters: {
...(inventoryRootFrame.filters ?? {}),
organization: organizationClarificationSelection
}
};
}
if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) {
return null;
}
const shouldAttachInventoryRootFrame = Boolean(inventoryRootFrame &&
(rootScopedPivot ||
isInventoryRootFrameIntent(sourceIntentHint) ||
isInventorySelectedObjectIntent(sourceIntentHint) ||
hasNavigationInventoryItemFocusHint ||
inventoryShortFollowupPrimary ||
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
hasSelectedObjectInventorySignalPrimary ||
hasSelectedObjectInventorySignalAlternate));
const carryoverTargetIntent = followupSelectionMode === "carry_root_context"
? inventoryRootFrame?.intent ?? explicitIntent ?? previousIntent ?? undefined
: explicitIntent ?? previousIntent ?? undefined;
return {
followupContext: {
previous_intent: previousIntent ?? undefined,
target_intent: carryoverTargetIntent,
previous_filters: previousFilters,
previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor,
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,
root_context_only: rootScopedPivot || undefined,
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,
root_filters: shouldAttachInventoryRootFrame ? inventoryRootFrame?.filters ?? undefined : undefined,
root_anchor_type: shouldAttachInventoryRootFrame ? inventoryRootFrame?.anchorType ?? undefined : undefined,
root_anchor_value: shouldAttachInventoryRootFrame ? inventoryRootFrame?.anchorValue ?? undefined : undefined,
current_frame_kind: shouldAttachInventoryRootFrame ? currentFrameKind ?? undefined : undefined
},
previousAddressIntent: previousIntent,
previousAddressAnchor: previousAnchor,
previousSourceIntent: sourceIntent,
followupSelectionMode,
hasImplicitContinuationSignal
};
return assistantTransitionPolicy.resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage, llmPreDecomposeMeta, addressNavigationState);
}
function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta) {
const sourceMessage = String(userMessage ?? "");
const canonicalMessage = String(effectiveMessage ?? sourceMessage);
const hasFollowupContext = Boolean(carryoverMeta?.followupContext);
const previousIntent = toNonEmptyString(carryoverMeta?.previousSourceIntent) ?? null;
const selectionMode = toNonEmptyString(carryoverMeta?.followupSelectionMode) ?? null;
const rootContextOnly = selectionMode === "carry_root_context";
const explicitIntentRaw = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const explicitIntent = explicitIntentRaw === "unknown" ? null : explicitIntentRaw;
const rootIntent = toNonEmptyString(carryoverMeta?.followupContext?.root_intent) ?? null;
const targetIntent = selectionMode === "switch_to_suggested_intent"
? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null
: rootContextOnly
? rootIntent ?? explicitIntent ?? null
: explicitIntent ?? toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
const rewrittenByPredecompose = compactWhitespace(sourceMessage.toLowerCase()) !== compactWhitespace(canonicalMessage.toLowerCase());
const hasExplicitIntent = Boolean(explicitIntent);
const decision = !hasFollowupContext
? "new_topic"
: selectionMode === "switch_to_suggested_intent"
? "switch_to_suggested"
: "continue_previous";
const reasons = [];
if (hasFollowupContext) {
reasons.push("followup_context_detected");
}
if (hasImplicitContinuationSignal) {
reasons.push("implicit_continuation_by_llm");
}
if (rewrittenByPredecompose) {
reasons.push("effective_message_rewritten_by_predecompose");
}
if (hasExplicitIntent) {
reasons.push("llm_contract_intent_available");
}
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,
effective_message: canonicalMessage,
decision,
decision_reasons: reasons,
followup_context_applied: hasFollowupContext,
previous_intent: previousIntent,
target_intent: targetIntent,
intent_selection_mode: selectionMode,
anchor_type: carryoverMeta?.followupContext?.previous_anchor_type ?? null,
anchor_value: carryoverMeta?.followupContext?.previous_anchor_value ?? null,
implicit_continuation_signal: hasImplicitContinuationSignal
};
return assistantTransitionPolicy.buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta);
}
function isRetryableAddressLimitedResult(addressLane) {
if (!addressLane || !addressLane.handled) {
@@ -4449,6 +4007,10 @@ function hasDataRetrievalRequestSignal(text) {
return assistantLivingModePolicy.hasDataRetrievalRequestSignal(text);
}
function hasOrganizationFactLookupSignal(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
if (/(?:активност[иь]\s+в\s+базе|в\s+базе\s+1с|в\s+1с\s+базе|перв(?:ая|ый|ое)\s+(?:операц|платеж|поступлен|списан|документ)|последн(?:яя|ий|ее)\s+активность|первая\s+активность|activity\s+in\s+1c|first\s+(?:payment|document|activity)|last\s+activity)/iu.test(normalized)) {
return false;
}
return assistantLivingModePolicy.hasOrganizationFactLookupSignal(text);
}
function findLastAssistantLivingChatDebug(items) {
@@ -4756,6 +4318,8 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli
resolveHardMetaMode: assistantMetaFollowupPolicy.resolveHardMetaMode,
isMetaFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isMetaFollowupOverGroundedAnswer,
hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal,
hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: assistantLivingModePolicy.hasOrganizationFactFollowupSignal,
hasAggregateBusinessAnalyticsSignal,
hasStandaloneAddressTopicSignal,
hasOpenContractsAddressSignal,
@@ -5464,154 +5028,27 @@ function applyLivingChatGroundingGuardFromPolicy(input) {
return assistantBoundaryPolicy.applyLivingChatGroundingGuard(input);
}
function buildAssistantDataScopeContractReply(scopeProbe = null) {
const channel = String(scopeProbe?.channel ?? config_1.ASSISTANT_MCP_CHANNEL ?? "default");
const organizations = Array.isArray(scopeProbe?.organizations) ? scopeProbe.organizations : [];
if (organizations.length === 1) {
return [
`Сейчас в активном MCP-канале \`${channel}\` доступна организация: ${organizations[0]}.`,
"Работаю в read-only режиме. Могу сразу показать по этой организации документы, операции, договоры или остатки."
].join(" ");
}
if (organizations.length > 1) {
const preview = organizations.slice(0, 10).join(", ");
return [
`Сейчас в активном MCP-канале \`${channel}\` доступны организации (${organizations.length}): ${preview}.`,
"Работаю в read-only режиме. Скажи, по какой организации смотреть документы/операции."
].join(" ");
}
if (scopeProbe?.status === "unresolved_with_error" && scopeProbe?.error) {
return [
`Не смог прочитать название организации из live MCP-канала \`${channel}\`: ${scopeProbe.error}.`,
"Работаю в read-only режиме и вижу только данные активного контура. Проверь подключение MCP/1С, после этого сразу назову контору."
].join(" ");
}
return [
`Работаю в read-only режиме и вижу только те данные, которые отдает текущий MCP-канал \`${channel}\`.`,
"Словарь компаний не зашит в код: рабочий контур определяется live-подключением.",
"Если подключено несколько баз, для автосписка нужен MCP-метод метаданных (перечень баз/организаций); без него можно анализировать только активный контур запросов."
].join(" ");
return buildAssistantDataScopeContractReplyFromPolicy(scopeProbe);
}
function buildAssistantDataScopeSelectionReply(organization) {
const selected = normalizeOrganizationScopeValue(organization) ?? String(organization ?? "").trim();
return [
`Отлично, фиксирую рабочую организацию: ${selected}.`,
"Дальше буду держать этот контур как активный, пока вы не переключите организацию."
].join(" ");
return buildAssistantDataScopeSelectionReplyFromPolicy(organization);
}
function buildAssistantOrganizationFactBoundaryReply(organization) {
const selected = normalizeOrganizationScopeValue(organization) ?? String(organization ?? "").trim();
if (selected) {
return [
`По организации ${selected} не буду называть дату/возраст без live-подтвержденного источника.`,
"Если нужно, запрошу факт из 1С и верну только подтвержденный ответ."
].join(" ");
}
return [
"Не буду называть дату/возраст организации без live-подтвержденного источника.",
"Сначала получу факт из 1С, потом дам точный ответ."
].join(" ");
return buildAssistantOrganizationFactBoundaryReplyFromPolicy(organization);
}
function buildAssistantOperationalBoundaryReply() {
return [
"Понимаю, что ситуация срочная.",
"Я не могу сам настраивать 1С или менять базу/конфигурацию.",
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/ИТ-админа."
].join(" ");
return buildAssistantOperationalBoundaryReplyFromPolicy();
}
function buildAssistantSafetyRefusalReply() {
return [
"Я не могу помогать с удалением базы или скрытием данных.",
"Если вам угрожает опасность, срочно звоните 112 и следуйте указаниям экстренных служб.",
"По 1С могу дать только безопасные диагностические рекомендации."
].join(" ");
}
function containsCjkChars(text) {
const source = String(text ?? "");
if (!source) {
return false;
}
return /[\u3400-\u9FFF\uF900-\uFAFF]/u.test(source);
}
function containsLetterLikeChars(text) {
const source = String(text ?? "");
if (!source) {
return false;
}
return /[A-Za-z\u0400-\u04FF]/u.test(source);
return buildAssistantSafetyRefusalReplyFromPolicy();
}
function applyLivingChatScriptGuard(chatText, userMessage) {
const source = String(chatText ?? "").trim();
if (!source) {
return {
text: "",
applied: false,
reason: null
};
}
if (!containsCjkChars(source) || containsCjkChars(userMessage)) {
return {
text: source,
applied: false,
reason: null
};
}
const stripped = source
.replace(/[\u3400-\u9FFF\uF900-\uFAFF]+/gu, "")
.replace(/[]/gu, "")
.replace(/\s{2,}/g, " ")
.replace(/\s+([,.!?;:])/g, "$1")
.trim();
if (stripped && containsLetterLikeChars(stripped)) {
return {
text: stripped,
applied: true,
reason: "unexpected_cjk_fragment_stripped"
};
}
return {
text: "Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.",
applied: true,
reason: "unexpected_cjk_fragment_fallback"
};
return applyLivingChatScriptGuardFromPolicy(chatText, userMessage);
}
function applyLivingChatGroundingGuard(input) {
const userMessage = String(input?.userMessage ?? "");
const chatText = String(input?.chatText ?? "").trim();
const organization = toNonEmptyString(input?.organization);
if (!chatText) {
return {
text: chatText,
applied: false,
reason: null
};
}
if (!hasOrganizationFactLookupSignal(userMessage)) {
return {
text: chatText,
applied: false,
reason: null
};
}
if (/(?:не\s+могу|не\s+вижу|после\s+проверки|live|подтвержден)/i.test(chatText)) {
return {
text: chatText,
applied: false,
reason: null
};
}
const hasSpecificUnverifiedFact = /(?:\b\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})\b|\b(?:19|20)\d{2}\b|\b\d+\s+лет\b)/i.test(chatText);
if (!hasSpecificUnverifiedFact) {
return {
text: chatText,
applied: false,
reason: null
};
}
return {
text: buildAssistantOrganizationFactBoundaryReply(organization),
applied: true,
reason: "organization_fact_without_live_source_blocked"
};
return applyLivingChatGroundingGuardFromPolicy(input);
}
export function resolveLivingAssistantModeDecision(input) {
return assistantLivingModePolicy.resolveLivingAssistantModeDecision(input);
@@ -3735,6 +3735,17 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
}
});
it("routes company activity-age wording into lifecycle aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("по Альтернативе Плюс сколько лет активности в базе 1С?", {
activeOrganization: 'ООО "Альтернатива Плюс"'
});
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle");
expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
});
it("routes debt-longevity wording into receivables lane with factual reply", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
@@ -4226,6 +4237,127 @@ describe("address decompose stage follow-up carryover", () => {
expect(result?.baseReasons).toContain("address_followup_context_applied");
});
it("restores inventory root follow-up after company selection for repeated stock request", () => {
const result = runAddressDecomposeStage("тогда покажи остатки на март 2021", {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
counterparty: "Альтернатива Плюс",
period_from: "2021-03-01",
period_to: "2021-03-31",
as_of_date: "2021-03-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Альтернатива Плюс",
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
organization: 'ООО "Альтернатива Плюс"',
counterparty: "Альтернатива Плюс",
period_from: "2021-03-01",
period_to: "2021-03-31",
as_of_date: "2021-03-31"
},
current_frame_kind: "inventory_root"
} as any);
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
expect(result?.filters.extracted_filters.organization).toBe('ООО "Альтернатива Плюс"');
expect(result?.filters.extracted_filters.period_from).toBe("2021-03-01");
expect(result?.filters.extracted_filters.period_to).toBe("2021-03-31");
expect(result?.baseReasons).toContain("address_followup_context_applied");
});
it("composes direct activity-age answer from lifecycle aggregate for focused counterparty", () => {
const reply = composeFactualReply(
"counterparty_activity_lifecycle",
[
{
period: "2019-02-14",
registrator: "CP_CUSTOMER_ACTIVITY_FIRST",
account_dt: "",
account_kt: "",
amount: 4,
analytics: ["Альтернатива Плюс"]
},
{
period: "2019-01-01",
registrator: "CP_CUSTOMER_ACTIVITY_YEAR",
account_dt: "",
account_kt: "",
amount: 4,
analytics: ["Альтернатива Плюс"]
},
{
period: "2020-01-01",
registrator: "CP_CUSTOMER_ACTIVITY_YEAR",
account_dt: "",
account_kt: "",
amount: 7,
analytics: ["Альтернатива Плюс"]
},
{
period: "2024-11-30",
registrator: "CP_CUSTOMER_ACTIVITY",
account_dt: "",
account_kt: "",
amount: 11,
analytics: ["Альтернатива Плюс"]
}
],
{
userMessage: "а по Альтернативе Плюс сколько лет активности в базе 1С?",
counterpartyHint: "Альтернатива Плюс"
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Альтернатива Плюс");
expect(reply.text).toContain("Первая подтвержденная активность");
expect(reply.text).toContain("Последняя подтвержденная активность");
expect(reply.text).toContain("не дата регистрации юрлица");
});
it("composes organization activity-age answer when company name is the selected organization scope", () => {
const reply = composeFactualReply(
"counterparty_activity_lifecycle",
[
{
period: "2018-03-12",
registrator: "CP_CUSTOMER_ACTIVITY_FIRST",
account_dt: "",
account_kt: "",
amount: 3,
analytics: ["Торговый дом Союз"]
},
{
period: "2018-01-01",
registrator: "CP_CUSTOMER_ACTIVITY_YEAR",
account_dt: "",
account_kt: "",
amount: 3,
analytics: ["Торговый дом Союз"]
},
{
period: "2024-12-20",
registrator: "CP_CUSTOMER_ACTIVITY",
account_dt: "",
account_kt: "",
amount: 9,
analytics: ["МебельТорг"]
}
],
{
userMessage: "а по Альтернативе Плюс сколько лет активности в базе 1С?",
counterpartyHint: "Альтернатива Плюс",
organizationHint: 'ООО "Альтернатива Плюс"'
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain('По активности организации ООО "Альтернатива Плюс"');
expect(reply.text).toContain("Первая подтвержденная активность");
expect(reply.text).toContain("Последняя подтвержденная активность");
expect(reply.text).toContain("не дата регистрации юрлица");
});
it("keeps short period follow-up in address lane and preserves previous counterparty anchor", () => {
const result = runAddressDecomposeStage("а теперь только за май 2020", {
previous_intent: "list_documents_by_counterparty",
@@ -285,6 +285,117 @@ describe("assistant orchestration contract", () => {
expect(decision.orchestrationContract?.followup_context_detected).toBe(true);
});
it("blocks organization age lookup from re-entering inventory lane on follow-up context", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "а какой возраст у Альтернативы Плюс?",
effectiveAddressUserMessage: "Какой возраст у контрагента Альтернатива Плюс?",
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: "ООО \"Альтернатива Плюс\"",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
previous_anchor_type: "item",
previous_anchor_value: "Модуль прямоугольный 1400*110*750"
},
llmPreDecomposeMeta: {
applied: true,
reason: "normalized_fragment_applied",
predecomposeContract: buildAddressLlmPredecomposeContractV1({
sourceMessage: "а какой возраст у Альтернативы Плюс?",
canonicalMessage: "Какой возраст у контрагента Альтернатива Плюс?",
mode: "address_query",
modeConfidence: "high",
intent: "unknown",
intentConfidence: "low",
entities: {
counterparty: "Альтернатива Плюс"
}
}),
semanticExtractionContract: buildAddressSemanticExtractionContractV1({
valid: true,
applyCanonicalRecommended: true
}),
llmCanonicalCandidateDetected: true
} as any,
useMock: false
} as any);
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateDecision).toBe("skip_address_lane");
expect(decision.toolGateReason).toBe("organization_fact_lookup_signal_detected");
expect(decision.livingMode).toBe("chat");
expect(decision.livingReason).toBe("organization_fact_lookup_signal_detected");
});
it("keeps organization activity-age lookup in address lane when the question is about 1C evidence", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "а по Альтернативе Плюс сколько лет активности в базе 1С?",
effectiveAddressUserMessage: "а по Альтернативе Плюс сколько лет активности в базе 1С?",
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
counterparty: "Альтернатива Плюс",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Альтернатива Плюс"
},
llmPreDecomposeMeta: null,
useMock: false
} as any);
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(["address_intent_resolver_detected", "address_mode_classifier_detected", "followup_context_detected"]).toContain(
String(decision.toolGateReason)
);
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("keeps inventory root restatement after company selection in address lane", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "тогда покажи остатки на март 2021",
effectiveAddressUserMessage: "тогда покажи остатки на март 2021",
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
counterparty: "Альтернатива Плюс",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Альтернатива Плюс",
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
organization: 'ООО "Альтернатива Плюс"',
counterparty: "Альтернатива Плюс",
as_of_date: "2021-03-31",
period_from: "2021-03-01",
period_to: "2021-03-31"
},
current_frame_kind: "inventory_root",
root_context_only: true
},
llmPreDecomposeMeta: null,
useMock: false
} as any);
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(["followup_context_detected", "address_mode_classifier_detected"]).toContain(String(decision.toolGateReason));
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("keeps VAT payable forecast query in address lane", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "какой прогноз оплаты ндс за 12 мая 2020",
@@ -62,6 +62,8 @@ function buildPolicy(overrides: Record<string, unknown> = {}) {
: null,
isMetaFollowupOverGroundedAnswer: () => false,
hasDataRetrievalRequestSignal: () => false,
hasOrganizationFactLookupSignal: () => false,
hasOrganizationFactFollowupSignal: () => false,
hasAggregateBusinessAnalyticsSignal: () => false,
hasStandaloneAddressTopicSignal: () => false,
hasOpenContractsAddressSignal: () => false,
@@ -187,6 +189,43 @@ describe("assistantRoutePolicy", () => {
expect(decision.livingReason).toBe("memory_recap_followup_detected");
});
it("routes organization fact lookup away from address lane even with follow-up context", () => {
const policy = buildPolicy({
hasDataRetrievalRequestSignal: () => true,
hasStrongDataIntentSignal: () => true,
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "low" }),
hasOrganizationFactLookupSignal: (text: unknown) =>
/возраст.*альтернатива плюс/i.test(String(text ?? "")),
resolveAddressToolGateDecision: () => ({
runAddressLane: true,
decision: "run_address_lane",
reason: "address_mode_classifier_detected"
})
});
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: "а какой возраст у Альтернативы Плюс?",
effectiveAddressUserMessage: "Какой возраст у контрагента Альтернатива Плюс?",
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: "ООО \"Альтернатива Плюс\"",
as_of_date: "2021-03-31"
},
previous_anchor_type: "item",
previous_anchor_value: "Модуль прямоугольный 1400*110*750"
},
llmPreDecomposeMeta: null,
useMock: false
});
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateReason).toBe("organization_fact_lookup_signal_detected");
expect(decision.livingMode).toBe("chat");
expect(decision.livingReason).toBe("organization_fact_lookup_signal_detected");
});
it("routes explicit recap wording with selected-object phrasing to chat even when address-like cues exist", () => {
const policy = buildPolicy({
hasStrongDataIntentSignal: () => true,