АРЧ АП11 - Оркестрация: дожать агентный прогон по выбору компании и возрасту активности в 1С
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user