5114 lines
238 KiB
JavaScript
5114 lines
238 KiB
JavaScript
"use strict";
|
||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||
if (k2 === undefined) k2 = k;
|
||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||
}
|
||
Object.defineProperty(o, k2, desc);
|
||
}) : (function(o, m, k, k2) {
|
||
if (k2 === undefined) k2 = k;
|
||
o[k2] = m[k];
|
||
}));
|
||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||
}) : function(o, v) {
|
||
o["default"] = v;
|
||
});
|
||
var __importStar = (this && this.__importStar) || (function () {
|
||
var ownKeys = function(o) {
|
||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||
var ar = [];
|
||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||
return ar;
|
||
};
|
||
return ownKeys(o);
|
||
};
|
||
return function (mod) {
|
||
if (mod && mod.__esModule) return mod;
|
||
var result = {};
|
||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||
__setModuleDefault(result, mod);
|
||
return result;
|
||
};
|
||
})();
|
||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||
};
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.AssistantService = void 0;
|
||
exports.evaluateCoverageForTests = evaluateCoverageForTests;
|
||
exports.extractSubjectTokensForTests = extractSubjectTokensForTests;
|
||
exports.resolveAssistantOrchestrationDecision = resolveAssistantOrchestrationDecision;
|
||
exports.resolveSessionOrganizationScopeContextForTests = resolveSessionOrganizationScopeContextForTests;
|
||
exports.buildRootScopedCarryoverFiltersForTests = buildRootScopedCarryoverFiltersForTests;
|
||
exports.extractOrganizationFactsFromRowsForTests = extractOrganizationFactsFromRowsForTests;
|
||
exports.resolveOrganizationNamesByRefsForTests = resolveOrganizationNamesByRefsForTests;
|
||
exports.resolveLivingAssistantModeDecision = resolveLivingAssistantModeDecision;
|
||
// @ts-nocheck
|
||
const nanoid_1 = __importStar(require("nanoid"));
|
||
const config_1 = __importStar(require("../config"));
|
||
const log_1 = __importStar(require("../utils/log"));
|
||
const answerComposer_1 = __importStar(require("./answerComposer"));
|
||
const assistantDataLayer_1 = __importStar(require("./assistantDataLayer"));
|
||
const assistantSessionLogger_1 = __importStar(require("./assistantSessionLogger"));
|
||
const investigationState_1 = __importStar(require("./investigationState"));
|
||
const retrievalResultNormalizer_1 = __importStar(require("./retrievalResultNormalizer"));
|
||
const addressQueryService_1 = __importStar(require("./addressQueryService"));
|
||
const addressQueryClassifier_1 = __importStar(require("./addressQueryClassifier"));
|
||
const addressIntentResolver_1 = __importStar(require("./addressIntentResolver"));
|
||
const addressFilterExtractor_1 = __importStar(require("./addressFilterExtractor"));
|
||
const decomposeStage_1 = __importStar(require("./address_runtime/decomposeStage"));
|
||
const predecomposeContract_1 = __importStar(require("./address_runtime/predecomposeContract"));
|
||
const openaiResponsesClient_1 = __importStar(require("./openaiResponsesClient"));
|
||
const addressMcpClient_1 = __importStar(require("./addressMcpClient"));
|
||
const capabilitiesRegistry_1 = __importStar(require("./capabilitiesRegistry"));
|
||
const assistantCanon_1 = __importStar(require("./assistantCanon"));
|
||
const assistantAddressAttemptRuntimeAdapter_1 = __importStar(require("./assistantAddressAttemptRuntimeAdapter"));
|
||
const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding"));
|
||
const assistantDataScopePolicy_1 = __importStar(require("./assistantDataScopePolicy"));
|
||
const assistantDebugPayloadAssembler_1 = __importStar(require("./assistantDebugPayloadAssembler"));
|
||
const assistantDeepTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAttemptRuntimeAdapter"));
|
||
const assistantBoundaryPolicy_1 = __importStar(require("./assistantBoundaryPolicy"));
|
||
const assistantLivingModePolicy_1 = __importStar(require("./assistantLivingModePolicy"));
|
||
const assistantMetaFollowupPolicy_1 = __importStar(require("./assistantMetaFollowupPolicy"));
|
||
const assistantMemoryRecapPolicy_1 = __importStar(require("./assistantMemoryRecapPolicy"));
|
||
const assistantProviderExecutionPolicy_1 = __importStar(require("./assistantProviderExecutionPolicy"));
|
||
const assistantRoutePolicy_1 = __importStar(require("./assistantRoutePolicy"));
|
||
const assistantTransitionPolicy_1 = __importStar(require("./assistantTransitionPolicy"));
|
||
const assistantOrganizationScopeRuntimeAdapter_1 = __importStar(require("./assistantOrganizationScopeRuntimeAdapter"));
|
||
const assistantOrganizationMatcher_1 = __importStar(require("./assistantOrganizationMatcher"));
|
||
const assistantTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantTurnAttemptRuntimeAdapter"));
|
||
const assistantTurnRuntimeDepsAdapter_1 = __importStar(require("./assistantTurnRuntimeDepsAdapter"));
|
||
const assistantTurnRuntimeInputBuilder_1 = __importStar(require("./assistantTurnRuntimeInputBuilder"));
|
||
const assistantUserTurnBootstrapRuntimeAdapter_1 = __importStar(require("./assistantUserTurnBootstrapRuntimeAdapter"));
|
||
const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning"));
|
||
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
||
const normalizeOrganizationScopeValue = assistantOrganizationMatcher_1.normalizeOrganizationScopeValue;
|
||
function retrievalSummaryForRoute(route) {
|
||
if (route === "store_canonical")
|
||
return "Canonical accounting data path selected.";
|
||
if (route === "store_feature_risk")
|
||
return "Risk/control profile path selected.";
|
||
if (route === "hybrid_store_plus_live")
|
||
return "Hybrid chain analysis path selected.";
|
||
if (route === "live_mcp_drilldown")
|
||
return "Live drilldown path selected.";
|
||
if (route === "batch_refresh_then_store")
|
||
return "Heavy analytical batch path selected.";
|
||
return "Route selected.";
|
||
}
|
||
function mapNoRouteReason(reason) {
|
||
if (reason === "out_of_scope")
|
||
return "Fragment out of scope.";
|
||
if (reason === "insufficient_specificity")
|
||
return "Needs clarification.";
|
||
if (reason === "missing_mapping")
|
||
return "Route mapping is missing.";
|
||
if (reason === "unsupported_fragment_type")
|
||
return "Fragment type unsupported.";
|
||
return "No-route decision.";
|
||
}
|
||
function extractFragments(normalized) {
|
||
if (!normalized || typeof normalized !== "object") {
|
||
return [];
|
||
}
|
||
const source = normalized;
|
||
return Array.isArray(source.fragments) ? source.fragments : [];
|
||
}
|
||
function hasExplicitPeriodAnchorFromNormalized(normalized) {
|
||
const fragments = extractFragments(normalized);
|
||
const explicitPeriodPattern = /(?:\b20\d{2}(?:[-./](?:0?[1-9]|1[0-2]))?(?:[-./](?:0?[1-9]|[12]\d|3[01]))?\b|\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b|\b(?:январ[ьяе]|феврал[ьяе]|март[ае]?|апрел[ьяе]|ма[йея]|июн[ьяе]|июл[ьяе]|август[ае]?|сентябр[ьяе]|октябр[ьяе]|ноябр[ьяе]|декабр[ьяе]|january|february|march|april|may|june|july|august|september|october|november|december)\b)/i;
|
||
for (const item of fragments) {
|
||
if (!item || typeof item !== "object") {
|
||
continue;
|
||
}
|
||
const fragment = item;
|
||
const timeScope = fragment.time_scope && typeof fragment.time_scope === "object" ? fragment.time_scope : null;
|
||
if (timeScope) {
|
||
const type = String(timeScope.type ?? "").trim().toLowerCase();
|
||
const value = String(timeScope.value ?? "").trim();
|
||
const confidence = String(timeScope.confidence ?? "").trim().toLowerCase();
|
||
if ((type === "explicit" || type === "range") && value.length > 0 && confidence !== "low") {
|
||
return true;
|
||
}
|
||
}
|
||
const rawText = `${typeof fragment.raw_fragment_text === "string" ? fragment.raw_fragment_text : ""} ${typeof fragment.normalized_fragment_text === "string" ? fragment.normalized_fragment_text : ""}`;
|
||
if (explicitPeriodPattern.test(rawText)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
function normalizeIsoDate(value) {
|
||
if (typeof value !== "string") {
|
||
return null;
|
||
}
|
||
const trimmed = value.trim();
|
||
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||
if (!match) {
|
||
return null;
|
||
}
|
||
const year = Number(match[1]);
|
||
const month = Number(match[2]);
|
||
const day = Number(match[3]);
|
||
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||
return null;
|
||
}
|
||
const candidate = new Date(Date.UTC(year, month - 1, day));
|
||
if (candidate.getUTCFullYear() !== year || candidate.getUTCMonth() + 1 !== month || candidate.getUTCDate() !== day) {
|
||
return null;
|
||
}
|
||
return `${match[1]}-${match[2]}-${match[3]}`;
|
||
}
|
||
function resolveRuntimeAnalysisContext(context) {
|
||
const input = context && typeof context === "object" ? context : {};
|
||
const analysis = input.analysis_context && typeof input.analysis_context === "object" ? input.analysis_context : {};
|
||
const asOfDate = normalizeIsoDate(analysis.as_of_date) ?? normalizeIsoDate(input.period_hint);
|
||
const periodFrom = normalizeIsoDate(analysis.period_from);
|
||
const periodTo = normalizeIsoDate(analysis.period_to);
|
||
const sourceRaw = typeof analysis.source === "string" ? analysis.source.trim() : "";
|
||
const source = sourceRaw || (asOfDate ? "period_hint_compat" : null);
|
||
const snapshotModeRaw = typeof analysis.snapshot_mode === "string" ? analysis.snapshot_mode.trim() : "";
|
||
const snapshotMode = snapshotModeRaw === "force_snapshot" || snapshotModeRaw === "force_live" ? snapshotModeRaw : "auto";
|
||
return {
|
||
as_of_date: asOfDate,
|
||
period_from: periodFrom,
|
||
period_to: periodTo,
|
||
source,
|
||
snapshot_mode: snapshotMode,
|
||
active: Boolean(asOfDate || (periodFrom && periodTo))
|
||
};
|
||
}
|
||
function extractExecutionState(normalized) {
|
||
const fragments = extractFragments(normalized);
|
||
return fragments.map((item) => {
|
||
if (!item || typeof item !== "object") {
|
||
return {};
|
||
}
|
||
const fragment = item;
|
||
return {
|
||
fragment_id: fragment.fragment_id ?? null,
|
||
execution_readiness: fragment.execution_readiness ?? null,
|
||
route_status: fragment.route_status ?? null,
|
||
no_route_reason: fragment.no_route_reason ?? null
|
||
};
|
||
});
|
||
}
|
||
function collectBusinessScopesFromNormalized(normalized) {
|
||
const scopes = [];
|
||
for (const item of extractFragments(normalized)) {
|
||
if (!item || typeof item !== "object") {
|
||
continue;
|
||
}
|
||
const scope = String(item.business_scope ?? "").trim();
|
||
if (scope) {
|
||
scopes.push(scope);
|
||
}
|
||
}
|
||
return Array.from(new Set(scopes));
|
||
}
|
||
function hasJuly2020SnapshotSignal(userMessage, companyAnchors) {
|
||
const lower = String(userMessage ?? "").toLowerCase();
|
||
if (/(?:\b2020[-/.]0?7\b|\bиюл[ьяе]?\b(?:\s+20\d{2})?|\bjuly\b(?:\s+20\d{2})?)/i.test(lower)) {
|
||
return true;
|
||
}
|
||
const periods = Array.isArray(companyAnchors?.periods) ? companyAnchors.periods : [];
|
||
const dates = Array.isArray(companyAnchors?.dates) ? companyAnchors.dates : [];
|
||
return [...periods, ...dates].some((item) => /2020[-/.]0?7|июл|july/i.test(String(item ?? "").toLowerCase()));
|
||
}
|
||
function hasP0DomainSignal(userMessage, companyAnchors) {
|
||
if (inferP0DomainFromMessage(userMessage)) {
|
||
return true;
|
||
}
|
||
const accounts = Array.isArray(companyAnchors?.accounts) ? companyAnchors.accounts : [];
|
||
if (accounts.some((item) => /^(?:01|02|08|19|20|21|23|25|26|28|29|44|51|60|62|68|76|97)(?:\.|$)/.test(String(item ?? "").trim()))) {
|
||
return true;
|
||
}
|
||
return /(?:ндс|vat|рбп|deferred|амортиз|supplier|customer|settlement|month\s*close|закрыти[ея]\s+месяц|поставщ|покупат)/i.test(String(userMessage ?? "").toLowerCase());
|
||
}
|
||
function resolveBusinessScopeAlignment(input) {
|
||
const rawScopes = collectBusinessScopesFromNormalized(input.normalized);
|
||
const needsCompanyGrounding = hasJuly2020SnapshotSignal(input.userMessage, input.companyAnchors) && hasP0DomainSignal(input.userMessage, input.companyAnchors);
|
||
const reasons = [];
|
||
if (needsCompanyGrounding) {
|
||
reasons.push("july_2020_snapshot_p0_signal");
|
||
}
|
||
if (!input.routeSummary || input.routeSummary.mode !== "deterministic_v2" || !needsCompanyGrounding) {
|
||
return {
|
||
business_scope_raw: rawScopes,
|
||
business_scope_resolved: rawScopes,
|
||
company_grounding_applied: false,
|
||
scope_resolution_reason: reasons,
|
||
route_summary_resolved: input.routeSummary
|
||
};
|
||
}
|
||
let changed = false;
|
||
const decisions = input.routeSummary.decisions.map((decision) => {
|
||
const scopeValue = String(decision.business_scope ?? "").trim();
|
||
if (scopeValue !== "generic_accounting" && scopeValue !== "unclear") {
|
||
return decision;
|
||
}
|
||
changed = true;
|
||
return {
|
||
...decision,
|
||
business_scope: "company_specific_accounting"
|
||
};
|
||
});
|
||
const resolvedSummary = changed
|
||
? {
|
||
...input.routeSummary,
|
||
decisions
|
||
}
|
||
: input.routeSummary;
|
||
const resolvedScopes = changed
|
||
? Array.from(new Set(decisions.map((decision) => String(decision.business_scope ?? "").trim()).filter(Boolean)))
|
||
: rawScopes;
|
||
return {
|
||
business_scope_raw: rawScopes,
|
||
business_scope_resolved: resolvedScopes,
|
||
company_grounding_applied: changed,
|
||
scope_resolution_reason: changed ? [...reasons, "generic_or_unclear_to_company_specific_override"] : reasons,
|
||
route_summary_resolved: resolvedSummary
|
||
};
|
||
}
|
||
function isJuly2020TemporalResolved(temporalGuard) {
|
||
if (!temporalGuard || typeof temporalGuard !== "object") {
|
||
return false;
|
||
}
|
||
const resolvedAnchor = String(temporalGuard.resolved_time_anchor ?? "").trim();
|
||
if (/^2020-07(?:-\d{2})?$/.test(resolvedAnchor)) {
|
||
return true;
|
||
}
|
||
const effective = temporalGuard.effective_primary_period && typeof temporalGuard.effective_primary_period === "object"
|
||
? temporalGuard.effective_primary_period
|
||
: null;
|
||
if (effective) {
|
||
const from = String(effective.from ?? "").trim();
|
||
const to = String(effective.to ?? "").trim();
|
||
if (/^2020-07-\d{2}$/.test(from) && /^2020-07-\d{2}$/.test(to)) {
|
||
return true;
|
||
}
|
||
}
|
||
const resolvedPrimary = temporalGuard.resolved_primary_period && typeof temporalGuard.resolved_primary_period === "object"
|
||
? temporalGuard.resolved_primary_period
|
||
: null;
|
||
if (!resolvedPrimary) {
|
||
return false;
|
||
}
|
||
const from = String(resolvedPrimary.from ?? "").trim();
|
||
const to = String(resolvedPrimary.to ?? "").trim();
|
||
return /^2020-07-\d{2}$/.test(from) && /^2020-07-\d{2}$/.test(to);
|
||
}
|
||
function hasP0ClaimSignal(claimType, focusDomainHint) {
|
||
const claim = String(claimType ?? "").trim();
|
||
if (claim === "prove_settlement_closure_state" ||
|
||
claim === "prove_advance_offset_state" ||
|
||
claim === "prove_vat_chain_completeness" ||
|
||
claim === "prove_month_close_state" ||
|
||
claim === "prove_rbp_tail_state" ||
|
||
claim === "prove_fixed_asset_amortization_coverage") {
|
||
return true;
|
||
}
|
||
return (focusDomainHint === "settlements_60_62" ||
|
||
focusDomainHint === "vat_document_register_book" ||
|
||
focusDomainHint === "month_close_costs_20_44" ||
|
||
focusDomainHint === "fixed_asset_amortization");
|
||
}
|
||
function hasSettlementScopeSignal(input) {
|
||
const claim = String(input.claimType ?? "").trim();
|
||
const domain = String(input.focusDomainHint ?? "").trim();
|
||
if (claim === "prove_settlement_closure_state" || claim === "prove_advance_offset_state" || domain === "settlements_60_62") {
|
||
return true;
|
||
}
|
||
if (Boolean(input.followupApplied) && domain === "settlements_60_62") {
|
||
return true;
|
||
}
|
||
const lower = String(input.userMessage ?? "").toLowerCase();
|
||
if (/(?:60(?:\\.\\d{2})?|62(?:\\.\\d{2})?|76(?:\\.\\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|хвост|supplier|customer|settlement|payable|receivable|поставщ|покупат)/i.test(lower)) {
|
||
return true;
|
||
}
|
||
const accounts = Array.isArray(input.companyAnchors?.accounts) ? input.companyAnchors.accounts : [];
|
||
return accounts.some((item) => /^(?:51|60|62|76)(?:\\.|$)/.test(String(item ?? "").trim()));
|
||
}
|
||
function resolveBusinessScopeFromLiveContext(input) {
|
||
const current = input.current;
|
||
const routeSummary = current?.route_summary_resolved;
|
||
const julyResolved = isJuly2020TemporalResolved(input.temporalGuard);
|
||
const p0Signal = hasP0ClaimSignal(input.claimType, input.focusDomainHint);
|
||
const settlementScopeSignal = hasSettlementScopeSignal({
|
||
userMessage: input.userMessage,
|
||
companyAnchors: input.companyAnchors,
|
||
claimType: input.claimType,
|
||
focusDomainHint: input.focusDomainHint,
|
||
followupApplied: input.followupApplied
|
||
});
|
||
const shouldRecoverScope = p0Signal && (julyResolved || settlementScopeSignal);
|
||
if (!shouldRecoverScope) {
|
||
return current;
|
||
}
|
||
const reasons = Array.isArray(current.scope_resolution_reason) ? [...current.scope_resolution_reason] : [];
|
||
if (julyResolved && !reasons.includes("temporal_claim_bound_company_scope_recovery")) {
|
||
reasons.push("temporal_claim_bound_company_scope_recovery");
|
||
}
|
||
if (settlementScopeSignal && !reasons.includes("settlement_claim_company_scope_recovery")) {
|
||
reasons.push("settlement_claim_company_scope_recovery");
|
||
}
|
||
const currentScopes = Array.isArray(current.business_scope_resolved) ? current.business_scope_resolved : [];
|
||
let changed = false;
|
||
const normalizedScopes = currentScopes
|
||
.map((item) => String(item ?? "").trim())
|
||
.filter(Boolean)
|
||
.map((item) => {
|
||
if (item === "generic_accounting" || item === "unclear") {
|
||
changed = true;
|
||
return "company_specific_accounting";
|
||
}
|
||
return item;
|
||
});
|
||
if (!normalizedScopes.includes("company_specific_accounting")) {
|
||
normalizedScopes.push("company_specific_accounting");
|
||
changed = true;
|
||
}
|
||
let routeSummaryResolved = routeSummary;
|
||
if (routeSummary && routeSummary.mode === "deterministic_v2" && Array.isArray(routeSummary.decisions)) {
|
||
const decisions = routeSummary.decisions.map((decision) => {
|
||
const scopeValue = String(decision.business_scope ?? "").trim();
|
||
if (scopeValue !== "generic_accounting" && scopeValue !== "unclear") {
|
||
return decision;
|
||
}
|
||
changed = true;
|
||
return {
|
||
...decision,
|
||
business_scope: "company_specific_accounting"
|
||
};
|
||
});
|
||
routeSummaryResolved = changed
|
||
? {
|
||
...routeSummary,
|
||
decisions
|
||
}
|
||
: routeSummary;
|
||
}
|
||
return {
|
||
...current,
|
||
business_scope_resolved: Array.from(new Set(normalizedScopes)),
|
||
company_grounding_applied: current.company_grounding_applied || changed,
|
||
scope_resolution_reason: reasons,
|
||
route_summary_resolved: routeSummaryResolved
|
||
};
|
||
}
|
||
function escapeRegex(value) {
|
||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
}
|
||
function enrichFragmentTextWithHints(fragment, text) {
|
||
const baseText = String(text ?? "").trim();
|
||
const accountHints = Array.isArray(fragment.account_hints)
|
||
? Array.from(new Set(fragment.account_hints
|
||
.map((item) => String(item ?? "").trim())
|
||
.filter((item) => item.length > 0)))
|
||
: [];
|
||
if (accountHints.length === 0) {
|
||
return baseText;
|
||
}
|
||
const hasAccountInText = accountHints.some((account) => new RegExp(`\\b${escapeRegex(account)}\\b`, "i").test(baseText));
|
||
if (hasAccountInText) {
|
||
return baseText;
|
||
}
|
||
return `${baseText}, по счету ${accountHints.join(", ")}`;
|
||
}
|
||
function fragmentTextById(normalized) {
|
||
return (0, assistantQueryPlanning_1.buildFragmentTextById)(extractFragments(normalized));
|
||
}
|
||
function extractDiscardedIntentSegments(normalized) {
|
||
if (!normalized || typeof normalized !== "object") {
|
||
return [];
|
||
}
|
||
const source = normalized;
|
||
if (!Array.isArray(source.discarded_fragments)) {
|
||
return [];
|
||
}
|
||
return source.discarded_fragments
|
||
.map((item) => {
|
||
if (!item || typeof item !== "object") {
|
||
return null;
|
||
}
|
||
const value = item;
|
||
const text = typeof value.raw_fragment_text === "string" ? value.raw_fragment_text.trim() : "";
|
||
return text || null;
|
||
})
|
||
.filter((item) => Boolean(item));
|
||
}
|
||
function collectDateSpans(text) {
|
||
const spans = [];
|
||
const datePatterns = [
|
||
/\b20\d{2}(?:[-/.](?:0?[1-9]|1[0-2]))(?:[-/.](?:0?[1-9]|[12]\d|3[01]))?\b/g,
|
||
/\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b/g,
|
||
/\b(?:0?[1-9]|[12]\d|3[01])\s+(?:январ[ьяе]|феврал[ьяе]|март[ае]?|апрел[ьяе]|ма[йея]|июн[ьяе]?|июл[ьяе]?|август[ае]?|сентябр[ьяе]?|октябр[ьяе]?|ноябр[ьяе]?|декабр[ьяе]?|january|february|march|april|may|june|july|august|september|october|november|december)(?:\s+20\d{2})?\b/giu
|
||
];
|
||
for (const datePattern of datePatterns) {
|
||
let match = null;
|
||
while ((match = datePattern.exec(text)) !== null) {
|
||
spans.push({
|
||
start: match.index,
|
||
end: match.index + match[0].length
|
||
});
|
||
}
|
||
}
|
||
return spans;
|
||
}
|
||
function collectContractSpans(text) {
|
||
const spans = [];
|
||
const contractPatterns = [
|
||
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
|
||
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
|
||
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
|
||
];
|
||
for (const contractPattern of contractPatterns) {
|
||
let match = null;
|
||
while ((match = contractPattern.exec(text)) !== null) {
|
||
spans.push({
|
||
start: match.index,
|
||
end: match.index + match[0].length
|
||
});
|
||
}
|
||
}
|
||
return spans;
|
||
}
|
||
function collectAmountSpans(text) {
|
||
const spans = [];
|
||
const amountPatterns = [/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g, /\b\d+[.,]\d{2}\b/g];
|
||
for (const amountPattern of amountPatterns) {
|
||
let match = null;
|
||
while ((match = amountPattern.exec(text)) !== null) {
|
||
spans.push({
|
||
start: match.index,
|
||
end: match.index + match[0].length
|
||
});
|
||
}
|
||
}
|
||
return spans;
|
||
}
|
||
function collectPercentSpans(text) {
|
||
const spans = [];
|
||
const percentPattern = /\b\d{1,3}(?:[.,]\d+)?\s*%/g;
|
||
let match = null;
|
||
while ((match = percentPattern.exec(text)) !== null) {
|
||
spans.push({
|
||
start: match.index,
|
||
end: match.index + match[0].length
|
||
});
|
||
}
|
||
return spans;
|
||
}
|
||
function intersectsAnySpan(start, end, spans) {
|
||
return spans.some((span) => start < span.end && end > span.start);
|
||
}
|
||
function hasAccountContextAround(text, start, end) {
|
||
const left = text.slice(Math.max(0, start - 28), start);
|
||
const right = text.slice(end, Math.min(text.length, end + 28));
|
||
return /(?:счет|сч\.?|account|schet|оплат|расч[её]т|расчет|аванс|зач[её]т|долг|постав|покуп|supplier|customer|settlement|payment|ндс|vat|проводк|posting)/iu.test(`${left} ${right}`);
|
||
}
|
||
function extractAccountTokens(text) {
|
||
const lower = String(text ?? "").toLowerCase();
|
||
const explicitAccounts = new Set();
|
||
const knownAccountPrefixes = new Set([
|
||
"01",
|
||
"02",
|
||
"07",
|
||
"08",
|
||
"10",
|
||
"13",
|
||
"19",
|
||
"20",
|
||
"21",
|
||
"23",
|
||
"25",
|
||
"26",
|
||
"28",
|
||
"29",
|
||
"41",
|
||
"43",
|
||
"44",
|
||
"45",
|
||
"50",
|
||
"51",
|
||
"52",
|
||
"55",
|
||
"57",
|
||
"58",
|
||
"60",
|
||
"62",
|
||
"66",
|
||
"67",
|
||
"68",
|
||
"69",
|
||
"70",
|
||
"71",
|
||
"73",
|
||
"76",
|
||
"90",
|
||
"91",
|
||
"94",
|
||
"96",
|
||
"97"
|
||
]);
|
||
const contextualPattern = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b)\s*(?:№|#|:)?\s*(\d{2}(?:\.\d{2})?)/giu;
|
||
let contextual = null;
|
||
while ((contextual = contextualPattern.exec(lower)) !== null) {
|
||
if (contextual[1]) {
|
||
const token = String(contextual[1]).trim();
|
||
const prefix = token.match(/^(\d{2})/)?.[1];
|
||
if (prefix && knownAccountPrefixes.has(prefix)) {
|
||
explicitAccounts.add(token);
|
||
}
|
||
}
|
||
}
|
||
const pairPattern = /\b(\d{2}\.\d{2})\s*\/\s*(\d{2}\.\d{2})\b/g;
|
||
let pairMatch = null;
|
||
while ((pairMatch = pairPattern.exec(lower)) !== null) {
|
||
const left = String(pairMatch[1] ?? "").trim();
|
||
const right = String(pairMatch[2] ?? "").trim();
|
||
const leftPrefix = left.match(/^(\d{2})/)?.[1];
|
||
const rightPrefix = right.match(/^(\d{2})/)?.[1];
|
||
if (leftPrefix && knownAccountPrefixes.has(leftPrefix)) {
|
||
explicitAccounts.add(left);
|
||
}
|
||
if (rightPrefix && knownAccountPrefixes.has(rightPrefix)) {
|
||
explicitAccounts.add(right);
|
||
}
|
||
}
|
||
if (explicitAccounts.size > 0) {
|
||
return Array.from(explicitAccounts);
|
||
}
|
||
const contractSpans = collectContractSpans(lower);
|
||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower), ...contractSpans];
|
||
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|аванс|долг|settlement|payment)/iu.test(lower);
|
||
if (!hasAccountingLexeme) {
|
||
return [];
|
||
}
|
||
const accountList = [];
|
||
const genericPattern = /\b\d{2}(?:\.\d{2})?\b/g;
|
||
let generic = null;
|
||
while ((generic = genericPattern.exec(lower)) !== null) {
|
||
const value = generic[0];
|
||
const start = generic.index;
|
||
const end = start + value.length;
|
||
if (intersectsAnySpan(start, end, spans)) {
|
||
continue;
|
||
}
|
||
if (!hasAccountContextAround(lower, start, end)) {
|
||
continue;
|
||
}
|
||
const prefix = value.match(/^(\d{2})/)?.[1];
|
||
if (!prefix || !knownAccountPrefixes.has(prefix)) {
|
||
continue;
|
||
}
|
||
accountList.push(value);
|
||
}
|
||
return Array.from(new Set(accountList));
|
||
}
|
||
function extractSubjectTokens(text) {
|
||
const lower = text.toLowerCase();
|
||
const tokens = [];
|
||
const push = (token, match) => {
|
||
if (match)
|
||
tokens.push(token);
|
||
};
|
||
push("nds", /\b(?:\u043d\u0434\u0441|vat)\b/iu.test(lower));
|
||
push("os", /(?:\b\u043e\u0441\b|\u043e\u0441\u043d\u043e\u0432\u043d(?:\u044b\u0435|\u044b\u0445)\s+\u0441\u0440\u0435\u0434|osnovn(?:ye|yh)\s+sred)/iu.test(lower));
|
||
push("counterparty", /(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|supplier|buyer|counterparty|kontragent|postavshch|pokupatel)/iu.test(lower));
|
||
push("document", /(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|\u0432\u044b\u043f\u0438\u0441\u043a|\u043f\u043b\u0430\u0442\u0435\u0436|document|invoice|posting|dokument|realiz|postuplen|vypisk|platezh)/iu.test(lower));
|
||
push("saldo", /(?:\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0441\u0442\u0430\u0442\u043a|saldo|balance)/iu.test(lower));
|
||
push("anomaly", /(?:\u0430\u043d\u043e\u043c\u0430\u043b|\u0440\u0438\u0441\u043a|\u043f\u043e\u0434\u043e\u0437\u0440|\u0445\u0432\u043e\u0441\u0442|anomaly|risk|anomali|hvost|tail)/iu.test(lower));
|
||
push("chain", /(?:\u0446\u0435\u043f\u043e\u0447|\u0440\u0430\u0437\u043b\u043e\u0436|\u0441\u0432\u044f\u0437\u043a|\u0440\u0430\u0437\u0440\u044b\u0432|chain|razlozh|svyaz|razryv)/iu.test(lower));
|
||
const accountMatches = extractAccountTokens(lower);
|
||
for (const account of accountMatches) {
|
||
tokens.push(`account_${account}`);
|
||
}
|
||
return Array.from(new Set(tokens));
|
||
}
|
||
function extractRequirements(routeSummary, normalized, userMessage) {
|
||
const fragmentText = fragmentTextById(normalized);
|
||
return (0, assistantCoverageGrounding_1.extractRequirementsForRoute)({
|
||
routeSummary,
|
||
userMessage,
|
||
fragmentTextById: fragmentText,
|
||
extractSubjectTokens
|
||
});
|
||
}
|
||
function toExecutionPlan(routeSummary, normalized, userMessage, requirementByFragment) {
|
||
const fragmentText = fragmentTextById(normalized);
|
||
return (0, assistantQueryPlanning_1.buildExecutionPlanFromRoute)({
|
||
routeSummary,
|
||
userMessage,
|
||
fragmentTextById: fragmentText,
|
||
requirementByFragment
|
||
});
|
||
}
|
||
function enrichRbpFragmentForLive(fragmentText, temporalGuard) {
|
||
const base = compactWhitespace(String(fragmentText ?? ""));
|
||
const hints = ["Списание РБП", "объект РБП", "остаток на конец периода", "счет 97"];
|
||
const effective = temporalGuard && typeof temporalGuard === "object" ? temporalGuard.effective_primary_period : null;
|
||
if (effective && effective.from && effective.to) {
|
||
hints.push(`период ${effective.from}..${effective.to}`);
|
||
}
|
||
const hintText = hints.filter(Boolean).join(", ");
|
||
if (!base) {
|
||
return hintText;
|
||
}
|
||
if (/списани[ея]\s+рбп|счет\s*97|account\s*97|остат/i.test(base)) {
|
||
return base;
|
||
}
|
||
return `${base}; ${hintText}`;
|
||
}
|
||
function enforceRbpLiveRoutePlan(input) {
|
||
if (input.claimType !== "prove_rbp_tail_state") {
|
||
return {
|
||
executionPlan: input.executionPlan,
|
||
audit: null
|
||
};
|
||
}
|
||
const requiredLiveCalls = [
|
||
"find_rbp_writeoff_documents_in_period",
|
||
"find_rbp_object_movements_account_97",
|
||
"find_month_close_entries_linked_to_rbp",
|
||
"compute_end_period_residual_by_rbp_object"
|
||
];
|
||
let routeAdjusted = 0;
|
||
let rescuedNoRoute = 0;
|
||
const replacedRoutes = [];
|
||
const adjustedPlan = input.executionPlan.map((item) => {
|
||
if (!item || typeof item !== "object") {
|
||
return item;
|
||
}
|
||
if (item.should_execute !== true && item.no_route_reason === "insufficient_specificity") {
|
||
rescuedNoRoute += 1;
|
||
routeAdjusted += 1;
|
||
return {
|
||
...item,
|
||
route: "live_mcp_drilldown",
|
||
should_execute: true,
|
||
no_route_reason: null,
|
||
clarification_reason: null,
|
||
fragment_text: enrichRbpFragmentForLive(item.fragment_text, input.temporalGuard)
|
||
};
|
||
}
|
||
if (item.should_execute === true && item.route !== "hybrid_store_plus_live" && item.route !== "live_mcp_drilldown") {
|
||
routeAdjusted += 1;
|
||
if (item.route && item.route !== "no_route") {
|
||
replacedRoutes.push(String(item.route));
|
||
}
|
||
return {
|
||
...item,
|
||
route: "hybrid_store_plus_live",
|
||
fragment_text: enrichRbpFragmentForLive(item.fragment_text, input.temporalGuard)
|
||
};
|
||
}
|
||
if (item.should_execute === true) {
|
||
return {
|
||
...item,
|
||
fragment_text: enrichRbpFragmentForLive(item.fragment_text, input.temporalGuard)
|
||
};
|
||
}
|
||
return item;
|
||
});
|
||
return {
|
||
executionPlan: adjustedPlan,
|
||
audit: {
|
||
claim_type: "prove_rbp_tail_state",
|
||
required_live_calls: requiredLiveCalls,
|
||
route_adjustments_applied: routeAdjusted,
|
||
rescued_no_route_fragments: rescuedNoRoute,
|
||
replaced_routes: Array.from(new Set(replacedRoutes)),
|
||
route_gap_reason: routeAdjusted > 0 ? "rbp_claim_bound_live_route_override_applied" : null
|
||
}
|
||
};
|
||
}
|
||
function collectRbpLiveRouteAudit(input) {
|
||
if (input.claimType !== "prove_rbp_tail_state") {
|
||
return null;
|
||
}
|
||
const required = new Set(Array.isArray(input.planAudit?.required_live_calls) ? input.planAudit.required_live_calls : []);
|
||
const executed = [];
|
||
const missing = new Set();
|
||
const routeGaps = [];
|
||
let matchedRowsTotal = 0;
|
||
let returnedRowsTotal = 0;
|
||
let fetchedRowsTotal = 0;
|
||
for (const result of input.retrievalResults) {
|
||
if (!result || typeof result !== "object") {
|
||
continue;
|
||
}
|
||
const summary = result.summary && typeof result.summary === "object" ? result.summary : null;
|
||
const live = summary && typeof summary.live_mcp === "object" && summary.live_mcp ? summary.live_mcp : null;
|
||
if (!live) {
|
||
continue;
|
||
}
|
||
const requiredCalls = Array.isArray(live.required_live_calls) ? live.required_live_calls : [];
|
||
for (const callId of requiredCalls) {
|
||
required.add(String(callId ?? "").trim());
|
||
}
|
||
const executedCalls = Array.isArray(live.executed_live_calls) ? live.executed_live_calls : [];
|
||
for (const call of executedCalls) {
|
||
if (!call || typeof call !== "object") {
|
||
continue;
|
||
}
|
||
executed.push(call);
|
||
}
|
||
const missingCalls = Array.isArray(live.missing_live_calls) ? live.missing_live_calls : [];
|
||
for (const callId of missingCalls) {
|
||
const token = String(callId ?? "").trim();
|
||
if (token) {
|
||
missing.add(token);
|
||
}
|
||
}
|
||
const routeGapReason = String(live.route_gap_reason ?? "").trim();
|
||
if (routeGapReason) {
|
||
routeGaps.push(routeGapReason);
|
||
}
|
||
fetchedRowsTotal += Number(live.fetched_rows ?? 0) || 0;
|
||
matchedRowsTotal += Number(live.matched_rows ?? 0) || 0;
|
||
returnedRowsTotal += Number(live.returned_rows ?? 0) || 0;
|
||
}
|
||
const requiredList = Array.from(required).filter(Boolean);
|
||
const executedList = executed;
|
||
const missingFromExecuted = requiredList.filter((callId) => !executedList.some((item) => String(item.call_id ?? "") === callId));
|
||
for (const callId of missingFromExecuted) {
|
||
missing.add(callId);
|
||
}
|
||
const missingList = Array.from(missing);
|
||
const routeGapReason = missingList.length > 0
|
||
? "required_live_calls_not_executed"
|
||
: matchedRowsTotal <= 0
|
||
? "claim_live_calls_executed_but_zero_matches"
|
||
: routeGaps[0] ?? null;
|
||
const executionRate = requiredList.length > 0
|
||
? Number(((requiredList.length - missingList.length) / requiredList.length).toFixed(4))
|
||
: 1;
|
||
return {
|
||
claim_type: "prove_rbp_tail_state",
|
||
required_live_calls: requiredList,
|
||
executed_live_calls: executedList,
|
||
missing_live_calls: missingList,
|
||
route_gap_reason: routeGapReason,
|
||
live_route_execution_rate: executionRate,
|
||
fetched_rows_total: fetchedRowsTotal,
|
||
matched_rows_total: matchedRowsTotal,
|
||
returned_rows_total: returnedRowsTotal,
|
||
plan_override: input.planAudit ?? null
|
||
};
|
||
}
|
||
function enrichFaFragmentForLive(fragmentText, temporalGuard) {
|
||
const base = compactWhitespace(String(fragmentText ?? ""));
|
||
const hints = [
|
||
"Начисление амортизации",
|
||
"объект ОС",
|
||
"expected set ОС",
|
||
"счет 01/02"
|
||
];
|
||
const effective = temporalGuard && typeof temporalGuard === "object" ? temporalGuard.effective_primary_period : null;
|
||
if (effective && effective.from && effective.to) {
|
||
hints.push(`период ${effective.from}..${effective.to}`);
|
||
}
|
||
const hintText = hints.filter(Boolean).join(", ");
|
||
if (!base) {
|
||
return hintText;
|
||
}
|
||
if (/амортиз|основн(?:ые|ых)\s+сред|fixed\s*asset|depreciat|счет\s*0[12]|account\s*0[12]/i.test(base)) {
|
||
return base;
|
||
}
|
||
return `${base}; ${hintText}`;
|
||
}
|
||
function enforceFaLiveRoutePlan(input) {
|
||
if (input.claimType !== "prove_fixed_asset_amortization_coverage") {
|
||
return {
|
||
executionPlan: input.executionPlan,
|
||
audit: null
|
||
};
|
||
}
|
||
const requiredLiveCalls = [
|
||
"find_amortization_documents_in_period",
|
||
"find_fixed_asset_movements_accounts_01_02",
|
||
"find_fixed_asset_cards_expected_for_period",
|
||
"match_expected_vs_actual_fa_coverage"
|
||
];
|
||
let routeAdjusted = 0;
|
||
let rescuedNoRoute = 0;
|
||
const replacedRoutes = [];
|
||
const adjustedPlan = input.executionPlan.map((item) => {
|
||
if (!item || typeof item !== "object") {
|
||
return item;
|
||
}
|
||
if (item.should_execute !== true && item.no_route_reason === "insufficient_specificity") {
|
||
rescuedNoRoute += 1;
|
||
routeAdjusted += 1;
|
||
return {
|
||
...item,
|
||
route: "live_mcp_drilldown",
|
||
should_execute: true,
|
||
no_route_reason: null,
|
||
clarification_reason: null,
|
||
fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard)
|
||
};
|
||
}
|
||
if (item.should_execute === true && item.route !== "hybrid_store_plus_live" && item.route !== "live_mcp_drilldown") {
|
||
routeAdjusted += 1;
|
||
if (item.route && item.route !== "no_route") {
|
||
replacedRoutes.push(String(item.route));
|
||
}
|
||
return {
|
||
...item,
|
||
route: "hybrid_store_plus_live",
|
||
fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard)
|
||
};
|
||
}
|
||
if (item.should_execute === true) {
|
||
return {
|
||
...item,
|
||
fragment_text: enrichFaFragmentForLive(item.fragment_text, input.temporalGuard)
|
||
};
|
||
}
|
||
return item;
|
||
});
|
||
return {
|
||
executionPlan: adjustedPlan,
|
||
audit: {
|
||
claim_type: "prove_fixed_asset_amortization_coverage",
|
||
required_live_calls: requiredLiveCalls,
|
||
route_adjustments_applied: routeAdjusted,
|
||
rescued_no_route_fragments: rescuedNoRoute,
|
||
replaced_routes: Array.from(new Set(replacedRoutes)),
|
||
route_gap_reason: routeAdjusted > 0 ? "fa_claim_bound_live_route_override_applied" : null
|
||
}
|
||
};
|
||
}
|
||
function collectFaLiveRouteAudit(input) {
|
||
if (input.claimType !== "prove_fixed_asset_amortization_coverage") {
|
||
return null;
|
||
}
|
||
const required = new Set(Array.isArray(input.planAudit?.required_live_calls) ? input.planAudit.required_live_calls : []);
|
||
const executed = [];
|
||
const missing = new Set();
|
||
const routeGaps = [];
|
||
let matchedRowsTotal = 0;
|
||
let returnedRowsTotal = 0;
|
||
let fetchedRowsTotal = 0;
|
||
for (const result of input.retrievalResults) {
|
||
if (!result || typeof result !== "object") {
|
||
continue;
|
||
}
|
||
const summary = result.summary && typeof result.summary === "object" ? result.summary : null;
|
||
const live = summary && typeof summary.live_mcp === "object" && summary.live_mcp ? summary.live_mcp : null;
|
||
if (!live) {
|
||
continue;
|
||
}
|
||
const requiredCalls = Array.isArray(live.required_live_calls) ? live.required_live_calls : [];
|
||
for (const callId of requiredCalls) {
|
||
required.add(String(callId ?? "").trim());
|
||
}
|
||
const executedCalls = Array.isArray(live.executed_live_calls) ? live.executed_live_calls : [];
|
||
for (const call of executedCalls) {
|
||
if (!call || typeof call !== "object") {
|
||
continue;
|
||
}
|
||
executed.push(call);
|
||
}
|
||
const missingCalls = Array.isArray(live.missing_live_calls) ? live.missing_live_calls : [];
|
||
for (const callId of missingCalls) {
|
||
const token = String(callId ?? "").trim();
|
||
if (token) {
|
||
missing.add(token);
|
||
}
|
||
}
|
||
const routeGapReason = String(live.route_gap_reason ?? "").trim();
|
||
if (routeGapReason) {
|
||
routeGaps.push(routeGapReason);
|
||
}
|
||
fetchedRowsTotal += Number(live.fetched_rows ?? 0) || 0;
|
||
matchedRowsTotal += Number(live.matched_rows ?? 0) || 0;
|
||
returnedRowsTotal += Number(live.returned_rows ?? 0) || 0;
|
||
}
|
||
const requiredList = Array.from(required).filter(Boolean);
|
||
const executedList = executed;
|
||
const missingFromExecuted = requiredList.filter((callId) => !executedList.some((item) => String(item.call_id ?? "") === callId));
|
||
for (const callId of missingFromExecuted) {
|
||
missing.add(callId);
|
||
}
|
||
const missingList = Array.from(missing);
|
||
const routeGapReason = missingList.length > 0
|
||
? "required_live_calls_not_executed"
|
||
: matchedRowsTotal <= 0
|
||
? "claim_live_calls_executed_but_zero_matches"
|
||
: routeGaps[0] ?? null;
|
||
const executionRate = requiredList.length > 0
|
||
? Number(((requiredList.length - missingList.length) / requiredList.length).toFixed(4))
|
||
: 1;
|
||
return {
|
||
claim_type: "prove_fixed_asset_amortization_coverage",
|
||
required_live_calls: requiredList,
|
||
executed_live_calls: executedList,
|
||
missing_live_calls: missingList,
|
||
route_gap_reason: routeGapReason,
|
||
live_route_execution_rate: executionRate,
|
||
fetched_rows_total: fetchedRowsTotal,
|
||
matched_rows_total: matchedRowsTotal,
|
||
returned_rows_total: returnedRowsTotal,
|
||
plan_override: input.planAudit ?? null
|
||
};
|
||
}
|
||
function toDebugRoutes(routeSummary) {
|
||
return (0, assistantQueryPlanning_1.buildDebugRoutesFromRoute)({
|
||
routeSummary,
|
||
resolveLegacyRouteReason: retrievalSummaryForRoute
|
||
});
|
||
}
|
||
function buildSkippedResult(item) {
|
||
return (0, retrievalResultNormalizer_1.normalizeRetrievalResult)(item.fragment_id, item.requirement_ids, "no_route", {
|
||
status: "empty",
|
||
result_type: "summary",
|
||
items: [],
|
||
summary: {
|
||
skipped: true,
|
||
reason: mapNoRouteReason(item.no_route_reason),
|
||
no_route_reason: item.no_route_reason,
|
||
clarification_reason: item.clarification_reason
|
||
},
|
||
evidence: [],
|
||
why_included: [],
|
||
selection_reason: [mapNoRouteReason(item.no_route_reason)],
|
||
risk_factors: [],
|
||
business_interpretation: ["Данный фрагмент не был выполнен из-за no-route решения."],
|
||
confidence: "low",
|
||
limitations: ["Фрагмент требует уточнения или отсутствует поддерживаемый маршрут."],
|
||
errors: []
|
||
});
|
||
}
|
||
function evaluateCoverage(requirements, retrievalResults) {
|
||
return (0, assistantCoverageGrounding_1.evaluateCoverageForRequirements)(requirements, retrievalResults);
|
||
}
|
||
function evaluateCoverageForTests(requirements, retrievalResults) {
|
||
return (0, assistantCoverageGrounding_1.evaluateCoverageForRequirements)(requirements, retrievalResults);
|
||
}
|
||
function extractSubjectTokensForTests(text) {
|
||
return extractSubjectTokens(text);
|
||
}
|
||
function checkGrounding(userMessage, requirements, coverage, retrievalResults) {
|
||
return (0, assistantCoverageGrounding_1.checkGroundingForRequirements)({
|
||
userMessage,
|
||
requirements,
|
||
coverage,
|
||
retrievalResults,
|
||
extractSubjectTokens
|
||
});
|
||
}
|
||
const FOLLOWUP_ROUTE_HINTS = new Set(["store_canonical", "store_feature_risk", "hybrid_store_plus_live", "live_mcp_drilldown", "batch_refresh_then_store"]);
|
||
const FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP = {
|
||
settlements_60_62: "hybrid_store_plus_live",
|
||
vat_document_register_book: "hybrid_store_plus_live",
|
||
month_close_costs_20_44: "hybrid_store_plus_live",
|
||
fixed_asset_amortization: "hybrid_store_plus_live"
|
||
};
|
||
const FOLLOWUP_BUSINESS_CONTEXT_MAX = 320;
|
||
const FOLLOWUP_SUBJECT_MAX = 160;
|
||
const FOLLOWUP_QUESTION_APPEND_MAX = 260;
|
||
function compactWhitespace(value) {
|
||
return value.replace(/\s+/g, " ").trim();
|
||
}
|
||
function hasAccountingSignal(text) {
|
||
const lower = repairAddressMojibake(String(text ?? "")).toLowerCase();
|
||
const excludedSpans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower), ...collectContractSpans(lower)];
|
||
const accountTokenPattern = /\b(?:01|02|07|08|10|13|19|20|21|23|25|26|28|29|41|43|44|50|51|52|55|57|58|60|62|66|67|68|69|70|71|73|75|76|80|81|84|90|91|97)(?:[.,]\d{1,2})?\b/g;
|
||
let accountMatch = null;
|
||
while ((accountMatch = accountTokenPattern.exec(lower)) !== null) {
|
||
const token = String(accountMatch[0] ?? "").trim();
|
||
if (!token) {
|
||
continue;
|
||
}
|
||
const start = accountMatch.index;
|
||
const end = start + token.length;
|
||
if (intersectsAnySpan(start, end, excludedSpans)) {
|
||
continue;
|
||
}
|
||
const hasExplicitSubaccount = /[.,]\d{1,2}/.test(token);
|
||
if (hasExplicitSubaccount || hasAccountContextAround(lower, start, end) || countTokens(lower) <= 4) {
|
||
return true;
|
||
}
|
||
}
|
||
return /(проводк|документ|реализац|поступлен|взаиморасчет|сальдо|остатк|счет|счёт|ндс|амортиз|рбп|контрагент|поставщик|покупател|оплат|банк|выписк|склад|товар|материал|закрыти|период|postavshchik|kontragent|schet|schetu|period|counterparty|supplier|invoice|posting|ledger|account|anomaly|risk)/i.test(lower);
|
||
}
|
||
function hasFollowupMarker(text) {
|
||
const compact = compactWhitespace(text.toLowerCase());
|
||
return /^(и|а\s+кто|а еще|а ещё|еще|ещё|добав|уточн|продолж|также|а если|а теперь|теперь|plus|also|dobav|utochn|prodolzh|then|now)/i.test(compact);
|
||
}
|
||
function hasReferentialPointer(text) {
|
||
return /(по этому|по тому|это же|этой|этим|этому|этого|этот|эту|этом|это|эти|этих|из этого|из них|из этих|из тех|в этом|тот же|same thing|that one|po etomu|po tomu)/i.test(text.toLowerCase());
|
||
}
|
||
function hasSmallTalkSignal(text) {
|
||
return /(привет|как дела|спасибо|благодарю|thanks|thank you|hello|hi)\b/i.test(text.toLowerCase());
|
||
}
|
||
function countTokens(text) {
|
||
return compactWhitespace(text)
|
||
.split(" ")
|
||
.filter(Boolean).length;
|
||
}
|
||
function hasPeriodLiteral(text) {
|
||
return /\b(20\d{2}(?:[-/.](?:0[1-9]|1[0-2]))?)\b/.test(text);
|
||
}
|
||
function hasShortNamedPeriodFollowupLiteral(text) {
|
||
const normalized = compactWhitespace(String(text ?? "").toLowerCase());
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
return /^(?:(?:\u0430|a|\u0438|i)\s+)?(?:\u043d\u0430|\u0437\u0430|\u043f\u043e|na|za|for)\s+(?:(?:\u044f\u043d\u0432(?:\u0430\u0440)?|\u0444\u0435\u0432(?:\u0440\u0430\u043b)?|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440(?:\u0435\u043b)?|\u043c\u0430(?:\u0439|\u044f)|\u0438\u044e\u043d(?:\u044c)?|\u0438\u044e\u043b(?:\u044c)?|\u0430\u0432\u0433(?:\u0443\u0441\u0442)?|\u0441\u0435\u043d\u0442(?:\u044f\u0431\u0440)?|\u043e\u043a\u0442(?:\u044f\u0431\u0440)?|\u043d\u043e\u044f(?:\u0431\u0440)?|\u0434\u0435\u043a(?:\u0430\u0431\u0440)?)(?:[\u0430-\u044f\u0451]*)?|q[1-4]|(?:[1-4]|[ivx]{1,4})\s*(?:-?\u0439)?\s*\u043a\u0432(?:\.|\u0430\u0440\u0442(?:\u0430\u043b(?:\u0430|\u0435|\u0443|\u043e\u043c)?)?)?|(?:20\d{2})\s*(?:\u0433(?:\.|\u043e\u0434(?:\u0430|\u0443|\u043e\u043c)?)?))(?=$|[\s,.;:!?])/iu.test(normalized);
|
||
}
|
||
function hasShortCurrentDateFollowupLiteral(text) {
|
||
const normalized = compactWhitespace(String(text ?? "").toLowerCase());
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
return /^(?:(?:\u0430|a|\u0438|i)\s+)?(?:(?:\u043d\u0430|na)\s+)?(?:(?:\u0442\u0435\u043a\u0443\u0449(?:[\u0430-\u044f\u0451]+)?\s+\u0434\u0430\u0442(?:[\u0430-\u044f\u0451]+)?|(?:\u043d\u0430\s+)?\u0441\u0435\u0433\u043e\u0434\u043d(?:\u044f|\u0435\u0448\u043d(?:[\u0430-\u044f\u0451]+)?\s+\u0434\u0430\u0442(?:[\u0430-\u044f\u0451]+)?)|(?:\u043d\u0430\s+)?\u0442\u0435\u043a\u0443\u0449(?:[\u0430-\u044f\u0451]+)?\s+\u043c\u043e\u043c\u0435\u043d\u0442|today|current\s+date|current\s+moment|now)|(?:\u043f\u043e\s+\u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e\s+\u043d\u0430|as\s+of)\s+(?:\u0441\u0435\u0433\u043e\u0434\u043d\u044f|today|current\s+date))(?=$|[\s,.;:!?])/iu.test(normalized);
|
||
}
|
||
function hasStandaloneAddressTopicSignal(text) {
|
||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
if (hasFollowupMarker(normalized) || hasReferentialPointer(normalized)) {
|
||
return false;
|
||
}
|
||
const hasRequestCue = /(?:^|[\s,.;:!?()\-])(?:покажи|показать|выведи|дай|найди|список|какие|какой|какая|каких|сколько|где|show|list|find|which|what)/iu.test(normalized);
|
||
if (!hasRequestCue) {
|
||
return false;
|
||
}
|
||
const hasBusinessObject = /(?:договор|контракт|контрагент|поставщик|покупател|клиент|документ|платеж|оплат|сальдо|остатк|сч[её]т|оборот|выруч|доход|прибыл|ндс|дебитор|кредитор|организац|компан|контор|склад|товар|номенклат|материал|contract|counterparty|supplier|customer|document|payment|turnover|revenue|profit|balance|account|vat|warehouse|inventory|stock|item)/iu.test(normalized);
|
||
if (!hasBusinessObject) {
|
||
return false;
|
||
}
|
||
const hasStructuredAnchor = hasPeriodLiteral(normalized) ||
|
||
/\b\d{2}(?:[.,]\d{1,2})?\b/.test(normalized) ||
|
||
/(?:альтернатива|лайсвуд|райм|ооо\s+[a-zа-яё])/iu.test(normalized);
|
||
return hasStructuredAnchor || countTokens(normalized) >= 6;
|
||
}
|
||
function extractNormalizedPeriodLiteral(text) {
|
||
const monthly = text.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])\b/);
|
||
if (monthly) {
|
||
return `${monthly[1]}-${monthly[2]}`;
|
||
}
|
||
const yearly = text.match(/\b(20\d{2})\b/);
|
||
if (yearly) {
|
||
return yearly[1];
|
||
}
|
||
return null;
|
||
}
|
||
function extractFollowupAccountAnchorsLoose(text) {
|
||
const lower = String(text ?? "").toLowerCase();
|
||
const spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower), ...collectContractSpans(lower)];
|
||
const anchors = [];
|
||
const followupAccountPattern = /\b(?:01|02|08|19|20|21|23|25|26|28|29|44|51|60|62|68|76|97)(?:\.\d{2})?\b/g;
|
||
let match = null;
|
||
while ((match = followupAccountPattern.exec(lower)) !== null) {
|
||
const value = String(match[0] ?? "").trim();
|
||
const start = match.index;
|
||
const end = start + value.length;
|
||
if (intersectsAnySpan(start, end, spans)) {
|
||
continue;
|
||
}
|
||
anchors.push(value);
|
||
}
|
||
return Array.from(new Set(anchors));
|
||
}
|
||
function accountPrefixToken(value) {
|
||
const token = String(value ?? "").trim();
|
||
const match = token.match(/^(\d{2})/);
|
||
return match ? match[1] : null;
|
||
}
|
||
function hasCrossScopeConflictWithState(userMessage, state) {
|
||
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
|
||
const statePeriod = compactWhitespace(state.focus.period ?? "");
|
||
if (explicitPeriod && statePeriod && explicitPeriod !== statePeriod) {
|
||
return true;
|
||
}
|
||
const inferredDomain = inferP0DomainFromMessage(userMessage);
|
||
const stateDomain = compactWhitespace(state.followup_context?.active_domain ?? state.focus.domain ?? "");
|
||
if (inferredDomain && stateDomain && inferredDomain !== stateDomain) {
|
||
const followupDomainRefinement = hasFollowupMarker(userMessage) ||
|
||
hasReferentialPointer(userMessage) ||
|
||
hasPeriodLiteral(userMessage);
|
||
if (!followupDomainRefinement) {
|
||
return true;
|
||
}
|
||
}
|
||
const explicitAccounts = extractAccountTokens(userMessage);
|
||
const fallbackAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
|
||
const knownAccounts = Array.isArray(state.focus.primary_accounts) ? state.focus.primary_accounts : [];
|
||
if (fallbackAccounts.length > 0 && knownAccounts.length > 0) {
|
||
const knownPrefixes = new Set(knownAccounts.map((item) => accountPrefixToken(item)).filter(Boolean));
|
||
const newPrefixes = new Set(fallbackAccounts.map((item) => accountPrefixToken(item)).filter(Boolean));
|
||
if (newPrefixes.size > 0 && knownPrefixes.size > 0) {
|
||
let intersects = false;
|
||
for (const prefix of newPrefixes) {
|
||
if (knownPrefixes.has(prefix)) {
|
||
intersects = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!intersects) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
function inferP0DomainFromMessage(text) {
|
||
if (typeof investigationState_1.inferP0DomainFromMessage === "function") {
|
||
return investigationState_1.inferP0DomainFromMessage(text);
|
||
}
|
||
return null;
|
||
}
|
||
function hasStrongFollowupAnchors(userMessage, state) {
|
||
const normalizedMessage = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||
const periodRefinementCue = /(?:^(?:\u0430\s+)?\u0435\u0441\u043b\u0438|\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0437\u0430|\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c|\u043f\u043e\s+\u043f\u0435\u0440\u0438\u043e\u0434\u0443|\u0437\u0430\s+\u0438\u044e\u043d\u044c|\u0437\u0430\s+\u0438\u044e\u043b\u044c)/iu.test(normalizedMessage);
|
||
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
|
||
if (explicitPeriod && state.focus.period && explicitPeriod !== state.focus.period) {
|
||
const periodLooksLikeFollowupRefinement = hasFollowupMarker(userMessage) || hasReferentialPointer(userMessage) || periodRefinementCue;
|
||
if (!periodLooksLikeFollowupRefinement) {
|
||
return true;
|
||
}
|
||
}
|
||
const inferredDomain = inferP0DomainFromMessage(userMessage);
|
||
const activeDomain = compactWhitespace(state.followup_context?.active_domain ?? state.focus.domain ?? "");
|
||
if (inferredDomain && activeDomain && inferredDomain !== activeDomain) {
|
||
const domainLooksLikeFollowupRefinement = hasFollowupMarker(userMessage) && hasReferentialPointer(userMessage);
|
||
if (!domainLooksLikeFollowupRefinement) {
|
||
return true;
|
||
}
|
||
}
|
||
const explicitAccounts = extractAccountTokens(userMessage);
|
||
const followupAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
|
||
if (followupAccounts.length > 0) {
|
||
const knownAccounts = new Set(state.focus.primary_accounts.map((item) => item.trim()));
|
||
if (knownAccounts.size === 0) {
|
||
return true;
|
||
}
|
||
if (followupAccounts.some((item) => !knownAccounts.has(item))) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
function routeFromInvestigationState(state) {
|
||
const rawDomain = compactWhitespace(state.focus.domain ?? "");
|
||
if (!rawDomain) {
|
||
const mappedFromFollowup = state.followup_context?.active_domain
|
||
? FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP[compactWhitespace(state.followup_context.active_domain)] ?? null
|
||
: null;
|
||
return mappedFromFollowup;
|
||
}
|
||
if (FOLLOWUP_ROUTE_HINTS.has(rawDomain)) {
|
||
return rawDomain;
|
||
}
|
||
for (const candidate of rawDomain.split(",").map((item) => compactWhitespace(item))) {
|
||
if (FOLLOWUP_ROUTE_HINTS.has(candidate)) {
|
||
return candidate;
|
||
}
|
||
if (Object.prototype.hasOwnProperty.call(FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP, candidate)) {
|
||
return FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP[candidate];
|
||
}
|
||
}
|
||
const mappedFromFollowup = state.followup_context?.active_domain
|
||
? FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP[compactWhitespace(state.followup_context.active_domain)] ?? null
|
||
: null;
|
||
if (mappedFromFollowup) {
|
||
return mappedFromFollowup;
|
||
}
|
||
return null;
|
||
}
|
||
function withCappedLength(value, max) {
|
||
return value.length <= max ? value : value.slice(0, max);
|
||
}
|
||
function mergeBusinessContext(existing, patchParts) {
|
||
const existingText = compactWhitespace(existing ?? "");
|
||
const patchText = compactWhitespace(patchParts.filter(Boolean).join("; "));
|
||
if (!existingText && !patchText) {
|
||
return undefined;
|
||
}
|
||
const merged = existingText && patchText ? `${existingText}; ${patchText}` : existingText || patchText;
|
||
return withCappedLength(merged, FOLLOWUP_BUSINESS_CONTEXT_MAX);
|
||
}
|
||
function buildFollowupStateBinding(input) {
|
||
const userMessage = String(input.userMessage ?? "").trim();
|
||
if (!userMessage || input.investigationState.status !== "active" || input.investigationState.turn_index <= 0) {
|
||
return {
|
||
normalizedQuestion: userMessage,
|
||
mergedContext: input.payloadContext,
|
||
usage: null
|
||
};
|
||
}
|
||
const strongSignal = hasAccountingSignal(userMessage);
|
||
const followupMarker = hasFollowupMarker(userMessage);
|
||
const referentialPointer = hasReferentialPointer(userMessage);
|
||
const shortPrompt = countTokens(userMessage) <= 10;
|
||
const smallTalkSignal = hasSmallTalkSignal(userMessage);
|
||
const problemState = input.investigationState.problem_unit_state;
|
||
const problemContinuityAvailable = config_1.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 &&
|
||
Boolean(problemState) &&
|
||
((problemState?.active_problem_units.length ?? 0) > 0 || (problemState?.focus_problem_types.length ?? 0) > 0);
|
||
const strongNewAnchorDetected = hasStrongFollowupAnchors(userMessage, input.investigationState);
|
||
const scopeConflictDetected = hasCrossScopeConflictWithState(userMessage, input.investigationState);
|
||
const periodRefinementFollowup = hasPeriodLiteral(userMessage) && problemContinuityAvailable;
|
||
const shouldBind = !smallTalkSignal &&
|
||
!strongNewAnchorDetected &&
|
||
!scopeConflictDetected &&
|
||
(followupMarker || referentialPointer || periodRefinementFollowup || (!strongSignal && shortPrompt));
|
||
if (!shouldBind) {
|
||
return {
|
||
normalizedQuestion: userMessage,
|
||
mergedContext: input.payloadContext,
|
||
usage: null
|
||
};
|
||
}
|
||
const context = {
|
||
...(input.payloadContext ?? {})
|
||
};
|
||
const hasExplicitExpectedRoute = Boolean(input.payloadContext?.expected_route);
|
||
const expectedRouteFromState = !context?.expected_route ? routeFromInvestigationState(input.investigationState) : null;
|
||
const periodHintFromState = !context?.period_hint ? input.investigationState.focus.period : null;
|
||
const followupContext = input.investigationState.followup_context;
|
||
if (expectedRouteFromState) {
|
||
context.expected_route = expectedRouteFromState;
|
||
}
|
||
if (periodHintFromState) {
|
||
context.period_hint = periodHintFromState;
|
||
const existingAnalysisContext = context.analysis_context && typeof context.analysis_context === "object" ? context.analysis_context : {};
|
||
if (!existingAnalysisContext.as_of_date) {
|
||
context.analysis_context = {
|
||
...existingAnalysisContext,
|
||
as_of_date: periodHintFromState,
|
||
source: existingAnalysisContext.source ?? "followup_state_period_hint"
|
||
};
|
||
}
|
||
}
|
||
const subject = withCappedLength(compactWhitespace(input.investigationState.focus.active_query_subject ?? ""), FOLLOWUP_SUBJECT_MAX);
|
||
const businessContextPatch = ["followup_state_binding_v1"];
|
||
let problemContinuityApplied = false;
|
||
let problemContinuitySkippedReason = null;
|
||
if (input.investigationState.focus.period) {
|
||
businessContextPatch.push("active_period");
|
||
}
|
||
if (subject) {
|
||
businessContextPatch.push(`focus_subject:${subject}`);
|
||
}
|
||
if (input.investigationState.focus.primary_accounts.length > 0) {
|
||
businessContextPatch.push(`focus_accounts:${input.investigationState.focus.primary_accounts.join(",")}`);
|
||
}
|
||
if (followupContext?.active_domain) {
|
||
businessContextPatch.push(`focus_domain:${followupContext.active_domain}`);
|
||
}
|
||
if ((followupContext?.active_requirement_ids?.length ?? 0) > 0) {
|
||
businessContextPatch.push(`active_requirements:${followupContext.active_requirement_ids.slice(0, 4).join(",")}`);
|
||
}
|
||
if ((followupContext?.uncovered_requirement_ids?.length ?? 0) > 0) {
|
||
businessContextPatch.push(`uncovered_requirements:${followupContext.uncovered_requirement_ids.slice(0, 4).join(",")}`);
|
||
}
|
||
if (followupContext?.last_problem_unit_id) {
|
||
businessContextPatch.push(`last_problem_unit:${followupContext.last_problem_unit_id}`);
|
||
}
|
||
if ((followupContext?.evidence_summary?.length ?? 0) > 0) {
|
||
businessContextPatch.push(`evidence_state:${followupContext.evidence_summary.slice(0, 3).join("|")}`);
|
||
}
|
||
if ((followupContext?.settlement_next_actions?.length ?? 0) > 0) {
|
||
businessContextPatch.push("settlement_focus_retained_v1");
|
||
}
|
||
if (problemContinuityAvailable) {
|
||
if (hasExplicitExpectedRoute) {
|
||
problemContinuitySkippedReason = "explicit_expected_route";
|
||
}
|
||
else {
|
||
const focusTypes = (problemState?.focus_problem_types ?? []).slice(0, 3);
|
||
const activeCount = problemState?.active_problem_units.length ?? 0;
|
||
businessContextPatch.push("problem_unit_continuity_v1");
|
||
if (focusTypes.length > 0) {
|
||
businessContextPatch.push(`problem_focus_types:${focusTypes.join(",")}`);
|
||
}
|
||
businessContextPatch.push(`problem_active_count:${activeCount}`);
|
||
problemContinuityApplied = true;
|
||
}
|
||
}
|
||
const mergedBusinessContext = mergeBusinessContext(context?.business_context, businessContextPatch);
|
||
if (mergedBusinessContext) {
|
||
context.business_context = mergedBusinessContext;
|
||
}
|
||
const shouldAugmentQuestion = Boolean(subject) && (followupMarker || referentialPointer || !strongSignal);
|
||
let normalizedQuestion = userMessage;
|
||
if (shouldAugmentQuestion) {
|
||
const appendParts = [`Фокус текущего разбора: ${subject}`];
|
||
if (input.investigationState.focus.primary_accounts.length > 0 && !/\b\d{2}(?:\.\d{2})?\b/.test(userMessage)) {
|
||
appendParts.push(`Счета фокуса: ${input.investigationState.focus.primary_accounts.join(", ")}`);
|
||
}
|
||
if (periodHintFromState && !hasPeriodLiteral(userMessage)) {
|
||
appendParts.push(`Период фокуса: ${periodHintFromState}`);
|
||
}
|
||
const appendBlock = withCappedLength(compactWhitespace(appendParts.join("; ")), FOLLOWUP_QUESTION_APPEND_MAX);
|
||
normalizedQuestion = `${userMessage}\n${appendBlock}`.trim();
|
||
}
|
||
const reason = followupMarker
|
||
? "followup_marker"
|
||
: referentialPointer
|
||
? "referential_pointer"
|
||
: "underspecified_short_followup";
|
||
return {
|
||
normalizedQuestion,
|
||
mergedContext: context,
|
||
usage: {
|
||
applied: true,
|
||
reason,
|
||
state_turn_index: input.investigationState.turn_index,
|
||
context_patch: {
|
||
period_hint_from_state: Boolean(periodHintFromState),
|
||
expected_route_from_state: Boolean(expectedRouteFromState),
|
||
business_context_from_state: Boolean(mergedBusinessContext),
|
||
question_augmented: shouldAugmentQuestion,
|
||
problem_continuity_available: problemContinuityAvailable,
|
||
problem_continuity_applied: problemContinuityApplied,
|
||
problem_continuity_skipped_reason: problemContinuityApplied ? null : problemContinuitySkippedReason,
|
||
strong_new_anchor_detected: strongNewAnchorDetected,
|
||
scope_isolation_applied: true,
|
||
scope_carryover_allowed: !scopeConflictDetected,
|
||
scope_reset_reason: scopeConflictDetected ? "cross_scope_conflict" : null
|
||
}
|
||
}
|
||
};
|
||
}
|
||
function cloneItems(items) {
|
||
return items.map((item) => ({
|
||
...item,
|
||
debug: item.debug ? { ...item.debug } : null
|
||
}));
|
||
}
|
||
function buildAddressCoverageReport() {
|
||
return (0, assistantDebugPayloadAssembler_1.buildEmptyCoverageReport)();
|
||
}
|
||
function buildAssistantBackendErrorDebugPayload(errorMessage) {
|
||
return (0, assistantDebugPayloadAssembler_1.buildAssistantBackendErrorDebugPayload)({
|
||
errorMessage,
|
||
traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`
|
||
});
|
||
}
|
||
function buildAssistantBackendErrorReply() {
|
||
return "Сейчас не удалось завершить разбор из-за внутренней ошибки контуров LLM. Могу продолжить в адресном режиме: проверить документы, договоры и операции по нужному периоду или контрагенту.";
|
||
}
|
||
function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
|
||
return (0, assistantDebugPayloadAssembler_1.buildAddressRuntimeDebugPayload)({
|
||
addressDebug,
|
||
llmPreDecomposeMeta,
|
||
traceIdFactory: () => `address-${(0, nanoid_1.nanoid)(10)}`
|
||
});
|
||
}
|
||
function toNonEmptyString(value) {
|
||
if (value === null || value === undefined) {
|
||
return null;
|
||
}
|
||
const text = String(value).trim();
|
||
return text.length > 0 ? text : null;
|
||
}
|
||
const ADDRESS_PREDECOMPOSE_NOISE_TOKENS = new Set([
|
||
"за",
|
||
"с",
|
||
"по",
|
||
"на",
|
||
"и",
|
||
"или",
|
||
"док",
|
||
"доки",
|
||
"docs",
|
||
"documents",
|
||
"doki",
|
||
"dokument",
|
||
"dokumenty",
|
||
"документ",
|
||
"документы",
|
||
"документов",
|
||
"банк",
|
||
"банковские",
|
||
"операции",
|
||
"платеж",
|
||
"платёж",
|
||
"платежи",
|
||
"контрагент",
|
||
"контрагенту",
|
||
"контрагента",
|
||
"год",
|
||
"года",
|
||
"г",
|
||
"year",
|
||
"god",
|
||
"плс",
|
||
"pls",
|
||
"пж",
|
||
"пжлст",
|
||
"пожалуйста",
|
||
"please",
|
||
"покеж",
|
||
"покажи",
|
||
"скажи",
|
||
"показать",
|
||
"show",
|
||
"list",
|
||
"skazhi",
|
||
"выведи",
|
||
"кроме",
|
||
"помимо",
|
||
"этого",
|
||
"этот",
|
||
"эта",
|
||
"эту",
|
||
"этом",
|
||
"это",
|
||
"эти",
|
||
"этих",
|
||
"другие",
|
||
"другое",
|
||
"остальное",
|
||
"что",
|
||
"чо",
|
||
"которые",
|
||
"какие",
|
||
"какой",
|
||
"активный",
|
||
"активная",
|
||
"активное",
|
||
"активности",
|
||
"месяц",
|
||
"месяца",
|
||
"месяцев",
|
||
"количество",
|
||
"количеству",
|
||
"количества",
|
||
"были",
|
||
"был",
|
||
"была",
|
||
"было",
|
||
"ли",
|
||
"списания",
|
||
"списание",
|
||
"поступления",
|
||
"поступление",
|
||
"расчетного",
|
||
"расчётного",
|
||
"счета",
|
||
"счёта",
|
||
"есть",
|
||
"est",
|
||
"kakie",
|
||
"kakoi",
|
||
"vse",
|
||
"all",
|
||
"\u043f\u0443\u043d\u043a\u0442",
|
||
"\u043f\u0443\u043d\u043a\u0442\u0430",
|
||
"\u043f\u0443\u043d\u043a\u0442\u0443",
|
||
"\u043f\u0443\u043d\u043a\u0442\u043e\u043c",
|
||
"\u043f\u043e\u0437\u0438\u0446\u0438\u044f",
|
||
"\u043f\u043e\u0437\u0438\u0446\u0438\u0438",
|
||
"\u043f\u043e\u0437\u0438\u0446\u0438\u044e",
|
||
"\u0441\u0442\u0440\u043e\u043a\u0430",
|
||
"\u0441\u0442\u0440\u043e\u043a\u0438",
|
||
"\u0441\u0442\u0440\u043e\u043a\u0443",
|
||
"item",
|
||
"row",
|
||
"line",
|
||
"blya",
|
||
"blyat",
|
||
"епт",
|
||
"ёпт",
|
||
"бля"
|
||
]);
|
||
const ADDRESS_FALLBACK_STRIP_TOKENS = new Set([
|
||
"бля",
|
||
"блять",
|
||
"blya",
|
||
"blyat",
|
||
"епт",
|
||
"ёпт",
|
||
"епта",
|
||
"нах",
|
||
"нахуй",
|
||
"плс",
|
||
"pls",
|
||
"пж",
|
||
"пжлст",
|
||
"пожалуйста",
|
||
"please"
|
||
]);
|
||
const ADDRESS_MONTH_ALIAS_MAP = {
|
||
"\u044f\u043d\u0432": "01",
|
||
"\u044f\u043d\u0432\u0430\u0440": "01",
|
||
january: "01",
|
||
jan: "01",
|
||
"\u0444\u0435\u0432": "02",
|
||
"\u0444\u0435\u0432\u0440\u0430\u043b": "02",
|
||
february: "02",
|
||
feb: "02",
|
||
"\u043c\u0430\u0440": "03",
|
||
"\u043c\u0430\u0440\u0442": "03",
|
||
march: "03",
|
||
apr: "04",
|
||
"\u0430\u043f\u0440": "04",
|
||
"\u0430\u043f\u0440\u0435\u043b": "04",
|
||
april: "04",
|
||
"\u043c\u0430\u0439": "05",
|
||
"\u043c\u0430": "05",
|
||
may: "05",
|
||
"\u0438\u044e\u043d": "06",
|
||
"\u0438\u044e\u043d\u044c": "06",
|
||
june: "06",
|
||
jun: "06",
|
||
"\u0438\u044e\u043b": "07",
|
||
"\u0438\u044e\u043b\u044c": "07",
|
||
july: "07",
|
||
jul: "07",
|
||
"\u0430\u0432\u0433": "08",
|
||
"\u0430\u0432\u0433\u0443\u0441\u0442": "08",
|
||
august: "08",
|
||
aug: "08",
|
||
"\u0441\u0435\u043d": "09",
|
||
"\u0441\u0435\u043d\u0442": "09",
|
||
"\u0441\u0435\u043d\u0442\u044f\u0431\u0440": "09",
|
||
september: "09",
|
||
sep: "09",
|
||
"\u043e\u043a\u0442": "10",
|
||
"\u043e\u043a\u0442\u044f\u0431\u0440": "10",
|
||
october: "10",
|
||
oct: "10",
|
||
"\u043d\u043e\u044f": "11",
|
||
"\u043d\u043e\u044f\u0431\u0440": "11",
|
||
november: "11",
|
||
nov: "11",
|
||
"\u0434\u0435\u043a": "12",
|
||
"\u0434\u0435\u043a\u0430\u0431\u0440": "12",
|
||
december: "12",
|
||
dec: "12"
|
||
};
|
||
const ADDRESS_DOCS_SIGNAL_PATTERN = /(?:док|доки|документ|документы|документов|docs?|documents?|doki|docy|doci|bank|выписк|плат[её]ж|оплат|поступлен|списан|операц|опер|transaction)/i;
|
||
const ADDRESS_BANK_SIGNAL_PATTERN = /(?:bank|банк|банков|выписк|плат[её]ж|оплат|поступлен|списан|операц|опер|расчетн|транзак)/i;
|
||
const ADDRESS_CONTRACT_SIGNAL_PATTERN = /(?:договор(?:а|у|ом|е)?|(?:^|[^\p{L}\p{N}_])(?:дог\.?|[dд][oо][gг]\.?|dog\.?)(?=$|[^\p{L}\p{N}_])|contract|dogovor)/iu;
|
||
const ADDRESS_BALANCE_SIGNAL_PATTERN = /(?:остат|сальдо|баланс|взаиморасч|долг|saldo|balance)/i;
|
||
const ADDRESS_ALL_TIME_PATTERN = /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|for\s+all\s+time|all\s+time|entire\s+period|full\s+history)/iu;
|
||
const ADDRESS_MANAGEMENT_PROFILE_PATTERN = /(?:за\s+какие\s+год[а-яё]*|сам(?:ый|ая|ое)\s+(?:актив|пассив)|наименее\s+актив|минимальн|покрыт(?:ие|ия)\s+период|диапазон\s+лет|тип[аы]\s+док(?:умент|ов|и)?|раздел[ыа]\s+уч[её]та|по\s+количеств[аоуе]|редк|реже|(?:сколько|скока|скок)\s+(?:всего\s+)?(?:уникальн(?:ых|ые|ого)?\s+)?контрагент(?:ов|а)?|(?:сколько|скока|скок)\s+(?:у\s+нас\s+)?(?:заказчик(?:ов|а)?|поставщик(?:ов|а)?|клиент(?:ов|а)?|покупател(?:ей|я)|смешан(?:ных|ые)\s+контрагент(?:ов|а)?)|(?:покажи|выведи|список|какие|кто).*(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?).*(?:за\s+вс[её]\s+время|all\s+time|(?:^|[^\d])(19|20)\d{2}(?:[^\d]|$)|(?:^|[^\d])\d{2}\s*(?:г(?:од|ода)?|г)(?:[^\p{L}\p{N}]|$)|за\s+год|в\s+году)|(?:какие|кто|покажи|выведи|список).*(?:заказчик(?:ов|а|и)?|клиент(?:ов|а|ы)?|покупател(?:ей|я|и)?).*(?:работал(?:и)?|активн(?:ые|ых|а|о)?).*(?:за\s+вс[её]\s+время|(?:19|20)\d{2}|за\s+год|в\s+году)|договорн(?:ая|ой)\s+баз[аы]|total\s+vs\s+used)/iu;
|
||
function normalizeAddressMonthAliasToken(token) {
|
||
const source = String(token ?? "").trim().toLowerCase();
|
||
if (!source) {
|
||
return null;
|
||
}
|
||
const direct = ADDRESS_MONTH_ALIAS_MAP[source];
|
||
if (direct) {
|
||
return direct;
|
||
}
|
||
for (const [key, value] of Object.entries(ADDRESS_MONTH_ALIAS_MAP)) {
|
||
if (source.startsWith(key)) {
|
||
return value;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function normalizeAddressShortYearMentions(text) {
|
||
return String(text ?? "").replace(/(^|[^0-9])(\d{2})\s*(?:г(?:од|ода)?|г|год|year|god)(?=$|[^a-zа-яё0-9])/giu, (_full, prefix, shortYear) => {
|
||
const normalized = Number(shortYear);
|
||
if (!Number.isFinite(normalized) || normalized < 0 || normalized > 99) {
|
||
return _full;
|
||
}
|
||
return `${prefix}${String(2000 + normalized)} год`;
|
||
});
|
||
}
|
||
function sanitizeAddressMessageForFallback(userMessage) {
|
||
const repaired = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")));
|
||
if (!repaired) {
|
||
return "";
|
||
}
|
||
let sanitized = repaired.toLowerCase();
|
||
sanitized = sanitized
|
||
.replace(/\bpokezh\b/giu, "покажи")
|
||
.replace(/\bpokazh(?:i)?\b/giu, "покажи")
|
||
.replace(/\bpokaji\b/giu, "покажи")
|
||
.replace(/\bop(?:er|ers?)\b/giu, "операции")
|
||
.replace(/(^|[^\p{L}\p{N}_])опер(?:аци[яиюе]|ы|)?(?=$|[^\p{L}\p{N}_])/giu, "$1операции")
|
||
.replace(/(^|[^\p{L}\p{N}_])дог\.?(?=$|[^\p{L}\p{N}_])/giu, "$1договор")
|
||
.replace(/(^|[^\p{L}\p{N}_])dog\.?(?=$|[^\p{L}\p{N}_])/giu, "$1contract")
|
||
.replace(/\bdoc(?:y|i)\b/giu, "доки")
|
||
.replace(/\bdok(?:i|y)?\b/giu, "доки")
|
||
.replace(/\bdocuments?\b/giu, "документы")
|
||
.replace(/\bdocs?\b/giu, "документы")
|
||
.replace(/\bschet(?:u)?\b/giu, "счет")
|
||
.replace(/\bsaldo\b/giu, "сальдо")
|
||
.replace(/\bgod\b/giu, "год");
|
||
sanitized = normalizeAddressShortYearMentions(sanitized);
|
||
const tokens = sanitized
|
||
.split(/\s+/)
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
const filteredTokens = tokens.filter((token) => {
|
||
const normalizedToken = token.replace(/^[^a-zа-яё0-9]+|[^a-zа-яё0-9]+$/giu, "");
|
||
if (!normalizedToken) {
|
||
return true;
|
||
}
|
||
return !ADDRESS_FALLBACK_STRIP_TOKENS.has(normalizedToken);
|
||
});
|
||
const compact = compactWhitespace(filteredTokens.join(" "));
|
||
return compact || compactWhitespace(repaired.toLowerCase());
|
||
}
|
||
function extractAddressFallbackYear(text) {
|
||
const source = String(text ?? "");
|
||
const fullYearMatch = source.match(/\b(20\d{2})\b/);
|
||
if (fullYearMatch) {
|
||
return fullYearMatch[1];
|
||
}
|
||
const shortYearMatch = source.match(/(?:^|[^0-9])(\d{2})\s*(?:г(?:од|ода)?|г|год|year|god)(?=$|[^a-zа-яё0-9])/iu);
|
||
if (shortYearMatch) {
|
||
const shortYear = Number(shortYearMatch[1]);
|
||
if (Number.isFinite(shortYear) && shortYear >= 0 && shortYear <= 99) {
|
||
return String(2000 + shortYear);
|
||
}
|
||
}
|
||
const shortOrdinalYearMatch = source.match(/(?:^|[^0-9])(\d{2})\s*(?:[-\s]?(?:й|ый|ой|th))(?=$|[^a-zа-яё0-9])/iu);
|
||
if (!shortOrdinalYearMatch) {
|
||
return null;
|
||
}
|
||
const shortOrdinalYear = Number(shortOrdinalYearMatch[1]);
|
||
if (!Number.isFinite(shortOrdinalYear) || shortOrdinalYear < 0 || shortOrdinalYear > 99) {
|
||
return null;
|
||
}
|
||
return String(2000 + shortOrdinalYear);
|
||
}
|
||
function extractAddressFallbackMonthYear(text) {
|
||
const source = String(text ?? "");
|
||
const numericYearMonth = source.match(/\b(20\d{2})[./-](0?[1-9]|1[0-2])\b/);
|
||
if (numericYearMonth) {
|
||
const year = numericYearMonth[1];
|
||
const month = String(Number(numericYearMonth[2])).padStart(2, "0");
|
||
return `${year}-${month}`;
|
||
}
|
||
const numericMonthYear = source.match(/\b(0?[1-9]|1[0-2])[./-](20\d{2})\b/);
|
||
if (numericMonthYear) {
|
||
const month = String(Number(numericMonthYear[1])).padStart(2, "0");
|
||
const year = numericMonthYear[2];
|
||
return `${year}-${month}`;
|
||
}
|
||
const namedMonthYear = source.match(/(?:^|[^a-zа-яё0-9])([a-zа-яё]+)\s+(20\d{2})(?=$|[^a-zа-яё0-9])/iu);
|
||
if (namedMonthYear) {
|
||
const month = normalizeAddressMonthAliasToken(namedMonthYear[1]);
|
||
if (month) {
|
||
return `${namedMonthYear[2]}-${month}`;
|
||
}
|
||
}
|
||
const yearNamedMonth = source.match(/(?:^|[^a-zа-яё0-9])(20\d{2})\s+([a-zа-яё]+)(?=$|[^a-zа-яё0-9])/iu);
|
||
if (yearNamedMonth) {
|
||
const month = normalizeAddressMonthAliasToken(yearNamedMonth[2]);
|
||
if (month) {
|
||
return `${yearNamedMonth[1]}-${month}`;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function extractAddressFallbackAccountToken(text) {
|
||
const source = String(text ?? "");
|
||
const explicitMatch = source.match(/(?:сч[её]т(?:а|у|ом|е)?|account)\D{0,12}(\d{2}(?:[.,]\d{1,2})?)/iu);
|
||
if (explicitMatch && explicitMatch[1]) {
|
||
return String(explicitMatch[1]).replace(",", ".");
|
||
}
|
||
const tokenPattern = /\b(\d{2}(?:[.,]\d{1,2})?)\b/giu;
|
||
let match = tokenPattern.exec(source);
|
||
while (match) {
|
||
const raw = String(match[1] ?? "");
|
||
const start = match.index;
|
||
const end = start + raw.length;
|
||
const prev = start > 0 ? source[start - 1] : " ";
|
||
const next = end < source.length ? source[end] : " ";
|
||
if (!/[./-]/.test(prev) && !/[./-]/.test(next) && !/\d/.test(prev) && !/\d/.test(next)) {
|
||
return raw.replace(",", ".");
|
||
}
|
||
match = tokenPattern.exec(source);
|
||
}
|
||
return null;
|
||
}
|
||
function pickAddressFallbackCounterpartyToken(text) {
|
||
const source = String(text ?? "");
|
||
const byAnchor = source.match(/(?:^|[\s,.;:!?()\-])(?:по|от)\s+([a-zа-яё][a-zа-яё0-9._-]{1,})(?=$|[\s,.;:!?()\-])/iu);
|
||
if (byAnchor && byAnchor[1]) {
|
||
const byToken = String(byAnchor[1]).trim();
|
||
const normalizedByToken = byToken.toLowerCase();
|
||
if (byToken &&
|
||
!ADDRESS_PREDECOMPOSE_NOISE_TOKENS.has(normalizedByToken) &&
|
||
!/^\d{2}(?:\.\d{1,2})?$/.test(normalizedByToken) &&
|
||
!/^(?:19|20)\d{2}$/.test(normalizedByToken) &&
|
||
!/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(normalizedByToken) &&
|
||
!/^(?:договор|договора|договору|договором|договоре|contract|dogovor|dog|дог|d[oо]g|д[oо]г)$/.test(normalizedByToken)) {
|
||
return byToken;
|
||
}
|
||
}
|
||
const candidates = extractAddressAnchorTokens(text);
|
||
for (const token of candidates) {
|
||
const normalized = String(token ?? "").toLowerCase();
|
||
if (!normalized || /^\d{2}(?:\.\d{1,2})?$/.test(normalized)) {
|
||
continue;
|
||
}
|
||
if (/^(?:19|20)\d{2}$/.test(normalized)) {
|
||
continue;
|
||
}
|
||
if (/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(normalized)) {
|
||
continue;
|
||
}
|
||
if (/^(?:договор|договора|договору|договором|договоре|contract|dogovor|dog|дог|d[oо]g|д[oо]г)$/.test(normalized)) {
|
||
continue;
|
||
}
|
||
return token;
|
||
}
|
||
return null;
|
||
}
|
||
function extractAddressFallbackContractToken(text) {
|
||
const source = String(text ?? "");
|
||
const patterns = [
|
||
/(?:договор(?:а|у|ом|е)?|дог\.?|[dд][oо][gг]\.?|contract|dogovor|dog\.?)\s*(?:№|#|n|no\.?)?\s*([a-zа-я0-9][a-zа-я0-9/_-]{1,})/iu,
|
||
/(?:№|#|n|no\.?)\s*([a-zа-я0-9][a-zа-я0-9/_-]{1,})\s*(?:договор(?:а|у|ом|е)?|дог\.?|[dд][oо][gг]\.?|contract|dogovor|dog\.?)/iu
|
||
];
|
||
for (const pattern of patterns) {
|
||
const match = pattern.exec(source);
|
||
if (!match || !match[1]) {
|
||
continue;
|
||
}
|
||
const candidate = String(match[1]).replace(/^[^a-zа-я0-9]+|[^a-zа-я0-9/_-]+$/giu, "");
|
||
if (!candidate || candidate.length < 2) {
|
||
continue;
|
||
}
|
||
if (/^(?:19|20)\d{2}$/.test(candidate)) {
|
||
continue;
|
||
}
|
||
if (/^(?:19|20)\d{2}[./-](?:0?[1-9]|1[0-2])(?:[./-](?:0?[1-9]|[12]\d|3[01]))?$/.test(candidate)) {
|
||
continue;
|
||
}
|
||
return candidate;
|
||
}
|
||
if (ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source) || /(?:^|[^\p{L}\p{N}_])(?:[dд][oо][gг]|dogovor)(?=$|[^\p{L}\p{N}_])/iu.test(source)) {
|
||
const generic = source.match(/\b([a-zа-я0-9]{1,10}[/-][a-zа-я0-9]{1,10}(?:[/-][a-zа-я0-9]{1,10})?)\b/iu);
|
||
if (generic && generic[1]) {
|
||
return generic[1];
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage) {
|
||
const sourceRaw = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")));
|
||
const source = compactWhitespace(String(sanitizedUserMessage ?? sourceRaw).toLowerCase());
|
||
if (!source) {
|
||
return null;
|
||
}
|
||
if (ADDRESS_MANAGEMENT_PROFILE_PATTERN.test(source)) {
|
||
return null;
|
||
}
|
||
const monthYear = extractAddressFallbackMonthYear(source);
|
||
const year = extractAddressFallbackYear(source);
|
||
const allTime = ADDRESS_ALL_TIME_PATTERN.test(source);
|
||
const account = extractAddressFallbackAccountToken(source);
|
||
const docsSignal = ADDRESS_DOCS_SIGNAL_PATTERN.test(source);
|
||
const bankSignal = ADDRESS_BANK_SIGNAL_PATTERN.test(source);
|
||
const contractSignal = ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source);
|
||
const balanceSignal = ADDRESS_BALANCE_SIGNAL_PATTERN.test(source);
|
||
const hasIndexPointerSignal = /(?:\u043f\u0443\u043d\u043a\u0442|\u043f\u043e\u0437\u0438\u0446|\u0441\u0442\u0440\u043e\u043a|item|row|line)/iu.test(sourceRaw);
|
||
if (hasIndexPointerSignal && extractDisplayedEntityIndexMention(sourceRaw) !== null) {
|
||
return null;
|
||
}
|
||
if (balanceSignal && account) {
|
||
let periodClause = "";
|
||
let rule = "balance_account_rewrite";
|
||
if (monthYear) {
|
||
periodClause = ` на ${monthYear}`;
|
||
rule = "balance_month_period_rewrite";
|
||
}
|
||
else if (year) {
|
||
periodClause = ` на ${year}-12-31`;
|
||
rule = "balance_year_period_rewrite";
|
||
}
|
||
const candidate = compactWhitespace(`остаток по счету ${account}${periodClause}`);
|
||
if (candidate && candidate !== sourceRaw.toLowerCase()) {
|
||
return {
|
||
candidate,
|
||
rule
|
||
};
|
||
}
|
||
}
|
||
if (!docsSignal && !contractSignal && !balanceSignal) {
|
||
const counterparty = pickAddressFallbackCounterpartyToken(source);
|
||
const genericLookupSignal = /(?:\bесть\b|\bпокажи\b|\bвыведи\b|\bч[её]\b|\bчто\b)/iu.test(source);
|
||
if (counterparty && (allTime || monthYear || year) && genericLookupSignal) {
|
||
let periodClause = "";
|
||
let rule = "documents_counterparty_rewrite_from_generic_lookup";
|
||
if (allTime) {
|
||
periodClause = " за все время";
|
||
rule = "documents_counterparty_all_time_rewrite_from_generic_lookup";
|
||
}
|
||
else if (monthYear) {
|
||
periodClause = ` за ${monthYear}`;
|
||
rule = "documents_counterparty_month_rewrite_from_generic_lookup";
|
||
}
|
||
else if (year) {
|
||
periodClause = ` за ${year} год`;
|
||
rule = "documents_counterparty_year_rewrite_from_generic_lookup";
|
||
}
|
||
const candidate = compactWhitespace(`документы по контрагенту ${counterparty}${periodClause}`);
|
||
if (candidate && candidate !== sourceRaw.toLowerCase()) {
|
||
return {
|
||
candidate,
|
||
rule
|
||
};
|
||
}
|
||
}
|
||
}
|
||
if (docsSignal) {
|
||
const contract = extractAddressFallbackContractToken(sourceRaw || source);
|
||
if (contractSignal || contract) {
|
||
if (contract) {
|
||
let periodClause = "";
|
||
let rule = bankSignal ? "bank_operations_contract_rewrite" : "documents_contract_rewrite";
|
||
if (allTime) {
|
||
periodClause = " за все время";
|
||
rule = bankSignal ? "bank_operations_contract_all_time_rewrite" : "documents_contract_all_time_rewrite";
|
||
}
|
||
else if (monthYear) {
|
||
periodClause = ` за ${monthYear}`;
|
||
rule = bankSignal ? "bank_operations_contract_month_rewrite" : "documents_contract_month_rewrite";
|
||
}
|
||
else if (year) {
|
||
periodClause = ` за ${year} год`;
|
||
rule = bankSignal ? "bank_operations_contract_year_rewrite" : "documents_contract_year_rewrite";
|
||
}
|
||
const subject = bankSignal ? "банковские операции" : "документы";
|
||
const candidate = compactWhitespace(`${subject} по договору ${contract}${periodClause}`);
|
||
if (candidate && candidate !== sourceRaw.toLowerCase()) {
|
||
return {
|
||
candidate,
|
||
rule
|
||
};
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
const counterparty = pickAddressFallbackCounterpartyToken(source);
|
||
if (counterparty) {
|
||
let periodClause = "";
|
||
const subject = bankSignal ? "банковские операции" : "документы";
|
||
const rulePrefix = bankSignal ? "bank_operations_counterparty" : "documents_counterparty";
|
||
let rule = `${rulePrefix}_rewrite`;
|
||
if (allTime) {
|
||
periodClause = " за все время";
|
||
rule = `${rulePrefix}_all_time_rewrite`;
|
||
}
|
||
else if (monthYear) {
|
||
periodClause = ` за ${monthYear}`;
|
||
rule = `${rulePrefix}_month_rewrite`;
|
||
}
|
||
else if (year) {
|
||
periodClause = ` за ${year} год`;
|
||
rule = `${rulePrefix}_year_rewrite`;
|
||
}
|
||
const candidate = compactWhitespace(`${subject} по контрагенту ${counterparty}${periodClause}`);
|
||
if (candidate && candidate !== sourceRaw.toLowerCase()) {
|
||
return {
|
||
candidate,
|
||
rule
|
||
};
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (source !== sourceRaw.toLowerCase() && isAddressLlmPreDecomposeCandidate(source)) {
|
||
return {
|
||
candidate: source,
|
||
rule: "noise_cleanup"
|
||
};
|
||
}
|
||
return null;
|
||
}
|
||
function textMojibakeScoreForAddress(value) {
|
||
const source = String(value ?? "");
|
||
const cyrillic = (source.match(/[А-Яа-яЁё]/g) ?? []).length;
|
||
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ<D08B>?’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||
const doubleEncodedMarkers = (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length;
|
||
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||
}
|
||
function looksLikeMojibakeForAddress(value) {
|
||
const source = String(value ?? "");
|
||
if (!source.trim()) {
|
||
return false;
|
||
}
|
||
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ<D08B>?’“”•–—™љ›њќћџ]/.test(source)) {
|
||
return true;
|
||
}
|
||
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
||
return true;
|
||
}
|
||
if ((source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length >= 2) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function repairAddressMojibake(value) {
|
||
const source = String(value ?? "");
|
||
if (!looksLikeMojibakeForAddress(source)) {
|
||
return source;
|
||
}
|
||
let candidate = source;
|
||
for (let pass = 0; pass < 3; pass += 1) {
|
||
let improved = false;
|
||
try {
|
||
const fromWin1251 = iconv_lite_1.default.encode(candidate, "win1251").toString("utf8");
|
||
if (textMojibakeScoreForAddress(fromWin1251) > textMojibakeScoreForAddress(candidate)) {
|
||
candidate = fromWin1251;
|
||
improved = true;
|
||
}
|
||
}
|
||
catch (_error) { }
|
||
try {
|
||
const fromLatin1 = Buffer.from(candidate, "latin1").toString("utf8");
|
||
if (textMojibakeScoreForAddress(fromLatin1) > textMojibakeScoreForAddress(candidate)) {
|
||
candidate = fromLatin1;
|
||
improved = true;
|
||
}
|
||
}
|
||
catch (_error) { }
|
||
if (!improved) {
|
||
break;
|
||
}
|
||
}
|
||
return candidate;
|
||
}
|
||
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
||
const repaired = repairAddressMojibake(String(value ?? ""));
|
||
const sanitized = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(repaired) ?? "").trim();
|
||
if (sanitized) {
|
||
return sanitized;
|
||
}
|
||
const fallbackText = String(fallback ?? "").trim();
|
||
return fallbackText || "Не смог сформировать читаемый ответ. Уточните запрос.";
|
||
}
|
||
function extractAddressAnchorTokens(value) {
|
||
const source = repairAddressMojibake(compactWhitespace(String(value ?? "").toLowerCase()));
|
||
if (!source) {
|
||
return [];
|
||
}
|
||
const tokens = source
|
||
.split(/[^a-zа-яё0-9._-]+/iu)
|
||
.map((item) => item.trim())
|
||
.filter((item) => item.length >= 2);
|
||
const filtered = [];
|
||
for (const token of tokens) {
|
||
if (/^\d+$/.test(token)) {
|
||
continue;
|
||
}
|
||
if (/^(?:19|20)\d{2}$/.test(token)) {
|
||
continue;
|
||
}
|
||
if (/^(?:0?[1-9]|1[0-2])[./-](?:19|20)\d{2}$/.test(token) || /^(?:19|20)\d{2}[./-](?:0?[1-9]|1[0-2])$/.test(token)) {
|
||
continue;
|
||
}
|
||
if (/^(?:янв|фев|мар|апр|май|июн|июл|авг|сен|сент|окт|ноя|дек|january|february|march|april|may|june|july|august|september|october|november|december)/i.test(token)) {
|
||
continue;
|
||
}
|
||
if (ADDRESS_PREDECOMPOSE_NOISE_TOKENS.has(token)) {
|
||
continue;
|
||
}
|
||
filtered.push(token);
|
||
}
|
||
return Array.from(new Set(filtered));
|
||
}
|
||
function selectPreferredAddressFragmentCandidate(rawText, normalizedText) {
|
||
const normalizedCandidate = compactWhitespace(repairAddressMojibake(normalizedText ?? ""));
|
||
const rawCandidate = compactWhitespace(repairAddressMojibake(rawText ?? ""));
|
||
if (!normalizedCandidate && !rawCandidate) {
|
||
return null;
|
||
}
|
||
if (!normalizedCandidate) {
|
||
return rawCandidate;
|
||
}
|
||
if (!rawCandidate) {
|
||
return normalizedCandidate;
|
||
}
|
||
const normalizedAnchors = extractAddressAnchorTokens(normalizedCandidate);
|
||
const rawAnchors = extractAddressAnchorTokens(rawCandidate);
|
||
if (rawAnchors.length > 0 && normalizedAnchors.length === 0) {
|
||
return rawCandidate;
|
||
}
|
||
return normalizedCandidate;
|
||
}
|
||
function readAddressFilterString(addressDebug, key) {
|
||
const filters = addressDebug?.extracted_filters;
|
||
if (!filters || typeof filters !== "object") {
|
||
return null;
|
||
}
|
||
return toNonEmptyString(filters[key]);
|
||
}
|
||
function readAddressInventoryItemFilter(addressDebug) {
|
||
return readAddressFilterString(addressDebug, "item");
|
||
}
|
||
function isAddressLaneDebugPayload(debug) {
|
||
if (!debug || typeof debug !== "object") {
|
||
return false;
|
||
}
|
||
if (debug.detected_mode === "address_query") {
|
||
return true;
|
||
}
|
||
if (typeof debug.selected_recipe === "string" && debug.selected_recipe.trim().length > 0) {
|
||
return true;
|
||
}
|
||
if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) {
|
||
return true;
|
||
}
|
||
if (typeof debug.anchor_type === "string" && debug.anchor_type.trim().length > 0) {
|
||
return true;
|
||
}
|
||
if (debug.extracted_filters && typeof debug.extracted_filters === "object") {
|
||
const keys = Object.keys(debug.extracted_filters);
|
||
if (keys.length > 0 && typeof debug.detected_intent === "string" && debug.detected_intent.trim().length > 0) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
function findLastAddressAssistantItem(items) {
|
||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||
const item = items[index];
|
||
if (!item || item.role !== "assistant" || !item.debug) {
|
||
continue;
|
||
}
|
||
const debug = item.debug;
|
||
if (isAddressLaneDebugPayload(debug)) {
|
||
return item;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function findLastAddressAssistantDebug(items) {
|
||
return findLastAddressAssistantItem(items)?.debug ?? null;
|
||
}
|
||
const FOLLOWUP_DISPLAY_COUNTERPARTY_STOPWORDS = new Set([
|
||
"группа",
|
||
"компания",
|
||
"организация",
|
||
"контрагент",
|
||
"контрагента",
|
||
"контрагенту",
|
||
"клиент",
|
||
"клиента",
|
||
"клиенту",
|
||
"заказчик",
|
||
"заказчика",
|
||
"заказчику",
|
||
"поставщик",
|
||
"поставщика",
|
||
"поставщику",
|
||
"ип",
|
||
"ооо",
|
||
"ао",
|
||
"зао",
|
||
"пао",
|
||
"оао",
|
||
"llc",
|
||
"ltd",
|
||
"inc",
|
||
"corp",
|
||
"company",
|
||
"group",
|
||
"vendor",
|
||
"supplier",
|
||
"customer",
|
||
"client"
|
||
]);
|
||
const FOLLOWUP_DISPLAY_COUNTERPARTY_LEGAL_TOKENS = new Set([
|
||
"ип",
|
||
"ооо",
|
||
"ао",
|
||
"зао",
|
||
"пао",
|
||
"оао",
|
||
"llc",
|
||
"ltd",
|
||
"inc",
|
||
"corp",
|
||
"company",
|
||
"group"
|
||
]);
|
||
const FOLLOWUP_DISPLAY_ENTITY_TYPE_BY_INTENT = {
|
||
counterparty_activity_lifecycle: "counterparty",
|
||
customer_revenue_and_payments: "counterparty",
|
||
supplier_payouts_profile: "counterparty",
|
||
counterparty_population_and_roles: "counterparty",
|
||
contract_usage_and_value: "contract",
|
||
list_contracts_by_counterparty: "contract"
|
||
};
|
||
function normalizeCounterpartyForFollowupMatch(value) {
|
||
return compactWhitespace(repairAddressMojibake(String(value ?? ""))
|
||
.toLowerCase()
|
||
.replace(/ё/g, "е")
|
||
.replace(/[«»"'`“”„’<E2809E>?]/g, " ")
|
||
.replace(/[^a-zа-я0-9\s._-]+/giu, " "));
|
||
}
|
||
function normalizeCounterpartyTokenForFollowupMatch(value) {
|
||
return normalizeCounterpartyForFollowupMatch(value).replace(/[._-]+/g, "");
|
||
}
|
||
function normalizeCounterpartyStemForFollowupMatch(value) {
|
||
const compact = normalizeCounterpartyTokenForFollowupMatch(value);
|
||
if (!compact || !/[а-яё]/iu.test(compact)) {
|
||
return compact;
|
||
}
|
||
const stem = compact.replace(/(?:иями|ями|ами|ией|ей|ий|ов|ев|ом|ем|ах|ях|ую|юю|ая|яя|ое|ее|ые|ие|ого|его|ому|ему|ыми|ими|ым|им|ам|ям|у|ю|а|я|е|и|ы|о)$/iu, "");
|
||
return stem.length >= 3 ? stem : compact;
|
||
}
|
||
function inferDisplayedEntityTypeFromIntent(intent) {
|
||
const normalized = compactWhitespace(String(intent ?? "").toLowerCase());
|
||
if (!normalized) {
|
||
return "unknown";
|
||
}
|
||
return FOLLOWUP_DISPLAY_ENTITY_TYPE_BY_INTENT[normalized] ?? "unknown";
|
||
}
|
||
function extractDisplayedAddressEntityCandidates(replyText, entityType = "unknown") {
|
||
if (entityType === "unknown") {
|
||
return [];
|
||
}
|
||
const lines = String(replyText ?? "").split(/\r?\n/);
|
||
const candidates = [];
|
||
for (const line of lines) {
|
||
const compactLine = compactWhitespace(line);
|
||
if (!compactLine) {
|
||
continue;
|
||
}
|
||
const numberedMatch = compactLine.match(/^(\d+)\.\s+(.+)$/);
|
||
if (!numberedMatch) {
|
||
continue;
|
||
}
|
||
const index = Number.parseInt(String(numberedMatch[1] ?? ""), 10);
|
||
if (!Number.isFinite(index) || index <= 0) {
|
||
continue;
|
||
}
|
||
const afterNumber = String(numberedMatch[2] ?? "");
|
||
const parts = afterNumber.split("|").map((item) => compactWhitespace(item));
|
||
let counterpartyCandidate = parts[0] ?? "";
|
||
if (parts.length >= 2 && /^\d{4}-\d{2}-\d{2}/.test(parts[0] ?? "")) {
|
||
counterpartyCandidate = parts[1] ?? counterpartyCandidate;
|
||
}
|
||
const cleanedCandidate = compactWhitespace(counterpartyCandidate.replace(/^["'«»“”„`’<>?]+|["'«»“”„`’<>?]+$/gu, ""));
|
||
if (!cleanedCandidate || cleanedCandidate.length < 2) {
|
||
continue;
|
||
}
|
||
candidates.push({
|
||
index,
|
||
value: cleanedCandidate,
|
||
entityType
|
||
});
|
||
}
|
||
const dedup = new Map();
|
||
for (const candidate of candidates) {
|
||
const key = `${candidate.entityType}:${candidate.index}:${normalizeCounterpartyForFollowupMatch(candidate.value)}`;
|
||
if (!dedup.has(key)) {
|
||
dedup.set(key, candidate);
|
||
}
|
||
}
|
||
return Array.from(dedup.values());
|
||
}
|
||
function buildCounterpartyAliasesForFollowupMatch(counterpartyName) {
|
||
const aliases = new Set();
|
||
const normalized = normalizeCounterpartyForFollowupMatch(counterpartyName);
|
||
if (!normalized) {
|
||
return [];
|
||
}
|
||
aliases.add(normalized);
|
||
const normalizedTokens = normalized
|
||
.split(/\s+/)
|
||
.map((token) => token.trim())
|
||
.filter(Boolean);
|
||
const tokensForAlias = Array.from(new Set(normalizedTokens.flatMap((token) => [token, ...token.split(/-+/).map((part) => part.trim()).filter(Boolean)])));
|
||
const withoutLegalTokens = tokensForAlias
|
||
.filter((token) => !FOLLOWUP_DISPLAY_COUNTERPARTY_LEGAL_TOKENS.has(token))
|
||
.join(" ");
|
||
if (withoutLegalTokens) {
|
||
aliases.add(withoutLegalTokens);
|
||
}
|
||
for (const token of tokensForAlias) {
|
||
const compactToken = normalizeCounterpartyTokenForFollowupMatch(token);
|
||
if (compactToken.length < 3) {
|
||
continue;
|
||
}
|
||
if (FOLLOWUP_DISPLAY_COUNTERPARTY_STOPWORDS.has(compactToken)) {
|
||
continue;
|
||
}
|
||
if (/^(?:19|20)\d{2}$/.test(compactToken)) {
|
||
continue;
|
||
}
|
||
aliases.add(compactToken);
|
||
const stemToken = normalizeCounterpartyStemForFollowupMatch(compactToken);
|
||
if (stemToken.length >= 4) {
|
||
aliases.add(stemToken);
|
||
}
|
||
}
|
||
return Array.from(aliases)
|
||
.map((alias) => compactWhitespace(alias))
|
||
.filter((alias) => alias.length > 0)
|
||
.sort((left, right) => right.length - left.length);
|
||
}
|
||
function hasCounterpartyAliasMention(normalizedMessage, alias) {
|
||
const trimmedAlias = compactWhitespace(String(alias ?? "").toLowerCase());
|
||
if (!trimmedAlias) {
|
||
return false;
|
||
}
|
||
const aliasPattern = escapeRegex(trimmedAlias).replace(/\s+/g, "\\s+");
|
||
const boundaryPattern = new RegExp(`(?:^|[^a-zа-я0-9])${aliasPattern}(?:$|[^a-zа-я0-9])`, "iu");
|
||
if (boundaryPattern.test(normalizedMessage)) {
|
||
return true;
|
||
}
|
||
if (trimmedAlias.length < 4 || !/[а-яё]/iu.test(trimmedAlias)) {
|
||
return false;
|
||
}
|
||
const fuzzyPattern = new RegExp(`(?:^|[^a-zа-я0-9])${aliasPattern}[а-яё]{0,3}(?:$|[^a-zа-я0-9])`, "iu");
|
||
return fuzzyPattern.test(normalizedMessage);
|
||
}
|
||
function extractDisplayedEntityIndexMention(userMessage) {
|
||
const normalized = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||
if (!normalized) {
|
||
return null;
|
||
}
|
||
const tokenStart = "(?:^|[^\\p{L}\\p{N}_])";
|
||
const tokenEnd = "(?=$|[^\\p{L}\\p{N}_])";
|
||
const pointerPattern = "(?:\\u043f\\u0443\\u043d\\u043a\\u0442(?:\\u0430|\\u0443|\\u043e\\u043c)?|\\u043f\\u043e\\u0437\\u0438\\u0446\\u0438(?:\\u044f|\\u0438|\\u044e|\\u0435\\u0439)|\\u0441\\u0442\\u0440\\u043e\\u043a(?:\\u0430|\\u0438|\\u0435|\\u0443)|item|row|line)";
|
||
const pointerSignalPattern = new RegExp(`${tokenStart}${pointerPattern}${tokenEnd}`, "iu");
|
||
const directPattern = new RegExp(`${tokenStart}${pointerPattern}${tokenEnd}\\D{0,8}(\\d{1,3})(?!\\d)`, "iu");
|
||
const directMatch = normalized.match(directPattern);
|
||
if (directMatch) {
|
||
const value = Number.parseInt(String(directMatch[1] ?? ""), 10);
|
||
return Number.isFinite(value) && value > 0 ? value : null;
|
||
}
|
||
const reversePattern = new RegExp(`${tokenStart}(\\d{1,3})(?:-?(?:\\u0439|\\u044f|\\u0435|\\u0433\\u043e|\\u043c\\u0443))?\\s+${pointerPattern}${tokenEnd}`, "iu");
|
||
const reverseMatch = normalized.match(reversePattern);
|
||
if (reverseMatch) {
|
||
const value = Number.parseInt(String(reverseMatch[1] ?? ""), 10);
|
||
return Number.isFinite(value) && value > 0 ? value : null;
|
||
}
|
||
if (pointerSignalPattern.test(normalized)) {
|
||
const numericMatches = Array.from(normalized.matchAll(/(?:^|[^\p{N}])(\d{1,3})(?!\d)/gu))
|
||
.map((match) => Number.parseInt(String(match[1] ?? ""), 10))
|
||
.filter((value) => Number.isFinite(value) && value > 0);
|
||
if (numericMatches.length === 1) {
|
||
return numericMatches[0];
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function resolveDisplayedAddressEntityMention(userMessage, displayedEntities) {
|
||
const normalizedMessage = normalizeCounterpartyForFollowupMatch(userMessage);
|
||
if (!normalizedMessage) {
|
||
return null;
|
||
}
|
||
if (!Array.isArray(displayedEntities) || displayedEntities.length === 0) {
|
||
return null;
|
||
}
|
||
const indexMention = extractDisplayedEntityIndexMention(userMessage);
|
||
if (indexMention !== null) {
|
||
const indexedCandidate = displayedEntities.find((candidate) => Number(candidate.index) === indexMention);
|
||
if (indexedCandidate) {
|
||
return {
|
||
value: indexedCandidate.value,
|
||
entityType: indexedCandidate.entityType,
|
||
matchKind: "index",
|
||
index: indexedCandidate.index
|
||
};
|
||
}
|
||
}
|
||
let bestMatch = null;
|
||
for (const candidate of displayedEntities) {
|
||
const aliases = buildCounterpartyAliasesForFollowupMatch(candidate.value);
|
||
for (const alias of aliases) {
|
||
if (!hasCounterpartyAliasMention(normalizedMessage, alias)) {
|
||
continue;
|
||
}
|
||
const score = alias.length * 10 + (normalizeCounterpartyForFollowupMatch(candidate.value) === alias ? 1 : 0);
|
||
if (!bestMatch || score > bestMatch.score) {
|
||
bestMatch = {
|
||
value: candidate.value,
|
||
entityType: candidate.entityType,
|
||
index: candidate.index,
|
||
matchKind: "alias",
|
||
score
|
||
};
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
if (!bestMatch) {
|
||
return null;
|
||
}
|
||
return {
|
||
value: bestMatch.value,
|
||
entityType: bestMatch.entityType,
|
||
matchKind: bestMatch.matchKind,
|
||
index: bestMatch.index
|
||
};
|
||
}
|
||
function findRecentAddressFilterValue(items, key) {
|
||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||
const item = items[index];
|
||
if (!item || item.role !== "assistant" || !item.debug) {
|
||
continue;
|
||
}
|
||
const debug = item.debug;
|
||
if (!isAddressLaneDebugPayload(debug)) {
|
||
continue;
|
||
}
|
||
const replyType = toNonEmptyString(item.reply_type);
|
||
const limitedReasonCategory = toNonEmptyString(debug.limited_reason_category);
|
||
if ((replyType && replyType !== "factual" && replyType !== "factual_with_explanation") ||
|
||
limitedReasonCategory) {
|
||
continue;
|
||
}
|
||
const directFilterValue = readAddressFilterString(debug, key);
|
||
if (directFilterValue) {
|
||
return directFilterValue;
|
||
}
|
||
if (key === "contract" && String(debug.anchor_type ?? "").trim() === "contract") {
|
||
const anchorValue = toNonEmptyString(debug.anchor_value_resolved) ?? toNonEmptyString(debug.anchor_value_raw);
|
||
if (anchorValue) {
|
||
return anchorValue;
|
||
}
|
||
}
|
||
if (key === "counterparty" && String(debug.anchor_type ?? "").trim() === "counterparty") {
|
||
const anchorValue = toNonEmptyString(debug.anchor_value_resolved) ?? toNonEmptyString(debug.anchor_value_raw);
|
||
if (anchorValue) {
|
||
return anchorValue;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function isInventoryRootFrameIntent(intent) {
|
||
return intent === "inventory_on_hand_as_of_date";
|
||
}
|
||
function isInventoryDrilldownFrameIntent(intent) {
|
||
return intent === "inventory_purchase_provenance_for_item" ||
|
||
intent === "inventory_purchase_documents_for_item" ||
|
||
intent === "inventory_sale_trace_for_item" ||
|
||
intent === "inventory_profitability_for_item" ||
|
||
intent === "inventory_purchase_to_sale_chain" ||
|
||
intent === "inventory_aging_by_purchase_date";
|
||
}
|
||
function resolveAddressIntentFamily(intent) {
|
||
const normalizedIntent = toNonEmptyString(intent);
|
||
if (!normalizedIntent) {
|
||
return null;
|
||
}
|
||
if (normalizedIntent.startsWith("inventory_")) {
|
||
return "inventory";
|
||
}
|
||
if (normalizedIntent.startsWith("vat_")) {
|
||
return "vat";
|
||
}
|
||
if (normalizedIntent === "account_balance_snapshot" || normalizedIntent === "documents_forming_balance") {
|
||
return "balance";
|
||
}
|
||
if (normalizedIntent === "open_items_by_counterparty_or_contract" ||
|
||
normalizedIntent === "list_documents_by_counterparty" ||
|
||
normalizedIntent === "bank_operations_by_counterparty" ||
|
||
normalizedIntent === "list_contracts_by_counterparty" ||
|
||
normalizedIntent === "list_documents_by_contract" ||
|
||
normalizedIntent === "bank_operations_by_contract" ||
|
||
normalizedIntent === "receivables_confirmed_for_period" ||
|
||
normalizedIntent === "payables_confirmed_for_period") {
|
||
return "settlements";
|
||
}
|
||
return null;
|
||
}
|
||
function extractAddressCarryoverAnchor(addressDebug) {
|
||
if (!isAddressLaneDebugPayload(addressDebug)) {
|
||
return {
|
||
anchorType: null,
|
||
anchorValue: null
|
||
};
|
||
}
|
||
return {
|
||
anchorType: toNonEmptyString(addressDebug.anchor_type),
|
||
anchorValue: toNonEmptyString(addressDebug.anchor_value_resolved) ??
|
||
toNonEmptyString(addressDebug.anchor_value_raw) ??
|
||
readAddressInventoryItemFilter(addressDebug) ??
|
||
readAddressFilterString(addressDebug, "counterparty") ??
|
||
readAddressFilterString(addressDebug, "contract") ??
|
||
readAddressFilterString(addressDebug, "account")
|
||
};
|
||
}
|
||
function findRecentInventoryRootFrame(items) {
|
||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||
const item = items[index];
|
||
if (!item || item.role !== "assistant" || !item.debug) {
|
||
continue;
|
||
}
|
||
const debug = item.debug;
|
||
if (!isAddressLaneDebugPayload(debug)) {
|
||
continue;
|
||
}
|
||
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
||
if (!isInventoryRootFrameIntent(detectedIntent)) {
|
||
continue;
|
||
}
|
||
const anchor = extractAddressCarryoverAnchor(debug);
|
||
const filtersRaw = debug.extracted_filters;
|
||
const filters = filtersRaw && typeof filtersRaw === "object"
|
||
? { ...filtersRaw }
|
||
: {};
|
||
return {
|
||
intent: detectedIntent,
|
||
filters,
|
||
anchorType: anchor.anchorType,
|
||
anchorValue: anchor.anchorValue,
|
||
messageId: toNonEmptyString(item.message_id)
|
||
};
|
||
}
|
||
return null;
|
||
}
|
||
const ADDRESS_FOLLOWUP_OFFER_BY_INTENT = {
|
||
list_documents_by_counterparty: ["bank_operations_by_counterparty", "list_contracts_by_counterparty"],
|
||
bank_operations_by_counterparty: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
|
||
list_contracts_by_counterparty: ["list_documents_by_contract", "bank_operations_by_contract"],
|
||
list_documents_by_contract: ["bank_operations_by_contract"],
|
||
bank_operations_by_contract: ["list_documents_by_contract"],
|
||
open_items_by_counterparty_or_contract: ["list_documents_by_counterparty", "bank_operations_by_counterparty"]
|
||
};
|
||
function buildAddressFollowupOffer(addressDebug) {
|
||
if (!isAddressLaneDebugPayload(addressDebug)) {
|
||
return null;
|
||
}
|
||
const intent = toNonEmptyString(addressDebug.detected_intent);
|
||
if (!intent) {
|
||
return null;
|
||
}
|
||
const suggestedIntents = ADDRESS_FOLLOWUP_OFFER_BY_INTENT[intent];
|
||
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
|
||
return null;
|
||
}
|
||
const anchorType = toNonEmptyString(addressDebug.anchor_type);
|
||
const anchorValue = toNonEmptyString(addressDebug.anchor_value_resolved) ??
|
||
toNonEmptyString(addressDebug.anchor_value_raw) ??
|
||
readAddressInventoryItemFilter(addressDebug) ??
|
||
readAddressFilterString(addressDebug, "counterparty") ??
|
||
readAddressFilterString(addressDebug, "contract") ??
|
||
readAddressFilterString(addressDebug, "account");
|
||
return {
|
||
enabled: true,
|
||
source_intent: intent,
|
||
anchor_type: anchorType ?? "unknown",
|
||
anchor_value: anchorValue,
|
||
suggested_intents: suggestedIntents
|
||
};
|
||
}
|
||
function hasAddressFollowupOffer(addressDebug) {
|
||
if (!addressDebug || typeof addressDebug !== "object") {
|
||
return false;
|
||
}
|
||
const existingOffer = addressDebug.address_followup_offer;
|
||
if (existingOffer && typeof existingOffer === "object") {
|
||
return existingOffer.enabled === true;
|
||
}
|
||
return Boolean(buildAddressFollowupOffer(addressDebug));
|
||
}
|
||
function isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) {
|
||
const contract = llmPreDecomposeMeta?.predecomposeContract;
|
||
if (!contract || typeof contract !== "object") {
|
||
return false;
|
||
}
|
||
const entities = contract.entities && typeof contract.entities === "object" ? contract.entities : {};
|
||
const hasEntity = [entities.account, entities.counterparty, entities.contract, entities.document_type, entities.document_ref, entities.organization]
|
||
.some((value) => Boolean(toNonEmptyString(value)));
|
||
if (hasEntity) {
|
||
return false;
|
||
}
|
||
const hasExplicitPeriod = Boolean(contract.period && typeof contract.period === "object" && contract.period.has_explicit_period);
|
||
if (hasExplicitPeriod) {
|
||
return false;
|
||
}
|
||
const mode = toNonEmptyString(contract.mode) ?? "unknown";
|
||
const modeConfidence = toNonEmptyString(contract.mode_confidence) ?? "low";
|
||
const intent = toNonEmptyString(contract.intent) ?? "unknown";
|
||
const intentConfidence = toNonEmptyString(contract.intent_confidence) ?? "low";
|
||
const shape = toNonEmptyString(contract.query_shape) ?? "UNKNOWN";
|
||
const shapeConfidence = toNonEmptyString(contract.query_shape_confidence) ?? "low";
|
||
const looksUnderspecified = (mode !== "address_query" || modeConfidence === "low") &&
|
||
(intent === "unknown" || intentConfidence === "low") &&
|
||
(shape === "UNKNOWN" || shapeConfidence === "low");
|
||
if (!looksUnderspecified) {
|
||
return false;
|
||
}
|
||
const normalized = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||
if (!normalized || hasSmallTalkSignal(normalized) || /[??]$/.test(normalized)) {
|
||
return false;
|
||
}
|
||
const tokenCount = countTokens(normalized);
|
||
return tokenCount > 0 && tokenCount <= 4;
|
||
}
|
||
function hasAddressFollowupContextSignal(userMessage) {
|
||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||
const repaired = repairAddressMojibake(String(userMessage ?? ""));
|
||
const repairedText = compactWhitespace(repaired.toLowerCase());
|
||
const samples = [rawText, repairedText].filter((item) => item.length > 0);
|
||
if (samples.length === 0) {
|
||
return false;
|
||
}
|
||
if (samples.some((sample) => /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(sample))) {
|
||
return true;
|
||
}
|
||
const hasAny = (pattern) => samples.some((sample) => pattern.test(sample));
|
||
const hasMarker = () => samples.some((sample) => hasFollowupMarker(sample));
|
||
const hasPointer = () => samples.some((sample) => hasReferentialPointer(sample));
|
||
const minTokens = samples.reduce((min, sample) => Math.min(min, countTokens(sample)), Number.POSITIVE_INFINITY);
|
||
const shortFollowup = minTokens <= 8;
|
||
const ultraShortFollowup = minTokens <= 3;
|
||
const debtRoleSwapToReceivables = shortFollowup &&
|
||
(/^(?:\u0430|a|\u0438|i)\s+(?:\u043d\u0430\u043c\s+)?\u043a\u0442\u043e(?=$|[\s,.;:!?])/iu.test(rawText) ||
|
||
/^(?:р°|a|рё|i)\s+(?:рЅр°рј\s+)?рєс‚рѕ(?=$|[\s,.;:!?])/iu.test(rawText));
|
||
if (debtRoleSwapToReceivables) {
|
||
return true;
|
||
}
|
||
const debtRoleSwapToPayables = shortFollowup &&
|
||
(/^(?:\u0430|a|\u0438|i)\s+(?:\u043c\u044b\s+)?\u043a\u043e\u043c\u0443(?=$|[\s,.;:!?])/iu.test(rawText) ||
|
||
/^(?:р°|a|рё|i)\s+(?:рјс‹\s+)?рєрѕрјсѓ(?=$|[\s,.;:!?])/iu.test(rawText));
|
||
if (debtRoleSwapToPayables) {
|
||
return true;
|
||
}
|
||
const shortContinuationCue = ultraShortFollowup &&
|
||
(/^(?:\u0434\u0430\u0432\u0430\u0439|\u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0439|\u043f\u043e\u043a\u0430\u0437\u044b\u0432\u044b\u0430\u0439|\u0435\u0449[\u0435\u0451]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu.test(rawText) ||
|
||
/^(?:рґр°рір°р№|рїрѕрєр°р·с‹рір°р№|рїрѕрєр°р·с‹ріс‹р°р№|рµс‰[рµс‘]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu.test(rawText));
|
||
if (shortContinuationCue) {
|
||
return true;
|
||
}
|
||
const shortVatCue = ultraShortFollowup &&
|
||
/^(?:(?:\u0430|\u0438)\s+)?(?:(?:\u043f\u043e|po)\s+)?(?:\u043d\u0434\u0441|vat)(?=$|[\s,.;:!?])/iu.test(rawText);
|
||
if (shortVatCue) {
|
||
return true;
|
||
}
|
||
const shortCurrentDateCue = shortFollowup &&
|
||
samples.some((sample) => hasShortCurrentDateFollowupLiteral(sample));
|
||
if (shortCurrentDateCue) {
|
||
return true;
|
||
}
|
||
if (shortFollowup && hasAny(/^(?:а|a|и|i)\s+(?:нам\s+)?кто(?=$|[\s,.;:!?])/iu)) {
|
||
return true;
|
||
}
|
||
if (shortFollowup && hasAny(/^(?:а|a|и|i)\s+(?:мы\s+)?кому(?=$|[\s,.;:!?])/iu)) {
|
||
return true;
|
||
}
|
||
if (ultraShortFollowup && hasAny(/^(?:давай|показывай|показывыай|ещ[её]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu)) {
|
||
return true;
|
||
}
|
||
if (hasStandaloneAddressTopicSignal(rawText || repairedText)) {
|
||
return false;
|
||
}
|
||
if (shouldHandleAsAssistantCapabilityMetaQuery(rawText || repairedText)) {
|
||
return false;
|
||
}
|
||
if (hasAny(/(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period)/iu)) {
|
||
return true;
|
||
}
|
||
if (hasPointer()) {
|
||
return true;
|
||
}
|
||
if (hasAny(/(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|same\s+date|the\s+same\s+date|as\s+of\s+same\s+date)/iu)) {
|
||
return true;
|
||
}
|
||
if (hasAny(/(?:кроме|помимо)\s+(?:этого|этой|этот|эту|этих|этого\s+документа|этого\s+договора|этого\s+контрагента)/iu)) {
|
||
return true;
|
||
}
|
||
if (hasAny(/(?:есть\s+ещ[её]|что\s+ещ[её]|ещ[её]\s+что|ещ[её]\s+что-?то|остал(?:ось|ось\?)|друг(?:ое|ие))/iu) && minTokens <= 12) {
|
||
return true;
|
||
}
|
||
if (shortFollowup && hasMarker()) {
|
||
return true;
|
||
}
|
||
if (shortFollowup && hasAny(/(?:^|\s)(?:также|тоже|also|same|again|ещ[её]|теперь|then|now)(?=$|[\s,.;:!?])/iu)) {
|
||
return true;
|
||
}
|
||
if (shortFollowup && hasAny(/(?:кто\s+из\s+(?:них|этих|тех)|кто\s+нов(?:ые|ых|ый)|кто\s+потом\s+исчез|кто\s+был\s+(?:только|ровно)\s+один\s+раз)/iu)) {
|
||
return true;
|
||
}
|
||
if (shortFollowup &&
|
||
hasAny(/(?:почему|why|из[-\s]?за\s+чего|как\s+так|reason)/iu) &&
|
||
hasAny(/(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu)) {
|
||
return true;
|
||
}
|
||
if (shortFollowup &&
|
||
hasAny(/(?:^|\s)по\s+[a-zа-яё][a-zа-яё0-9._-]{1,}(?=$|[\s,.;:!?])/iu) &&
|
||
!hasAny(/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu)) {
|
||
return true;
|
||
}
|
||
if (shortFollowup && samples.some((sample) => hasPeriodLiteral(sample))) {
|
||
return true;
|
||
}
|
||
if (shortFollowup && samples.some((sample) => hasShortNamedPeriodFollowupLiteral(sample))) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function hasShortDebtMirrorFollowupSignal(userMessage) {
|
||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||
const samples = [rawText, repairedText].filter((item) => item.length > 0);
|
||
if (samples.length === 0) {
|
||
return false;
|
||
}
|
||
const minTokens = samples.reduce((min, sample) => Math.min(min, countTokens(sample)), Number.POSITIVE_INFINITY);
|
||
if (minTokens > 8) {
|
||
return false;
|
||
}
|
||
return samples.some((sample) => /^(?:а|a|и|i)\s+(?:нам\s+)?кто(?=$|[\s,.;:!?])/iu.test(sample) ||
|
||
/^(?:а|a|и|i)\s+нам(?=$|[\s,.;:!?])/iu.test(sample) ||
|
||
/^(?:а|a|и|i)\s+(?:мы\s+)?кому(?=$|[\s,.;:!?])/iu.test(sample) ||
|
||
/^(?:р°|a|рё|i)\s+(?:рЅр°рј\s+)?рєс‚рѕ(?=$|[\s,.;:!?])/iu.test(sample) ||
|
||
/^(?:р°|a|рё|i)\s+рЅр°рј(?=$|[\s,.;:!?])/iu.test(sample) ||
|
||
/^(?:р°|a|рё|i)\s+(?:рјс‹\s+)?рєрѕрјсѓ(?=$|[\s,.;:!?])/iu.test(sample));
|
||
}
|
||
function isInventorySelectedObjectIntent(intent) {
|
||
return intent === "inventory_purchase_provenance_for_item" ||
|
||
intent === "inventory_purchase_documents_for_item" ||
|
||
intent === "inventory_sale_trace_for_item" ||
|
||
intent === "inventory_profitability_for_item" ||
|
||
intent === "inventory_purchase_to_sale_chain";
|
||
}
|
||
function hasShortInventoryObjectFollowupSignal(userMessage) {
|
||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||
const samples = [rawText, repairedText].filter((item) => item.length > 0);
|
||
if (samples.length === 0) {
|
||
return false;
|
||
}
|
||
const minTokens = samples.reduce((min, sample) => Math.min(min, countTokens(sample)), Number.POSITIVE_INFINITY);
|
||
if (minTokens > 8) {
|
||
return false;
|
||
}
|
||
const hasDirectPurchaseFollowupCue = (sample) => /(?:^|[\s,.;:!?])(?:а\s+)?(?:у|от)\s+кого(?:\s+\S+){0,5}\s+купил(?:и|о)?(?=$|[\s,.;:!?])/iu.test(sample) ||
|
||
/(?:^|[\s,.;:!?])(?:а\s+)?купил(?:и|о)?(?:\s+\S+){0,5}\s+(?:у|от)\s+кого(?=$|[\s,.;:!?])/iu.test(sample);
|
||
const hasDirectSaleFollowupCue = (sample) => /(?:кому|каму|куда)(?:\s+\S+){0,4}\s+(?:продали|продано|продан(?:о|а|ы)?|реализовали|реализован(?:о|а|ы)?)|(?:продали|продано|реализовали|реализован(?:о|а|ы)?)(?:\s+\S+){0,4}\s+(?:кому|каму|куда)|(?:^|\s)(?:продано|продали|реализовано|реализовали)(?=$|[\s,.;:!?])/iu.test(sample);
|
||
return samples.some((sample) => /^(?:кто|когда|документы|сумма|поставщик|покупатель|продавец|seller)(?:\?)?$/iu.test(sample) ||
|
||
hasDirectPurchaseFollowupCue(sample) ||
|
||
hasDirectSaleFollowupCue(sample) ||
|
||
(0, decomposeStage_1.hasInventorySupplierFollowupCue)(sample) ||
|
||
(0, decomposeStage_1.hasInventoryPurchaseDocumentsFollowupCue)(sample) ||
|
||
(0, decomposeStage_1.hasInventoryPurchaseDateFollowupCue)(sample) ||
|
||
(0, decomposeStage_1.hasBareInventoryPurchaseDateFollowupCue)(sample) ||
|
||
(0, decomposeStage_1.hasInventorySaleFollowupCue)(sample) ||
|
||
(0, decomposeStage_1.hasInventoryPurchaseToSaleChainFollowupCue)(sample));
|
||
}
|
||
function hasInventoryRootTemporalFollowupSignal(userMessage, sourceIntentHint, hasInventoryRootFrame) {
|
||
if (!hasInventoryRootFrame) {
|
||
return false;
|
||
}
|
||
if (!(sourceIntentHint === "inventory_on_hand_as_of_date" ||
|
||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||
isInventorySelectedObjectIntent(sourceIntentHint))) {
|
||
return false;
|
||
}
|
||
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||
const samples = [rawText, repairedText].filter((item) => item.length > 0);
|
||
if (samples.length === 0) {
|
||
return false;
|
||
}
|
||
const minTokens = samples.reduce((min, sample) => Math.min(min, countTokens(sample)), Number.POSITIVE_INFINITY);
|
||
if (minTokens > 8) {
|
||
return false;
|
||
}
|
||
const hasMonthYearCue = samples.some((sample) => /(?:январ|феврал|март|апрел|ма(?:й|е|я)|июн|июл|август|сентябр|октябр|ноябр|декабр)(?:\s+\d{4})?/iu.test(sample) ||
|
||
/\b(?:19|20)\d{2}\b/u.test(sample));
|
||
const hasTemporalCue = samples.some((sample) => hasPeriodLiteral(sample) ||
|
||
hasShortNamedPeriodFollowupLiteral(sample) ||
|
||
hasShortCurrentDateFollowupLiteral(sample) ||
|
||
/(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|same\s+date|the\s+same\s+date)/iu.test(sample));
|
||
if (!hasTemporalCue && !hasMonthYearCue) {
|
||
return false;
|
||
}
|
||
if (samples.some((sample) => hasForeignAccountingPivotOverInventoryMessage(sample))) {
|
||
return false;
|
||
}
|
||
const hasInventoryLexeme = samples.some((sample) => /(?:остат|склад|товар|номенклат|позиц)/iu.test(sample));
|
||
const hasPlainInventoryLexeme = samples.some((sample) => /(?:остат|склад|товар|номенклатур|позиц)/iu.test(sample));
|
||
return hasInventoryLexeme || hasPlainInventoryLexeme || minTokens <= 3;
|
||
}
|
||
function hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMessage = null) {
|
||
const samples = [
|
||
compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()),
|
||
compactWhitespace(String(userMessage ?? "").toLowerCase()),
|
||
compactWhitespace(repairAddressMojibake(String(alternateMessage ?? "")).toLowerCase()),
|
||
compactWhitespace(String(alternateMessage ?? "").toLowerCase())
|
||
].filter((item) => item.length > 0);
|
||
if (samples.length === 0) {
|
||
return false;
|
||
}
|
||
return samples.some((sample) => /(?:ндс|vat|налог(?:и|ов|ом|у|ами|ах)?|налогов(?:ый|ого)?|tax(?:es)?|сч[её]т[\s-]?фактур|книга\s+покупок|книга\s+продаж|вычет)/iu.test(sample) ||
|
||
/(?:амортиз|основн(?:ые|ых|ым)?\s+средств|fixed\s*asset|depreciat|\bос\b)/iu.test(sample) ||
|
||
/(?:закрыти|месяц|затрат|рбп|period\s*close|month\s*close|allocation|residual|cost)/iu.test(sample) ||
|
||
/(?:оплат|плат(?:е|ё)ж|аванс|зач(?:е|ё)т|выписк|statement|wire|settlement|payment|\b51(?:\.\d{1,2})?\b|\b60(?:\.\d{1,2})?\b|\b62(?:\.\d{1,2})?\b)/iu.test(sample));
|
||
}
|
||
function buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame) {
|
||
const candidateFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
|
||
? inventoryRootFrame.filters
|
||
: {};
|
||
const nextFilters = {};
|
||
const organization = toNonEmptyString(candidateFilters?.organization) ?? toNonEmptyString(previousFilters?.organization);
|
||
const warehouse = toNonEmptyString(candidateFilters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse);
|
||
const asOfDate = toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(candidateFilters?.as_of_date);
|
||
const periodFrom = toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(candidateFilters?.period_from);
|
||
const periodTo = toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(candidateFilters?.period_to);
|
||
if (organization) {
|
||
nextFilters.organization = organization;
|
||
}
|
||
if (warehouse) {
|
||
nextFilters.warehouse = warehouse;
|
||
}
|
||
if (asOfDate) {
|
||
nextFilters.as_of_date = asOfDate;
|
||
}
|
||
if (periodFrom) {
|
||
nextFilters.period_from = periodFrom;
|
||
}
|
||
if (periodTo) {
|
||
nextFilters.period_to = periodTo;
|
||
}
|
||
return nextFilters;
|
||
}
|
||
function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) {
|
||
const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||
if (!normalized || countTokens(normalized) > 10) {
|
||
return null;
|
||
}
|
||
const hasReceivablesCue = /(?:нам\s+кто\s+долж|кто\s+нам\s+долж|кто\s+долж[а-яё]*\s+нам|дебитор|к\s+получению|к\s+взысканию|receivable)/iu.test(normalized) ||
|
||
/^(?:а|a|и|i)\s+(?:нам\s+)?кто(?=$|[\s,.;:!?])/iu.test(normalized) ||
|
||
/^(?:а|a|и|i)\s+нам(?=$|[\s,.;:!?])/iu.test(normalized) ||
|
||
/^(?:р°|a|рё|i)\s+(?:рЅр°рј\s+)?рєс‚рѕ(?=$|[\s,.;:!?])/iu.test(normalized);
|
||
const hasPayablesCue = /(?:мы\s+кому\s+долж|кому\s+мы\s+долж|кому\s+долж[а-яё]*\s+мы|кредитор|к\s+уплате|payable)/iu.test(normalized) ||
|
||
/^(?:а|a|и|i)\s+(?:мы\s+)?кому(?=$|[\s,.;:!?])/iu.test(normalized) ||
|
||
/^(?:р°|a|рё|i)\s+(?:рјс‹\s+)?рєрѕрјсѓ(?=$|[\s,.;:!?])/iu.test(normalized);
|
||
if ((previousIntent === "payables_confirmed_as_of_date" || previousIntent === "list_payables_counterparties") &&
|
||
hasReceivablesCue) {
|
||
return "receivables_confirmed_as_of_date";
|
||
}
|
||
if ((previousIntent === "receivables_confirmed_as_of_date" || previousIntent === "list_receivables_counterparties") &&
|
||
hasPayablesCue) {
|
||
return "payables_confirmed_as_of_date";
|
||
}
|
||
return null;
|
||
}
|
||
function hasExplicitOrganizationScopeCue(text) {
|
||
return /(?:(?:^|[\s"'«»„“()\\\/])(?:ооо|ао|пао|зао|оао|ип|гку|муп|гуп|llc|ltd|inc|corp)(?=$|[\s"'«»„“()\\\/.,;:]))|организац|компан|контор|фирм/u.test(String(text ?? "").toLowerCase());
|
||
}
|
||
function looksLikeBareCounterpartyScopeTarget(text) {
|
||
const normalized = compactWhitespace(String(text ?? "").toLowerCase());
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
if (hasExplicitOrganizationScopeCue(normalized)) {
|
||
return false;
|
||
}
|
||
if (/[0-9]/u.test(normalized)) {
|
||
return false;
|
||
}
|
||
const tokens = normalized.split(/\s+/u).filter(Boolean);
|
||
if (tokens.length === 0 || tokens.length > 4) {
|
||
return false;
|
||
}
|
||
return tokens.every((token) => /^[a-zа-яё._-]{2,}$/iu.test(token));
|
||
}
|
||
function shouldDowngradeOrganizationSemanticHint(scopeTargetText, fragmentText) {
|
||
const target = toNonEmptyString(scopeTargetText);
|
||
if (!target) {
|
||
return false;
|
||
}
|
||
if (hasExplicitOrganizationScopeCue(target) || hasExplicitOrganizationScopeCue(fragmentText)) {
|
||
return false;
|
||
}
|
||
return looksLikeBareCounterpartyScopeTarget(target);
|
||
}
|
||
function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent) {
|
||
const normalized = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
|
||
if (!normalized || countTokens(normalized) > 4) {
|
||
return false;
|
||
}
|
||
if (sourceIntent !== "list_documents_by_counterparty" && sourceIntent !== "list_documents_by_contract") {
|
||
return false;
|
||
}
|
||
if (/(?:банк|выписк|плат[её]ж|оплат|списан|поступлен|bank|payment|wire|statement)/iu.test(normalized)) {
|
||
return false;
|
||
}
|
||
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) {
|
||
return assistantTransitionPolicy.resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage, llmPreDecomposeMeta, addressNavigationState);
|
||
}
|
||
function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta) {
|
||
return assistantTransitionPolicy.buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta);
|
||
}
|
||
function isRetryableAddressLimitedResult(addressLane) {
|
||
if (!addressLane || !addressLane.handled) {
|
||
return false;
|
||
}
|
||
if (String(addressLane.reply_type ?? "").trim() !== "partial_coverage") {
|
||
return false;
|
||
}
|
||
const category = String(addressLane?.debug?.limited_reason_category ?? "").trim().toLowerCase();
|
||
return category === "missing_anchor" || category === "empty_match" || category === "unsupported";
|
||
}
|
||
function isAddressLlmPreDecomposeCandidate(userMessage) {
|
||
const repaired = repairAddressMojibake(String(userMessage ?? ""));
|
||
const text = compactWhitespace(repaired.toLowerCase());
|
||
if (!text) {
|
||
return false;
|
||
}
|
||
return /(?:\bдок\b|доки|документ|контрагент|договор|остаток|сч(?:е|ё)т|сальдо|банк|выписк|платеж|оплат|поступлен|поступлени|списан|реализац|сверк|взаиморасч|кто\s+должен|show|list|documents?|counterparty|contract|account|balance|bank\s+operations?|doki|dokument(?:y|ov|am|a)?|platezh|oplata|schet|saldo)/i.test(text);
|
||
}
|
||
function normalizeAddressSemanticHintsFromFragment(fragment) {
|
||
if (!fragment || typeof fragment !== "object") {
|
||
return null;
|
||
}
|
||
const hints = fragment.semantic_hints;
|
||
if (!hints || typeof hints !== "object") {
|
||
return null;
|
||
}
|
||
const scopeTargetKind = toNonEmptyString(hints.scope_target_kind);
|
||
const scopeTargetText = toNonEmptyString(hints.scope_target_text);
|
||
const fragmentText = toNonEmptyString(fragment.raw_fragment_text) ?? toNonEmptyString(fragment.normalized_fragment_text) ?? "";
|
||
const normalizedScopeTargetKind = scopeTargetKind === "organization" &&
|
||
shouldDowngradeOrganizationSemanticHint(scopeTargetText, fragmentText)
|
||
? "counterparty"
|
||
: scopeTargetKind;
|
||
const dateScopeKind = toNonEmptyString(hints.date_scope_kind);
|
||
return {
|
||
scope_target_kind: normalizedScopeTargetKind ?? "none",
|
||
scope_target_text: scopeTargetText,
|
||
date_scope_kind: dateScopeKind ?? "missing",
|
||
self_scope_detected: hints.self_scope_detected === true || normalizedScopeTargetKind === "self_scope",
|
||
selected_object_scope_detected: hints.selected_object_scope_detected === true || normalizedScopeTargetKind === "selected_object"
|
||
};
|
||
}
|
||
function extractAddressPredecomposeCandidateFromFragments(fragments) {
|
||
for (const item of Array.isArray(fragments) ? fragments : []) {
|
||
if (!item || typeof item !== "object") {
|
||
continue;
|
||
}
|
||
const fragment = item;
|
||
const domainRelevance = String(fragment.domain_relevance ?? "").trim().toLowerCase();
|
||
if (domainRelevance === "out_of_scope" || domainRelevance === "offtopic") {
|
||
continue;
|
||
}
|
||
const normalizedText = toNonEmptyString(fragment.normalized_fragment_text);
|
||
const rawText = toNonEmptyString(fragment.raw_fragment_text);
|
||
const candidate = selectPreferredAddressFragmentCandidate(rawText ?? "", normalizedText ?? "");
|
||
if (!candidate) {
|
||
continue;
|
||
}
|
||
if (candidate.length >= 3 && candidate.length <= 500) {
|
||
return {
|
||
candidate,
|
||
semanticHints: normalizeAddressSemanticHintsFromFragment(fragment)
|
||
};
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function extractAddressPredecomposeCandidateFromNormalized(normalized) {
|
||
if (!normalized || typeof normalized !== "object") {
|
||
return null;
|
||
}
|
||
return extractAddressPredecomposeCandidateFromFragments(normalized.fragments);
|
||
}
|
||
function stripMarkdownJsonFence(text) {
|
||
return String(text ?? "")
|
||
.trim()
|
||
.replace(/^```json\s*/i, "")
|
||
.replace(/^```\s*/i, "")
|
||
.replace(/```$/i, "")
|
||
.trim();
|
||
}
|
||
function safeParseLooseJson(text) {
|
||
const fenced = stripMarkdownJsonFence(text);
|
||
if (!fenced) {
|
||
return null;
|
||
}
|
||
try {
|
||
return JSON.parse(fenced);
|
||
}
|
||
catch (_error) {
|
||
// Local OpenAI-compatible models often wrap JSON with extra text.
|
||
// Try extracting the first top-level JSON object defensively.
|
||
const start = fenced.indexOf("{");
|
||
const end = fenced.lastIndexOf("}");
|
||
if (start < 0 || end < 0 || end <= start) {
|
||
return null;
|
||
}
|
||
const candidate = fenced.slice(start, end + 1).trim();
|
||
try {
|
||
return JSON.parse(candidate);
|
||
}
|
||
catch (_nestedError) {
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
function extractOutputTextFromRawNormalizerOutput(raw) {
|
||
if (!raw || typeof raw !== "object") {
|
||
return null;
|
||
}
|
||
const source = raw;
|
||
if (typeof source.output_text === "string" && source.output_text.trim().length > 0) {
|
||
return source.output_text;
|
||
}
|
||
if (Array.isArray(source.output)) {
|
||
for (const item of source.output) {
|
||
if (!item || typeof item !== "object") {
|
||
continue;
|
||
}
|
||
const content = item.content;
|
||
if (!Array.isArray(content)) {
|
||
continue;
|
||
}
|
||
for (const block of content) {
|
||
if (!block || typeof block !== "object") {
|
||
continue;
|
||
}
|
||
if (typeof block.text === "string" && block.text.trim().length > 0) {
|
||
return block.text;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (source.response && typeof source.response === "object") {
|
||
const nested = source.response;
|
||
if (typeof nested.output_text === "string" && nested.output_text.trim().length > 0) {
|
||
return nested.output_text;
|
||
}
|
||
}
|
||
if (Array.isArray(source.choices) && source.choices.length > 0) {
|
||
const first = source.choices[0];
|
||
if (first && typeof first === "object" && first.message && typeof first.message === "object") {
|
||
const message = first.message;
|
||
if (typeof message.content === "string" && message.content.trim().length > 0) {
|
||
return message.content;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function extractAddressPredecomposeCandidateFromRawNormalizerOutput(rawModelOutput) {
|
||
const outputText = extractOutputTextFromRawNormalizerOutput(rawModelOutput);
|
||
if (!outputText) {
|
||
return null;
|
||
}
|
||
const parsed = safeParseLooseJson(outputText);
|
||
if (!parsed || typeof parsed !== "object") {
|
||
return null;
|
||
}
|
||
return extractAddressPredecomposeCandidateFromFragments(parsed.fragments);
|
||
}
|
||
const ADDRESS_PREDECOMPOSE_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
|
||
"есть",
|
||
"же",
|
||
"что",
|
||
"все",
|
||
"всё",
|
||
"год",
|
||
"года",
|
||
"году",
|
||
"контрагентам",
|
||
"предоставьте",
|
||
"получить",
|
||
"скажи",
|
||
"skazhi",
|
||
"покажи",
|
||
"выведи",
|
||
"сверка",
|
||
"теперь",
|
||
"сейчас",
|
||
"этому",
|
||
"этомуже",
|
||
"тому",
|
||
"томуже",
|
||
"нему",
|
||
"ней",
|
||
"ним",
|
||
"неуказанному",
|
||
"неуказанный",
|
||
"неуказанная",
|
||
"неуказанное",
|
||
"указанному",
|
||
"указанный",
|
||
"указанная",
|
||
"указанное",
|
||
"объект",
|
||
"объекту",
|
||
"период",
|
||
"периоду",
|
||
"сводные",
|
||
"сводный",
|
||
"сводная",
|
||
"сводную",
|
||
"сводном",
|
||
"сводного",
|
||
"сводному",
|
||
"кроме",
|
||
"помимо",
|
||
"этого",
|
||
"этот",
|
||
"эта",
|
||
"эту",
|
||
"этом",
|
||
"это",
|
||
"эти",
|
||
"этих",
|
||
"документ",
|
||
"документа",
|
||
"документы",
|
||
"документов",
|
||
"договор",
|
||
"договора",
|
||
"контрагент",
|
||
"контрагента",
|
||
"еще",
|
||
"ещё",
|
||
"другие",
|
||
"другое",
|
||
"остальное"
|
||
]);
|
||
const ADDRESS_PREDECOMPOSE_LOW_QUALITY_CONTRACT_TOKENS = new Set([
|
||
"за",
|
||
"же",
|
||
"это",
|
||
"указанный",
|
||
"указанному",
|
||
"период",
|
||
"периоду",
|
||
"тот",
|
||
"тотже",
|
||
"этот",
|
||
"этому",
|
||
"этомуже",
|
||
"договор",
|
||
"договору",
|
||
"номер"
|
||
]);
|
||
function normalizePredecomposeAnchorTokens(value) {
|
||
return String(value ?? "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/ё/g, "е")
|
||
.split(/[^a-zа-я0-9]+/iu)
|
||
.map((token) => token.trim())
|
||
.filter(Boolean);
|
||
}
|
||
function isLowQualityPredecomposeCounterpartyAnchor(value) {
|
||
const tokens = normalizePredecomposeAnchorTokens(value);
|
||
if (tokens.length === 0) {
|
||
return true;
|
||
}
|
||
const meaningful = tokens.filter((token) => {
|
||
if (token.length < 2) {
|
||
return false;
|
||
}
|
||
if (/^(?:19|20)\d{2}$/.test(token)) {
|
||
return false;
|
||
}
|
||
return !ADDRESS_PREDECOMPOSE_LOW_QUALITY_COUNTERPARTY_TOKENS.has(token);
|
||
});
|
||
return meaningful.length === 0;
|
||
}
|
||
function normalizePredecomposeCounterpartyAnchorTokensForMatch(value) {
|
||
return normalizePredecomposeAnchorTokens(value).filter((token) => {
|
||
if (token.length < 2) {
|
||
return false;
|
||
}
|
||
if (/^(?:19|20)\d{2}$/.test(token)) {
|
||
return false;
|
||
}
|
||
return !ADDRESS_PREDECOMPOSE_LOW_QUALITY_COUNTERPARTY_TOKENS.has(token);
|
||
});
|
||
}
|
||
function hasCounterpartyAnchorSubstitution(sourceValue, candidateValue) {
|
||
const sourceNormalized = String(sourceValue ?? "").trim().toLowerCase().replace(/ё/g, "е");
|
||
const candidateNormalized = String(candidateValue ?? "").trim().toLowerCase().replace(/ё/g, "е");
|
||
if (!sourceNormalized || !candidateNormalized) {
|
||
return false;
|
||
}
|
||
if (sourceNormalized === candidateNormalized) {
|
||
return false;
|
||
}
|
||
if (sourceNormalized.includes(candidateNormalized) || candidateNormalized.includes(sourceNormalized)) {
|
||
return false;
|
||
}
|
||
const sourceTokens = new Set(normalizePredecomposeCounterpartyAnchorTokensForMatch(sourceNormalized));
|
||
const candidateTokens = normalizePredecomposeCounterpartyAnchorTokensForMatch(candidateNormalized);
|
||
if (sourceTokens.size === 0 || candidateTokens.length === 0) {
|
||
return false;
|
||
}
|
||
for (const token of candidateTokens) {
|
||
if (sourceTokens.has(token)) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
function isLowQualityPredecomposeContractAnchor(value) {
|
||
const normalized = String(value ?? "").trim().toLowerCase().replace(/ё/g, "е");
|
||
if (!normalized) {
|
||
return true;
|
||
}
|
||
if (/\b[a-zа-я0-9]{1,20}[\/_-][a-zа-я0-9]{1,20}(?:[\/_-][a-zа-я0-9]{1,20})?\b/iu.test(normalized)) {
|
||
return false;
|
||
}
|
||
if (!/\d/.test(normalized)) {
|
||
return true;
|
||
}
|
||
const tokens = normalizePredecomposeAnchorTokens(normalized);
|
||
if (tokens.length === 0) {
|
||
return true;
|
||
}
|
||
const meaningful = tokens.filter((token) => !ADDRESS_PREDECOMPOSE_LOW_QUALITY_CONTRACT_TOKENS.has(token));
|
||
return meaningful.length === 0;
|
||
}
|
||
function resolveRequiredAnchorTypeForIntent(intent) {
|
||
if (intent === "list_documents_by_counterparty" || intent === "bank_operations_by_counterparty" || intent === "list_contracts_by_counterparty") {
|
||
return "counterparty";
|
||
}
|
||
if (intent === "list_documents_by_contract" || intent === "bank_operations_by_contract") {
|
||
return "contract";
|
||
}
|
||
if (intent === "inventory_purchase_provenance_for_item" ||
|
||
intent === "inventory_purchase_documents_for_item" ||
|
||
intent === "inventory_sale_trace_for_item" ||
|
||
intent === "inventory_profitability_for_item" ||
|
||
intent === "inventory_purchase_to_sale_chain" ||
|
||
intent === "inventory_aging_by_purchase_date") {
|
||
return "item";
|
||
}
|
||
return null;
|
||
}
|
||
function evaluateAddressAnchorQuality(message) {
|
||
const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(String(message ?? ""));
|
||
const intent = intentResolution.intent;
|
||
const anchorType = resolveRequiredAnchorTypeForIntent(intent);
|
||
if (!anchorType) {
|
||
return {
|
||
intent,
|
||
anchorType: null,
|
||
anchorValue: null,
|
||
quality: 0
|
||
};
|
||
}
|
||
const extracted = (0, addressFilterExtractor_1.extractAddressFilters)(String(message ?? ""), intent);
|
||
const anchorValue = anchorType === "counterparty"
|
||
? toNonEmptyString(extracted?.extracted_filters?.counterparty)
|
||
: anchorType === "contract"
|
||
? toNonEmptyString(extracted?.extracted_filters?.contract)
|
||
: toNonEmptyString(extracted?.extracted_filters?.item);
|
||
if (!anchorValue) {
|
||
return {
|
||
intent,
|
||
anchorType,
|
||
anchorValue: null,
|
||
quality: 0
|
||
};
|
||
}
|
||
const lowQuality = anchorType === "counterparty"
|
||
? isLowQualityPredecomposeCounterpartyAnchor(anchorValue)
|
||
: anchorType === "contract"
|
||
? isLowQualityPredecomposeContractAnchor(anchorValue)
|
||
: (0, addressFilterExtractor_1.isLowQualityInventoryItemAnchorValue)(anchorValue);
|
||
return {
|
||
intent,
|
||
anchorType,
|
||
anchorValue,
|
||
quality: lowQuality ? 1 : 2
|
||
};
|
||
}
|
||
function hasPredecomposeExplicitDrilldownSignal(text) {
|
||
const source = String(text ?? "");
|
||
return ADDRESS_DOCS_SIGNAL_PATTERN.test(source) || ADDRESS_BANK_SIGNAL_PATTERN.test(source) || ADDRESS_CONTRACT_SIGNAL_PATTERN.test(source);
|
||
}
|
||
function hasSelectedObjectInventoryFollowupSignalForPredecompose(text) {
|
||
return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(text ?? ""));
|
||
}
|
||
function isInventorySelectedObjectFollowupIntent(intent) {
|
||
return intent === "inventory_purchase_provenance_for_item" ||
|
||
intent === "inventory_purchase_documents_for_item" ||
|
||
intent === "inventory_profitability_for_item";
|
||
}
|
||
function hasSameDateAccountFollowupSignalForPredecompose(text) {
|
||
const source = String(text ?? "");
|
||
const hasSameDate = /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|та\s+же\s+дата|same\s+date|the\s+same\s+date|as\s+of\s+same\s+date)/iu.test(source);
|
||
if (!hasSameDate) {
|
||
return false;
|
||
}
|
||
return (/(?:сч[её]т|счет|account)\D{0,12}\d{2}(?:[.,]\d{1,2})?/iu.test(source) ||
|
||
/(?:^|\s)по\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/iu.test(source) ||
|
||
/\b\d{2}(?:[.,]\d{1,2})\b/u.test(source));
|
||
}
|
||
function hasPredecomposeDiagnosticUncertaintyLead(text) {
|
||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
return /^(?:неясно|не\s+ясно|непонятно|не\s+понятно|unclear|not\s+clear|ambiguous|unknown)(?=$|[\s,.;:!?])/iu.test(normalized);
|
||
}
|
||
function attachAddressPredecomposeContract(meta, sourceMessage) {
|
||
const sourceMeta = meta && typeof meta === "object" ? meta : {};
|
||
const { providerExecutionInput, providerExecutionContract: providerExecutionContractInput, ...restMeta } = sourceMeta;
|
||
const canonicalMessage = toNonEmptyString(meta?.effectiveMessage) ?? String(sourceMessage ?? "");
|
||
const predecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||
sourceMessage: String(sourceMessage ?? ""),
|
||
canonicalMessage,
|
||
semanticHints: meta?.semanticHints ?? null
|
||
});
|
||
const semanticExtractionContract = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
|
||
sourceMessage: String(sourceMessage ?? ""),
|
||
canonicalMessage,
|
||
predecomposeContract
|
||
});
|
||
const providerExecutionContract = providerExecutionContractInput && typeof providerExecutionContractInput === "object"
|
||
? providerExecutionContractInput
|
||
: assistantProviderExecutionPolicy.resolveProviderExecutionState({
|
||
llmProvider: providerExecutionInput?.llmProvider,
|
||
useMock: providerExecutionInput?.useMock,
|
||
baseUrl: providerExecutionInput?.baseUrl,
|
||
llmPreDecomposeReason: restMeta?.reason
|
||
});
|
||
return {
|
||
...restMeta,
|
||
providerExecutionContract,
|
||
predecomposeContract,
|
||
semanticExtractionContract
|
||
};
|
||
}
|
||
async function runAddressLlmPreDecompose(normalizerService, payload, userMessage) {
|
||
const provider = assistantProviderExecutionPolicy.normalizeProvider(payload?.llmProvider);
|
||
const sanitizedUserMessage = sanitizeAddressMessageForFallback(userMessage);
|
||
const fallbackCandidate = resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage);
|
||
const baseMeta = {
|
||
attempted: false,
|
||
applied: false,
|
||
provider,
|
||
providerExecutionInput: {
|
||
llmProvider: payload?.llmProvider,
|
||
useMock: payload?.useMock,
|
||
baseUrl: payload?.baseUrl
|
||
},
|
||
traceId: null,
|
||
effectiveMessage: userMessage,
|
||
reason: "not_attempted",
|
||
llmCanonicalCandidateDetected: false,
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage,
|
||
toolGateDecision: null,
|
||
toolGateReason: null
|
||
};
|
||
if (Boolean(payload?.useMock)) {
|
||
if (fallbackCandidate) {
|
||
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
|
||
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
|
||
if (fallbackApplied) {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
applied: true,
|
||
effectiveMessage: fallbackCandidate.candidate,
|
||
reason: "fallback_rule_applied_without_llm",
|
||
fallbackRuleHit: fallbackCandidate.rule
|
||
}, userMessage);
|
||
}
|
||
}
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
reason: "skipped_in_mock"
|
||
}, userMessage);
|
||
}
|
||
const normalizePayload = {
|
||
llmProvider: payload?.llmProvider,
|
||
apiKey: payload?.apiKey,
|
||
model: payload?.model,
|
||
baseUrl: payload?.baseUrl,
|
||
temperature: 0,
|
||
maxOutputTokens: payload?.maxOutputTokens,
|
||
promptVersion: "normalizer_v2_0_2",
|
||
userQuestion: userMessage,
|
||
context: payload?.context,
|
||
useMock: Boolean(payload?.useMock),
|
||
retryPolicy: "single-pass-strict"
|
||
};
|
||
try {
|
||
const normalized = await normalizerService.normalize(normalizePayload);
|
||
const candidateFromNormalized = extractAddressPredecomposeCandidateFromNormalized(normalized?.normalized);
|
||
const candidateFromRaw = candidateFromNormalized ? null : extractAddressPredecomposeCandidateFromRawNormalizerOutput(normalized?.raw_model_output);
|
||
const candidateMeta = candidateFromNormalized ?? candidateFromRaw;
|
||
const candidate = candidateMeta?.candidate ?? null;
|
||
if (!candidate) {
|
||
if (fallbackCandidate) {
|
||
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
|
||
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
|
||
if (fallbackApplied) {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: true,
|
||
traceId: normalized?.trace_id ?? null,
|
||
effectiveMessage: fallbackCandidate.candidate,
|
||
reason: "fallback_rule_applied_after_llm",
|
||
fallbackRuleHit: fallbackCandidate.rule,
|
||
semanticHints: null
|
||
}, userMessage);
|
||
}
|
||
}
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
traceId: normalized?.trace_id ?? null,
|
||
reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed",
|
||
semanticHints: null
|
||
}, userMessage);
|
||
}
|
||
const repairedSourceMessage = repairAddressMojibake(userMessage);
|
||
const sourceIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(repairedSourceMessage || userMessage);
|
||
const candidateIntentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(candidate);
|
||
const sourceIntentKnown = sourceIntentResolution.intent !== "unknown";
|
||
const candidateIntentKnown = candidateIntentResolution.intent !== "unknown";
|
||
const candidateStartsWithDiagnosticUncertainty = hasPredecomposeDiagnosticUncertaintyLead(candidate);
|
||
if (candidateStartsWithDiagnosticUncertainty && sourceIntentKnown) {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: false,
|
||
traceId: normalized?.trace_id ?? null,
|
||
llmCanonicalCandidateDetected: true,
|
||
effectiveMessage: userMessage,
|
||
reason: "normalized_fragment_rejected_diagnostic_rewrite",
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
}, userMessage);
|
||
}
|
||
const intentConflict = sourceIntentKnown &&
|
||
candidateIntentKnown &&
|
||
sourceIntentResolution.intent !== candidateIntentResolution.intent;
|
||
const intentDroppedByCandidate = sourceIntentKnown && !candidateIntentKnown;
|
||
const rejectCandidateForIntentSafety = intentDroppedByCandidate ||
|
||
(intentConflict &&
|
||
(sourceIntentResolution.confidence === "high" || candidateIntentResolution.confidence !== "high"));
|
||
if (rejectCandidateForIntentSafety) {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: false,
|
||
traceId: normalized?.trace_id ?? null,
|
||
llmCanonicalCandidateDetected: true,
|
||
effectiveMessage: userMessage,
|
||
reason: intentDroppedByCandidate
|
||
? "normalized_fragment_rejected_intent_drop"
|
||
: "normalized_fragment_rejected_intent_conflict",
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
}, userMessage);
|
||
}
|
||
const sourceHasExplicitDrilldownSignal = hasPredecomposeExplicitDrilldownSignal(repairedSourceMessage || userMessage);
|
||
const candidateHasExplicitDrilldownSignal = hasPredecomposeExplicitDrilldownSignal(candidate);
|
||
const sourceLooksLikeSameDateAccountFollowup = hasSameDateAccountFollowupSignalForPredecompose(repairedSourceMessage || userMessage);
|
||
const candidateInjectsDrilldownIntent = candidateIntentResolution.intent === "documents_forming_balance";
|
||
if (sourceLooksLikeSameDateAccountFollowup &&
|
||
!sourceHasExplicitDrilldownSignal &&
|
||
candidateHasExplicitDrilldownSignal &&
|
||
candidateInjectsDrilldownIntent &&
|
||
sourceIntentResolution.intent !== "documents_forming_balance") {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: false,
|
||
traceId: normalized?.trace_id ?? null,
|
||
llmCanonicalCandidateDetected: true,
|
||
effectiveMessage: userMessage,
|
||
reason: "normalized_fragment_rejected_followup_intent_injection",
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
}, userMessage);
|
||
}
|
||
const sourceHasSelectedObjectInventoryFollowup = hasSelectedObjectInventoryFollowupSignalForPredecompose(repairedSourceMessage || userMessage);
|
||
const candidateHasSelectedObjectInventoryFollowup = hasSelectedObjectInventoryFollowupSignalForPredecompose(candidate);
|
||
const candidateInjectsGenericDocsIntent = candidateIntentResolution.intent === "list_documents_by_counterparty" ||
|
||
candidateIntentResolution.intent === "list_documents_by_contract";
|
||
if (sourceHasSelectedObjectInventoryFollowup &&
|
||
isInventorySelectedObjectFollowupIntent(sourceIntentResolution.intent) &&
|
||
!candidateHasSelectedObjectInventoryFollowup &&
|
||
candidateInjectsGenericDocsIntent) {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: false,
|
||
traceId: normalized?.trace_id ?? null,
|
||
llmCanonicalCandidateDetected: true,
|
||
effectiveMessage: userMessage,
|
||
reason: "normalized_fragment_rejected_selected_object_context_loss",
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
}, userMessage);
|
||
}
|
||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||
const sourceSelectedObjectItemAnchorValue = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage)) ??
|
||
toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(repairedSourceMessage || userMessage));
|
||
const candidateSemanticItemAnchorValue = (((sameIntentForAnchorSafety &&
|
||
sourceAnchorQuality.anchorType === "item") ||
|
||
Boolean(sourceSelectedObjectItemAnchorValue)) &&
|
||
candidateMeta?.semanticHints?.scope_target_kind === "item"
|
||
? toNonEmptyString(candidateMeta.semanticHints.scope_target_text)
|
||
: null);
|
||
const counterpartyAnchorSubstitutedByCandidate = sameIntentForAnchorSafety &&
|
||
sourceAnchorQuality.anchorType === "counterparty" &&
|
||
sourceAnchorQuality.quality >= 2 &&
|
||
Boolean(sourceAnchorQuality.anchorValue) &&
|
||
((candidateAnchorQuality.anchorType === "counterparty" &&
|
||
candidateAnchorQuality.quality >= 2 &&
|
||
Boolean(candidateAnchorQuality.anchorValue) &&
|
||
hasCounterpartyAnchorSubstitution(sourceAnchorQuality.anchorValue ?? "", candidateAnchorQuality.anchorValue ?? "")) ||
|
||
(candidateAnchorQuality.quality < sourceAnchorQuality.quality &&
|
||
hasCounterpartyAnchorSubstitution(sourceAnchorQuality.anchorValue ?? "", candidate)));
|
||
if (counterpartyAnchorSubstitutedByCandidate) {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: false,
|
||
traceId: normalized?.trace_id ?? null,
|
||
llmCanonicalCandidateDetected: true,
|
||
effectiveMessage: userMessage,
|
||
reason: "normalized_fragment_rejected_anchor_substitution",
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
}, userMessage);
|
||
}
|
||
const itemSemanticAnchorDegradedByCandidate = (sameIntentForAnchorSafety ||
|
||
Boolean(sourceSelectedObjectItemAnchorValue)) &&
|
||
Boolean(sourceSelectedObjectItemAnchorValue ?? sourceAnchorQuality.anchorValue) &&
|
||
Boolean(candidateSemanticItemAnchorValue) &&
|
||
(0, addressFilterExtractor_1.isInventoryItemAnchorDegradation)(sourceSelectedObjectItemAnchorValue ?? sourceAnchorQuality.anchorValue ?? "", candidateSemanticItemAnchorValue ?? "");
|
||
if (itemSemanticAnchorDegradedByCandidate) {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: false,
|
||
traceId: normalized?.trace_id ?? null,
|
||
llmCanonicalCandidateDetected: true,
|
||
effectiveMessage: userMessage,
|
||
reason: "normalized_fragment_rejected_anchor_degradation",
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
}, userMessage);
|
||
}
|
||
const anchorDegradedByCandidate = sameIntentForAnchorSafety &&
|
||
sourceAnchorQuality.anchorType &&
|
||
sourceAnchorQuality.quality >= 2 &&
|
||
candidateAnchorQuality.quality < sourceAnchorQuality.quality;
|
||
if (anchorDegradedByCandidate) {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: false,
|
||
traceId: normalized?.trace_id ?? null,
|
||
llmCanonicalCandidateDetected: true,
|
||
effectiveMessage: userMessage,
|
||
reason: "normalized_fragment_rejected_anchor_degradation",
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
}, userMessage);
|
||
}
|
||
if (fallbackCandidate) {
|
||
const fallbackAnchorQuality = evaluateAddressAnchorQuality(String(fallbackCandidate.candidate ?? ""));
|
||
const fallbackPreferredForAnchorSafety = sameIntentForAnchorSafety &&
|
||
fallbackAnchorQuality.intent === sourceAnchorQuality.intent &&
|
||
fallbackAnchorQuality.quality >= 2 &&
|
||
fallbackAnchorQuality.quality > candidateAnchorQuality.quality;
|
||
if (fallbackPreferredForAnchorSafety) {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: true,
|
||
traceId: normalized?.trace_id ?? null,
|
||
llmCanonicalCandidateDetected: true,
|
||
effectiveMessage: fallbackCandidate.candidate,
|
||
reason: "fallback_rule_preferred_over_llm_candidate_anchor_quality",
|
||
fallbackRuleHit: fallbackCandidate.rule,
|
||
sanitizedUserMessage,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
}, userMessage);
|
||
}
|
||
}
|
||
const semanticContractForCandidate = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
|
||
sourceMessage: String(userMessage ?? ""),
|
||
canonicalMessage: candidate,
|
||
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||
sourceMessage: String(userMessage ?? ""),
|
||
canonicalMessage: candidate,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
})
|
||
});
|
||
if (!semanticContractForCandidate.apply_canonical_recommended) {
|
||
const sourceDataSignalDetected = Boolean(semanticContractForCandidate?.guard_hints?.source_data_signal_detected);
|
||
const rawFragmentCandidatePreferred = Boolean(sourceDataSignalDetected &&
|
||
candidateFromNormalized &&
|
||
candidateFromNormalized.candidate === candidate &&
|
||
toNonEmptyString(candidate));
|
||
if (rawFragmentCandidatePreferred) {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: true,
|
||
traceId: normalized?.trace_id ?? null,
|
||
llmCanonicalCandidateDetected: true,
|
||
effectiveMessage: candidate,
|
||
reason: "normalized_fragment_semantic_guard_raw_fragment_preferred",
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
}, userMessage);
|
||
}
|
||
if (fallbackCandidate) {
|
||
const fallbackSemanticContract = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
|
||
sourceMessage: String(userMessage ?? ""),
|
||
canonicalMessage: String(fallbackCandidate.candidate ?? "")
|
||
});
|
||
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
|
||
const sourceCompactForFallback = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompactForFallback;
|
||
if (fallbackApplied && fallbackSemanticContract.apply_canonical_recommended && !sourceDataSignalDetected) {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: true,
|
||
traceId: normalized?.trace_id ?? null,
|
||
llmCanonicalCandidateDetected: true,
|
||
effectiveMessage: String(fallbackCandidate.candidate ?? ""),
|
||
reason: "fallback_rule_preferred_over_llm_candidate_semantic_guard",
|
||
fallbackRuleHit: fallbackCandidate.rule,
|
||
sanitizedUserMessage,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
}, userMessage);
|
||
}
|
||
}
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: false,
|
||
traceId: normalized?.trace_id ?? null,
|
||
llmCanonicalCandidateDetected: true,
|
||
effectiveMessage: userMessage,
|
||
reason: "normalized_fragment_rejected_semantic_guard",
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
}, userMessage);
|
||
}
|
||
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||
const candidateCompact = compactWhitespace(candidate.toLowerCase());
|
||
const applied = sourceCompact !== candidateCompact;
|
||
const candidateSource = candidateFromNormalized ? "normalized" : "raw";
|
||
const reason = candidateSource === "normalized"
|
||
? applied
|
||
? "normalized_fragment_applied"
|
||
: "normalized_fragment_same"
|
||
: normalized?.ok
|
||
? applied
|
||
? "raw_fragment_applied"
|
||
: "raw_fragment_same"
|
||
: applied
|
||
? "raw_fragment_applied_after_normalize_failed"
|
||
: "raw_fragment_same_after_normalize_failed";
|
||
return attachAddressPredecomposeContract({
|
||
attempted: true,
|
||
applied,
|
||
provider,
|
||
traceId: normalized?.trace_id ?? null,
|
||
effectiveMessage: applied ? candidate : userMessage,
|
||
reason,
|
||
llmCanonicalCandidateDetected: true,
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage,
|
||
semanticHints: candidateMeta?.semanticHints ?? null
|
||
}, userMessage);
|
||
}
|
||
catch (error) {
|
||
if (fallbackCandidate) {
|
||
const fallbackCompact = compactWhitespace(String(fallbackCandidate.candidate ?? "").toLowerCase());
|
||
const sourceCompact = compactWhitespace(String(userMessage ?? "").toLowerCase());
|
||
const fallbackApplied = fallbackCompact.length > 0 && fallbackCompact !== sourceCompact;
|
||
if (fallbackApplied) {
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
applied: true,
|
||
effectiveMessage: fallbackCandidate.candidate,
|
||
reason: "fallback_rule_applied_after_llm_error",
|
||
fallbackRuleHit: fallbackCandidate.rule
|
||
}, userMessage);
|
||
}
|
||
}
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
reason: `error:${error instanceof Error ? error.message : String(error)}`
|
||
}, userMessage);
|
||
}
|
||
}
|
||
function resolveAddressToolGateDecision(addressInputMessage, followupContext, llmPreDecomposeMeta = null, rawUserMessage = null) {
|
||
const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? ""));
|
||
const rawMessageForGate = String(rawUserMessage ?? addressInputMessage ?? "");
|
||
const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawMessageForGate) ||
|
||
hasAssistantDataScopeMetaQuestionSignal(repairedInputMessage);
|
||
const capabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(rawMessageForGate) ||
|
||
shouldHandleAsAssistantCapabilityMetaQuery(repairedInputMessage);
|
||
const dataRetrievalSignal = hasDataRetrievalRequestSignal(rawMessageForGate) ||
|
||
hasDataRetrievalRequestSignal(repairedInputMessage);
|
||
if (dataScopeMetaQuery || (capabilityMetaQuery && !dataRetrievalSignal)) {
|
||
return {
|
||
runAddressLane: false,
|
||
decision: "skip_address_lane",
|
||
reason: dataScopeMetaQuery ? "assistant_data_scope_query_detected" : "assistant_capability_query_detected"
|
||
};
|
||
}
|
||
const directDeepAnalysisSignal = hasDirectDeepAnalysisSignal(rawMessageForGate) ||
|
||
hasDirectDeepAnalysisSignal(repairedInputMessage);
|
||
const deepAnalysisPreferenceSignal = directDeepAnalysisSignal ||
|
||
hasDeepAnalysisPreferenceSignal(rawMessageForGate) ||
|
||
hasDeepAnalysisPreferenceSignal(repairedInputMessage);
|
||
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedInputMessage || addressInputMessage);
|
||
const modeDetectionRaw = (0, addressQueryClassifier_1.detectAddressQuestionMode)(String(addressInputMessage ?? ""));
|
||
const hasClassifierSignal = modeDetection.mode === "address_query" || modeDetectionRaw.mode === "address_query";
|
||
const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(repairedInputMessage || addressInputMessage);
|
||
const intentResolutionRaw = (0, addressIntentResolver_1.resolveAddressIntent)(String(addressInputMessage ?? ""));
|
||
const hasIntentSignal = intentResolution.intent !== "unknown" || intentResolutionRaw.intent !== "unknown";
|
||
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
|
||
const llmContractModeConfidence = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode_confidence);
|
||
const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||
const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract &&
|
||
typeof llmPreDecomposeMeta.semanticExtractionContract === "object"
|
||
? llmPreDecomposeMeta.semanticExtractionContract
|
||
: null;
|
||
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
||
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
|
||
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
||
semanticCanonicalRecommended;
|
||
const llmCanonicalEntitySignal = /(?:\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
||
const hasLlmCanonicalSignal = semanticCanonicalRecommended &&
|
||
Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
|
||
((llmContractMode === "address_query" && llmContractModeConfidence !== "low") ||
|
||
(llmCanonicalAppliedSignal &&
|
||
(hasStrongDataIntentSignal(repairedInputMessage) || llmCanonicalEntitySignal)));
|
||
const hasLlmCanonicalDataSignal = semanticCanonicalRecommended &&
|
||
Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected) &&
|
||
Boolean(llmPreDecomposeMeta?.applied) &&
|
||
(llmContractMode === "address_query" || llmContractMode === "unsupported" || llmContractMode === null) &&
|
||
hasStrongDataIntentSignal(repairedInputMessage);
|
||
const hasBusinessRankingAddressSignal = /(?:\u043a\u0442\u043e\s+(?:\u043d\u0430\u043c\s+)?(?:\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u043f\u0440\u0438\u043d\u0435\u0441|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?).*(?:\u0434\u0435\u043d\u0435\u0433)?|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||
const sameDateAccountFollowupSignal = hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate) ||
|
||
hasSameDateAccountFollowupSignalForPredecompose(repairedInputMessage);
|
||
const hasLexicalAddressSignal = isAddressLlmPreDecomposeCandidate(addressInputMessage) ||
|
||
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
|
||
hasAccountingSignal(addressInputMessage) ||
|
||
hasAccountingSignal(repairedInputMessage) ||
|
||
hasShortDebtMirrorFollowupSignal(rawMessageForGate) ||
|
||
hasShortDebtMirrorFollowupSignal(repairedInputMessage) ||
|
||
hasBusinessRankingAddressSignal ||
|
||
sameDateAccountFollowupSignal;
|
||
const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" &&
|
||
(llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") &&
|
||
llmContractIntent === "unknown";
|
||
const hasAnyAddressSignal = hasClassifierSignal || hasIntentSignal || hasLlmCanonicalSignal || hasLlmCanonicalDataSignal || hasLexicalAddressSignal || llmSupportedDeepAddressIntentSignal;
|
||
const strongDataSignalFromRawMessage = hasStrongDataIntentSignal(rawMessageForGate) ||
|
||
hasDataRetrievalRequestSignal(rawMessageForGate) ||
|
||
hasAccountingSignal(rawMessageForGate) ||
|
||
hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate);
|
||
const strongDataSignalFromEffectiveMessage = hasStrongDataIntentSignal(repairedInputMessage) ||
|
||
hasAccountingSignal(repairedInputMessage) ||
|
||
hasDataRetrievalRequestSignal(repairedInputMessage);
|
||
if (!semanticCanonicalRecommended &&
|
||
llmContractIntent === "unknown" &&
|
||
!followupContext &&
|
||
!hasClassifierSignal &&
|
||
!hasIntentSignal &&
|
||
!hasLexicalAddressSignal &&
|
||
!llmSupportedDeepAddressIntentSignal &&
|
||
!strongDataSignalFromRawMessage &&
|
||
!strongDataSignalFromEffectiveMessage) {
|
||
return {
|
||
runAddressLane: false,
|
||
decision: "skip_address_lane",
|
||
reason: "llm_predecompose_semantic_guard_rejected"
|
||
};
|
||
}
|
||
if (hasUnsupportedLowConfidencePredecomposeSignal && !followupContext &&
|
||
!hasAnyAddressSignal &&
|
||
!llmSupportedDeepAddressIntentSignal &&
|
||
!strongDataSignalFromRawMessage &&
|
||
!strongDataSignalFromEffectiveMessage) {
|
||
return {
|
||
runAddressLane: false,
|
||
decision: "skip_address_lane",
|
||
reason: "llm_predecompose_unsupported_mode"
|
||
};
|
||
}
|
||
const hasMessageSignal = hasAnyAddressSignal || strongDataSignalFromRawMessage || strongDataSignalFromEffectiveMessage;
|
||
if (hasMessageSignal) {
|
||
return {
|
||
runAddressLane: true,
|
||
decision: "run_address_lane",
|
||
reason: hasClassifierSignal
|
||
? "address_mode_classifier_detected"
|
||
: hasIntentSignal
|
||
? "address_intent_resolver_detected"
|
||
: hasLlmCanonicalSignal
|
||
? "llm_canonical_candidate_detected"
|
||
: hasLlmCanonicalDataSignal
|
||
? "llm_canonical_data_signal_detected"
|
||
: llmSupportedDeepAddressIntentSignal
|
||
? "address_signal_detected"
|
||
: "address_signal_detected"
|
||
};
|
||
}
|
||
if (followupContext) {
|
||
return {
|
||
runAddressLane: true,
|
||
decision: "run_address_lane",
|
||
reason: "followup_context_detected"
|
||
};
|
||
}
|
||
return {
|
||
runAddressLane: false,
|
||
decision: "skip_address_lane",
|
||
reason: "no_address_signal_after_l0"
|
||
};
|
||
}
|
||
function hasLooseAllTimeAddressLookupSignal(text) {
|
||
const repaired = repairAddressMojibake(String(text ?? ""));
|
||
const normalized = compactWhitespace(repaired.toLowerCase());
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
if (shouldHandleAsAssistantCapabilityMetaQuery(normalized) || hasAssistantDataScopeMetaQuestionSignal(normalized)) {
|
||
return false;
|
||
}
|
||
const hasAllTimeSignal = /(?:\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u0435\s+\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|for\s+all\s+time|all\s+time|entire\s+period|full\s+period)/iu.test(normalized);
|
||
if (!hasAllTimeSignal) {
|
||
return false;
|
||
}
|
||
return /(?:\u0447\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u0447[\u0435\u0451]\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u043a\u0430\u0436\u0438|\u0432\u044b\u0432\u0435\u0434\u0438|\u0434\u0430\u0439|show|list|find)/iu.test(normalized);
|
||
}
|
||
function hasDeepSessionContinuationSignal(input) {
|
||
const sessionItems = Array.isArray(input?.sessionItems) ? input.sessionItems : [];
|
||
if (sessionItems.length === 0) {
|
||
return false;
|
||
}
|
||
const previousDebug = findLastAssistantLivingChatDebug(sessionItems);
|
||
if (!previousDebug || typeof previousDebug !== "object") {
|
||
return false;
|
||
}
|
||
const investigationState = previousDebug.investigation_state_snapshot;
|
||
if (!investigationState || typeof investigationState !== "object") {
|
||
return false;
|
||
}
|
||
const candidateTexts = [
|
||
input?.rawUserMessage,
|
||
input?.repairedRawUserMessage,
|
||
input?.effectiveAddressUserMessage,
|
||
input?.repairedEffectiveAddressUserMessage
|
||
]
|
||
.map((value) => compactWhitespace(repairAddressMojibake(String(value ?? "")).toLowerCase()))
|
||
.filter((value) => value.length > 0);
|
||
if (candidateTexts.length === 0) {
|
||
return false;
|
||
}
|
||
return candidateTexts.some((text) => {
|
||
const hasContinuationCue = /^(?:\u0438|\u0430|\u0442\u0430\u043a\u0436\u0435|\u0435\u0449[\u0435\u0451]|\u0434\u043e\u0431\u0430\u0432\u044c|\u0434\u043e\u043f\u043e\u043b\u043d\u0438|\u0443\u0442\u043e\u0447\u043d\u0438|\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438|\u0442\u0435\u043f\u0435\u0440\u044c|then|also|and)\b/iu.test(text) ||
|
||
/(?:\u043f\u043e\s+\u0442\u043e\u043c\u0443\s+\u0436\u0435|\u043f\u043e\s+\u044d\u0442\u043e\u043c\u0443|\u0432\s+\u044d\u0442\u043e\u043c\s+\u0436\u0435|\u0438\s+\u043f\u043e\s+\u043f\u0435\u0440\u0438\u043e\u0434\u0443|\u0434\u043e\u0431\u0430\u0432\u044c\s+\u0443\u0442\u043e\u0447\u043d\u0435\u043d\u0438\u0435|\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u043c|\u0430\s+\u0435\u0441\u043b\u0438|\u0435\u0441\u043b\u0438\s+\u0442\u043e\u043b\u044c\u043a\u043e|\u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e)/iu.test(text);
|
||
const hasAccountOrPeriodCue = /(?:\u0441\u0447[\u0435\u0451]\u0442|account|\b\d{2}(?:[.,]\d{1,2})?\b|\b20\d{2}(?:[-/.]\d{1,2})?\b|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446)/iu.test(text);
|
||
const hasDeepRebindCue = /(?:\u0430\u043c\u043e\u0440\u0442\u0438\u0437|fixed\s*asset|\u043e\u0441\b|\u043d\u0434\u0441|vat|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447\u043a|\u0430\u043d\u043e\u043c\u0430\u043b|lifecycle|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447)/iu.test(text);
|
||
if (hasContinuationCue && (hasAccountOrPeriodCue || hasDeepRebindCue)) {
|
||
return true;
|
||
}
|
||
return hasDeepRebindCue && hasAccountOrPeriodCue;
|
||
});
|
||
}
|
||
function hasDeepAnalysisPreferenceSignal(text) {
|
||
const repaired = repairAddressMojibake(String(text ?? ""));
|
||
const lower = compactWhitespace(repaired.toLowerCase());
|
||
if (!lower) {
|
||
return false;
|
||
}
|
||
const riskOrAnomalySignal = /(?:\u0440\u0438\u0441\u043a|risk|\u0430\u043d\u043e\u043c\u0430\u043b|anomal|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442|conflict|deviation|\u043e\u0442\u043a\u043b\u043e\u043d\u0435\u043d|\u043d\u0435\u0441\u044b\u043a\u043e\u0432\u043a|\u043d\u0435\u0441\u0445\u043e\u0434|\u043e\u0448\u0438\u0431|error|issue|\u043f\u0440\u043e\u0431\u043b\u0435\u043c)/iu.test(lower);
|
||
const chainSignal = /(?:\u0446\u0435\u043f\u043e\u0447\u043a|chain|trace\s*chain|lifecycle|\u0436\u0438\u0437\u043d\u0435\u043d\u043d[\u0430-\u044f]+\s+\u0446\u0438\u043a\u043b|state\s+transition|\u0440\u0430\u0437\u0440\u044b\u0432[\u0430-\u044f]*)/iu.test(lower);
|
||
const diagnosticsKeywordSignal = /(?:\u0440\u0430\u0437\u043b\u043e\u0436\u0438|\u0434\u0435\u043a\u043e\u043c\u043f\u043e\u0437|\u0440\u0430\u0437\u0431\u0435\u0440\u0438|\u043f\u043e\u0447\u0435\u043c\u0443|why|audit|scan|\u043a\u043e\u0440\u043d\u0435\u0432[\u0430-\u044f]+\s+\u043f\u0440\u0438\u0447\u0438\u043d|root\s*cause|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c[\u0430-\u044f]*|\u0433\u0434\u0435\s+\u0440\u0430\u0437\u0440\u044b\u0432|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442)/iu.test(lower);
|
||
const diagnosticsCheckVerbSignal = /(?:^|[\s,.;:!?()\-])\u043f\u0440\u043e\u0432\u0435\u0440(?:\u044c|\u0438\u0442\u044c|\u043a\u0443|\u0438\u043c|\u043a\u0430)(?:$|[\s,.;:!?()\-])/iu.test(lower);
|
||
const diagnosticsSignal = diagnosticsKeywordSignal || diagnosticsCheckVerbSignal;
|
||
const closureSignal = /(?:\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]\s+\u043f\u0435\u0440\u0438\u043e\u0434|period\s*close|\u043d\u0435\s+\u0437\u0430\u043a\u0440\u044b\u043b[\u0430-\u044f]*|\u0445\u0432\u043e\u0441\u0442[\u0430-\u044f]*)/iu.test(lower);
|
||
const closureIntentSignal = /(?:\u0437\u0430\u043a\u0440\u044b\u0442[\u0430-\u044f]*|period\s*close|close\s+period)/iu.test(lower);
|
||
const closureDiagnosticPhraseSignal = /(?:\u0447\u0442\u043e(?:\s+\S+){0,8}\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442)/iu.test(lower);
|
||
const signalVsNoiseDiagnostic = /(?:\u043d\u0435\s+\u043f\u0440\u043e\u0441\u0442\u043e\s+(?:\u043d\u0430\s+)?\u0448\u0443\u043c|\u043f\u043e\u0445\u043e\u0436[\u0438\u0435]\s+(?:\u0438\u043c\u0435\u043d\u043d\u043e\s+)?\u043d\u0430\s+\u043f\u0440\u043e\u0431\u043b\u0435\u043c)/iu.test(lower);
|
||
const lifecycleMismatchSignal = /(?:\u043d\u0435\s+\u0442\u0435\u043c\s+\u0442\u0438\u043f(?:\u043e\u043c)?\s+\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]+\s+\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043d\u0435\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|wrong\s+closing\s+document|expected\s+transition)/iu.test(lower);
|
||
const lifecycleTransitionGapSignal = /(?:\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]+\s+\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432|\u043f\u0435\u0440\u0435\u0445\u043e\u0434[\u0430-\u044f]*\s+\u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432|\u0441\u0442\u0430\u0434\u0438[\u0438\u044f\u0435]\s+.*\u043f\u0440\u043e\u0439\u0434\u0435\u043d.*\u043f\u0435\u0440\u0435\u0445\u043e\u0434)/iu.test(lower);
|
||
const expectedActualMismatchSignal = /(?:\u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a[\u0430-\u044f]+\s+\u0441\u043e\u0441\u0442\u043e\u044f\u043d[\u0438\u0435\u044f]+\s+.*\u0440\u0430\u0441\u0445\u043e\u0434[\u0430-\u044f]*\s+\u0441\s+\u043e\u0436\u0438\u0434\u0430\u0435\u043c|\u043e\u0436\u0438\u0434\u0430\u0435\u043c[\u0430-\u044f]+\s+\u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d[\u0430-\u044f]*\s+\u0441\u043f\u0438\u0441\u0430\u043d)/iu.test(lower);
|
||
return lifecycleMismatchSignal ||
|
||
(chainSignal && lifecycleTransitionGapSignal) ||
|
||
expectedActualMismatchSignal ||
|
||
(chainSignal && diagnosticsSignal) ||
|
||
(riskOrAnomalySignal && (chainSignal || diagnosticsSignal || lifecycleTransitionGapSignal)) ||
|
||
(diagnosticsSignal && (closureSignal || closureIntentSignal)) ||
|
||
closureDiagnosticPhraseSignal ||
|
||
signalVsNoiseDiagnostic;
|
||
}
|
||
function hasDirectDeepAnalysisSignal(text) {
|
||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
return /(?:\u0440\u0430\u0437\u043b\u043e\u0436|\u0446\u0435\u043f\u043e\u0447|lifecycle|\u0440\u0430\u0437\u0440\u044b\u0432|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]\s+\u043f\u0435\u0440\u0438\u043e\u0434|period\s*close|close\s+period|\u0447\u0442\u043e\s+\u043c\u0435\u0448\u0430[\u0430-\u044f]+\s+\u0437\u0430\u043a\u0440\u044b\u0442|state\s+transition|root\s*cause|trace\s*chain)/iu.test(normalized);
|
||
}
|
||
function hasStrictDeepInvestigationCue(text) {
|
||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
const hasInvestigativeVerb = /(?:\u043f\u0440\u043e\u0432\u0435\u0440(?:\u044c|\u0438\u0442\u044c)|\u0440\u0430\u0437\u0431\u0435\u0440(?:\u0438|\u0430\u0442\u044c)|\u0440\u0430\u0437\u043b\u043e\u0436(?:\u0438|\u0438\u0442\u044c)|\u043f\u043e\u0447\u0435\u043c\u0443|\u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c|root\s*cause|trace\s*chain)/iu.test(normalized);
|
||
if (!hasInvestigativeVerb) {
|
||
return false;
|
||
}
|
||
return /(?:\u0445\u0432\u043e\u0441\u0442|\u0440\u0430\u0437\u0440\u044b\u0432|\u0446\u0435\u043f\u043e\u0447|\u0430\u043d\u043e\u043c\u0430\u043b|\u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447|\u0437\u0430\u043a\u0440\u044b\u0442\u0438[\u0435\u044f]|\u043e\u0431\u044a\u0435\u043a\u0442(?:\u0443)?\s+\u0440\u0430\u0441\u0447(?:\u0435|\u0451)\u0442|lifecycle|state\s+transition)/iu.test(normalized);
|
||
}
|
||
function hasAggregateBusinessAnalyticsSignal(text) {
|
||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
const hasTurnoverByEntityBridgeSignal = /(?:\u043e\u0431\u043e\u0440\u043e\u0442(?:\u044b)?|\u0432\u044b\u0440\u0443\u0447\u043a|\u0434\u043e\u0445\u043e\u0434|turnover|revenue).*(?:\u0437\u0430\s+\d{4}\s+\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u0433\u043e\u0434|year|month|quarter)/iu.test(normalized) &&
|
||
/(?:\b\u043f\u043e\s+[\p{L}\d._-]{3,}|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|customer|supplier|counterparty|company)/iu.test(normalized);
|
||
if (hasTurnoverByEntityBridgeSignal) {
|
||
return true;
|
||
}
|
||
const hasMetricCue = /(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u043e\u0445\u043e\u0434|\u043f\u0440\u0438\u0431\u044b\u043b|\u043c\u0430\u0440\u0436|\u0440\u0435\u043d\u0442\u0430\u0431\u0435\u043b|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b|turnover|revenue|profit|margin)/iu.test(normalized);
|
||
if (!hasMetricCue) {
|
||
return false;
|
||
}
|
||
const hasRankingOrTrendCue = /(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435)|\u0442\u043e\u043f|\u043b\u0443\u0447\u0448|\u0445\u0443\u0434\u0448|\u043c\u0430\u043a\u0441(?:\u0438\u043c\u0443\u043c)?|\u043c\u0438\u043d(?:\u0438\u043c\u0443\u043c)?|\u0434\u0438\u043d\u0430\u043c|\u0442\u0440\u0435\u043d\u0434|\u0441\u0440\u0430\u0432\u043d|ranking|top|best|worst)/iu.test(normalized);
|
||
const hasPeriodAggregateCue = /(?:\u043f\u043e\s+\u0433\u043e\u0434\u0430\u043c|\u0437\u0430\s+\d{4}\s+\u0433\u043e\u0434|\u0433\u043e\u0434(?:\u0430|\u0443|\u044b)?|year|years|\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043c\u0435\u0441\u044f\u0446|\u043f\u0435\u0440\u0438\u043e\u0434)/iu.test(normalized);
|
||
return hasRankingOrTrendCue || hasPeriodAggregateCue;
|
||
}
|
||
function hasOpenContractsAddressSignal(text) {
|
||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
const hasContractCue = /(?:договор|контракт|contract)/iu.test(normalized);
|
||
if (!hasContractCue) {
|
||
return false;
|
||
}
|
||
const hasOpenCue = /(?:незакрыт|не\s+закрыт|открыт|open\s+contract|open\s+item|open)/iu.test(normalized);
|
||
if (!hasOpenCue) {
|
||
return false;
|
||
}
|
||
const hasRequestCue = /(?:покажи|показать|список|какие|какой|show|list|find|на\s+дату|as\s+of)/iu.test(normalized);
|
||
const hasTemporalCue = hasPeriodLiteral(normalized) || /\b\d{4}[-/.]\d{2}[-/.]\d{2}\b/.test(normalized);
|
||
return hasRequestCue || hasTemporalCue;
|
||
}
|
||
function resolveAssistantOrchestrationDecision(input) {
|
||
return assistantRoutePolicy.resolveAssistantOrchestrationDecision(input);
|
||
}
|
||
function hasStrongDataIntentSignal(text) {
|
||
return assistantLivingModePolicy.hasStrongDataIntentSignal(text);
|
||
}
|
||
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) {
|
||
if (!Array.isArray(items)) {
|
||
return null;
|
||
}
|
||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||
const item = items[index];
|
||
if (!item || item.role !== "assistant") {
|
||
continue;
|
||
}
|
||
if (item.debug && typeof item.debug === "object") {
|
||
return item.debug;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function findLastGroundedAddressAnswerDebug(items) {
|
||
if (!Array.isArray(items)) {
|
||
return null;
|
||
}
|
||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||
const item = items[index];
|
||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||
continue;
|
||
}
|
||
const debug = item.debug;
|
||
if (debug.execution_lane !== "address_query") {
|
||
continue;
|
||
}
|
||
const groundingStatus = toNonEmptyString(debug.answer_grounding_check?.status);
|
||
if (groundingStatus === "grounded") {
|
||
return debug;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function findLastOrganizationClarificationAddressDebug(items) {
|
||
if (!Array.isArray(items)) {
|
||
return null;
|
||
}
|
||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||
const item = items[index];
|
||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||
continue;
|
||
}
|
||
const debug = item.debug;
|
||
if (debug.execution_lane !== "address_query" && debug.detected_mode !== "address_query") {
|
||
continue;
|
||
}
|
||
const limitedCategory = toNonEmptyString(debug.limited_reason_category);
|
||
const candidates = Array.isArray(debug.organization_candidates)
|
||
? mergeKnownOrganizations(debug.organization_candidates)
|
||
: [];
|
||
if (limitedCategory === "missing_anchor" && candidates.length > 0) {
|
||
return debug;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function hasMetaAnswerFollowupSignal(userMessage) {
|
||
return assistantLivingModePolicy.hasMetaAnswerFollowupSignal(userMessage);
|
||
}
|
||
function hasConversationMemoryRecallFollowupSignal(userMessage) {
|
||
return assistantLivingModePolicy.hasConversationMemoryRecallFollowupSignal(userMessage);
|
||
}
|
||
function hasHistoricalCapabilityFollowupSignal(text) {
|
||
return assistantLivingModePolicy.hasHistoricalCapabilityFollowupSignal(text);
|
||
}
|
||
function isGroundedInventoryContextDebug(debug) {
|
||
if (!debug || typeof debug !== "object") {
|
||
return false;
|
||
}
|
||
const detectedIntent = toNonEmptyString(debug.detected_intent);
|
||
const capabilityId = toNonEmptyString(debug.capability_id);
|
||
const rootFrameContext = debug.address_root_frame_context && typeof debug.address_root_frame_context === "object"
|
||
? debug.address_root_frame_context
|
||
: null;
|
||
const rootIntent = toNonEmptyString(rootFrameContext?.root_intent);
|
||
return detectedIntent === "inventory_on_hand_as_of_date" ||
|
||
capabilityId === "confirmed_inventory_on_hand_as_of_date" ||
|
||
rootIntent === "inventory_on_hand_as_of_date";
|
||
}
|
||
function hasOrganizationFactFollowupSignal(userMessage, items) {
|
||
return assistantLivingModePolicy.hasOrganizationFactFollowupSignal(userMessage, items);
|
||
}
|
||
function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization) {
|
||
return assistantLivingModePolicy.shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization);
|
||
}
|
||
function hasOperationalAdminActionRequestSignal(text) {
|
||
const lower = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
||
const normalized = lower.replace(/\b1\s*[cс]\b/giu, "1с");
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
const hasAdminVerb = /(?:настро(?:й|ить|ите|им)|установ(?:и|ить|ите)|подключ(?:и|ить|ите)|обнов(?:и|ить|ите)|почин(?:и|ить|ите)|исправ(?:ь|ить|ьте)|перенастро|перезапуст|удал(?:и|ить|ите|яй)|снес(?:и|ти)|очист(?:и|ить)|восстанов(?:и|ить)|созд(?:ай|ать)|провед(?:и|и)|заведи|завести)/i.test(normalized);
|
||
const hasAdminObject = /(?:1с|1c|баз|сервер|конфиг|конфигурац|платформ|админ|доступ|роль|права|db|database|документ)/i.test(normalized);
|
||
if (hasAdminVerb && hasAdminObject) {
|
||
return true;
|
||
}
|
||
return /(?:удаляй?\s+баз|удали\s+баз|снеси\s+баз|delete\s+database|drop\s+database)/i.test(normalized);
|
||
}
|
||
function hasDangerOrCoercionSignal(text) {
|
||
const normalized = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
return /(?:убью|убить|убьют|убиют|угрож|опасн|омон|полици|насили|заставля|принужда|шантаж|выпил(?:юсь|иться|ился|илась|иться)|суицид|самоубий|покончу\s+с\s+собой|не\s+хочу\s+жить|хочу\s+умереть|сдохнуть|вскрыть\s+вены|повешусь|повеситься|спрыгну|kill\s+myself|end\s+my\s+life|suicid|self[\s-]?harm)/iu.test(normalized);
|
||
}
|
||
function hasDestructiveDataActionSignal(text) {
|
||
const lower = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
||
if (!lower) {
|
||
return false;
|
||
}
|
||
return /(?:удаляй?\s+баз|удали\s+баз|снеси\s+баз|drop\s+database|delete\s+database|очисти\s+баз)/i.test(lower);
|
||
}
|
||
function hasAssistantCapabilityQuestionSignal(text) {
|
||
const repaired = repairAddressMojibake(String(text ?? ""));
|
||
const lower = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
|
||
const normalized = lower.replace(/\b1\s*[cс]\b/giu, "1с");
|
||
if (!lower) {
|
||
return false;
|
||
}
|
||
const directCapabilityPhrases = [
|
||
"кто ты",
|
||
"что ты можешь",
|
||
"что конкретно ты можешь",
|
||
"чем ты можешь помочь",
|
||
"что ты умеешь",
|
||
"какой у тебя функционал",
|
||
"какие у тебя функции",
|
||
"какие фичи",
|
||
"что отработано",
|
||
"что у тебя отработано",
|
||
"полный список возможностей",
|
||
"полный список"
|
||
];
|
||
if (directCapabilityPhrases.some((phrase) => normalized.includes(phrase))) {
|
||
return true;
|
||
}
|
||
if (/(?:каки[ею].*(?:фич|функц|возможност|отработан)|какого\s+рода\s+ошибк.*ты\s+мож(?:ешь|ете)|какие\s+ошибк.*ты\s+мож(?:ешь|ете))/iu.test(normalized)) {
|
||
return true;
|
||
}
|
||
const hasCanVerb = /(?:можешь|можете|умеешь|умеете|можно)/i.test(normalized);
|
||
const hasControlAction = /(?:настро|установ|подключ|обнов|созда|подготов|сдела|делат|дела)/i.test(normalized);
|
||
const hasAnalysisAction = /(?:найт|искать|провер|анализ|разоб|объясн|расска|подсказ|показ)/i.test(normalized);
|
||
const hasCapabilityObject = /(?:1с|1c|док|документ|баз|отчет|отч[её]т|конфигурац|настройк)/i.test(normalized);
|
||
if (hasCanVerb && hasControlAction && hasCapabilityObject) {
|
||
return true;
|
||
}
|
||
if (hasCanVerb && hasAnalysisAction && !hasDataRetrievalRequestSignal(normalized)) {
|
||
return true;
|
||
}
|
||
const hasCapabilityMetaQuestion = /(?:что|чем)\s+(?:ты\s+)?(?:мож(?:ешь|ете)|уме(?:ешь|ете)|можно)(?=$|[\s,.!?;:])/iu.test(normalized);
|
||
if (hasCapabilityMetaQuestion && hasCapabilityObject) {
|
||
return true;
|
||
}
|
||
if ((normalized.includes("1с") || normalized.includes("1c")) && hasCanVerb && /(?:настро|установ|подключ|обнов)/i.test(normalized)) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function hasAssistantDataScopeMetaQuestionSignal(text) {
|
||
return assistantLivingModePolicy.hasAssistantDataScopeMetaQuestionSignal(text);
|
||
}
|
||
function shouldHandleAsAssistantCapabilityMetaQuery(text) {
|
||
return assistantLivingModePolicy.shouldHandleAsAssistantCapabilityMetaQuery(text);
|
||
}
|
||
function hasLivingChatSignal(text) {
|
||
return assistantLivingModePolicy.hasLivingChatSignal(text);
|
||
}
|
||
function buildAssistantCapabilityContractReply() {
|
||
return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)();
|
||
}
|
||
const assistantProviderExecutionPolicy = (0, assistantProviderExecutionPolicy_1.createAssistantProviderExecutionPolicy)();
|
||
const assistantLivingModePolicy = (0, assistantLivingModePolicy_1.createAssistantLivingModePolicy)({
|
||
featureAssistantLivingChatRouterV1: config_1.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1,
|
||
compactWhitespace,
|
||
repairAddressMojibake,
|
||
toNonEmptyString,
|
||
normalizeOrganizationScopeValue,
|
||
hasReferentialPointer,
|
||
hasSmallTalkSignal,
|
||
hasAssistantCapabilityQuestionSignal,
|
||
hasOperationalAdminActionRequestSignal,
|
||
resolveProviderExecutionState: assistantProviderExecutionPolicy.resolveProviderExecutionState
|
||
});
|
||
const assistantMetaFollowupPolicy = (0, assistantMetaFollowupPolicy_1.createAssistantMetaFollowupPolicy)({
|
||
hasAssistantDataScopeMetaQuestionSignal: assistantLivingModePolicy.hasAssistantDataScopeMetaQuestionSignal,
|
||
shouldHandleAsAssistantCapabilityMetaQuery: assistantLivingModePolicy.shouldHandleAsAssistantCapabilityMetaQuery,
|
||
hasMetaAnswerFollowupSignal: assistantLivingModePolicy.hasMetaAnswerFollowupSignal
|
||
});
|
||
const assistantMemoryRecapPolicy = (0, assistantMemoryRecapPolicy_1.createAssistantMemoryRecapPolicy)({
|
||
hasHistoricalCapabilityFollowupSignal: assistantLivingModePolicy.hasHistoricalCapabilityFollowupSignal,
|
||
hasConversationMemoryRecallFollowupSignal: assistantLivingModePolicy.hasConversationMemoryRecallFollowupSignal,
|
||
isGroundedInventoryContextDebug
|
||
});
|
||
const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePolicy)({
|
||
repairAddressMojibake,
|
||
findLastGroundedAddressAnswerDebug,
|
||
findLastOrganizationClarificationAddressDebug,
|
||
mergeKnownOrganizations,
|
||
normalizeOrganizationScopeValue,
|
||
resolveOrganizationSelectionFromMessage,
|
||
resolveMetaSignalSet: assistantMetaFollowupPolicy.resolveMetaSignalSet,
|
||
resolveHardMetaMode: assistantMetaFollowupPolicy.resolveHardMetaMode,
|
||
isMetaFollowupOverGroundedAnswer: assistantMetaFollowupPolicy.isMetaFollowupOverGroundedAnswer,
|
||
hasDataRetrievalRequestSignal: assistantLivingModePolicy.hasDataRetrievalRequestSignal,
|
||
hasOrganizationFactLookupSignal,
|
||
hasOrganizationFactFollowupSignal: assistantLivingModePolicy.hasOrganizationFactFollowupSignal,
|
||
hasAggregateBusinessAnalyticsSignal,
|
||
hasStandaloneAddressTopicSignal,
|
||
hasOpenContractsAddressSignal,
|
||
detectAddressQuestionMode: addressQueryClassifier_1.detectAddressQuestionMode,
|
||
resolveAddressIntent: addressIntentResolver_1.resolveAddressIntent,
|
||
toNonEmptyString,
|
||
hasStrictDeepInvestigationCue,
|
||
hasStrongDataIntentSignal: assistantLivingModePolicy.hasStrongDataIntentSignal,
|
||
hasAccountingSignal,
|
||
hasDangerOrCoercionSignal,
|
||
hasAddressFollowupContextSignal,
|
||
hasShortDebtMirrorFollowupSignal,
|
||
isInventorySelectedObjectIntent,
|
||
hasShortInventoryObjectFollowupSignal,
|
||
resolveRouteMemorySignals: assistantMemoryRecapPolicy.resolveRouteMemorySignals,
|
||
findLastAddressAssistantItem,
|
||
resolveAddressToolGateDecision,
|
||
hasSameDateAccountFollowupSignalForPredecompose,
|
||
hasLooseAllTimeAddressLookupSignal,
|
||
hasDeepAnalysisPreferenceSignal,
|
||
hasDirectDeepAnalysisSignal,
|
||
compactWhitespace,
|
||
hasDeepSessionContinuationSignal,
|
||
resolveLivingAssistantModeDecision: assistantLivingModePolicy.resolveLivingAssistantModeDecision,
|
||
resolveProviderExecutionState: assistantProviderExecutionPolicy.resolveProviderExecutionState
|
||
});
|
||
const assistantTransitionPolicy = (0, assistantTransitionPolicy_1.createAssistantTransitionPolicy)({
|
||
compactWhitespace,
|
||
repairAddressMojibake,
|
||
countTokens,
|
||
findLastAddressAssistantItem,
|
||
findLastOrganizationClarificationAddressDebug,
|
||
mergeKnownOrganizations,
|
||
resolveOrganizationSelectionFromMessage,
|
||
toNonEmptyString,
|
||
buildAddressFollowupOffer,
|
||
isImplicitAddressContinuationByLlm,
|
||
isInventorySelectedObjectIntent,
|
||
hasShortInventoryObjectFollowupSignal,
|
||
resolveDebtRoleSwapFollowupIntent,
|
||
hasAddressFollowupContextSignal,
|
||
extractDisplayedEntityIndexMention,
|
||
findRecentInventoryRootFrame,
|
||
hasInventoryRootTemporalFollowupSignal,
|
||
hasFollowupMarker,
|
||
hasReferentialPointer,
|
||
hasStandaloneAddressTopicSignal,
|
||
resolveAddressIntent: addressIntentResolver_1.resolveAddressIntent,
|
||
resolveAddressIntentFamily,
|
||
readAddressFilterString,
|
||
normalizeOrganizationScopeValue,
|
||
isInventoryDrilldownFrameIntent,
|
||
isInventoryRootFrameIntent,
|
||
findRecentAddressFilterValue,
|
||
hasForeignAccountingPivotOverInventoryMessage,
|
||
buildRootScopedCarryoverFilters,
|
||
inferDisplayedEntityTypeFromIntent,
|
||
extractDisplayedAddressEntityCandidates,
|
||
resolveDisplayedAddressEntityMention
|
||
});
|
||
const assistantDataScopePolicy = (0, assistantDataScopePolicy_1.createAssistantDataScopePolicy)({
|
||
activeMcpChannel: config_1.ASSISTANT_MCP_CHANNEL,
|
||
mcpProxyUrl: config_1.ASSISTANT_MCP_PROXY_URL,
|
||
executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery,
|
||
repairAddressMojibake
|
||
});
|
||
function normalizeScopeKey(value) {
|
||
return repairAddressMojibake(String(value ?? "")).toLowerCase().replace(/ё/g, "е");
|
||
}
|
||
const ORGANIZATION_SCOPE_STOPWORDS = new Set([
|
||
"ооо",
|
||
"ao",
|
||
"ао",
|
||
"зао",
|
||
"ип",
|
||
"llc",
|
||
"ltd",
|
||
"company",
|
||
"компания",
|
||
"организация",
|
||
"организации",
|
||
"контора",
|
||
"конторы",
|
||
"фирма",
|
||
"фирмы",
|
||
"по",
|
||
"для",
|
||
"над",
|
||
"под",
|
||
"без",
|
||
"с",
|
||
"со",
|
||
"в",
|
||
"во",
|
||
"на",
|
||
"и",
|
||
"или",
|
||
"а",
|
||
"но",
|
||
"не",
|
||
"мы",
|
||
"нам",
|
||
"наш",
|
||
"наша",
|
||
"наше",
|
||
"наши",
|
||
"ты",
|
||
"тебе",
|
||
"твой",
|
||
"сейчас",
|
||
"щас",
|
||
"тут",
|
||
"вот",
|
||
"давай",
|
||
"го",
|
||
"погнали",
|
||
"тогда",
|
||
"обсудим",
|
||
"обсуждать",
|
||
"работать",
|
||
"работаем",
|
||
"работаешь",
|
||
"работаете",
|
||
"можем",
|
||
"можно",
|
||
"какая",
|
||
"какой",
|
||
"какие",
|
||
"чья",
|
||
"чье",
|
||
"чьи"
|
||
]);
|
||
function normalizeOrganizationScopeSearchText(value) {
|
||
const source = normalizeScopeKey(value);
|
||
return source
|
||
.replace(/[^a-zа-я0-9]+/giu, " ")
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
}
|
||
function tokenizeOrganizationScope(value) {
|
||
const normalized = normalizeOrganizationScopeSearchText(value);
|
||
if (!normalized) {
|
||
return [];
|
||
}
|
||
return normalized
|
||
.split(" ")
|
||
.map((token) => token.trim())
|
||
.filter((token) => token.length >= 3 && !ORGANIZATION_SCOPE_STOPWORDS.has(token));
|
||
}
|
||
function organizationTokenVariants(token) {
|
||
const source = String(token ?? "").trim().toLowerCase();
|
||
if (!source) {
|
||
return [];
|
||
}
|
||
const variants = new Set([source]);
|
||
const withoutLongEnding = source.replace(/(?:ами|ями|ого|ему|ому|ыми|ими|иях|ях|ах|ей|ой|ом|ем|ам|ям|ую|юю|ая|яя|ое|ее|ые|ие|ов|ев|ий|ый|ой)$/iu, "");
|
||
if (withoutLongEnding.length >= 4) {
|
||
variants.add(withoutLongEnding);
|
||
}
|
||
const withoutShortEnding = source.replace(/[аеёиоуыэюя]$/iu, "");
|
||
if (withoutShortEnding.length >= 4) {
|
||
variants.add(withoutShortEnding);
|
||
}
|
||
return Array.from(variants);
|
||
}
|
||
function scoreOrganizationMentionInMessage(message, organization) {
|
||
const messageNorm = normalizeOrganizationScopeSearchText(message);
|
||
const organizationNorm = normalizeOrganizationScopeSearchText(organization);
|
||
if (!messageNorm || !organizationNorm) {
|
||
return 0;
|
||
}
|
||
if (messageNorm.includes(organizationNorm)) {
|
||
return 10_000 + organizationNorm.length;
|
||
}
|
||
const organizationTokens = tokenizeOrganizationScope(organizationNorm);
|
||
if (organizationTokens.length === 0) {
|
||
return 0;
|
||
}
|
||
const messageTokens = tokenizeOrganizationScope(messageNorm);
|
||
if (messageTokens.length === 0) {
|
||
return 0;
|
||
}
|
||
let matchedTokens = 0;
|
||
let score = 0;
|
||
for (const token of organizationTokens) {
|
||
const variants = organizationTokenVariants(token);
|
||
let matched = false;
|
||
let variantScore = 0;
|
||
for (const variant of variants) {
|
||
if (!variant) {
|
||
continue;
|
||
}
|
||
if (messageNorm.includes(variant)) {
|
||
matched = true;
|
||
variantScore = Math.max(variantScore, variant.length * 5);
|
||
continue;
|
||
}
|
||
const fuzzyMatched = messageTokens.some((messageToken) => {
|
||
if (messageToken === variant) {
|
||
return true;
|
||
}
|
||
if (messageToken.length >= 5 && variant.length >= 5) {
|
||
return messageToken.startsWith(variant) || variant.startsWith(messageToken);
|
||
}
|
||
return false;
|
||
});
|
||
if (fuzzyMatched) {
|
||
matched = true;
|
||
variantScore = Math.max(variantScore, Math.max(20, variant.length * 3));
|
||
}
|
||
}
|
||
if (matched) {
|
||
matchedTokens += 1;
|
||
score += variantScore > 0 ? variantScore : 10;
|
||
}
|
||
}
|
||
if (matchedTokens === 0) {
|
||
return 0;
|
||
}
|
||
if (matchedTokens === organizationTokens.length) {
|
||
score += 400;
|
||
}
|
||
else {
|
||
score += matchedTokens * 50;
|
||
}
|
||
return score;
|
||
}
|
||
function parseOrganizationsFromDataScopeAssistantText(text) {
|
||
const source = repairAddressMojibake(String(text ?? ""));
|
||
if (!source) {
|
||
return [];
|
||
}
|
||
const extracted = [];
|
||
const singleMatch = source.match(/доступна\s+организация:\s*([^.\n]+)/iu);
|
||
if (singleMatch) {
|
||
const value = normalizeOrganizationScopeValue(singleMatch[1]);
|
||
if (value) {
|
||
extracted.push(value);
|
||
}
|
||
}
|
||
const multiMatch = source.match(/доступны\s+организац(?:ии|ия)\s*(?:\(\d+\))?:\s*([^.\n]+)/iu);
|
||
if (multiMatch) {
|
||
const parts = String(multiMatch[1] ?? "")
|
||
.split(",")
|
||
.map((item) => normalizeOrganizationScopeValue(item))
|
||
.filter(Boolean);
|
||
extracted.push(...parts);
|
||
}
|
||
return Array.from(new Set(extracted));
|
||
}
|
||
function mergeKnownOrganizations(values) {
|
||
const dedup = new Map();
|
||
for (const raw of Array.isArray(values) ? values : []) {
|
||
const normalized = normalizeOrganizationScopeValue(raw);
|
||
if (!normalized) {
|
||
continue;
|
||
}
|
||
const key = normalizeOrganizationScopeSearchText(normalized);
|
||
if (!key) {
|
||
continue;
|
||
}
|
||
if (!dedup.has(key)) {
|
||
dedup.set(key, normalized);
|
||
}
|
||
}
|
||
return Array.from(dedup.values()).slice(0, 20);
|
||
}
|
||
function extractKnownOrganizationsFromNavigationState(addressNavigationState) {
|
||
if (!addressNavigationState || typeof addressNavigationState !== "object") {
|
||
return [];
|
||
}
|
||
const collected = [];
|
||
const sessionContext = addressNavigationState.session_context && typeof addressNavigationState.session_context === "object"
|
||
? addressNavigationState.session_context
|
||
: null;
|
||
const directOrganization = normalizeOrganizationScopeValue(sessionContext?.organization_scope);
|
||
if (directOrganization) {
|
||
collected.push(directOrganization);
|
||
}
|
||
const resultSets = Array.isArray(addressNavigationState.result_sets) ? addressNavigationState.result_sets : [];
|
||
for (const resultSet of resultSets) {
|
||
const filters = resultSet?.filters && typeof resultSet.filters === "object" ? resultSet.filters : null;
|
||
const scopedOrganization = normalizeOrganizationScopeValue(filters?.organization);
|
||
if (scopedOrganization) {
|
||
collected.push(scopedOrganization);
|
||
}
|
||
}
|
||
return mergeKnownOrganizations(collected);
|
||
}
|
||
function extractKnownOrganizationsFromHistory(items, addressNavigationState = null) {
|
||
const collected = [];
|
||
const navigationOrganizations = extractKnownOrganizationsFromNavigationState(addressNavigationState);
|
||
if (navigationOrganizations.length > 0) {
|
||
collected.push(...navigationOrganizations);
|
||
}
|
||
for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) {
|
||
const item = items[index];
|
||
if (!item || item.role !== "assistant") {
|
||
continue;
|
||
}
|
||
const debug = item.debug && typeof item.debug === "object" ? item.debug : null;
|
||
if (debug) {
|
||
const directFromProbe = Array.isArray(debug.living_chat_data_scope_probe_organizations)
|
||
? debug.living_chat_data_scope_probe_organizations
|
||
: [];
|
||
const knownFromDebug = Array.isArray(debug.assistant_known_organizations)
|
||
? debug.assistant_known_organizations
|
||
: [];
|
||
const directFromCandidates = Array.isArray(debug.organization_candidates)
|
||
? debug.organization_candidates
|
||
: [];
|
||
const directFromResolved = [
|
||
normalizeOrganizationScopeValue(debug.assistant_active_organization),
|
||
normalizeOrganizationScopeValue(debug.living_chat_selected_organization),
|
||
normalizeOrganizationScopeValue(debug.extracted_filters?.organization),
|
||
normalizeOrganizationScopeValue(debug.address_root_frame_context?.organization)
|
||
].filter(Boolean);
|
||
if (directFromProbe.length > 0 || knownFromDebug.length > 0 || directFromCandidates.length > 0 || directFromResolved.length > 0) {
|
||
collected.push(...directFromProbe, ...knownFromDebug, ...directFromCandidates, ...directFromResolved);
|
||
}
|
||
}
|
||
const parsedFromText = parseOrganizationsFromDataScopeAssistantText(item.text);
|
||
if (parsedFromText.length > 0) {
|
||
collected.push(...parsedFromText);
|
||
}
|
||
if (collected.length >= 20) {
|
||
break;
|
||
}
|
||
}
|
||
return mergeKnownOrganizations(collected);
|
||
}
|
||
function findLastAssistantActiveOrganization(items, addressNavigationState = null) {
|
||
const sessionContext = addressNavigationState && typeof addressNavigationState === "object"
|
||
? (addressNavigationState.session_context && typeof addressNavigationState.session_context === "object"
|
||
? addressNavigationState.session_context
|
||
: null)
|
||
: null;
|
||
const navigationOrganization = normalizeOrganizationScopeValue(sessionContext?.organization_scope);
|
||
if (navigationOrganization) {
|
||
return navigationOrganization;
|
||
}
|
||
for (let index = (Array.isArray(items) ? items.length : 0) - 1; index >= 0; index -= 1) {
|
||
const item = items[index];
|
||
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
|
||
continue;
|
||
}
|
||
const direct = normalizeOrganizationScopeValue(item.debug.assistant_active_organization);
|
||
if (direct) {
|
||
return direct;
|
||
}
|
||
const selected = normalizeOrganizationScopeValue(item.debug.living_chat_selected_organization);
|
||
if (selected) {
|
||
return selected;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations) {
|
||
const known = mergeKnownOrganizations(knownOrganizations);
|
||
if (!userMessage || known.length === 0) {
|
||
return null;
|
||
}
|
||
const messageNorm = normalizeOrganizationScopeSearchText(userMessage);
|
||
if (!messageNorm) {
|
||
return null;
|
||
}
|
||
const scored = known
|
||
.map((organization) => ({
|
||
organization,
|
||
score: scoreOrganizationMentionInMessage(messageNorm, organization)
|
||
}))
|
||
.filter((item) => item.score > 0)
|
||
.sort((a, b) => b.score - a.score || a.organization.length - b.organization.length);
|
||
if (scored.length === 0) {
|
||
return null;
|
||
}
|
||
const best = scored[0];
|
||
const second = scored[1];
|
||
if (best.score < 90) {
|
||
return null;
|
||
}
|
||
if (second && second.score === best.score) {
|
||
return null;
|
||
}
|
||
return best.organization;
|
||
}
|
||
function resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState = null) {
|
||
return (0, assistantOrganizationScopeRuntimeAdapter_1.resolveSessionOrganizationScopeContextRuntime)({
|
||
userMessage,
|
||
items,
|
||
addressNavigationState,
|
||
extractKnownOrganizationsFromHistory: assistantDataScopePolicy.extractKnownOrganizationsFromHistory,
|
||
resolveOrganizationSelectionFromMessage,
|
||
findLastAssistantActiveOrganization: assistantDataScopePolicy.findLastAssistantActiveOrganization,
|
||
normalizeOrganizationScopeValue
|
||
});
|
||
}
|
||
function mergeFollowupContextWithOrganizationScope(followupContext, organization) {
|
||
return (0, assistantOrganizationScopeRuntimeAdapter_1.mergeFollowupContextWithOrganizationScopeRuntime)({
|
||
followupContext,
|
||
organization,
|
||
normalizeOrganizationScopeValue,
|
||
toNonEmptyString
|
||
});
|
||
}
|
||
function resolveSessionOrganizationScopeContextForTests(userMessage, items, addressNavigationState = null) {
|
||
return resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState);
|
||
}
|
||
function buildRootScopedCarryoverFiltersForTests(previousFilters, inventoryRootFrame) {
|
||
return buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame);
|
||
}
|
||
function normalizeGuidValue(value) {
|
||
const source = normalizeScopeLabel(value);
|
||
if (!source) {
|
||
return null;
|
||
}
|
||
const match = source.match(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i);
|
||
return match ? String(match[0]).toLowerCase() : null;
|
||
}
|
||
function extractGuidValuesFromText(value) {
|
||
const source = normalizeScopeLabel(value);
|
||
if (!source) {
|
||
return [];
|
||
}
|
||
const matches = source.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ig);
|
||
if (!matches || matches.length === 0) {
|
||
return [];
|
||
}
|
||
return Array.from(new Set(matches.map((item) => String(item).toLowerCase())));
|
||
}
|
||
function hasOrganizationKeyHint(key) {
|
||
const source = String(key ?? "");
|
||
return /(?:организац|organization|company|контор|org|ð¾ñð³ð°ð½ð¸ð·ð°ñ)/i.test(source);
|
||
}
|
||
function hasNameKeyHint(key) {
|
||
const source = String(key ?? "");
|
||
return /(?:представ|наимен|name|title|display|presentation|description|ð¿ñðµð´ññð°ð²|ð½ð°ð¸ð¼ðµð½)/i.test(source);
|
||
}
|
||
function hasGuidKeyHint(key) {
|
||
const source = String(key ?? "");
|
||
return /(?:идентифик|guid|uuid|key|ref|ссылк|\bid\b|ð¸ð´ðµð½ñð¸ñ|ñññð»)/i.test(source);
|
||
}
|
||
function looksLikeOrganizationTypeMarker(value) {
|
||
const normalized = normalizeScopeKey(value);
|
||
const raw = String(value ?? "").toLowerCase();
|
||
return /(?:справочникссылка\.\s*организац|catalogref\.\s*organization|организац|organization|company|ð¾ñð³ð°ð½ð¸ð·ð°ñ|ð¡ð¿ñð°ð²ð¾ñð½ð¸ðºð¡ññð»ðºð°\.ðñð³ð°ð½ð¸ð·)/i.test(normalized)
|
||
|| /(?:ð¾ñð³ð°ð½ð¸ð·ð°ñ|ð¡ð¿ñð°ð²ð¾ñð½ð¸ðºð¡ññð»ðºð°\.ðñð³ð°ð½ð¸ð·)/i.test(raw);
|
||
}
|
||
function isPlausibleOrganizationName(value) {
|
||
const candidate = normalizeScopeLabel(value);
|
||
if (!candidate) {
|
||
return false;
|
||
}
|
||
if (/^(?:период|регистратор|счетдт|счеткт|amount|period|registrator|accountdt|accountkt)$/i.test(candidate)) {
|
||
return false;
|
||
}
|
||
if (/^[0-9._:/\\-]+$/i.test(candidate)) {
|
||
return false;
|
||
}
|
||
if (/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.test(candidate)) {
|
||
return false;
|
||
}
|
||
if (/(?:справочникссылка|документссылка|плансчетовссылка|standardodata|recordtype|cmp:)/i.test(candidate)) {
|
||
return false;
|
||
}
|
||
return /[A-Za-z\u0400-\u04FF]/u.test(candidate);
|
||
}
|
||
function appendOrganizationFactsFromValue(value, hints, bucket, depth = 0) {
|
||
if (depth > 4 || value === null || value === undefined) {
|
||
return;
|
||
}
|
||
if (typeof value === "string") {
|
||
for (const guid of extractGuidValuesFromText(value)) {
|
||
if (hints.guidHint || hints.organizationHint || hints.nameHint) {
|
||
bucket.refs.push(guid);
|
||
}
|
||
}
|
||
if ((hints.organizationHint || hints.nameHint) && isPlausibleOrganizationName(value)) {
|
||
const normalized = normalizeScopeLabel(value);
|
||
if (normalized) {
|
||
bucket.names.push(normalized);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
if (Array.isArray(value)) {
|
||
for (const item of value) {
|
||
appendOrganizationFactsFromValue(item, hints, bucket, depth + 1);
|
||
}
|
||
return;
|
||
}
|
||
if (typeof value === "object") {
|
||
const entries = Object.entries(value);
|
||
let objectIsOrganization = false;
|
||
let hasObjectRefMarker = false;
|
||
let hasGuidLikeField = false;
|
||
let hasTypeMarker = false;
|
||
for (const [rawKey, rawVal] of entries) {
|
||
const key = normalizeScopeKey(rawKey);
|
||
if ((key.includes("objectref") || key.includes("_objectref")) && rawVal === true) {
|
||
hasObjectRefMarker = true;
|
||
}
|
||
if (typeof rawVal === "string" && normalizeGuidValue(rawVal)) {
|
||
hasGuidLikeField = true;
|
||
}
|
||
if (hasOrganizationKeyHint(key)) {
|
||
objectIsOrganization = true;
|
||
break;
|
||
}
|
||
if ((key.includes("типобъекта") || key.includes("type")) && typeof rawVal === "string" && looksLikeOrganizationTypeMarker(rawVal)) {
|
||
objectIsOrganization = true;
|
||
hasTypeMarker = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!objectIsOrganization && hasObjectRefMarker && hasGuidLikeField) {
|
||
const hasNameLikeValue = entries.some(([rawKey, rawVal]) => {
|
||
if (typeof rawVal !== "string") {
|
||
return false;
|
||
}
|
||
const key = normalizeScopeKey(rawKey);
|
||
return hasNameKeyHint(key) || isPlausibleOrganizationName(rawVal);
|
||
});
|
||
if (hasTypeMarker || hasNameLikeValue) {
|
||
objectIsOrganization = true;
|
||
}
|
||
}
|
||
for (const [rawKey, rawVal] of entries) {
|
||
if (String(rawKey ?? "").startsWith("__")) {
|
||
continue;
|
||
}
|
||
const key = normalizeScopeKey(rawKey);
|
||
const childHints = {
|
||
organizationHint: hints.organizationHint || objectIsOrganization || hasOrganizationKeyHint(key),
|
||
nameHint: hints.nameHint || objectIsOrganization || hasNameKeyHint(key),
|
||
guidHint: hints.guidHint || objectIsOrganization || hasGuidKeyHint(key)
|
||
};
|
||
if (typeof rawVal === "string") {
|
||
const guid = normalizeGuidValue(rawVal);
|
||
if (guid && childHints.guidHint) {
|
||
bucket.refs.push(guid);
|
||
}
|
||
}
|
||
appendOrganizationFactsFromValue(rawVal, childHints, bucket, depth + 1);
|
||
}
|
||
}
|
||
}
|
||
function extractOrganizationFactsFromRows(rows) {
|
||
const names = [];
|
||
const refs = [];
|
||
const pairs = [];
|
||
for (const row of Array.isArray(rows) ? rows : []) {
|
||
if (!row || typeof row !== "object") {
|
||
continue;
|
||
}
|
||
const rowNames = [];
|
||
const rowRefs = [];
|
||
for (const [rawKey, rawValue] of Object.entries(row)) {
|
||
if (String(rawKey ?? "").startsWith("__")) {
|
||
continue;
|
||
}
|
||
const key = normalizeScopeKey(rawKey);
|
||
const hints = {
|
||
organizationHint: hasOrganizationKeyHint(key),
|
||
nameHint: hasNameKeyHint(key),
|
||
guidHint: hasGuidKeyHint(key)
|
||
};
|
||
appendOrganizationFactsFromValue(rawValue, hints, {
|
||
names: rowNames,
|
||
refs: rowRefs
|
||
});
|
||
}
|
||
const dedupRowNames = Array.from(new Set(rowNames))
|
||
.filter((item) => isPlausibleOrganizationName(item))
|
||
.slice(0, 20);
|
||
const dedupRowRefs = Array.from(new Set(rowRefs))
|
||
.map((item) => String(item ?? "").toLowerCase())
|
||
.filter((item) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(item))
|
||
.slice(0, 20);
|
||
if (dedupRowNames.length === 0 && dedupRowRefs.length === 0) {
|
||
const fallbackBucket = { names: [], refs: [] };
|
||
appendOrganizationFactsFromValue(row, {
|
||
organizationHint: true,
|
||
nameHint: true,
|
||
guidHint: true
|
||
}, fallbackBucket);
|
||
for (const value of fallbackBucket.names) {
|
||
if (isPlausibleOrganizationName(value)) {
|
||
dedupRowNames.push(value);
|
||
}
|
||
}
|
||
for (const value of fallbackBucket.refs) {
|
||
const normalized = String(value ?? "").toLowerCase();
|
||
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(normalized)) {
|
||
dedupRowRefs.push(normalized);
|
||
}
|
||
}
|
||
}
|
||
for (const name of dedupRowNames) {
|
||
names.push(name);
|
||
}
|
||
for (const ref of dedupRowRefs) {
|
||
refs.push(ref);
|
||
}
|
||
if (dedupRowRefs.length > 0 && dedupRowNames.length > 0) {
|
||
for (const ref of dedupRowRefs) {
|
||
for (const name of dedupRowNames) {
|
||
pairs.push({ ref, name });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return {
|
||
names: Array.from(new Set(names)).slice(0, 20),
|
||
refs: Array.from(new Set(refs)).slice(0, 20),
|
||
pairs: Array.from(new Set(pairs.map((item) => `${item.ref}||${item.name}`)))
|
||
.map((token) => {
|
||
const [ref, name] = token.split("||");
|
||
return { ref, name };
|
||
})
|
||
.slice(0, 100)
|
||
};
|
||
}
|
||
function resolveOrganizationNamesByRefs(refs, facts) {
|
||
const refSet = new Set((Array.isArray(refs) ? refs : [])
|
||
.map((item) => String(item ?? "").toLowerCase())
|
||
.filter((item) => item.length > 0));
|
||
if (refSet.size === 0) {
|
||
return [];
|
||
}
|
||
const names = [];
|
||
for (const pair of Array.isArray(facts?.pairs) ? facts.pairs : []) {
|
||
const ref = String(pair?.ref ?? "").toLowerCase();
|
||
const name = normalizeScopeLabel(pair?.name ?? "");
|
||
if (!ref || !name || !refSet.has(ref)) {
|
||
continue;
|
||
}
|
||
names.push(name);
|
||
}
|
||
return Array.from(new Set(names)).slice(0, 20);
|
||
}
|
||
function buildResolvedDataScopeProbe(status, organizations) {
|
||
return {
|
||
status,
|
||
channel: config_1.ASSISTANT_MCP_CHANNEL,
|
||
organizations: Array.from(new Set(Array.isArray(organizations) ? organizations : [])).slice(0, 20),
|
||
error: null
|
||
};
|
||
}
|
||
function extractOrganizationFactsFromRowsForTests(rows) {
|
||
return assistantDataScopePolicy.extractOrganizationFactsFromRows(rows);
|
||
}
|
||
function resolveOrganizationNamesByRefsForTests(refs, facts) {
|
||
return assistantDataScopePolicy.resolveOrganizationNamesByRefs(refs, facts);
|
||
}
|
||
async function resolveAssistantDataScopeProbe() {
|
||
return assistantDataScopePolicy.resolveAssistantDataScopeProbe();
|
||
const cacheKey = `${config_1.ASSISTANT_MCP_PROXY_URL}|${config_1.ASSISTANT_MCP_CHANNEL}`;
|
||
const now = Date.now();
|
||
const cached = dataScopeProbeCache.get(cacheKey);
|
||
if (cached && typeof cached === "object" && Number(cached.expiresAt ?? 0) > now) {
|
||
return cached.value;
|
||
}
|
||
if (String(process.env.NODE_ENV ?? "").toLowerCase() === "test") {
|
||
return {
|
||
status: "skipped_test_env",
|
||
channel: config_1.ASSISTANT_MCP_CHANNEL,
|
||
organizations: [],
|
||
error: null
|
||
};
|
||
}
|
||
const catalogQueryCandidates = [
|
||
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.Наименование КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.НаименованиеПолное КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||
"ВЫБРАТЬ ПЕРВЫЕ 100 Организации.Ссылка КАК Организация, ПРЕДСТАВЛЕНИЕ(Организации.Ссылка) КАК ОрганизацияПредставление ИЗ Справочник.Организации КАК Организации"
|
||
];
|
||
const movementProbeCandidates = [
|
||
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация ИЗ РегистрБухгалтерии.Хозрасчетный КАК Движения УПОРЯДОЧИТЬ ПО Движения.Период УБЫВ",
|
||
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация ИЗ РегистрБухгалтерии.Хозрасчетный КАК Движения"
|
||
];
|
||
let lastError = null;
|
||
const catalogFacts = { names: [], refs: [], pairs: [] };
|
||
for (const queryText of catalogQueryCandidates) {
|
||
const probe = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||
query: queryText,
|
||
limit: 100
|
||
});
|
||
if (probe.error) {
|
||
lastError = probe.error;
|
||
continue;
|
||
}
|
||
const facts = extractOrganizationFactsFromRows(probe.rows);
|
||
catalogFacts.names.push(...facts.names);
|
||
catalogFacts.refs.push(...facts.refs);
|
||
catalogFacts.pairs.push(...facts.pairs);
|
||
if (facts.names.length > 0) {
|
||
const resolved = buildResolvedDataScopeProbe("resolved", facts.names);
|
||
dataScopeProbeCache.set(cacheKey, {
|
||
expiresAt: now + DATA_SCOPE_CACHE_TTL_MS,
|
||
value: resolved
|
||
});
|
||
return resolved;
|
||
}
|
||
}
|
||
const movementFacts = { names: [], refs: [], pairs: [] };
|
||
for (const queryText of movementProbeCandidates) {
|
||
const probe = await (0, addressMcpClient_1.executeAddressMcpQuery)({
|
||
query: queryText,
|
||
limit: 60
|
||
});
|
||
if (probe.error) {
|
||
lastError = probe.error;
|
||
continue;
|
||
}
|
||
const facts = extractOrganizationFactsFromRows(probe.rows);
|
||
movementFacts.names.push(...facts.names);
|
||
movementFacts.refs.push(...facts.refs);
|
||
movementFacts.pairs.push(...facts.pairs);
|
||
if (facts.names.length > 0) {
|
||
const resolved = buildResolvedDataScopeProbe("resolved_from_activity", facts.names);
|
||
dataScopeProbeCache.set(cacheKey, {
|
||
expiresAt: now + DATA_SCOPE_CACHE_TTL_MS,
|
||
value: resolved
|
||
});
|
||
return resolved;
|
||
}
|
||
}
|
||
const movementRefs = Array.from(new Set(movementFacts.refs))
|
||
.map((item) => String(item ?? "").toLowerCase())
|
||
.filter((item) => item.length > 0);
|
||
if (movementRefs.length > 0) {
|
||
const namesFromCatalogPairs = resolveOrganizationNamesByRefs(movementRefs, {
|
||
names: Array.from(new Set(catalogFacts.names)),
|
||
refs: Array.from(new Set(catalogFacts.refs)),
|
||
pairs: catalogFacts.pairs
|
||
});
|
||
if (namesFromCatalogPairs.length > 0) {
|
||
const resolved = buildResolvedDataScopeProbe("resolved_from_ref_lookup", namesFromCatalogPairs);
|
||
dataScopeProbeCache.set(cacheKey, {
|
||
expiresAt: now + DATA_SCOPE_CACHE_TTL_MS,
|
||
value: resolved
|
||
});
|
||
return resolved;
|
||
}
|
||
}
|
||
const fallback = {
|
||
status: lastError ? "unresolved_with_error" : "unresolved",
|
||
channel: config_1.ASSISTANT_MCP_CHANNEL,
|
||
organizations: [],
|
||
error: lastError
|
||
};
|
||
dataScopeProbeCache.set(cacheKey, {
|
||
expiresAt: now + DATA_SCOPE_CACHE_TTL_MS,
|
||
value: fallback
|
||
});
|
||
return fallback;
|
||
}
|
||
const assistantBoundaryPolicy = (0, assistantBoundaryPolicy_1.createAssistantBoundaryPolicy)({
|
||
activeMcpChannel: config_1.ASSISTANT_MCP_CHANNEL,
|
||
normalizeOrganizationScopeValue,
|
||
toNonEmptyString,
|
||
hasOrganizationFactLookupSignal
|
||
});
|
||
function buildAssistantDataScopeContractReplyFromPolicy(scopeProbe = null) {
|
||
return assistantBoundaryPolicy.buildAssistantDataScopeContractReply(scopeProbe);
|
||
}
|
||
function buildAssistantDataScopeSelectionReplyFromPolicy(organization) {
|
||
return assistantBoundaryPolicy.buildAssistantDataScopeSelectionReply(organization);
|
||
}
|
||
function buildAssistantOrganizationFactBoundaryReplyFromPolicy(organization) {
|
||
return assistantBoundaryPolicy.buildAssistantOrganizationFactBoundaryReply(organization);
|
||
}
|
||
function buildAssistantOperationalBoundaryReplyFromPolicy() {
|
||
return assistantBoundaryPolicy.buildAssistantOperationalBoundaryReply();
|
||
}
|
||
function buildAssistantSafetyRefusalReplyFromPolicy() {
|
||
return assistantBoundaryPolicy.buildAssistantSafetyRefusalReply();
|
||
}
|
||
function applyLivingChatScriptGuardFromPolicy(chatText, userMessage) {
|
||
return assistantBoundaryPolicy.applyLivingChatScriptGuard(chatText, userMessage);
|
||
}
|
||
function applyLivingChatGroundingGuardFromPolicy(input) {
|
||
return assistantBoundaryPolicy.applyLivingChatGroundingGuard(input);
|
||
}
|
||
function buildAssistantDataScopeContractReply(scopeProbe = null) {
|
||
return buildAssistantDataScopeContractReplyFromPolicy(scopeProbe);
|
||
}
|
||
function buildAssistantDataScopeSelectionReply(organization) {
|
||
return buildAssistantDataScopeSelectionReplyFromPolicy(organization);
|
||
}
|
||
function buildAssistantOrganizationFactBoundaryReply(organization) {
|
||
return buildAssistantOrganizationFactBoundaryReplyFromPolicy(organization);
|
||
}
|
||
function buildAssistantOperationalBoundaryReply() {
|
||
return buildAssistantOperationalBoundaryReplyFromPolicy();
|
||
}
|
||
function buildAssistantSafetyRefusalReply() {
|
||
return buildAssistantSafetyRefusalReplyFromPolicy();
|
||
}
|
||
function applyLivingChatScriptGuard(chatText, userMessage) {
|
||
return applyLivingChatScriptGuardFromPolicy(chatText, userMessage);
|
||
}
|
||
function applyLivingChatGroundingGuard(input) {
|
||
return applyLivingChatGroundingGuardFromPolicy(input);
|
||
}
|
||
function resolveLivingAssistantModeDecision(input) {
|
||
return assistantLivingModePolicy.resolveLivingAssistantModeDecision(input);
|
||
}
|
||
class AssistantService {
|
||
normalizerService;
|
||
sessions;
|
||
dataLayer;
|
||
sessionLogger;
|
||
addressQueryService;
|
||
chatClient;
|
||
constructor(normalizerService, sessions, dataLayer = new assistantDataLayer_1.AssistantDataLayer(), sessionLogger = new assistantSessionLogger_1.AssistantSessionLogger(), addressQueryService = new addressQueryService_1.AddressQueryService(), chatClient = new openaiResponsesClient_1.OpenAIResponsesClient()) {
|
||
this.normalizerService = normalizerService;
|
||
this.sessions = sessions;
|
||
this.dataLayer = dataLayer;
|
||
this.sessionLogger = sessionLogger;
|
||
this.addressQueryService = addressQueryService;
|
||
this.chatClient = chatClient;
|
||
}
|
||
getSession(sessionId) {
|
||
return this.sessions.getSession(sessionId);
|
||
}
|
||
async handleMessage(payload) {
|
||
const turnRuntimeDeps = (0, assistantTurnRuntimeDepsAdapter_1.buildAssistantTurnRuntimeDeps)({
|
||
sessions: this.sessions,
|
||
sessionLogger: this.sessionLogger,
|
||
normalizerService: this.normalizerService,
|
||
dataLayer: this.dataLayer,
|
||
addressQueryService: this.addressQueryService,
|
||
chatClient: this.chatClient,
|
||
messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||
nowIso: () => new Date().toISOString(),
|
||
defaultApiKey: process.env.OPENAI_API_KEY ?? "",
|
||
logEvent: (runtimePayload) => (0, log_1.logJson)(runtimePayload),
|
||
flags: {
|
||
featureAssistantAddressQueryV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1,
|
||
featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1,
|
||
featureInvestigationStateV1: config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1,
|
||
featureStateFollowupBindingV1: config_1.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1,
|
||
featureContractsV11: config_1.FEATURE_ASSISTANT_CONTRACTS_V11,
|
||
featureAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11,
|
||
featureProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1,
|
||
featureLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1
|
||
},
|
||
defaults: {
|
||
defaultModel: config_1.DEFAULT_MODEL,
|
||
defaultBaseUrl: config_1.DEFAULT_OPENAI_BASE_URL
|
||
},
|
||
helpers: {
|
||
compactWhitespace,
|
||
repairAddressMojibake,
|
||
resolveRuntimeAnalysisContext,
|
||
runAddressLlmPreDecompose: async (runtimePayload, runtimeUserMessage) => runAddressLlmPreDecompose(this.normalizerService, runtimePayload, runtimeUserMessage),
|
||
buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1,
|
||
sanitizeAddressMessageForFallback,
|
||
toNonEmptyString,
|
||
resolveAddressFollowupCarryoverContext: assistantTransitionPolicy.resolveAddressFollowupCarryoverContext,
|
||
resolveAssistantOrchestrationDecision,
|
||
buildAddressDialogContinuationContractV2: assistantTransitionPolicy.buildAddressDialogContinuationContractV2,
|
||
mergeFollowupContextWithOrganizationScope,
|
||
isRetryableAddressLimitedResult,
|
||
mergeKnownOrganizations,
|
||
hasAssistantDataScopeMetaQuestionSignal,
|
||
shouldHandleAsAssistantCapabilityMetaQuery,
|
||
hasDestructiveDataActionSignal,
|
||
hasDangerOrCoercionSignal,
|
||
hasOperationalAdminActionRequestSignal,
|
||
hasOrganizationFactLookupSignal,
|
||
hasOrganizationFactFollowupSignal,
|
||
shouldEmitOrganizationSelectionReply,
|
||
hasAssistantCapabilityQuestionSignal,
|
||
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
||
applyScriptGuard: applyLivingChatScriptGuardFromPolicy,
|
||
applyGroundingGuard: applyLivingChatGroundingGuardFromPolicy,
|
||
buildAssistantSafetyRefusalReply: buildAssistantSafetyRefusalReplyFromPolicy,
|
||
buildAssistantDataScopeContractReply: buildAssistantDataScopeContractReplyFromPolicy,
|
||
buildAssistantOrganizationFactBoundaryReply: buildAssistantOrganizationFactBoundaryReplyFromPolicy,
|
||
buildAssistantDataScopeSelectionReply: buildAssistantDataScopeSelectionReplyFromPolicy,
|
||
buildAssistantOperationalBoundaryReply: buildAssistantOperationalBoundaryReplyFromPolicy,
|
||
buildAssistantCapabilityContractReply,
|
||
loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt,
|
||
sanitizeOutgoingAssistantText,
|
||
buildAddressDebugPayload,
|
||
buildAddressFollowupOffer,
|
||
buildFollowupStateBinding,
|
||
resolveBusinessScopeAlignment,
|
||
inferP0DomainFromMessage,
|
||
resolveBusinessScopeFromLiveContext,
|
||
extractRequirements,
|
||
toExecutionPlan,
|
||
enforceRbpLiveRoutePlan,
|
||
enforceFaLiveRoutePlan,
|
||
mapNoRouteReason,
|
||
buildSkippedResult,
|
||
evaluateCoverage,
|
||
checkGrounding,
|
||
collectRbpLiveRouteAudit,
|
||
collectFaLiveRouteAudit,
|
||
hasExplicitPeriodAnchorFromNormalized,
|
||
extractDroppedIntentSegments: (normalizedPayload) => extractDiscardedIntentSegments(normalizedPayload),
|
||
toDebugRoutes: (routeSummary) => toDebugRoutes(routeSummary),
|
||
extractExecutionState
|
||
}
|
||
});
|
||
try {
|
||
const turnRuntime = await (0, assistantTurnAttemptRuntimeAdapter_1.runAssistantTurnAttemptRuntime)({
|
||
payload,
|
||
runUserTurnBootstrapRuntime: (runtimePayload) => (0, assistantUserTurnBootstrapRuntimeAdapter_1.runAssistantUserTurnBootstrapRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantUserTurnBootstrapRuntimeInput)(runtimePayload, turnRuntimeDeps)),
|
||
resolveSessionOrganizationScopeContext: (runtimeUserMessage, sessionItems) => resolveSessionOrganizationScopeContext(runtimeUserMessage, sessionItems),
|
||
runAddressAttemptRuntime: async (runtimeInput) => (0, assistantAddressAttemptRuntimeAdapter_1.runAssistantAddressAttemptRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantAddressAttemptRuntimeInput)(runtimeInput, turnRuntimeDeps)),
|
||
runDeepTurnAttemptRuntime: async (runtimeInput) => (0, assistantDeepTurnAttemptRuntimeAdapter_1.runAssistantDeepTurnAttemptRuntime)((0, assistantTurnRuntimeInputBuilder_1.buildAssistantDeepTurnAttemptRuntimeInput)(runtimeInput, turnRuntimeDeps))
|
||
});
|
||
return turnRuntime.response;
|
||
}
|
||
catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||
const sessionId = String(payload?.session_id ?? payload?.sessionId ?? "").trim() || `asst-${(0, nanoid_1.nanoid)(10)}`;
|
||
const ensuredSession = this.sessions.ensureSession(sessionId);
|
||
const existingAssistant = [...ensuredSession.items].reverse().find((item) => item.role === "assistant") ?? null;
|
||
if (existingAssistant) {
|
||
return {
|
||
ok: true,
|
||
session_id: sessionId,
|
||
assistant_reply: existingAssistant.text,
|
||
reply_type: existingAssistant.reply_type ?? "backend_error",
|
||
conversation_item: existingAssistant,
|
||
debug: existingAssistant.debug ?? buildAssistantBackendErrorDebugPayload(errorMessage),
|
||
conversation: cloneItems(ensuredSession.items)
|
||
};
|
||
}
|
||
const createdAt = new Date().toISOString();
|
||
const debugPayload = buildAssistantBackendErrorDebugPayload(errorMessage);
|
||
const assistantItem = this.sessions.appendItem(sessionId, {
|
||
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||
session_id: sessionId,
|
||
role: "assistant",
|
||
text: buildAssistantBackendErrorReply(),
|
||
reply_type: "backend_error",
|
||
created_at: createdAt,
|
||
trace_id: debugPayload.trace_id ?? null,
|
||
debug: debugPayload
|
||
});
|
||
const sessionSnapshot = this.sessions.getSession(sessionId) ?? this.sessions.ensureSession(sessionId);
|
||
this.sessionLogger.persistSession(sessionSnapshot);
|
||
return {
|
||
ok: true,
|
||
session_id: sessionId,
|
||
assistant_reply: assistantItem.text,
|
||
reply_type: "backend_error",
|
||
conversation_item: assistantItem,
|
||
debug: debugPayload,
|
||
conversation: cloneItems(sessionSnapshot.items)
|
||
};
|
||
}
|
||
}
|
||
}
|
||
exports.AssistantService = AssistantService;
|