363 lines
20 KiB
JavaScript
363 lines
20 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.buildAssistantAddressOrchestrationRuntime = buildAssistantAddressOrchestrationRuntime;
|
||
const assistantRoutePolicyRuntimeAdapter_1 = require("./assistantRoutePolicyRuntimeAdapter");
|
||
const assistantMcpDiscoveryRuntimeEntryPoint_1 = require("./assistantMcpDiscoveryRuntimeEntryPoint");
|
||
const inventoryLifecycleCueHelpers_1 = require("./inventoryLifecycleCueHelpers");
|
||
function toRecordObject(value) {
|
||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||
return null;
|
||
}
|
||
return value;
|
||
}
|
||
function sessionOrganizationName(sessionOrganizationScope, toNonEmptyString) {
|
||
const scope = toRecordObject(sessionOrganizationScope);
|
||
return toNonEmptyString(scope?.selectedOrganization) ?? toNonEmptyString(scope?.activeOrganization);
|
||
}
|
||
function sessionKnownOrganizations(sessionOrganizationScope) {
|
||
const scope = toRecordObject(sessionOrganizationScope);
|
||
const knownOrganizations = scope?.knownOrganizations;
|
||
return Array.isArray(knownOrganizations) ? knownOrganizations : [];
|
||
}
|
||
function predecomposeOrganizationName(predecomposeContract, toNonEmptyString) {
|
||
const entities = toRecordObject(predecomposeContract?.entities);
|
||
return (toNonEmptyString(entities?.organization) ??
|
||
toNonEmptyString(predecomposeContract?.organization));
|
||
}
|
||
function mergeOrganizationIntoDiscoveryFollowupContext(followupContext, organization) {
|
||
if (!organization) {
|
||
return followupContext;
|
||
}
|
||
const base = followupContext ? { ...followupContext } : {};
|
||
const previousFilters = toRecordObject(base.previous_filters)
|
||
? { ...base.previous_filters }
|
||
: {};
|
||
if (!previousFilters.organization) {
|
||
previousFilters.organization = organization;
|
||
}
|
||
base.previous_filters = previousFilters;
|
||
const rootFilters = toRecordObject(base.root_filters)
|
||
? { ...base.root_filters }
|
||
: {};
|
||
if (!rootFilters.organization) {
|
||
rootFilters.organization = organization;
|
||
}
|
||
base.root_filters = rootFilters;
|
||
if (!base.previous_anchor_type) {
|
||
base.previous_anchor_type = "organization";
|
||
}
|
||
if (!base.previous_anchor_value) {
|
||
base.previous_anchor_value = organization;
|
||
}
|
||
return base;
|
||
}
|
||
function compactLower(value) {
|
||
return String(value ?? "")
|
||
.toLowerCase()
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
}
|
||
function hasCompactCashflowFollowupSignal(text) {
|
||
const value = compactLower(text);
|
||
if (!value) {
|
||
return false;
|
||
}
|
||
return /(?:\u0431\u0435\u0437\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0431\u0438\u0432\p{L}*|\u0431\u0435\u0437\s+\u0440\u0430\u0437\u0440\u0435\u0437\p{L}*|\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b\p{L}*|\u043e\u0434\u043d\u043e\u0439\s+\u0441\u0442\u0440\u043e\u043a\p{L}*|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0438\u0442\u043e\u0433|\u043f\u0440\u0438\u0448\p{L}*[\s\S]{0,80}\u0443\u0448\p{L}*[\s\S]{0,80}\u043d\u0435\u0442\u0442\u043e|\u043f\u043e\u043b\u0443\u0447\p{L}*[\s\S]{0,80}\u0437\u0430\u043f\u043b\u0430\u0442\p{L}*[\s\S]{0,80}\u0431\u0435\u0437\s+\u0434\u0435\u0442\u0430\u043b)/iu.test(value);
|
||
}
|
||
function dateScopeToFilterWindow(dateScope) {
|
||
if (!dateScope) {
|
||
return null;
|
||
}
|
||
if (/^(?:19|20)\d{2}$/.test(dateScope)) {
|
||
return {
|
||
period_from: `${dateScope}-01-01`,
|
||
period_to: `${dateScope}-12-31`
|
||
};
|
||
}
|
||
if (/^(?:19|20)\d{2}-\d{2}-\d{2}$/.test(dateScope)) {
|
||
return { as_of_date: dateScope };
|
||
}
|
||
return null;
|
||
}
|
||
function looksLikeReliableOrganizationScope(value) {
|
||
const text = compactLower(value);
|
||
if (!text) {
|
||
return false;
|
||
}
|
||
if (/(?:\u0440\u0430\u0437\u0431\u0438\u0432|\u0440\u0430\u0437\u0440\u0435\u0437|\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043d\u0435\u0442\u0442\u043e|\u0442\u043e\u043f)/iu.test(text)) {
|
||
return false;
|
||
}
|
||
return /(?:^|[\s"'\u00ab])(?:\u043e\u043e\u043e|\u0438\u043f|\u043f\u0430\u043e|\u0430\u043e|\u0437\u0430\u043e)(?:[\s"'\u00bb]|$)|\u043e\u0431\u0449\u0435\u0441\u0442\u0432\p{L}*\s+\u0441\s+\u043e\u0433\u0440\u0430\u043d\u0438\u0447\p{L}*\s+\u043e\u0442\u0432\p{L}*|\b(?:llc|inc|corp)\b/iu.test(text);
|
||
}
|
||
function inferBusinessOverviewDiscoveryContextFromSessionItems(sessionItems, toNonEmptyString) {
|
||
for (let index = sessionItems.length - 1; index >= 0; index -= 1) {
|
||
const item = toRecordObject(sessionItems[index]);
|
||
if (toNonEmptyString(item?.role) !== "assistant") {
|
||
continue;
|
||
}
|
||
const debug = toRecordObject(item?.debug);
|
||
const entryPoint = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
|
||
const turnInput = toRecordObject(entryPoint?.turn_input);
|
||
const dataNeedGraph = toRecordObject(turnInput?.data_need_graph);
|
||
if (toNonEmptyString(dataNeedGraph?.business_fact_family) !== "business_overview") {
|
||
continue;
|
||
}
|
||
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
|
||
const filterWindow = dateScopeToFilterWindow(toNonEmptyString(turnMeaningRef?.explicit_date_scope));
|
||
if (!filterWindow) {
|
||
continue;
|
||
}
|
||
const previousFilters = { ...filterWindow };
|
||
const organization = toNonEmptyString(turnMeaningRef?.explicit_organization_scope);
|
||
if (looksLikeReliableOrganizationScope(organization)) {
|
||
previousFilters.organization = organization;
|
||
}
|
||
const rankingNeed = toNonEmptyString(dataNeedGraph?.ranking_need);
|
||
return {
|
||
previous_discovery_pilot_scope: "business_overview_route_template_v1",
|
||
previous_filters: previousFilters,
|
||
root_filters: { ...previousFilters },
|
||
previous_seeded_ranking_need: rankingNeed,
|
||
previous_intent: "business_overview",
|
||
root_intent: "business_overview"
|
||
};
|
||
}
|
||
return null;
|
||
}
|
||
function mergeBusinessOverviewDateContextForCompactCashflow(input) {
|
||
if (!hasCompactCashflowFollowupSignal(input.userMessage)) {
|
||
return input.followupContext;
|
||
}
|
||
const existingFilters = toRecordObject(input.followupContext?.previous_filters);
|
||
if (input.toNonEmptyString(existingFilters?.period_from) ||
|
||
input.toNonEmptyString(existingFilters?.period_to) ||
|
||
input.toNonEmptyString(existingFilters?.as_of_date)) {
|
||
return input.followupContext;
|
||
}
|
||
const inferred = inferBusinessOverviewDiscoveryContextFromSessionItems(input.sessionItems, input.toNonEmptyString);
|
||
if (!inferred) {
|
||
return input.followupContext;
|
||
}
|
||
return {
|
||
...inferred,
|
||
...(input.followupContext ?? {}),
|
||
previous_filters: {
|
||
...(toRecordObject(inferred.previous_filters) ?? {}),
|
||
...(toRecordObject(input.followupContext?.previous_filters) ?? {})
|
||
},
|
||
root_filters: {
|
||
...(toRecordObject(inferred.root_filters) ?? {}),
|
||
...(toRecordObject(input.followupContext?.root_filters) ?? {})
|
||
}
|
||
};
|
||
}
|
||
function hasSelectedObjectInventorySignal(text) {
|
||
return /(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ним|selected\s+object)/iu.test(String(text ?? ""));
|
||
}
|
||
function hasSelectedObjectInventoryActionCue(text) {
|
||
const value = String(text ?? "");
|
||
return (/(?:кому[\s\S]{0,80}(?:продал[аи]?|реализова[нлт][а-я]*|поставил[аи]?|поставлен[а-я]*|отгрузил[аи]?|отгружен[а-я]*)|кому\s+был\s+продан|куда[\s\S]{0,80}(?:продал[аи]?|реализова[нлт][а-я]*|поставил[аи]?|поставлен[а-я]*|отгрузил[аи]?|отгружен[а-я]*)|кто[\s\S]{0,40}купил|кто\s+это\s+поставил|кто\s+поставил|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+мы\s+купили|где\s+куплено|по\s+каким\s+документам|какими\s+документами|покажи\s+документы|документы[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|документы\s+закупки|buyer|sale\s+trace|supplier|vendor|purchase\s+documents|purchase[\s-]?to[\s-]?sale|old\s+purchase|aged\s+stock)/iu.test(value) || (0, inventoryLifecycleCueHelpers_1.hasInventoryProfitabilityCue)(value));
|
||
}
|
||
function hasShortInventoryPurchaseFollowupCue(text) {
|
||
return /(?:^|[\s,.;:!?])(а\s+)?(?:купили\s+у\s+кого|у\s+кого\s+купили|поставщик|продавец|seller)(?:[\s,.;:!?]|$)/iu.test(String(text ?? ""));
|
||
}
|
||
function isInventorySelectedObjectOrRootIntent(intent) {
|
||
return (intent === "inventory_on_hand_as_of_date" ||
|
||
intent === "inventory_purchase_provenance_for_item" ||
|
||
intent === "inventory_purchase_documents_for_item" ||
|
||
intent === "inventory_sale_trace_for_item" ||
|
||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||
intent === "inventory_profitability_for_item" ||
|
||
intent === "inventory_purchase_to_sale_chain" ||
|
||
intent === "inventory_aging_by_purchase_date");
|
||
}
|
||
function isGenericCanonicalDriftIntent(intent) {
|
||
return (intent === "open_items_by_counterparty_or_contract" ||
|
||
intent === "customer_revenue_and_payments" ||
|
||
intent === "list_documents_by_counterparty" ||
|
||
intent === "list_documents_by_contract" ||
|
||
intent === "bank_operations_by_counterparty" ||
|
||
intent === "bank_operations_by_contract" ||
|
||
intent === "documents_forming_balance");
|
||
}
|
||
function hasInventoryMarginRankingAccountCorrectionCue(text) {
|
||
const value = String(text ?? "").toLowerCase();
|
||
if (!value.trim()) {
|
||
return false;
|
||
}
|
||
return (/\b41(?:[.,]\d{1,2})?\b/iu.test(value) &&
|
||
/\b01(?:[.,]\d{1,2})?\b/iu.test(value) &&
|
||
/(?:\u0430\s+\u043d\u0435|\u043d\u0435|\u0432\u043c\u0435\u0441\u0442\u043e|not|instead)/iu.test(value));
|
||
}
|
||
function hasSameDateFollowupSignal(text) {
|
||
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? ""));
|
||
}
|
||
function hasExplicitCurrentDateSignal(text) {
|
||
return /(?:текущ(?:ую|ая|ий|ее|ей)\s+дат(?:у|а|е|ой)|сегодняшн(?:юю|ий|ей)\s+дат(?:у|а|е|ой)|today|current\s+date)/iu.test(String(text ?? ""));
|
||
}
|
||
function hasInventoryTemporalRootFollowupCue(text) {
|
||
const value = String(text ?? "").trim().toLowerCase();
|
||
if (!value) {
|
||
return false;
|
||
}
|
||
const tokenCount = value.split(/\s+/).filter(Boolean).length;
|
||
const hasMonthYearCue = /(?:январ(?:ь|е)|феврал(?:ь|е)|март(?:е)?|апрел(?:ь|е)|ма(?:й|е)|июн(?:ь|е)|июл(?:ь|е)|август(?:е)?|сентябр(?:ь|е)|октябр(?:ь|е)|ноябр(?:ь|е)|декабр(?:ь|е))(?:\s+\d{4})?/iu.test(value) || /\b(?:19|20)\d{2}\b/u.test(value);
|
||
if (tokenCount <= 3 && hasMonthYearCue) {
|
||
return true;
|
||
}
|
||
const hasInventoryLexeme = /(?:остат|склад|товар|позици|номенклатур)/iu.test(value);
|
||
return hasInventoryLexeme && (hasMonthYearCue || hasSameDateFollowupSignal(value));
|
||
}
|
||
function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryover, addressPreDecompose, toNonEmptyString) {
|
||
if (!carryover?.followupContext || typeof carryover.followupContext !== "object") {
|
||
return false;
|
||
}
|
||
const rawMessage = toNonEmptyString(userMessage);
|
||
const canonicalMessage = toNonEmptyString(addressInputMessage);
|
||
if (!rawMessage || !canonicalMessage || rawMessage === canonicalMessage) {
|
||
return false;
|
||
}
|
||
const predecomposeContract = addressPreDecompose?.predecomposeContract && typeof addressPreDecompose.predecomposeContract === "object"
|
||
? addressPreDecompose.predecomposeContract
|
||
: null;
|
||
const mode = toNonEmptyString(predecomposeContract?.mode) ?? "unknown";
|
||
const intent = toNonEmptyString(predecomposeContract?.intent) ?? "unknown";
|
||
const followupContext = carryover.followupContext && typeof carryover.followupContext === "object"
|
||
? carryover.followupContext
|
||
: null;
|
||
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
|
||
const rootIntent = toNonEmptyString(followupContext?.root_intent);
|
||
const previousAnchorType = toNonEmptyString(followupContext?.previous_anchor_type);
|
||
const hasReferentialDocumentExclusionFollowupCue = /(?:\u043a\u0440\u043e\u043c\u0435|\u043f\u043e\u043c\u0438\u043c\u043e)\s+(?:\u044d\u0442\u043e\u0433\u043e|\u044d\u0442\u043e\u0439|\u044d\u0442\u043e\u0442|\u044d\u0442\u0443|\u044d\u0442\u0438\u0445)(?:\s+(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430|\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430))?/iu.test(rawMessage);
|
||
const hasInventoryItemCarryover = previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
|
||
const hasInventoryFrameCarryover = isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
||
isInventorySelectedObjectOrRootIntent(rootIntent);
|
||
const hasInventoryMarginRankingCarryover = previousIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||
rootIntent === "inventory_margin_ranking_for_nomenclature";
|
||
const hasInventoryMarginRankingAccountCorrection = hasInventoryMarginRankingCarryover &&
|
||
[rawMessage, canonicalMessage].some((message) => hasInventoryMarginRankingAccountCorrectionCue(message));
|
||
const hasDocumentCarryover = previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
||
if (mode === "unsupported" && intent === "unknown") {
|
||
return true;
|
||
}
|
||
if (hasDocumentCarryover && hasReferentialDocumentExclusionFollowupCue) {
|
||
return true;
|
||
}
|
||
if (hasSameDateFollowupSignal(rawMessage) && hasExplicitCurrentDateSignal(canonicalMessage)) {
|
||
return true;
|
||
}
|
||
if (hasInventoryFrameCarryover &&
|
||
hasInventoryTemporalRootFollowupCue(rawMessage) &&
|
||
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
||
return true;
|
||
}
|
||
if (hasInventoryMarginRankingAccountCorrection &&
|
||
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
||
return true;
|
||
}
|
||
return ((hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
||
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
||
(isGenericCanonicalDriftIntent(intent) || intent === "unknown"));
|
||
}
|
||
function fallbackAddressPreDecompose(userMessage, llmProvider, buildAddressLlmPredecomposeContractV1, sanitizeAddressMessageForFallback) {
|
||
const provider = llmProvider === "local" ? "local" : llmProvider === "openai" ? "openai" : null;
|
||
return {
|
||
attempted: false,
|
||
applied: false,
|
||
provider,
|
||
traceId: null,
|
||
effectiveMessage: userMessage,
|
||
reason: "disabled_by_feature_flag",
|
||
llmCanonicalCandidateDetected: false,
|
||
predecomposeContract: buildAddressLlmPredecomposeContractV1({
|
||
sourceMessage: userMessage,
|
||
canonicalMessage: userMessage
|
||
}),
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage: sanitizeAddressMessageForFallback(userMessage),
|
||
toolGateDecision: null,
|
||
toolGateReason: null
|
||
};
|
||
}
|
||
async function buildAssistantAddressOrchestrationRuntime(input) {
|
||
const initialAddressPreDecompose = input.featureAddressLlmPredecomposeV1
|
||
? await input.runAddressLlmPreDecompose()
|
||
: fallbackAddressPreDecompose(input.userMessage, input.llmProvider, input.buildAddressLlmPredecomposeContractV1, input.sanitizeAddressMessageForFallback);
|
||
let addressPreDecompose = initialAddressPreDecompose;
|
||
let addressInputMessage = input.toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? input.userMessage;
|
||
let carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose, input.sessionAddressNavigationState);
|
||
if (shouldPreferRawFollowupMessage(input.userMessage, addressInputMessage, carryover, addressPreDecompose, input.toNonEmptyString)) {
|
||
addressInputMessage = input.userMessage;
|
||
addressPreDecompose = {
|
||
...addressPreDecompose,
|
||
applied: false,
|
||
effectiveMessage: input.userMessage,
|
||
reason: "followup_raw_message_preferred_over_llm_rewrite",
|
||
predecomposeContract: input.buildAddressLlmPredecomposeContractV1({
|
||
sourceMessage: input.userMessage,
|
||
canonicalMessage: input.userMessage
|
||
})
|
||
};
|
||
carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose, input.sessionAddressNavigationState);
|
||
}
|
||
const followupContext = toRecordObject(carryover?.followupContext);
|
||
const routePolicyRuntime = (0, assistantRoutePolicyRuntimeAdapter_1.runAssistantRoutePolicyRuntime)({
|
||
rawUserMessage: input.userMessage,
|
||
effectiveAddressUserMessage: addressInputMessage,
|
||
followupContext,
|
||
llmPreDecomposeMeta: addressPreDecompose,
|
||
sessionItems: input.sessionItems,
|
||
sessionOrganizationScope: input.sessionOrganizationScope ?? null,
|
||
useMock: input.useMock,
|
||
resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision
|
||
});
|
||
const orchestrationDecision = routePolicyRuntime.orchestrationDecision;
|
||
const orchestrationContract = toRecordObject(orchestrationDecision.orchestrationContract);
|
||
const predecomposeContract = toRecordObject(addressPreDecompose.predecomposeContract);
|
||
const explicitPredecomposeOrganization = predecomposeOrganizationName(predecomposeContract, input.toNonEmptyString);
|
||
const discoveryFollowupContextWithOrganization = mergeOrganizationIntoDiscoveryFollowupContext(followupContext, explicitPredecomposeOrganization
|
||
? null
|
||
: sessionOrganizationName(input.sessionOrganizationScope ?? null, input.toNonEmptyString));
|
||
const discoveryFollowupContext = mergeBusinessOverviewDateContextForCompactCashflow({
|
||
userMessage: input.userMessage,
|
||
followupContext: discoveryFollowupContextWithOrganization,
|
||
sessionItems: input.sessionItems,
|
||
toNonEmptyString: input.toNonEmptyString
|
||
});
|
||
const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose);
|
||
const runDiscoveryEntryPoint = input.runMcpDiscoveryRuntimeEntryPoint ?? assistantMcpDiscoveryRuntimeEntryPoint_1.runAssistantMcpDiscoveryRuntimeEntryPoint;
|
||
let mcpDiscoveryRuntimeEntryPoint = null;
|
||
let mcpDiscoveryRuntimeEntryPointError = null;
|
||
try {
|
||
mcpDiscoveryRuntimeEntryPoint = (await runDiscoveryEntryPoint({
|
||
userMessage: input.userMessage,
|
||
effectiveMessage: addressInputMessage,
|
||
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
|
||
predecomposeContract,
|
||
followupContext: discoveryFollowupContext,
|
||
knownOrganizations: sessionKnownOrganizations(input.sessionOrganizationScope ?? null)
|
||
}));
|
||
}
|
||
catch (error) {
|
||
mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280);
|
||
}
|
||
const addressRuntimeMeta = {
|
||
...addressPreDecompose,
|
||
toolGateDecision: orchestrationDecision.toolGateDecision ?? null,
|
||
toolGateReason: orchestrationDecision.toolGateReason ?? null,
|
||
dialogContinuationContract,
|
||
orchestrationContract: orchestrationContract ?? null,
|
||
routePolicyContract: routePolicyRuntime.routePolicyContract,
|
||
mcpDiscoveryRuntimeEntryPoint,
|
||
mcpDiscoveryRuntimeEntryPointError
|
||
};
|
||
return {
|
||
addressPreDecompose,
|
||
addressInputMessage,
|
||
carryover,
|
||
orchestrationDecision,
|
||
addressRuntimeMeta,
|
||
livingModeDecision: routePolicyRuntime.livingModeDecision
|
||
};
|
||
}
|