4995 lines
235 KiB
JavaScript
4995 lines
235 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.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 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 assistantDeepTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAttemptRuntimeAdapter"));
|
||
const assistantOrganizationScopeRuntimeAdapter_1 = __importStar(require("./assistantOrganizationScopeRuntimeAdapter"));
|
||
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 DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||
const dataScopeProbeCache = new Map();
|
||
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 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)/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 {
|
||
requirements_total: 0,
|
||
requirements_covered: 0,
|
||
requirements_uncovered: [],
|
||
requirements_partially_covered: [],
|
||
clarification_needed_for: [],
|
||
out_of_scope_requirements: []
|
||
};
|
||
}
|
||
function buildAssistantBackendErrorDebugPayload(errorMessage) {
|
||
return {
|
||
trace_id: `chat-${(0, nanoid_1.nanoid)(10)}`,
|
||
prompt_version: "assistant_backend_error_fallback_v1",
|
||
schema_version: "assistant_backend_error_fallback_v1",
|
||
fallback_type: "unknown",
|
||
route_summary: null,
|
||
fragments: [],
|
||
requirements_extracted: [],
|
||
coverage_report: buildAddressCoverageReport(),
|
||
routes: [],
|
||
retrieval_status: [],
|
||
retrieval_results: [],
|
||
answer_grounding_check: {
|
||
status: "no_grounded_answer",
|
||
route_subject_match: true,
|
||
missing_requirements: [],
|
||
reasons: [
|
||
`backend_error:${String(errorMessage ?? "unknown_error").slice(0, 280)}`
|
||
],
|
||
why_included_summary: [],
|
||
selection_reason_summary: []
|
||
},
|
||
dropped_intent_segments: []
|
||
};
|
||
}
|
||
function buildAssistantBackendErrorReply() {
|
||
return "Сейчас не удалось завершить разбор из-за внутренней ошибки контуров LLM. Могу продолжить в адресном режиме: проверить документы, договоры и операции по нужному периоду или контрагенту.";
|
||
}
|
||
function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
|
||
const grounded = addressDebug.response_type === "LIMITED_WITH_REASON" ? "partial" : "grounded";
|
||
const llmMeta = llmPreDecomposeMeta && typeof llmPreDecomposeMeta === "object" ? llmPreDecomposeMeta : null;
|
||
return {
|
||
trace_id: `address-${(0, nanoid_1.nanoid)(10)}`,
|
||
prompt_version: "address_query_runtime_v1",
|
||
schema_version: "address_query_runtime_v1",
|
||
fallback_type: addressDebug.response_type === "LIMITED_WITH_REASON" ? "partial" : "none",
|
||
route_summary: null,
|
||
fragments: [],
|
||
requirements_extracted: [],
|
||
coverage_report: buildAddressCoverageReport(),
|
||
routes: [],
|
||
retrieval_status: [],
|
||
retrieval_results: [],
|
||
answer_grounding_check: {
|
||
status: grounded,
|
||
route_subject_match: true,
|
||
missing_requirements: [],
|
||
reasons: addressDebug.reasons ?? [],
|
||
why_included_summary: [],
|
||
selection_reason_summary: []
|
||
},
|
||
dropped_intent_segments: [],
|
||
detected_mode: addressDebug.detected_mode,
|
||
detected_mode_confidence: addressDebug.detected_mode_confidence,
|
||
query_shape: addressDebug.query_shape,
|
||
query_shape_confidence: addressDebug.query_shape_confidence,
|
||
detected_intent: addressDebug.detected_intent,
|
||
detected_intent_confidence: addressDebug.detected_intent_confidence,
|
||
extracted_filters: addressDebug.extracted_filters,
|
||
missing_required_filters: addressDebug.missing_required_filters,
|
||
selected_recipe: addressDebug.selected_recipe,
|
||
mcp_call_status_legacy: addressDebug.mcp_call_status_legacy,
|
||
account_scope_mode: addressDebug.account_scope_mode,
|
||
account_scope_fallback_applied: addressDebug.account_scope_fallback_applied,
|
||
anchor_type: addressDebug.anchor_type,
|
||
anchor_value_raw: addressDebug.anchor_value_raw,
|
||
anchor_value_resolved: addressDebug.anchor_value_resolved,
|
||
resolver_confidence: addressDebug.resolver_confidence,
|
||
ambiguity_count: addressDebug.ambiguity_count,
|
||
match_failure_stage: addressDebug.match_failure_stage,
|
||
match_failure_reason: addressDebug.match_failure_reason,
|
||
mcp_call_status: addressDebug.mcp_call_status,
|
||
rows_fetched: addressDebug.rows_fetched,
|
||
raw_rows_received: addressDebug.raw_rows_received,
|
||
rows_after_account_scope: addressDebug.rows_after_account_scope,
|
||
rows_after_recipe_filter: addressDebug.rows_after_recipe_filter,
|
||
rows_materialized: addressDebug.rows_materialized,
|
||
rows_matched: addressDebug.rows_matched,
|
||
raw_row_keys_sample: addressDebug.raw_row_keys_sample,
|
||
materialization_drop_reason: addressDebug.materialization_drop_reason,
|
||
account_token_raw: addressDebug.account_token_raw,
|
||
account_token_normalized: addressDebug.account_token_normalized,
|
||
account_scope_fields_checked: addressDebug.account_scope_fields_checked,
|
||
account_scope_match_strategy: addressDebug.account_scope_match_strategy,
|
||
account_scope_drop_reason: addressDebug.account_scope_drop_reason,
|
||
runtime_readiness: addressDebug.runtime_readiness,
|
||
limited_reason_category: addressDebug.limited_reason_category,
|
||
response_type: addressDebug.response_type,
|
||
execution_lane: "address_query",
|
||
llm_decomposition_applied: Boolean(llmMeta?.applied),
|
||
llm_decomposition_attempted: Boolean(llmMeta?.attempted),
|
||
llm_provider_used: llmMeta?.provider ?? null,
|
||
llm_decomposition_trace_id: llmMeta?.traceId ?? null,
|
||
llm_decomposition_effective_message: llmMeta?.effectiveMessage ?? null,
|
||
llm_decomposition_reason: llmMeta?.reason ?? null,
|
||
llm_canonical_candidate_detected: Boolean(llmMeta?.llmCanonicalCandidateDetected),
|
||
llm_predecompose_contract: llmMeta?.predecomposeContract ?? null,
|
||
fallback_rule_hit: llmMeta?.fallbackRuleHit ?? null,
|
||
sanitized_user_message: llmMeta?.sanitizedUserMessage ?? null,
|
||
tool_gate_decision: llmMeta?.toolGateDecision ?? null,
|
||
tool_gate_reason: llmMeta?.toolGateReason ?? null,
|
||
orchestration_contract_v1: llmMeta?.orchestrationContract ?? null,
|
||
dialog_continuation_contract_v2: llmMeta?.dialogContinuationContract ?? null,
|
||
address_retry_audit: llmMeta?.addressRetryAudit ?? null,
|
||
answer_structure_v11: null,
|
||
investigation_state_snapshot: null,
|
||
normalized: null,
|
||
normalizer_output: llmMeta?.traceId
|
||
? {
|
||
trace_id: llmMeta.traceId,
|
||
prompt_version: "normalizer_v2_0_2",
|
||
applied: Boolean(llmMeta?.applied),
|
||
effective_message: llmMeta?.effectiveMessage ?? null
|
||
}
|
||
: null
|
||
};
|
||
}
|
||
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",
|
||
"blya",
|
||
"blyat",
|
||
"епт",
|
||
"ёпт",
|
||
"бля"
|
||
]);
|
||
const ADDRESS_FALLBACK_STRIP_TOKENS = new Set([
|
||
"бля",
|
||
"блять",
|
||
"blya",
|
||
"blyat",
|
||
"епт",
|
||
"ёпт",
|
||
"епта",
|
||
"нах",
|
||
"нахуй",
|
||
"плс",
|
||
"pls",
|
||
"пж",
|
||
"пжлст",
|
||
"пожалуйста",
|
||
"please"
|
||
]);
|
||
const ADDRESS_MONTH_ALIAS_MAP = {
|
||
янв: "01",
|
||
январ: "01",
|
||
january: "01",
|
||
jan: "01",
|
||
фев: "02",
|
||
феврал: "02",
|
||
february: "02",
|
||
feb: "02",
|
||
мар: "03",
|
||
март: "03",
|
||
march: "03",
|
||
apr: "04",
|
||
апр: "04",
|
||
апрел: "04",
|
||
april: "04",
|
||
май: "05",
|
||
ма: "05",
|
||
may: "05",
|
||
июн: "06",
|
||
июнь: "06",
|
||
june: "06",
|
||
jun: "06",
|
||
июл: "07",
|
||
июль: "07",
|
||
july: "07",
|
||
jul: "07",
|
||
авг: "08",
|
||
август: "08",
|
||
august: "08",
|
||
aug: "08",
|
||
сен: "09",
|
||
сент: "09",
|
||
сентябр: "09",
|
||
september: "09",
|
||
sep: "09",
|
||
окт: "10",
|
||
октябр: "10",
|
||
october: "10",
|
||
oct: "10",
|
||
ноя: "11",
|
||
ноябр: "11",
|
||
november: "11",
|
||
nov: "11",
|
||
дек: "12",
|
||
декабр: "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);
|
||
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(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/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 (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/.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 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 findLastAddressAssistantDebug(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 debug;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
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 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;
|
||
}
|
||
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) ??
|
||
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 repaired = repairAddressMojibake(String(userMessage ?? ""));
|
||
const text = compactWhitespace(repaired.toLowerCase());
|
||
if (!text) {
|
||
return false;
|
||
}
|
||
if (hasStandaloneAddressTopicSignal(text)) {
|
||
return false;
|
||
}
|
||
if (shouldHandleAsAssistantCapabilityMetaQuery(text)) {
|
||
return false;
|
||
}
|
||
if (/(?:за\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.test(text)) {
|
||
return true;
|
||
}
|
||
if (hasReferentialPointer(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|same\s+date|the\s+same\s+date|as\s+of\s+same\s+date)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
const shortFollowup = countTokens(text) <= 8;
|
||
if (/(?:кроме|помимо)\s+(?:этого|этой|этот|эту|этих|этого\s+документа|этого\s+договора|этого\s+контрагента)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (/(?:есть\s+ещ[её]|что\s+ещ[её]|ещ[её]\s+что|ещ[её]\s+что-?то|остал(?:ось|ось\?)|друг(?:ое|ие))/iu.test(text) && countTokens(text) <= 12) {
|
||
return true;
|
||
}
|
||
if (shortFollowup && hasFollowupMarker(text)) {
|
||
return true;
|
||
}
|
||
if (shortFollowup && /(?:^|\s)(?:также|тоже|also|same|again|ещ[её]|теперь|then|now)(?=$|[\s,.;:!?])/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (shortFollowup &&
|
||
/(?:кто\s+из\s+(?:них|этих|тех)|кто\s+нов(?:ые|ых|ый)|кто\s+потом\s+исчез|кто\s+был\s+(?:только|ровно)\s+один\s+раз)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (shortFollowup && /^(?:а|и)\s+кто\b/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (shortFollowup &&
|
||
/(?:почему|why|из[-\s]?за\s+чего|как\s+так|reason)/iu.test(text) &&
|
||
/(?:ндс|vat|прогноз|к\s+уплате|нул|ноль|\b0(?:[.,]0+)?\b)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (shortFollowup &&
|
||
/(?:^|\s)по\s+[a-zа-яё][a-zа-яё0-9._-]{1,}(?=$|[\s,.;:!?])/iu.test(text) &&
|
||
!/(?:по\s+этому|по\s+тому|по\s+нему|по\s+ней|по\s+ним)/iu.test(text)) {
|
||
return true;
|
||
}
|
||
if (shortFollowup && hasPeriodLiteral(text)) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null) {
|
||
const previousAddressDebug = findLastAddressAssistantDebug(items);
|
||
const followupOffer = previousAddressDebug ? buildAddressFollowupOffer(previousAddressDebug) : null;
|
||
const hasImplicitContinuationSignal = Boolean(previousAddressDebug) &&
|
||
Boolean(followupOffer?.enabled) &&
|
||
(isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
|
||
(toNonEmptyString(alternateMessage) ? isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta) : false));
|
||
const hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage);
|
||
const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||
? hasAddressFollowupContextSignal(alternateMessage)
|
||
: false;
|
||
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
|
||
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
|
||
if (hasStandaloneAddressTopic && !hasImplicitContinuationSignal) {
|
||
return null;
|
||
}
|
||
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal && !hasImplicitContinuationSignal) {
|
||
return null;
|
||
}
|
||
if (!previousAddressDebug) {
|
||
return null;
|
||
}
|
||
const sourceIntent = toNonEmptyString(previousAddressDebug.detected_intent);
|
||
let previousIntent = sourceIntent;
|
||
let followupSelectionMode = "carry_previous_intent";
|
||
if (hasImplicitContinuationSignal) {
|
||
const suggestedIntent = Array.isArray(followupOffer?.suggested_intents)
|
||
? toNonEmptyString(followupOffer.suggested_intents[0])
|
||
: null;
|
||
if (suggestedIntent) {
|
||
previousIntent = suggestedIntent;
|
||
followupSelectionMode = "switch_to_suggested_intent";
|
||
}
|
||
}
|
||
const previousAnchorType = toNonEmptyString(previousAddressDebug.anchor_type);
|
||
const previousAnchor = toNonEmptyString(previousAddressDebug.anchor_value_resolved) ??
|
||
toNonEmptyString(previousAddressDebug.anchor_value_raw) ??
|
||
readAddressFilterString(previousAddressDebug, "counterparty") ??
|
||
readAddressFilterString(previousAddressDebug, "account") ??
|
||
readAddressFilterString(previousAddressDebug, "contract");
|
||
const previousFiltersRaw = previousAddressDebug.extracted_filters;
|
||
const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
|
||
? { ...previousFiltersRaw }
|
||
: {};
|
||
if (!toNonEmptyString(previousFilters.contract)) {
|
||
const historicalContract = findRecentAddressFilterValue(items, "contract");
|
||
if (historicalContract) {
|
||
previousFilters.contract = historicalContract;
|
||
}
|
||
}
|
||
if (!toNonEmptyString(previousFilters.counterparty)) {
|
||
const historicalCounterparty = findRecentAddressFilterValue(items, "counterparty");
|
||
if (historicalCounterparty) {
|
||
previousFilters.counterparty = historicalCounterparty;
|
||
}
|
||
}
|
||
if (!toNonEmptyString(previousFilters.organization)) {
|
||
const historicalOrganization = findRecentAddressFilterValue(items, "organization");
|
||
if (historicalOrganization) {
|
||
previousFilters.organization = historicalOrganization;
|
||
}
|
||
}
|
||
if (!previousIntent && !previousAnchor && Object.keys(previousFilters).length === 0) {
|
||
return null;
|
||
}
|
||
return {
|
||
followupContext: {
|
||
previous_intent: previousIntent ?? undefined,
|
||
previous_filters: previousFilters,
|
||
previous_anchor_type: previousAnchorType ?? undefined,
|
||
previous_anchor_value: previousAnchor
|
||
},
|
||
previousAddressIntent: previousIntent,
|
||
previousAddressAnchor: previousAnchor,
|
||
previousSourceIntent: sourceIntent,
|
||
followupSelectionMode,
|
||
hasImplicitContinuationSignal
|
||
};
|
||
}
|
||
function buildAddressDialogContinuationContractV2(userMessage, effectiveMessage, carryoverMeta, llmPreDecomposeMeta) {
|
||
const sourceMessage = String(userMessage ?? "");
|
||
const canonicalMessage = String(effectiveMessage ?? sourceMessage);
|
||
const hasFollowupContext = Boolean(carryoverMeta?.followupContext);
|
||
const previousIntent = toNonEmptyString(carryoverMeta?.previousSourceIntent) ?? null;
|
||
const targetIntent = toNonEmptyString(carryoverMeta?.previousAddressIntent) ?? null;
|
||
const selectionMode = toNonEmptyString(carryoverMeta?.followupSelectionMode) ?? null;
|
||
const hasImplicitContinuationSignal = Boolean(carryoverMeta?.hasImplicitContinuationSignal);
|
||
const rewrittenByPredecompose = compactWhitespace(sourceMessage.toLowerCase()) !== compactWhitespace(canonicalMessage.toLowerCase());
|
||
const hasExplicitIntent = Boolean(toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent));
|
||
const decision = !hasFollowupContext
|
||
? "new_topic"
|
||
: selectionMode === "switch_to_suggested_intent"
|
||
? "switch_to_suggested"
|
||
: "continue_previous";
|
||
const reasons = [];
|
||
if (hasFollowupContext) {
|
||
reasons.push("followup_context_detected");
|
||
}
|
||
if (hasImplicitContinuationSignal) {
|
||
reasons.push("implicit_continuation_by_llm");
|
||
}
|
||
if (rewrittenByPredecompose) {
|
||
reasons.push("effective_message_rewritten_by_predecompose");
|
||
}
|
||
if (hasExplicitIntent) {
|
||
reasons.push("llm_contract_intent_available");
|
||
}
|
||
return {
|
||
schema_version: "address_dialog_continuation_contract_v2",
|
||
source_message: sourceMessage,
|
||
effective_message: canonicalMessage,
|
||
decision,
|
||
decision_reasons: reasons,
|
||
followup_context_applied: hasFollowupContext,
|
||
previous_intent: previousIntent,
|
||
target_intent: targetIntent,
|
||
intent_selection_mode: selectionMode,
|
||
anchor_type: carryoverMeta?.followupContext?.previous_anchor_type ?? null,
|
||
anchor_value: carryoverMeta?.followupContext?.previous_anchor_value ?? null,
|
||
implicit_continuation_signal: hasImplicitContinuationSignal
|
||
};
|
||
}
|
||
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";
|
||
}
|
||
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 extractAddressQuestionFromNormalized(normalized) {
|
||
if (!normalized || typeof normalized !== "object") {
|
||
return null;
|
||
}
|
||
const source = normalized;
|
||
const fragments = Array.isArray(source.fragments) ? source.fragments : [];
|
||
for (const item of fragments) {
|
||
if (!item || typeof item !== "object") {
|
||
continue;
|
||
}
|
||
const fragment = item;
|
||
const domainRelevance = String(fragment.domain_relevance ?? "").trim().toLowerCase();
|
||
if (domainRelevance === "out_of_scope") {
|
||
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;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
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 extractAddressQuestionFromRawNormalizerOutput(rawModelOutput) {
|
||
const outputText = extractOutputTextFromRawNormalizerOutput(rawModelOutput);
|
||
if (!outputText) {
|
||
return null;
|
||
}
|
||
const parsed = safeParseLooseJson(outputText);
|
||
if (!parsed || typeof parsed !== "object") {
|
||
return null;
|
||
}
|
||
const source = parsed;
|
||
const fragments = Array.isArray(source.fragments) ? source.fragments : [];
|
||
for (const item of fragments) {
|
||
if (!item || typeof item !== "object") {
|
||
continue;
|
||
}
|
||
const fragment = item;
|
||
const domainRelevance = fragment.domain_relevance;
|
||
if (typeof domainRelevance === "string" && domainRelevance.trim().toLowerCase() === "out_of_scope") {
|
||
continue;
|
||
}
|
||
if (domainRelevance === false) {
|
||
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;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
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";
|
||
}
|
||
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)
|
||
: toNonEmptyString(extracted?.extracted_filters?.contract);
|
||
if (!anchorValue) {
|
||
return {
|
||
intent,
|
||
anchorType,
|
||
anchorValue: null,
|
||
quality: 0
|
||
};
|
||
}
|
||
const lowQuality = anchorType === "counterparty"
|
||
? isLowQualityPredecomposeCounterpartyAnchor(anchorValue)
|
||
: isLowQualityPredecomposeContractAnchor(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 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 attachAddressPredecomposeContract(meta, sourceMessage) {
|
||
const canonicalMessage = toNonEmptyString(meta?.effectiveMessage) ?? String(sourceMessage ?? "");
|
||
const predecomposeContract = (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||
sourceMessage: String(sourceMessage ?? ""),
|
||
canonicalMessage
|
||
});
|
||
const semanticExtractionContract = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
|
||
sourceMessage: String(sourceMessage ?? ""),
|
||
canonicalMessage,
|
||
predecomposeContract
|
||
});
|
||
return {
|
||
...meta,
|
||
predecomposeContract,
|
||
semanticExtractionContract
|
||
};
|
||
}
|
||
async function runAddressLlmPreDecompose(normalizerService, payload, userMessage) {
|
||
const provider = payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null;
|
||
const sanitizedUserMessage = sanitizeAddressMessageForFallback(userMessage);
|
||
const fallbackCandidate = resolveAddressDeterministicFallback(userMessage, sanitizedUserMessage);
|
||
const baseMeta = {
|
||
attempted: false,
|
||
applied: false,
|
||
provider,
|
||
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 = extractAddressQuestionFromNormalized(normalized?.normalized);
|
||
const candidateFromRaw = candidateFromNormalized ? null : extractAddressQuestionFromRawNormalizerOutput(normalized?.raw_model_output);
|
||
const candidate = candidateFromNormalized ?? candidateFromRaw;
|
||
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
|
||
}, userMessage);
|
||
}
|
||
}
|
||
return attachAddressPredecomposeContract({
|
||
...baseMeta,
|
||
attempted: true,
|
||
traceId: normalized?.trace_id ?? null,
|
||
reason: normalized?.ok ? "no_usable_fragment" : "normalize_failed"
|
||
}, 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 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
|
||
}, 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
|
||
}, userMessage);
|
||
}
|
||
const sourceAnchorQuality = evaluateAddressAnchorQuality(repairedSourceMessage || userMessage);
|
||
const candidateAnchorQuality = evaluateAddressAnchorQuality(candidate);
|
||
const sameIntentForAnchorSafety = sourceAnchorQuality.intent !== "unknown" && sourceAnchorQuality.intent === candidateAnchorQuality.intent;
|
||
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
|
||
}, 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
|
||
}, 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
|
||
}, userMessage);
|
||
}
|
||
}
|
||
const semanticContractForCandidate = (0, predecomposeContract_1.buildAddressSemanticExtractionContractV1)({
|
||
sourceMessage: String(userMessage ?? ""),
|
||
canonicalMessage: candidate
|
||
});
|
||
if (!semanticContractForCandidate.apply_canonical_recommended) {
|
||
const sourceDataSignalDetected = Boolean(semanticContractForCandidate?.guard_hints?.source_data_signal_detected);
|
||
const rawFragmentCandidatePreferred = Boolean(sourceDataSignalDetected &&
|
||
candidateFromNormalized &&
|
||
candidateFromNormalized === 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
|
||
}, 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
|
||
}, 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
|
||
}, 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
|
||
}, 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 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 sameDateAccountFollowupSignal = hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate) ||
|
||
hasSameDateAccountFollowupSignalForPredecompose(repairedInputMessage);
|
||
const hasLexicalAddressSignal = isAddressLlmPreDecomposeCandidate(addressInputMessage) ||
|
||
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
|
||
hasAccountingSignal(addressInputMessage) ||
|
||
hasAccountingSignal(repairedInputMessage) ||
|
||
sameDateAccountFollowupSignal;
|
||
const hasUnsupportedLowConfidencePredecomposeSignal = llmContractMode === "unsupported" &&
|
||
(llmContractModeConfidence === "low" || llmContractModeConfidence === "medium") &&
|
||
llmContractIntent === "unknown";
|
||
const hasAnyAddressSignal = hasClassifierSignal || hasIntentSignal || hasLlmCanonicalSignal || hasLlmCanonicalDataSignal || hasLexicalAddressSignal;
|
||
const strongDataSignalFromRawMessage = hasStrongDataIntentSignal(rawMessageForGate) ||
|
||
hasDataRetrievalRequestSignal(rawMessageForGate) ||
|
||
hasAccountingSignal(rawMessageForGate) ||
|
||
hasSameDateAccountFollowupSignalForPredecompose(rawMessageForGate);
|
||
const strongDataSignalFromEffectiveMessage = hasStrongDataIntentSignal(repairedInputMessage) ||
|
||
hasAccountingSignal(repairedInputMessage) ||
|
||
hasDataRetrievalRequestSignal(repairedInputMessage);
|
||
if (!semanticCanonicalRecommended &&
|
||
llmContractIntent === "unknown" &&
|
||
!followupContext &&
|
||
!hasClassifierSignal &&
|
||
!strongDataSignalFromRawMessage) {
|
||
return {
|
||
runAddressLane: false,
|
||
decision: "skip_address_lane",
|
||
reason: "llm_predecompose_semantic_guard_rejected"
|
||
};
|
||
}
|
||
if (hasUnsupportedLowConfidencePredecomposeSignal && !followupContext &&
|
||
!hasAnyAddressSignal &&
|
||
!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"
|
||
: "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 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;
|
||
}
|
||
const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||
"list_open_contracts",
|
||
"open_items_by_counterparty_or_contract",
|
||
"list_documents_by_contract",
|
||
"bank_operations_by_contract",
|
||
"list_documents_by_counterparty",
|
||
"bank_operations_by_counterparty",
|
||
"list_contracts_by_counterparty",
|
||
"contract_usage_overview",
|
||
"contract_usage_and_value",
|
||
"vat_payable_forecast"
|
||
]);
|
||
function resolveAssistantOrchestrationDecision(input) {
|
||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
||
const repairedRawUserMessage = repairAddressMojibake(rawUserMessage);
|
||
const repairedEffectiveAddressUserMessage = repairAddressMojibake(effectiveAddressUserMessage);
|
||
const followupContext = input?.followupContext ?? null;
|
||
const llmPreDecomposeMeta = input?.llmPreDecomposeMeta ?? null;
|
||
const useMock = Boolean(input?.useMock);
|
||
const sessionItems = Array.isArray(input?.sessionItems) ? input.sessionItems : null;
|
||
const dataScopeMetaQuery = hasAssistantDataScopeMetaQuestionSignal(rawUserMessage) ||
|
||
hasAssistantDataScopeMetaQuestionSignal(repairedRawUserMessage) ||
|
||
hasAssistantDataScopeMetaQuestionSignal(effectiveAddressUserMessage) ||
|
||
hasAssistantDataScopeMetaQuestionSignal(repairedEffectiveAddressUserMessage);
|
||
const capabilityMetaQuery = shouldHandleAsAssistantCapabilityMetaQuery(rawUserMessage) ||
|
||
shouldHandleAsAssistantCapabilityMetaQuery(repairedRawUserMessage) ||
|
||
shouldHandleAsAssistantCapabilityMetaQuery(effectiveAddressUserMessage) ||
|
||
shouldHandleAsAssistantCapabilityMetaQuery(repairedEffectiveAddressUserMessage);
|
||
const dataRetrievalSignal = hasDataRetrievalRequestSignal(rawUserMessage) ||
|
||
hasDataRetrievalRequestSignal(repairedRawUserMessage) ||
|
||
hasDataRetrievalRequestSignal(effectiveAddressUserMessage) ||
|
||
hasDataRetrievalRequestSignal(repairedEffectiveAddressUserMessage);
|
||
const aggregateBusinessAnalyticsSignal = hasAggregateBusinessAnalyticsSignal(rawUserMessage) ||
|
||
hasAggregateBusinessAnalyticsSignal(repairedRawUserMessage) ||
|
||
hasAggregateBusinessAnalyticsSignal(effectiveAddressUserMessage) ||
|
||
hasAggregateBusinessAnalyticsSignal(repairedEffectiveAddressUserMessage);
|
||
const standaloneAddressTopicSignal = hasStandaloneAddressTopicSignal(rawUserMessage) ||
|
||
hasStandaloneAddressTopicSignal(repairedRawUserMessage) ||
|
||
hasStandaloneAddressTopicSignal(effectiveAddressUserMessage) ||
|
||
hasStandaloneAddressTopicSignal(repairedEffectiveAddressUserMessage);
|
||
const openContractsAddressSignal = hasOpenContractsAddressSignal(rawUserMessage) ||
|
||
hasOpenContractsAddressSignal(repairedRawUserMessage) ||
|
||
hasOpenContractsAddressSignal(effectiveAddressUserMessage) ||
|
||
hasOpenContractsAddressSignal(repairedEffectiveAddressUserMessage);
|
||
const modeSample = repairedEffectiveAddressUserMessage || effectiveAddressUserMessage;
|
||
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(modeSample);
|
||
const intentResolution = (0, addressIntentResolver_1.resolveAddressIntent)(modeSample);
|
||
const llmContractIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
|
||
const llmPreDecomposeReason = toNonEmptyString(llmPreDecomposeMeta?.reason);
|
||
const llmRuntimeUnavailableDetected = Boolean(llmPreDecomposeReason &&
|
||
/(?:openai\s+api\s+key\s+is\s+missing|api\s+key\s+is\s+missing|missing\s+api\s+key|authentication)/iu.test(llmPreDecomposeReason));
|
||
const semanticExtractionContract = llmPreDecomposeMeta?.semanticExtractionContract &&
|
||
typeof llmPreDecomposeMeta.semanticExtractionContract === "object"
|
||
? llmPreDecomposeMeta.semanticExtractionContract
|
||
: null;
|
||
const semanticContractValid = semanticExtractionContract?.valid !== false;
|
||
const semanticApplyCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
||
const semanticReasonCodes = Array.isArray(semanticExtractionContract?.reason_codes)
|
||
? semanticExtractionContract.reason_codes
|
||
: [];
|
||
const strictDeepInvestigationCueDetected = hasStrictDeepInvestigationCue(rawUserMessage) ||
|
||
hasStrictDeepInvestigationCue(repairedRawUserMessage) ||
|
||
hasStrictDeepInvestigationCue(effectiveAddressUserMessage) ||
|
||
hasStrictDeepInvestigationCue(repairedEffectiveAddressUserMessage);
|
||
const keepAddressLaneByIntent = semanticApplyCanonicalRecommended &&
|
||
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
|
||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
|
||
openContractsAddressSignal) &&
|
||
!strictDeepInvestigationCueDetected;
|
||
const strongDataSignal = hasStrongDataIntentSignal(rawUserMessage) ||
|
||
hasStrongDataIntentSignal(repairedRawUserMessage) ||
|
||
hasStrongDataIntentSignal(effectiveAddressUserMessage) ||
|
||
hasStrongDataIntentSignal(repairedEffectiveAddressUserMessage) ||
|
||
hasAccountingSignal(rawUserMessage) ||
|
||
hasAccountingSignal(repairedRawUserMessage) ||
|
||
hasAccountingSignal(effectiveAddressUserMessage) ||
|
||
hasAccountingSignal(repairedEffectiveAddressUserMessage) ||
|
||
hasDataRetrievalRequestSignal(rawUserMessage) ||
|
||
hasDataRetrievalRequestSignal(repairedRawUserMessage);
|
||
const hardMetaMode = dataScopeMetaQuery
|
||
? "data_scope"
|
||
: capabilityMetaQuery && !dataRetrievalSignal
|
||
? "capability"
|
||
: null;
|
||
if (hardMetaMode === "data_scope") {
|
||
return {
|
||
runAddressLane: false,
|
||
toolGateDecision: "skip_address_lane",
|
||
toolGateReason: "assistant_data_scope_query_detected",
|
||
livingMode: "chat",
|
||
livingReason: "assistant_data_scope_query_detected",
|
||
orchestrationContract: {
|
||
schema_version: "assistant_orchestration_contract_v1",
|
||
hard_meta_mode: "data_scope",
|
||
address_mode: modeDetection.mode,
|
||
address_mode_confidence: modeDetection.confidence,
|
||
address_intent: intentResolution.intent,
|
||
address_intent_confidence: intentResolution.confidence,
|
||
strong_data_signal_detected: strongDataSignal,
|
||
data_retrieval_signal_detected: dataRetrievalSignal,
|
||
followup_context_detected: Boolean(followupContext),
|
||
unsupported_address_intent_fallback_to_deep: false,
|
||
final_decision: {
|
||
run_address_lane: false,
|
||
tool_gate_decision: "skip_address_lane",
|
||
tool_gate_reason: "assistant_data_scope_query_detected",
|
||
living_mode: "chat",
|
||
living_reason: "assistant_data_scope_query_detected"
|
||
}
|
||
}
|
||
};
|
||
}
|
||
if (hardMetaMode === "capability") {
|
||
return {
|
||
runAddressLane: false,
|
||
toolGateDecision: "skip_address_lane",
|
||
toolGateReason: "assistant_capability_query_detected",
|
||
livingMode: "chat",
|
||
livingReason: "assistant_capability_query_detected",
|
||
orchestrationContract: {
|
||
schema_version: "assistant_orchestration_contract_v1",
|
||
hard_meta_mode: "capability",
|
||
address_mode: modeDetection.mode,
|
||
address_mode_confidence: modeDetection.confidence,
|
||
address_intent: intentResolution.intent,
|
||
address_intent_confidence: intentResolution.confidence,
|
||
strong_data_signal_detected: strongDataSignal,
|
||
data_retrieval_signal_detected: dataRetrievalSignal,
|
||
followup_context_detected: Boolean(followupContext),
|
||
unsupported_address_intent_fallback_to_deep: false,
|
||
final_decision: {
|
||
run_address_lane: false,
|
||
tool_gate_decision: "skip_address_lane",
|
||
tool_gate_reason: "assistant_capability_query_detected",
|
||
living_mode: "chat",
|
||
living_reason: "assistant_capability_query_detected"
|
||
}
|
||
}
|
||
};
|
||
}
|
||
const baseToolGate = resolveAddressToolGateDecision(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage);
|
||
const llmContractMode = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.mode);
|
||
const preserveAddressLaneSignal = Boolean((llmPreDecomposeMeta?.llmCanonicalCandidateDetected &&
|
||
llmPreDecomposeMeta?.applied &&
|
||
llmContractMode === "address_query") ||
|
||
hasSameDateAccountFollowupSignalForPredecompose(rawUserMessage) ||
|
||
hasSameDateAccountFollowupSignalForPredecompose(effectiveAddressUserMessage) ||
|
||
hasSameDateAccountFollowupSignalForPredecompose(repairedRawUserMessage) ||
|
||
hasSameDateAccountFollowupSignalForPredecompose(repairedEffectiveAddressUserMessage) ||
|
||
hasLooseAllTimeAddressLookupSignal(rawUserMessage) ||
|
||
hasLooseAllTimeAddressLookupSignal(effectiveAddressUserMessage) ||
|
||
hasLooseAllTimeAddressLookupSignal(repairedRawUserMessage) ||
|
||
hasLooseAllTimeAddressLookupSignal(repairedEffectiveAddressUserMessage) ||
|
||
hasAddressFollowupContextSignal(rawUserMessage) ||
|
||
hasAddressFollowupContextSignal(effectiveAddressUserMessage) ||
|
||
hasAddressFollowupContextSignal(repairedRawUserMessage) ||
|
||
hasAddressFollowupContextSignal(repairedEffectiveAddressUserMessage));
|
||
const supportedAddressIntentDetected = !strictDeepInvestigationCueDetected &&
|
||
Boolean((intentResolution.intent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(intentResolution.intent)) ||
|
||
(llmContractIntent && ADDRESS_INTENTS_KEEP_ADDRESS_LANE.has(llmContractIntent)) ||
|
||
openContractsAddressSignal);
|
||
const semanticGuardHints = semanticExtractionContract?.guard_hints &&
|
||
typeof semanticExtractionContract.guard_hints === "object"
|
||
? semanticExtractionContract.guard_hints
|
||
: null;
|
||
const semanticExtraction = semanticExtractionContract?.extraction &&
|
||
typeof semanticExtractionContract.extraction === "object"
|
||
? semanticExtractionContract.extraction
|
||
: null;
|
||
const semanticDeepInvestigationHintDetected = semanticGuardHints?.deep_investigation_signal_detected === true;
|
||
const semanticAggregateShapeDetected = semanticExtraction?.query_shape === "AGGREGATE_LOOKUP" ||
|
||
semanticExtraction?.aggregation_profile === "management_profile";
|
||
const followupSemanticOverrideToDeepAllowed = Boolean(followupContext &&
|
||
!supportedAddressIntentDetected &&
|
||
(llmContractMode === "unsupported" ||
|
||
semanticAggregateShapeDetected ||
|
||
semanticDeepInvestigationHintDetected ||
|
||
!semanticApplyCanonicalRecommended));
|
||
const unsupportedIntentOrMode = (modeDetection.mode !== "address_query" && intentResolution.intent === "unknown") ||
|
||
llmContractMode === "unsupported";
|
||
const unsupportedAddressIntentFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||
!llmRuntimeUnavailableDetected &&
|
||
unsupportedIntentOrMode &&
|
||
strongDataSignal &&
|
||
(llmContractMode === "deep_analysis" ||
|
||
!dataRetrievalSignal ||
|
||
strictDeepInvestigationCueDetected ||
|
||
semanticDeepInvestigationHintDetected ||
|
||
aggregateBusinessAnalyticsSignal) &&
|
||
!preserveAddressLaneSignal &&
|
||
!keepAddressLaneByIntent &&
|
||
!supportedAddressIntentDetected &&
|
||
(!followupContext || followupSemanticOverrideToDeepAllowed));
|
||
const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) ||
|
||
hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) ||
|
||
hasDeepAnalysisPreferenceSignal(effectiveAddressUserMessage) ||
|
||
hasDeepAnalysisPreferenceSignal(repairedEffectiveAddressUserMessage) ||
|
||
hasDirectDeepAnalysisSignal(rawUserMessage) ||
|
||
hasDirectDeepAnalysisSignal(repairedRawUserMessage) ||
|
||
hasDirectDeepAnalysisSignal(effectiveAddressUserMessage) ||
|
||
hasDirectDeepAnalysisSignal(repairedEffectiveAddressUserMessage));
|
||
const vatExplainFollowupSignal = Boolean(followupContext &&
|
||
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
||
/(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
|
||
const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||
!llmRuntimeUnavailableDetected &&
|
||
(deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) &&
|
||
!keepAddressLaneByIntent &&
|
||
!supportedAddressIntentDetected &&
|
||
!vatExplainFollowupSignal &&
|
||
(!followupContext || !dataRetrievalSignal || followupSemanticOverrideToDeepAllowed));
|
||
const aggregateAnalyticsFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
|
||
!llmRuntimeUnavailableDetected &&
|
||
aggregateBusinessAnalyticsSignal &&
|
||
!keepAddressLaneByIntent &&
|
||
!supportedAddressIntentDetected &&
|
||
(!followupContext ||
|
||
llmContractMode === "unsupported" ||
|
||
semanticAggregateShapeDetected ||
|
||
!semanticApplyCanonicalRecommended ||
|
||
standaloneAddressTopicSignal));
|
||
const deepSessionContinuationFallbackToDeep = Boolean(!followupContext &&
|
||
baseToolGate?.runAddressLane &&
|
||
!llmRuntimeUnavailableDetected &&
|
||
hasDeepSessionContinuationSignal({
|
||
rawUserMessage,
|
||
repairedRawUserMessage,
|
||
effectiveAddressUserMessage,
|
||
repairedEffectiveAddressUserMessage,
|
||
sessionItems
|
||
}));
|
||
let runAddressLane = Boolean(baseToolGate?.runAddressLane);
|
||
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
|
||
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
|
||
if (unsupportedAddressIntentFallbackToDeep) {
|
||
runAddressLane = false;
|
||
toolGateDecision = "skip_address_lane";
|
||
toolGateReason = "address_signal_unsupported_intent_fallback_to_deep";
|
||
}
|
||
if (deepAnalysisSignalFallbackToDeep && !unsupportedAddressIntentFallbackToDeep) {
|
||
runAddressLane = false;
|
||
toolGateDecision = "skip_address_lane";
|
||
toolGateReason = "deep_analysis_signal_fallback_to_deep";
|
||
}
|
||
if (aggregateAnalyticsFallbackToDeep &&
|
||
!unsupportedAddressIntentFallbackToDeep &&
|
||
!deepAnalysisSignalFallbackToDeep) {
|
||
runAddressLane = false;
|
||
toolGateDecision = "skip_address_lane";
|
||
toolGateReason = "aggregate_analytics_signal_fallback_to_deep";
|
||
}
|
||
if (deepSessionContinuationFallbackToDeep) {
|
||
runAddressLane = false;
|
||
toolGateDecision = "skip_address_lane";
|
||
toolGateReason = "deep_session_continuation_fallback_to_deep";
|
||
}
|
||
let livingDecision = resolveLivingAssistantModeDecision({
|
||
userMessage: rawUserMessage,
|
||
addressLaneTriggered: runAddressLane,
|
||
useMock,
|
||
predecomposeMode: llmPreDecomposeMeta?.predecomposeContract?.mode ?? null,
|
||
predecomposeModeConfidence: llmPreDecomposeMeta?.predecomposeContract?.mode_confidence ?? null
|
||
});
|
||
if (unsupportedAddressIntentFallbackToDeep) {
|
||
livingDecision = {
|
||
mode: "deep_analysis",
|
||
reason: "unsupported_address_intent_fallback_to_deep"
|
||
};
|
||
}
|
||
if (deepAnalysisSignalFallbackToDeep && !unsupportedAddressIntentFallbackToDeep) {
|
||
livingDecision = {
|
||
mode: "deep_analysis",
|
||
reason: "deep_analysis_signal_fallback_to_deep"
|
||
};
|
||
}
|
||
if (aggregateAnalyticsFallbackToDeep &&
|
||
!unsupportedAddressIntentFallbackToDeep &&
|
||
!deepAnalysisSignalFallbackToDeep) {
|
||
livingDecision = {
|
||
mode: "deep_analysis",
|
||
reason: "aggregate_analytics_signal_fallback_to_deep"
|
||
};
|
||
}
|
||
if (deepSessionContinuationFallbackToDeep) {
|
||
livingDecision = {
|
||
mode: "deep_analysis",
|
||
reason: "deep_session_continuation_fallback_to_deep"
|
||
};
|
||
}
|
||
return {
|
||
runAddressLane,
|
||
toolGateDecision,
|
||
toolGateReason,
|
||
livingMode: livingDecision.mode,
|
||
livingReason: livingDecision.reason,
|
||
orchestrationContract: {
|
||
schema_version: "assistant_orchestration_contract_v1",
|
||
hard_meta_mode: null,
|
||
address_mode: modeDetection.mode,
|
||
address_mode_confidence: modeDetection.confidence,
|
||
address_intent: intentResolution.intent,
|
||
address_intent_confidence: intentResolution.confidence,
|
||
strong_data_signal_detected: strongDataSignal,
|
||
data_retrieval_signal_detected: dataRetrievalSignal,
|
||
semantic_contract_valid: semanticContractValid,
|
||
semantic_apply_canonical_recommended: semanticApplyCanonicalRecommended,
|
||
semantic_reason_codes: semanticReasonCodes,
|
||
semantic_route_arbitration: {
|
||
supported_address_intent_detected: supportedAddressIntentDetected,
|
||
semantic_deep_investigation_hint_detected: semanticDeepInvestigationHintDetected,
|
||
semantic_aggregate_shape_detected: semanticAggregateShapeDetected,
|
||
followup_semantic_override_to_deep_allowed: followupSemanticOverrideToDeepAllowed
|
||
},
|
||
followup_context_detected: Boolean(followupContext),
|
||
unsupported_address_intent_fallback_to_deep: unsupportedAddressIntentFallbackToDeep,
|
||
deep_analysis_signal_fallback_to_deep: deepAnalysisSignalFallbackToDeep,
|
||
aggregate_analytics_signal_fallback_to_deep: aggregateAnalyticsFallbackToDeep,
|
||
deep_session_continuation_fallback_to_deep: deepSessionContinuationFallbackToDeep,
|
||
final_decision: {
|
||
run_address_lane: runAddressLane,
|
||
tool_gate_decision: toolGateDecision,
|
||
tool_gate_reason: toolGateReason,
|
||
living_mode: livingDecision.mode,
|
||
living_reason: livingDecision.reason
|
||
}
|
||
}
|
||
};
|
||
}
|
||
function hasStrongDataIntentSignal(text) {
|
||
const lower = String(text ?? "").toLowerCase();
|
||
return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|mcp|bank|counterparty|contract|document|ledger|posting|account|organization|company|advance|prepay|shipment|receivab|payab|организац|компан|контор|фирм)/i.test(lower);
|
||
}
|
||
function hasDataRetrievalRequestSignal(text) {
|
||
const lower = compactWhitespace(String(text ?? "").toLowerCase());
|
||
if (!lower) {
|
||
return false;
|
||
}
|
||
const hasBroadInterrogative = /(?:\u0433\u0434\u0435|\u0432\s+\u043a\u0430\u043a\u0438\u0445|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c|\u043f\u043e\s+\u043a\u043e\u043c\u0443|\u043a\u0430\u043a\u0438\u0435|\u043a\u0430\u043a\u043e\u0439|\u043a\u0442\u043e|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|where|which|who|how\s+many)/iu.test(lower);
|
||
const hasBroadBusinessObject = /(?:\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0433\u043e\u0434|advance|prepay|shipment|receivab|payab|counterparty|contract|document|account|balance|turnover)/iu.test(lower);
|
||
if (hasBroadInterrogative && hasBroadBusinessObject) {
|
||
return true;
|
||
}
|
||
const hasRussianRetrievalAction = /(?:^|\s)(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|\u043d\u0430\u0439\u0434\u0438|\u0432\u044b\u0432\u0435\u0434\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u043a\u0440\u043e\u0439|\u0441\u043f\u0438\u0441\u043e\u043a|\u043f\u0440\u043e\u0432\u0435\u0440\u044c|\u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c)(?:$|[\s,.!?;:])/iu.test(lower);
|
||
const hasRussianRetrievalObject = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0441\u0442\u0430\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043b\u0438\u0435\u043d\u0442|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433)/iu.test(lower);
|
||
if (hasRussianRetrievalAction && hasRussianRetrievalObject) {
|
||
return true;
|
||
}
|
||
const hasExplicitRetrievalAction = /(?:\bпокажи\b|\bпоказать\b|\bвыведи\b|\bнайди\b|\bсписок\b|\bдай\b|\bраскрой\b|\bshow\b|\blist\b|\bfind\b|\bcount\b)/i.test(lower);
|
||
const hasInterrogativeRetrievalAction = /(?:\bсколько\b|\bкакой\b|\bкакая\b|\bкакое\b|\bкакую\b|\bкакие\b|\bкто\b|\bгде\b|\bпо\s+каким\b|\bпо\s+кому\b|\bу\s+кого\b|\bwhich\b|\bwho\b|\bwhere\b)/i.test(lower);
|
||
if (!hasExplicitRetrievalAction && !hasInterrogativeRetrievalAction) {
|
||
return false;
|
||
}
|
||
const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|bank|counterparty|contract|document|account|balance|ledger|posting|advance|prepay|shipment|receivab|payab|организац|компан|контор|фирм|возраст|дата\s+регистрац|регистрац|основан)/i.test(lower);
|
||
if (!hasRetrievalObject) {
|
||
return false;
|
||
}
|
||
if (hasExplicitRetrievalAction) {
|
||
return true;
|
||
}
|
||
const hasMetaCapabilityShape = /(?:мож(?:ем|ешь|ете|но)|уме(?:ешь|ете)|доступ|подключ|чья|как\s+называ(?:ет|ется)|работ(?:ать|аем|аешь|аете)|в\s+тебе|у\s+тебя)/i.test(lower);
|
||
return !hasMetaCapabilityShape;
|
||
}
|
||
function hasOrganizationFactLookupSignal(text) {
|
||
const repaired = repairAddressMojibake(String(text ?? ""));
|
||
const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
const hasFactCue = /(?:возраст|сколько\s+лет|дата\s+регистрац|когда\s+(?:зарегистр|создан|основан)|год\s+регистрац|год\s+основан|с\s+какого\s+года|when\s+was\s+(?:it\s+)?(?:registered|founded|created))/i.test(normalized);
|
||
if (!hasFactCue) {
|
||
return false;
|
||
}
|
||
return /(?:организац|компан|контор|фирм|ооо|ао|зао|ип|альтернатив|лайсвуд|райм|organization|company)/i.test(normalized);
|
||
}
|
||
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 hasOrganizationFactFollowupSignal(userMessage, items) {
|
||
const repaired = repairAddressMojibake(String(userMessage ?? ""));
|
||
const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
if (hasOrganizationFactLookupSignal(normalized)) {
|
||
return false;
|
||
}
|
||
const hasFollowupCue = /(?:^|\s)(?:давай|го|погнали|ок(?:ей)?|хорошо|принято|подтверждаю|запрашивай|запроси|проверь|продолжай|ну\s+давай|да\s+давай)(?=$|[\s,.!?;:])/iu.test(normalized);
|
||
if (!hasFollowupCue) {
|
||
return false;
|
||
}
|
||
const lastDebug = findLastAssistantLivingChatDebug(items);
|
||
const lastSource = toNonEmptyString(lastDebug?.living_chat_response_source);
|
||
const lastGuardReason = toNonEmptyString(lastDebug?.living_chat_grounding_guard_reason);
|
||
const inOrganizationFactBoundary = lastSource === "deterministic_organization_fact_boundary" ||
|
||
lastSource === "deterministic_organization_fact_boundary_followup" ||
|
||
lastGuardReason === "organization_fact_without_live_source_blocked";
|
||
return inOrganizationFactBoundary;
|
||
}
|
||
function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization) {
|
||
const selected = normalizeOrganizationScopeValue(selectedOrganization);
|
||
if (!selected) {
|
||
return false;
|
||
}
|
||
const repaired = repairAddressMojibake(String(userMessage ?? ""));
|
||
const normalized = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
if (hasOrganizationFactLookupSignal(normalized) || hasDataRetrievalRequestSignal(normalized) || hasStrongDataIntentSignal(normalized)) {
|
||
return false;
|
||
}
|
||
const hasAnalyticalCue = /(?:какой|какая|какие|когда|сколько|кто|почему|зачем|возраст|дата|регистрац|ндс|налог|контракт|договор|документ|операц|оборот|сумм|остат|сальдо|founded|registered|created)/i.test(normalized);
|
||
if (hasAnalyticalCue) {
|
||
return false;
|
||
}
|
||
const hasSelectionCue = /(?:давай|го|погнали|ок(?:ей)?|хорошо|отлично|берем|выберем|выбираем|переключ(?:им|аем|ай)|фиксир|работаем|обсудим|тогда)\b/i.test(normalized);
|
||
if (hasSelectionCue) {
|
||
return true;
|
||
}
|
||
return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? ""));
|
||
}
|
||
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 lower = compactWhitespace(String(text ?? "").toLowerCase()).replace(/ё/g, "е");
|
||
if (!lower) {
|
||
return false;
|
||
}
|
||
return /(?:убьют|убьют|убью|убить|убиют|угрож|опасн|омон|полици|насили|заставля|принужда|шантаж)/i.test(lower);
|
||
}
|
||
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) {
|
||
const repaired = repairAddressMojibake(String(text ?? ""));
|
||
const lower = compactWhitespace(repaired.toLowerCase()).replace(/ё/g, "е");
|
||
const normalized = lower.replace(/\b1\s*[cс]\b/giu, "1с");
|
||
if (!normalized) {
|
||
return false;
|
||
}
|
||
const hasDirectSlangScopeLead = /(?:по\s+каким\s+(?:контор(?:ам|ы|а)?|кантор(?:ам|ы|а)?|компан(?:иям|ии|ию|ия)|организац(?:иям|ии|ию|ия))\s+мож(?:ем|но)\s+(?:общат|работ)|база\s+какой\s+(?:контор|компан|организац|фирм)|какая\s+база\s+(?:подключ|подруб|актив))/iu.test(normalized);
|
||
if (hasDirectSlangScopeLead) {
|
||
return true;
|
||
}
|
||
const hasSlangScopeQuestion = /(?:\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440(?:\u0430\u043c|\u044b|\u0430)?|\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446(?:\u0438\u044f\u043c|\u0438\u0438|\u0438\u044e|\u0438\u044f)|\u0444\u0438\u0440\u043c(?:\u0430\u043c|\u0435|\u0443|\u0430)).*(?:\u043c\u043e\u0436(?:\u0435\u043c|\u043d\u043e)|\u0440\u0430\u0431\u043e\u0442|\u043e\u0431\u0449\u0430\u0442|\u043f\u043e\u0434\u0440\u0443\u0431|\u043f\u043e\u0434\u043a\u043b\u044e\u0447)|(?:\u0431\u0430\u0437\u0430\s+\u043a\u0430\u043a\u043e\u0439\s+(?:\u043a\u043e\u043d\u0442\u043e\u0440|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0444\u0438\u0440\u043c))|(?:\u043a\u0430\u043a\u0430\u044f\s+\u0431\u0430\u0437\u0430\s+(?:\u043f\u043e\u0434\u043a\u043b\u044e\u0447|\u0430\u043a\u0442\u0438\u0432)))/iu.test(normalized);
|
||
if (hasSlangScopeQuestion) {
|
||
return true;
|
||
}
|
||
const hasBaseOrTenantObject = /(?:баз(?:а|е|у|ы)?|тенант|tenant|контур)/i.test(normalized);
|
||
const hasCompanyObject = /(?:компан(?:ия|ии|ию|ией)|компин(?:ия|ии|ию|ией)?|компини(?:я|и|ю|ей)?|компани[яеию]|организац(?:ия|ии|ию|ией)|контор(?:а|ы|у|ой)?|фирм(?:а|ы|у|ой)?)/i.test(normalized);
|
||
const hasConnectionCue = /(?:подключен(?:а|о|ы)?|подруб|воткнут|активн(?:ый|ая)\s+канал|mcp-?канал|канал)/i.test(normalized);
|
||
const hasNamingCue = /(?:как\s+называ(?:ет|ется)|что\s+за\s+(?:контор|компан|организац|фирм))/i.test(normalized);
|
||
const hasWorkabilityCue = /(?:мож(?:ем|ешь|ете|но)\s+работ|работ(?:ать|аем|аешь|аете))/i.test(normalized);
|
||
const hasScopeObject = hasBaseOrTenantObject || hasCompanyObject || hasConnectionCue;
|
||
if (!hasScopeObject) {
|
||
return false;
|
||
}
|
||
const hasMetaPerspective = /(?:ты|тебе|твой|у\s+тебя|в\s+тебе|мы|нам|наш(?:а|е|и|у|ей)?|сейчас|щас)/i.test(normalized);
|
||
const hasScopedInterrogativePair = /(?:^|\s)(?:по\s+какой|с\s+какой|какая|какой|какие)\s+(?:баз|компан|компин|компини|компани|организац|контор|фирм|тенант|контур)/i.test(normalized);
|
||
const hasScopeQuestion = /(?:чья|чье|чьи|доступн|подключен|подруб|воткнут|какая\s+баз|какой\s+баз)/i.test(normalized) ||
|
||
hasNamingCue ||
|
||
hasWorkabilityCue ||
|
||
hasScopedInterrogativePair;
|
||
const hasInterrogativeScopeLead = /(?:^|\s)(?:по\s+какой|с\s+какой|чья|чье|чьи|which|who|what)/i.test(normalized);
|
||
const isQuestionLike = /[?]/.test(String(text ?? "")) || hasInterrogativeScopeLead || hasScopedInterrogativePair;
|
||
const hasExplicitScopeContext = hasBaseOrTenantObject || hasConnectionCue || hasWorkabilityCue || hasNamingCue;
|
||
const hasRetrievalSignal = hasDataRetrievalRequestSignal(normalized);
|
||
const hasContractAnalyticsCue = /(?:договор|контракт|contract).*(?:топ|сам(?:ый|ая|ое|ые)|крупн|жирн|оборот|бюджет|сумм|стоим|value|turnover|all\s+time|всю\s+истори|за\s+вс[её]\s+время)/iu.test(normalized);
|
||
if (hasContractAnalyticsCue) {
|
||
return false;
|
||
}
|
||
if (hasRetrievalSignal && !hasExplicitScopeContext) {
|
||
return false;
|
||
}
|
||
const hasEligibleScopeObject = hasBaseOrTenantObject || (hasCompanyObject && (hasConnectionCue || hasWorkabilityCue || hasNamingCue || hasMetaPerspective));
|
||
return hasEligibleScopeObject && hasScopeQuestion && (hasMetaPerspective || isQuestionLike || hasExplicitScopeContext);
|
||
}
|
||
function shouldHandleAsAssistantCapabilityMetaQuery(text) {
|
||
const raw = String(text ?? "");
|
||
const repaired = repairAddressMojibake(raw);
|
||
const hasScopeMetaSignal = hasAssistantDataScopeMetaQuestionSignal(raw) || hasAssistantDataScopeMetaQuestionSignal(repaired);
|
||
if (hasScopeMetaSignal) {
|
||
return true;
|
||
}
|
||
const hasCapabilitySignal = hasAssistantCapabilityQuestionSignal(raw) ||
|
||
hasAssistantCapabilityQuestionSignal(repaired) ||
|
||
hasOperationalAdminActionRequestSignal(raw) ||
|
||
hasOperationalAdminActionRequestSignal(repaired);
|
||
const hasRetrievalSignal = hasDataRetrievalRequestSignal(raw) || hasDataRetrievalRequestSignal(repaired);
|
||
return hasCapabilitySignal && !hasRetrievalSignal;
|
||
}
|
||
function hasLivingChatSignal(text) {
|
||
const lower = compactWhitespace(String(text ?? "").toLowerCase());
|
||
if (!lower) {
|
||
return false;
|
||
}
|
||
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) {
|
||
return true;
|
||
}
|
||
if (/(как дела|как ты|что нового|расскажи о себе|чем можешь помочь|давай поговорим|поговорим|обсудим|посоветуй|что думаешь)/i.test(lower)) {
|
||
return true;
|
||
}
|
||
return hasSmallTalkSignal(lower);
|
||
}
|
||
function buildAssistantCapabilityContractReply() {
|
||
return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)();
|
||
}
|
||
function normalizeScopeLabel(value) {
|
||
const repaired = repairAddressMojibake(String(value ?? ""));
|
||
let normalized = compactWhitespace(repaired.trim());
|
||
for (let index = 0; index < 2; index += 1) {
|
||
const first = normalized[0];
|
||
const last = normalized[normalized.length - 1];
|
||
const wrappedInQuotes = (first === "\"" && last === "\"") ||
|
||
(first === "'" && last === "'") ||
|
||
(first === "«" && last === "»");
|
||
if (!wrappedInQuotes) {
|
||
break;
|
||
}
|
||
normalized = compactWhitespace(normalized.slice(1, -1).trim());
|
||
}
|
||
if (!normalized) {
|
||
return null;
|
||
}
|
||
if (/^(?:null|undefined|nan|0|не\s*заполнено)$/i.test(normalized)) {
|
||
return null;
|
||
}
|
||
return normalized;
|
||
}
|
||
function normalizeScopeKey(value) {
|
||
return repairAddressMojibake(String(value ?? "")).toLowerCase().replace(/ё/g, "е");
|
||
}
|
||
const ORGANIZATION_SCOPE_STOPWORDS = new Set([
|
||
"ооо",
|
||
"ao",
|
||
"ао",
|
||
"зао",
|
||
"ип",
|
||
"llc",
|
||
"ltd",
|
||
"company",
|
||
"компания",
|
||
"организация",
|
||
"организации",
|
||
"контора",
|
||
"конторы",
|
||
"фирма",
|
||
"фирмы",
|
||
"по",
|
||
"для",
|
||
"над",
|
||
"под",
|
||
"без",
|
||
"с",
|
||
"со",
|
||
"в",
|
||
"во",
|
||
"на",
|
||
"и",
|
||
"или",
|
||
"а",
|
||
"но",
|
||
"не",
|
||
"мы",
|
||
"нам",
|
||
"наш",
|
||
"наша",
|
||
"наше",
|
||
"наши",
|
||
"ты",
|
||
"тебе",
|
||
"твой",
|
||
"сейчас",
|
||
"щас",
|
||
"тут",
|
||
"вот",
|
||
"давай",
|
||
"го",
|
||
"погнали",
|
||
"тогда",
|
||
"обсудим",
|
||
"обсуждать",
|
||
"работать",
|
||
"работаем",
|
||
"работаешь",
|
||
"работаете",
|
||
"можем",
|
||
"можно",
|
||
"какая",
|
||
"какой",
|
||
"какие",
|
||
"чья",
|
||
"чье",
|
||
"чьи"
|
||
]);
|
||
function normalizeOrganizationScopeValue(value) {
|
||
const normalized = normalizeScopeLabel(value);
|
||
if (!normalized) {
|
||
return null;
|
||
}
|
||
const unwrapped = normalized
|
||
.replace(/^\\+|\\+$/g, "")
|
||
.replace(/^"+|"+$/g, "")
|
||
.replace(/^'+|'+$/g, "")
|
||
.trim();
|
||
return unwrapped ? unwrapped : null;
|
||
}
|
||
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 extractKnownOrganizationsFromHistory(items) {
|
||
const collected = [];
|
||
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
|
||
: [];
|
||
if (directFromProbe.length > 0 || knownFromDebug.length > 0) {
|
||
collected.push(...directFromProbe, ...knownFromDebug);
|
||
}
|
||
}
|
||
const parsedFromText = parseOrganizationsFromDataScopeAssistantText(item.text);
|
||
if (parsedFromText.length > 0) {
|
||
collected.push(...parsedFromText);
|
||
}
|
||
if (collected.length >= 20) {
|
||
break;
|
||
}
|
||
}
|
||
return mergeKnownOrganizations(collected);
|
||
}
|
||
function findLastAssistantActiveOrganization(items) {
|
||
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) {
|
||
return (0, assistantOrganizationScopeRuntimeAdapter_1.resolveSessionOrganizationScopeContextRuntime)({
|
||
userMessage,
|
||
items,
|
||
extractKnownOrganizationsFromHistory,
|
||
resolveOrganizationSelectionFromMessage,
|
||
findLastAssistantActiveOrganization,
|
||
normalizeOrganizationScopeValue
|
||
});
|
||
}
|
||
function mergeFollowupContextWithOrganizationScope(followupContext, organization) {
|
||
return (0, assistantOrganizationScopeRuntimeAdapter_1.mergeFollowupContextWithOrganizationScopeRuntime)({
|
||
followupContext,
|
||
organization,
|
||
normalizeOrganizationScopeValue,
|
||
toNonEmptyString
|
||
});
|
||
}
|
||
function resolveSessionOrganizationScopeContextForTests(userMessage, items) {
|
||
return resolveSessionOrganizationScopeContext(userMessage, items);
|
||
}
|
||
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А-Яа-яЁё]/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 extractOrganizationFactsFromRows(rows);
|
||
}
|
||
function resolveOrganizationNamesByRefsForTests(refs, facts) {
|
||
return resolveOrganizationNamesByRefs(refs, facts);
|
||
}
|
||
async function 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 Организации.Наименование КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||
"ВЫБРАТЬ ПЕРВЫЕ 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;
|
||
}
|
||
function buildAssistantDataScopeContractReply(scopeProbe = null) {
|
||
const channel = String(scopeProbe?.channel ?? config_1.ASSISTANT_MCP_CHANNEL ?? "default");
|
||
const organizations = Array.isArray(scopeProbe?.organizations) ? scopeProbe.organizations : [];
|
||
if (organizations.length === 1) {
|
||
return [
|
||
`Сейчас в активном MCP-канале \`${channel}\` доступна организация: ${organizations[0]}.`,
|
||
"Работаю в read-only режиме. Могу сразу показать по этой организации документы, операции, договоры или остатки."
|
||
].join(" ");
|
||
}
|
||
if (organizations.length > 1) {
|
||
const preview = organizations.slice(0, 10).join(", ");
|
||
return [
|
||
`Сейчас в активном MCP-канале \`${channel}\` доступны организации (${organizations.length}): ${preview}.`,
|
||
"Работаю в read-only режиме. Скажи, по какой организации смотреть документы/операции."
|
||
].join(" ");
|
||
}
|
||
if (scopeProbe?.status === "unresolved_with_error" && scopeProbe?.error) {
|
||
return [
|
||
`Не смог прочитать название организации из live MCP-канала \`${channel}\`: ${scopeProbe.error}.`,
|
||
"Работаю в read-only режиме и вижу только данные активного контура. Проверь подключение MCP/1С, после этого сразу назову контору."
|
||
].join(" ");
|
||
}
|
||
return [
|
||
`Работаю в read-only режиме и вижу только те данные, которые отдает текущий MCP-канал \`${channel}\`.`,
|
||
"Словарь компаний не зашит в код: рабочий контур определяется live-подключением.",
|
||
"Если подключено несколько баз, для автосписка нужен MCP-метод метаданных (перечень баз/организаций); без него можно анализировать только активный контур запросов."
|
||
].join(" ");
|
||
}
|
||
function buildAssistantDataScopeSelectionReply(organization) {
|
||
const selected = normalizeOrganizationScopeValue(organization) ?? String(organization ?? "").trim();
|
||
return [
|
||
`Отлично, фиксирую рабочую организацию: ${selected}.`,
|
||
"Дальше буду держать этот контур как активный, пока вы не переключите организацию."
|
||
].join(" ");
|
||
}
|
||
function buildAssistantOrganizationFactBoundaryReply(organization) {
|
||
const selected = normalizeOrganizationScopeValue(organization) ?? String(organization ?? "").trim();
|
||
if (selected) {
|
||
return [
|
||
`По организации ${selected} не буду называть дату/возраст без live-подтвержденного источника.`,
|
||
"Если нужно, запрошу факт из 1С и верну только подтвержденный ответ."
|
||
].join(" ");
|
||
}
|
||
return [
|
||
"Не буду называть дату/возраст организации без live-подтвержденного источника.",
|
||
"Сначала получу факт из 1С, потом дам точный ответ."
|
||
].join(" ");
|
||
}
|
||
function buildAssistantOperationalBoundaryReply() {
|
||
return [
|
||
"Понимаю, что ситуация срочная.",
|
||
"Я не могу сам настраивать 1С или менять базу/конфигурацию.",
|
||
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/ИТ-админа."
|
||
].join(" ");
|
||
}
|
||
function buildAssistantSafetyRefusalReply() {
|
||
return [
|
||
"Я не могу помогать с удалением базы или скрытием данных.",
|
||
"Если вам угрожает опасность, срочно звоните 112 и следуйте указаниям экстренных служб.",
|
||
"По 1С могу дать только безопасные диагностические рекомендации."
|
||
].join(" ");
|
||
}
|
||
function containsCjkChars(text) {
|
||
const source = String(text ?? "");
|
||
if (!source) {
|
||
return false;
|
||
}
|
||
return /[\u3400-\u9FFF\uF900-\uFAFF]/u.test(source);
|
||
}
|
||
function containsLetterLikeChars(text) {
|
||
const source = String(text ?? "");
|
||
if (!source) {
|
||
return false;
|
||
}
|
||
return /[A-Za-z\u0400-\u04FF]/u.test(source);
|
||
}
|
||
function applyLivingChatScriptGuard(chatText, userMessage) {
|
||
const source = String(chatText ?? "").trim();
|
||
if (!source) {
|
||
return {
|
||
text: "",
|
||
applied: false,
|
||
reason: null
|
||
};
|
||
}
|
||
if (!containsCjkChars(source) || containsCjkChars(userMessage)) {
|
||
return {
|
||
text: source,
|
||
applied: false,
|
||
reason: null
|
||
};
|
||
}
|
||
const stripped = source
|
||
.replace(/[\u3400-\u9FFF\uF900-\uFAFF]+/gu, "")
|
||
.replace(/[,。!?;:]/gu, "")
|
||
.replace(/\s{2,}/g, " ")
|
||
.replace(/\s+([,.!?;:])/g, "$1")
|
||
.trim();
|
||
if (stripped && containsLetterLikeChars(stripped)) {
|
||
return {
|
||
text: stripped,
|
||
applied: true,
|
||
reason: "unexpected_cjk_fragment_stripped"
|
||
};
|
||
}
|
||
return {
|
||
text: "Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.",
|
||
applied: true,
|
||
reason: "unexpected_cjk_fragment_fallback"
|
||
};
|
||
}
|
||
function applyLivingChatGroundingGuard(input) {
|
||
const userMessage = String(input?.userMessage ?? "");
|
||
const chatText = String(input?.chatText ?? "").trim();
|
||
const organization = toNonEmptyString(input?.organization);
|
||
if (!chatText) {
|
||
return {
|
||
text: chatText,
|
||
applied: false,
|
||
reason: null
|
||
};
|
||
}
|
||
if (!hasOrganizationFactLookupSignal(userMessage)) {
|
||
return {
|
||
text: chatText,
|
||
applied: false,
|
||
reason: null
|
||
};
|
||
}
|
||
if (/(?:не\s+могу|не\s+вижу|после\s+проверки|live|подтвержден)/i.test(chatText)) {
|
||
return {
|
||
text: chatText,
|
||
applied: false,
|
||
reason: null
|
||
};
|
||
}
|
||
const hasSpecificUnverifiedFact = /(?:\b\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})\b|\b(?:19|20)\d{2}\b|\b\d+\s+лет\b)/i.test(chatText);
|
||
if (!hasSpecificUnverifiedFact) {
|
||
return {
|
||
text: chatText,
|
||
applied: false,
|
||
reason: null
|
||
};
|
||
}
|
||
return {
|
||
text: buildAssistantOrganizationFactBoundaryReply(organization),
|
||
applied: true,
|
||
reason: "organization_fact_without_live_source_blocked"
|
||
};
|
||
}
|
||
function resolveLivingAssistantModeDecision(input) {
|
||
const userMessage = String(input?.userMessage ?? "");
|
||
if (input?.addressLaneTriggered) {
|
||
return {
|
||
mode: "address_data",
|
||
reason: "address_lane_triggered"
|
||
};
|
||
}
|
||
if (!config_1.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1) {
|
||
return {
|
||
mode: "deep_analysis",
|
||
reason: "living_chat_router_disabled"
|
||
};
|
||
}
|
||
if (Boolean(input?.useMock)) {
|
||
return {
|
||
mode: "deep_analysis",
|
||
reason: "mock_mode_keeps_deep_pipeline"
|
||
};
|
||
}
|
||
if (hasAssistantDataScopeMetaQuestionSignal(userMessage)) {
|
||
return {
|
||
mode: "chat",
|
||
reason: "assistant_data_scope_query_detected"
|
||
};
|
||
}
|
||
if (shouldHandleAsAssistantCapabilityMetaQuery(userMessage)) {
|
||
return {
|
||
mode: "chat",
|
||
reason: "assistant_capability_query_detected"
|
||
};
|
||
}
|
||
if (hasOrganizationFactLookupSignal(userMessage) || hasOrganizationFactFollowupSignal(userMessage)) {
|
||
return {
|
||
mode: "chat",
|
||
reason: "organization_fact_lookup_signal_detected"
|
||
};
|
||
}
|
||
if (hasStrongDataIntentSignal(userMessage)) {
|
||
return {
|
||
mode: "deep_analysis",
|
||
reason: "strong_data_signal_detected"
|
||
};
|
||
}
|
||
if (hasLivingChatSignal(userMessage)) {
|
||
return {
|
||
mode: "chat",
|
||
reason: "living_chat_signal_detected"
|
||
};
|
||
}
|
||
const predecomposeMode = toNonEmptyString(input?.predecomposeMode);
|
||
const predecomposeConfidence = toNonEmptyString(input?.predecomposeModeConfidence);
|
||
if (predecomposeMode === "unsupported" && (predecomposeConfidence === "low" || predecomposeConfidence === "medium")) {
|
||
return {
|
||
mode: "deep_analysis",
|
||
reason: "predecompose_unsupported_mode_fallback_to_deep"
|
||
};
|
||
}
|
||
return {
|
||
mode: "deep_analysis",
|
||
reason: "default_deep_pipeline"
|
||
};
|
||
}
|
||
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,
|
||
resolveAssistantOrchestrationDecision,
|
||
buildAddressDialogContinuationContractV2,
|
||
mergeFollowupContextWithOrganizationScope,
|
||
isRetryableAddressLimitedResult,
|
||
mergeKnownOrganizations,
|
||
hasAssistantDataScopeMetaQuestionSignal,
|
||
shouldHandleAsAssistantCapabilityMetaQuery,
|
||
hasDestructiveDataActionSignal,
|
||
hasDangerOrCoercionSignal,
|
||
hasOperationalAdminActionRequestSignal,
|
||
hasOrganizationFactLookupSignal,
|
||
hasOrganizationFactFollowupSignal,
|
||
shouldEmitOrganizationSelectionReply,
|
||
hasAssistantCapabilityQuestionSignal,
|
||
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
||
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
|
||
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
|
||
buildAssistantSafetyRefusalReply,
|
||
buildAssistantDataScopeContractReply,
|
||
buildAssistantOrganizationFactBoundaryReply,
|
||
buildAssistantDataScopeSelectionReply,
|
||
buildAssistantOperationalBoundaryReply,
|
||
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;
|