3795 lines
170 KiB
JavaScript
3795 lines
170 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;
|
||
// @ts-nocheck
|
||
const nanoid_1 = __importStar(require("nanoid"));
|
||
const stage1Contracts_1 = __importStar(require("../types/stage1Contracts"));
|
||
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 questionTypeResolver_1 = __importStar(require("./questionTypeResolver"));
|
||
const companyAnchorResolver_1 = __importStar(require("./companyAnchorResolver"));
|
||
const assistantRuntimeGuards_1 = __importStar(require("./assistantRuntimeGuards"));
|
||
const assistantClaimBoundEvidence_1 = __importStar(require("./assistantClaimBoundEvidence"));
|
||
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 iconv_lite_1 = __importDefault(require("iconv-lite"));
|
||
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 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) {
|
||
const result = new Map();
|
||
for (const item of extractFragments(normalized)) {
|
||
if (!item || typeof item !== "object") {
|
||
continue;
|
||
}
|
||
const fragment = item;
|
||
const fragmentId = typeof fragment.fragment_id === "string" ? fragment.fragment_id : "";
|
||
if (!fragmentId) {
|
||
continue;
|
||
}
|
||
const text = (typeof fragment.raw_fragment_text === "string" && fragment.raw_fragment_text.trim()) ||
|
||
(typeof fragment.normalized_fragment_text === "string" && fragment.normalized_fragment_text.trim()) ||
|
||
"";
|
||
result.set(fragmentId, enrichFragmentTextWithHints(fragment, text));
|
||
}
|
||
return result;
|
||
}
|
||
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);
|
||
const byFragment = new Map();
|
||
const requirements = [];
|
||
const pushRequirement = (input) => {
|
||
const subjectTokens = extractSubjectTokens(input.requirement_text);
|
||
requirements.push({
|
||
requirement_id: input.requirement_id,
|
||
source_fragment_id: input.source_fragment_id,
|
||
requirement_text: input.requirement_text,
|
||
subject_tokens: subjectTokens,
|
||
status: input.status,
|
||
route: input.route
|
||
});
|
||
if (input.source_fragment_id) {
|
||
const current = byFragment.get(input.source_fragment_id) ?? [];
|
||
current.push(input.requirement_id);
|
||
byFragment.set(input.source_fragment_id, current);
|
||
}
|
||
};
|
||
if (!routeSummary) {
|
||
pushRequirement({
|
||
requirement_id: "R1",
|
||
source_fragment_id: null,
|
||
requirement_text: userMessage,
|
||
status: "clarification_needed",
|
||
route: null
|
||
});
|
||
return { requirements, byFragment };
|
||
}
|
||
if (routeSummary.mode === "legacy_v1") {
|
||
pushRequirement({
|
||
requirement_id: "R1",
|
||
source_fragment_id: "F1",
|
||
requirement_text: userMessage,
|
||
status: "covered",
|
||
route: routeSummary.route_hint
|
||
});
|
||
return { requirements, byFragment };
|
||
}
|
||
routeSummary.decisions.forEach((decision, index) => {
|
||
const requirementId = `R${index + 1}`;
|
||
const text = fragmentText.get(decision.fragment_id) ?? userMessage;
|
||
let status = "covered";
|
||
if (decision.route === "no_route") {
|
||
if (decision.no_route_reason === "out_of_scope") {
|
||
status = "out_of_scope";
|
||
}
|
||
else if (decision.no_route_reason === "insufficient_specificity") {
|
||
status = "clarification_needed";
|
||
}
|
||
else {
|
||
status = "uncovered";
|
||
}
|
||
}
|
||
pushRequirement({
|
||
requirement_id: requirementId,
|
||
source_fragment_id: decision.fragment_id,
|
||
requirement_text: text,
|
||
status,
|
||
route: decision.route === "no_route" ? null : decision.route
|
||
});
|
||
});
|
||
return { requirements, byFragment };
|
||
}
|
||
function toExecutionPlan(routeSummary, normalized, userMessage, requirementByFragment) {
|
||
if (!routeSummary) {
|
||
return [];
|
||
}
|
||
const fragmentText = fragmentTextById(normalized);
|
||
if (routeSummary.mode === "legacy_v1") {
|
||
return [
|
||
{
|
||
fragment_id: "F1",
|
||
requirement_ids: requirementByFragment.get("F1") ?? ["R1"],
|
||
route: routeSummary.route_hint,
|
||
should_execute: true,
|
||
fragment_text: userMessage,
|
||
no_route_reason: null,
|
||
clarification_reason: null
|
||
}
|
||
];
|
||
}
|
||
return routeSummary.decisions.map((decision) => {
|
||
const text = fragmentText.get(decision.fragment_id) ?? userMessage;
|
||
if (decision.route === "no_route") {
|
||
return {
|
||
fragment_id: decision.fragment_id,
|
||
requirement_ids: requirementByFragment.get(decision.fragment_id) ?? [],
|
||
route: "no_route",
|
||
should_execute: false,
|
||
fragment_text: text,
|
||
no_route_reason: decision.no_route_reason ?? null,
|
||
clarification_reason: decision.clarification_reason ?? null
|
||
};
|
||
}
|
||
return {
|
||
fragment_id: decision.fragment_id,
|
||
requirement_ids: requirementByFragment.get(decision.fragment_id) ?? [],
|
||
route: decision.route,
|
||
should_execute: true,
|
||
fragment_text: text,
|
||
no_route_reason: null,
|
||
clarification_reason: decision.clarification_reason ?? null
|
||
};
|
||
});
|
||
}
|
||
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) {
|
||
if (!routeSummary) {
|
||
return [];
|
||
}
|
||
if (routeSummary.mode === "legacy_v1") {
|
||
return [
|
||
{
|
||
fragment_id: "F1",
|
||
route: routeSummary.route_hint,
|
||
reason: retrievalSummaryForRoute(routeSummary.route_hint),
|
||
confidence: routeSummary.confidence,
|
||
intent_class: routeSummary.intent_class
|
||
}
|
||
];
|
||
}
|
||
return routeSummary.decisions.map((decision) => ({
|
||
fragment_id: decision.fragment_id,
|
||
route: decision.route,
|
||
reason: decision.reason,
|
||
route_status: decision.route_status ?? null,
|
||
no_route_reason: decision.no_route_reason ?? null,
|
||
clarification_reason: decision.clarification_reason ?? null,
|
||
execution_readiness: decision.execution_readiness ?? null
|
||
}));
|
||
}
|
||
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 summarizeUnique(values, limit = 6) {
|
||
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
|
||
}
|
||
const SUBJECT_TOKEN_RULES = {
|
||
nds: {
|
||
critical: true,
|
||
patterns: [
|
||
"vat",
|
||
"accumulationregister",
|
||
"\u043d\u0434\u0441",
|
||
"\u043a\u043d\u0438\u0433\u0438\u043f\u043e\u043a\u0443\u043f\u043e\u043a",
|
||
"\u043a\u043d\u0438\u0433\u0438\u043f\u0440\u043e\u0434\u0430\u0436",
|
||
"\u043d\u0430\u043b\u043e\u0433\u043d\u0430\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043d\u0443\u044e\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c"
|
||
]
|
||
},
|
||
os: {
|
||
critical: true,
|
||
patterns: ["fixed_asset", "fixedasset", "\u043e\u0441\u043d\u043e\u0432\u043d", "\u0430\u043c\u043e\u0440\u0442\u0438\u0437"]
|
||
},
|
||
saldo: {
|
||
critical: true,
|
||
patterns: ["balance", "saldo", "\u0441\u0430\u043b\u044c\u0434\u043e", "\u043e\u0441\u0442\u0430\u0442"]
|
||
},
|
||
counterparty: {
|
||
critical: false,
|
||
patterns: [
|
||
"counterparty",
|
||
"supplier",
|
||
"buyer",
|
||
"counterparty_id",
|
||
"journal_counterparty",
|
||
"document_has_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"
|
||
],
|
||
routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical"]
|
||
},
|
||
document: {
|
||
critical: false,
|
||
patterns: [
|
||
"document",
|
||
"recorder",
|
||
"journal",
|
||
"document_refs_count",
|
||
"recorded_by_document",
|
||
"journal_refers_to_document",
|
||
"\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442"
|
||
],
|
||
routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical", "live_mcp_drilldown"]
|
||
},
|
||
anomaly: {
|
||
critical: false,
|
||
patterns: [
|
||
"risk",
|
||
"risk_score",
|
||
"unknown_link_count",
|
||
"zero_guid",
|
||
"navigation_links",
|
||
"missing_counterparty_link",
|
||
"\u0430\u043d\u043e\u043c\u0430\u043b",
|
||
"\u0440\u0438\u0441\u043a"
|
||
],
|
||
routes: ["store_feature_risk", "batch_refresh_then_store"]
|
||
},
|
||
chain: {
|
||
critical: false,
|
||
patterns: ["chain", "cross_entity_chain", "relation_types", "operations_count", "matched_counterparties", "\u0446\u0435\u043f\u043e\u0447"],
|
||
routes: ["hybrid_store_plus_live"]
|
||
}
|
||
};
|
||
function hasRegexMatch(corpus, pattern) {
|
||
try {
|
||
return pattern.test(corpus);
|
||
}
|
||
catch {
|
||
return false;
|
||
}
|
||
}
|
||
function evaluateSubjectTokenMatch(token, corpus, executedRoutes) {
|
||
if (token.startsWith("account_")) {
|
||
const account = token.slice("account_".length).trim();
|
||
if (!account) {
|
||
return { matched: false, critical: true };
|
||
}
|
||
const escaped = account.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
const accountPattern = new RegExp(`(^|[^0-9])${escaped}([^0-9]|$)`, "i");
|
||
return { matched: hasRegexMatch(corpus, accountPattern), critical: true };
|
||
}
|
||
const rule = SUBJECT_TOKEN_RULES[token];
|
||
if (rule) {
|
||
const byPattern = rule.patterns.some((pattern) => corpus.includes(pattern));
|
||
const byRoute = Array.isArray(rule.routes) ? rule.routes.some((route) => executedRoutes.has(route)) : false;
|
||
return { matched: byPattern || byRoute, critical: rule.critical };
|
||
}
|
||
return { matched: corpus.includes(token), critical: false };
|
||
}
|
||
function evidenceCountForRequirement(requirementId, result) {
|
||
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
|
||
if (evidence.length === 0) {
|
||
return 0;
|
||
}
|
||
const tagged = evidence.filter((item) => {
|
||
const claimRef = typeof item?.claim_ref === "string" ? item.claim_ref : "";
|
||
return claimRef.toLowerCase() === `requirement:${String(requirementId).toLowerCase()}`;
|
||
}).length;
|
||
if (tagged > 0) {
|
||
return tagged;
|
||
}
|
||
if (Array.isArray(result.requirement_ids) &&
|
||
result.requirement_ids.length === 1 &&
|
||
result.requirement_ids[0] === requirementId) {
|
||
return evidence.length;
|
||
}
|
||
return 0;
|
||
}
|
||
function hasSubstantiveCoverageForRequirement(requirementId, result) {
|
||
const evidenceCount = evidenceCountForRequirement(requirementId, result);
|
||
if (evidenceCount > 0) {
|
||
return true;
|
||
}
|
||
const problemUnitsCount = Array.isArray(result.problem_units) ? result.problem_units.length : 0;
|
||
const candidateEvidenceCount = Array.isArray(result.candidate_evidence) ? result.candidate_evidence.length : 0;
|
||
if (problemUnitsCount > 0 || candidateEvidenceCount > 0) {
|
||
if (Array.isArray(result.requirement_ids) &&
|
||
result.requirement_ids.length === 1 &&
|
||
result.requirement_ids[0] === requirementId) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
function evaluateCoverage(requirements, retrievalResults) {
|
||
const statusByRequirement = new Map();
|
||
for (const result of retrievalResults) {
|
||
for (const requirementId of result.requirement_ids) {
|
||
const list = statusByRequirement.get(requirementId) ?? [];
|
||
list.push({
|
||
status: result.status,
|
||
substantive: hasSubstantiveCoverageForRequirement(requirementId, result)
|
||
});
|
||
statusByRequirement.set(requirementId, list);
|
||
}
|
||
}
|
||
const resolvedRequirements = requirements.map((requirement) => {
|
||
if (requirement.status === "out_of_scope" || requirement.status === "clarification_needed") {
|
||
return requirement;
|
||
}
|
||
const states = statusByRequirement.get(requirement.requirement_id) ?? [];
|
||
if (states.length === 0) {
|
||
return { ...requirement, status: "uncovered" };
|
||
}
|
||
const hasAnySubstantive = states.some((item) => item.substantive);
|
||
if (!hasAnySubstantive) {
|
||
return { ...requirement, status: "uncovered" };
|
||
}
|
||
const hasOk = states.some((item) => item.status === "ok");
|
||
const hasPartial = states.some((item) => item.status === "partial");
|
||
const hasEmpty = states.some((item) => item.status === "empty");
|
||
const hasError = states.some((item) => item.status === "error");
|
||
const hasWeakOk = states.some((item) => item.status === "ok" && !item.substantive);
|
||
const hasSubstantiveOk = states.some((item) => item.status === "ok" && item.substantive);
|
||
const hasSubstantivePartial = states.some((item) => item.status === "partial" && item.substantive);
|
||
if (hasSubstantiveOk && !hasSubstantivePartial && !hasWeakOk && !hasEmpty && !hasError) {
|
||
return { ...requirement, status: "covered" };
|
||
}
|
||
if (hasSubstantiveOk || hasSubstantivePartial || hasOk || hasPartial) {
|
||
return { ...requirement, status: "partially_covered" };
|
||
}
|
||
return { ...requirement, status: "uncovered" };
|
||
});
|
||
const requirementsCovered = resolvedRequirements.filter((item) => item.status === "covered").length;
|
||
const requirementsUncovered = resolvedRequirements
|
||
.filter((item) => item.status === "uncovered")
|
||
.map((item) => item.requirement_id);
|
||
const requirementsPartiallyCovered = resolvedRequirements
|
||
.filter((item) => item.status === "partially_covered")
|
||
.map((item) => item.requirement_id);
|
||
const clarificationNeededFor = resolvedRequirements
|
||
.filter((item) => item.status === "clarification_needed")
|
||
.map((item) => item.requirement_id);
|
||
const outOfScopeRequirements = resolvedRequirements
|
||
.filter((item) => item.status === "out_of_scope")
|
||
.map((item) => item.requirement_id);
|
||
return {
|
||
requirements: resolvedRequirements,
|
||
coverage: {
|
||
requirements_total: resolvedRequirements.length,
|
||
requirements_covered: requirementsCovered,
|
||
requirements_uncovered: requirementsUncovered,
|
||
requirements_partially_covered: requirementsPartiallyCovered,
|
||
clarification_needed_for: clarificationNeededFor,
|
||
out_of_scope_requirements: outOfScopeRequirements
|
||
}
|
||
};
|
||
}
|
||
function evaluateCoverageForTests(requirements, retrievalResults) {
|
||
return evaluateCoverage(requirements, retrievalResults);
|
||
}
|
||
function extractSubjectTokensForTests(text) {
|
||
return extractSubjectTokens(text);
|
||
}
|
||
function checkGrounding(userMessage, requirements, coverage, retrievalResults) {
|
||
const whyIncludedSummary = summarizeUnique(retrievalResults.flatMap((item) => item.why_included));
|
||
const selectionReasonSummary = summarizeUnique(retrievalResults.flatMap((item) => item.selection_reason));
|
||
const hasMaterialResults = retrievalResults.some((item) => item.status === "ok" || item.status === "partial");
|
||
const subjectTokens = extractSubjectTokens(userMessage);
|
||
const executedRoutes = new Set(retrievalResults
|
||
.filter((item) => item.status !== "error")
|
||
.map((item) => item.route)
|
||
.filter(Boolean));
|
||
const retrievalCorpus = JSON.stringify(retrievalResults.map((item) => ({
|
||
route: item.route,
|
||
result_type: item.result_type,
|
||
summary: item.summary,
|
||
items: item.items,
|
||
evidence: item.evidence,
|
||
why_included: item.why_included,
|
||
selection_reason: item.selection_reason,
|
||
risk_factors: item.risk_factors,
|
||
business_interpretation: item.business_interpretation
|
||
}))).toLowerCase();
|
||
const missingSubjectTokens = [];
|
||
const missingCriticalTokens = [];
|
||
for (const token of subjectTokens) {
|
||
const match = evaluateSubjectTokenMatch(token, retrievalCorpus, executedRoutes);
|
||
if (!match.matched) {
|
||
missingSubjectTokens.push(token);
|
||
if (match.critical) {
|
||
missingCriticalTokens.push(token);
|
||
}
|
||
}
|
||
}
|
||
const onlyAccountCriticalMissing = missingCriticalTokens.length > 0 && missingCriticalTokens.every((token) => token.startsWith("account_"));
|
||
const accountOnlyMismatchRecoverable = hasMaterialResults &&
|
||
coverage.requirements_covered > 0 &&
|
||
onlyAccountCriticalMissing &&
|
||
(whyIncludedSummary.length > 0 || selectionReasonSummary.length > 0);
|
||
const routeSubjectMatch = !hasMaterialResults || missingCriticalTokens.length === 0 || accountOnlyMismatchRecoverable;
|
||
let status = "grounded";
|
||
const reasons = [];
|
||
if (!routeSubjectMatch) {
|
||
status = "route_mismatch_blocked";
|
||
reasons.push(`Не подтверждены критичные предметные токены запроса: ${missingCriticalTokens.join(", ")}`);
|
||
}
|
||
else if (accountOnlyMismatchRecoverable) {
|
||
status = "partial";
|
||
reasons.push(`Счет-токены не подтверждены напрямую (${missingCriticalTokens.join(", ")}), но есть релевантная опора для ограниченного вывода.`);
|
||
}
|
||
else if (coverage.requirements_covered === 0) {
|
||
status = "no_grounded_answer";
|
||
reasons.push("Ни одно требование не получило подтвержденного покрытия.");
|
||
}
|
||
else if (coverage.requirements_uncovered.length > 0 ||
|
||
coverage.requirements_partially_covered.length > 0 ||
|
||
coverage.clarification_needed_for.length > 0 ||
|
||
coverage.out_of_scope_requirements.length > 0) {
|
||
status = "partial";
|
||
reasons.push("Вопрос покрыт частично: есть непокрытые или требующие уточнения требования.");
|
||
}
|
||
if (whyIncludedSummary.length === 0) {
|
||
reasons.push("Нет explainable-сигналов why_included в результатах выборки.");
|
||
}
|
||
if (missingSubjectTokens.length > 0 && missingCriticalTokens.length === 0) {
|
||
reasons.push(`Часть контекстных токенов не подтверждена напрямую: ${missingSubjectTokens.join(", ")}`);
|
||
}
|
||
const missingRequirements = [
|
||
...coverage.requirements_uncovered,
|
||
...coverage.requirements_partially_covered,
|
||
...coverage.clarification_needed_for,
|
||
...coverage.out_of_scope_requirements
|
||
];
|
||
return {
|
||
status,
|
||
route_subject_match: routeSubjectMatch,
|
||
missing_requirements: missingRequirements,
|
||
reasons,
|
||
why_included_summary: whyIncludedSummary,
|
||
selection_reason_summary: selectionReasonSummary
|
||
};
|
||
}
|
||
function firstNonEmptyLine(text) {
|
||
const line = text
|
||
.split("\n")
|
||
.map((item) => item.trim())
|
||
.find((item) => item.length > 0);
|
||
return (line ?? text).slice(0, 220);
|
||
}
|
||
function buildClaimEvidenceLinks(retrievalResults) {
|
||
const byClaim = new Map();
|
||
for (const result of retrievalResults) {
|
||
for (const evidence of result.evidence) {
|
||
const claimRef = String(evidence.claim_ref ?? "").trim();
|
||
if (!claimRef) {
|
||
continue;
|
||
}
|
||
const evidenceId = String(evidence.evidence_id ?? "").trim();
|
||
if (!evidenceId) {
|
||
continue;
|
||
}
|
||
const current = byClaim.get(claimRef) ?? [];
|
||
current.push(evidenceId);
|
||
byClaim.set(claimRef, current);
|
||
}
|
||
}
|
||
return Array.from(byClaim.entries())
|
||
.slice(0, 10)
|
||
.map(([claimRef, evidenceIds]) => ({
|
||
claim_ref: claimRef,
|
||
evidence_ids: summarizeUnique(evidenceIds, 10)
|
||
}));
|
||
}
|
||
function buildAnswerStructureV11(input) {
|
||
const evidenceIds = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence.map((evidence) => evidence.evidence_id)), 10);
|
||
const mechanismNotes = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence
|
||
.map((evidence) => evidence.mechanism_note)
|
||
.filter((note) => typeof note === "string" && note.trim().length > 0)), 6);
|
||
const sourceRefs = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence
|
||
.map((evidence) => evidence.source_ref?.canonical_ref)
|
||
.filter((value) => typeof value === "string" && value.trim().length > 0)), 8);
|
||
const limitationReasonCodes = summarizeUnique(input.retrievalResults.flatMap((item) => item.evidence
|
||
.flatMap((evidence) => {
|
||
const code = evidence.limitation?.reason_code;
|
||
return typeof code === "string" && code.trim().length > 0 ? [code] : [];
|
||
})), 8);
|
||
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
|
||
const limitations = summarizeUnique([...input.retrievalResults.flatMap((item) => item.limitations), ...input.groundingCheck.reasons], 8);
|
||
const clarificationQuestions = input.coverageReport.clarification_needed_for.map((item) => `Уточните требование ${item}.`);
|
||
const recommendedActions = summarizeUnique([
|
||
...input.coverageReport.requirements_uncovered.map((item) => `Проверить непокрытое требование ${item}.`),
|
||
...input.coverageReport.requirements_partially_covered.map((item) => `Доуточнить частично покрытое требование ${item}.`)
|
||
], 6);
|
||
const mechanismStatus = mechanismNotes.length === 0
|
||
? "unresolved"
|
||
: limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
|
||
? "limited"
|
||
: "grounded";
|
||
return {
|
||
schema_version: stage1Contracts_1.ANSWER_STRUCTURE_SCHEMA_VERSION,
|
||
answer_summary: firstNonEmptyLine(input.assistantReply),
|
||
direct_answer: input.assistantReply,
|
||
mechanism_block: {
|
||
status: mechanismStatus,
|
||
mechanism_notes: mechanismNotes,
|
||
limitation_reason_codes: limitationReasonCodes
|
||
},
|
||
evidence_block: {
|
||
evidence_ids: evidenceIds,
|
||
source_refs: sourceRefs,
|
||
mechanism_notes: mechanismNotes,
|
||
coverage_note: input.coverageReport.requirements_total === input.coverageReport.requirements_covered
|
||
? "coverage_full_or_near_full"
|
||
: "coverage_partial_or_limited",
|
||
...(config_1.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 && claimEvidenceLinks.length > 0
|
||
? {
|
||
claim_evidence_links: claimEvidenceLinks
|
||
}
|
||
: {})
|
||
},
|
||
uncertainty_block: {
|
||
open_uncertainties: input.groundingCheck.missing_requirements,
|
||
limitations
|
||
},
|
||
next_step_block: {
|
||
recommended_actions: recommendedActions,
|
||
clarification_questions: clarificationQuestions
|
||
}
|
||
};
|
||
}
|
||
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 = text.toLowerCase();
|
||
if (/(?:^|[\s,;:])\d{2}(?:\.\d{2})?(?=$|[\s,.;:])/i.test(lower)) {
|
||
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 /^(и|а еще|а ещё|еще|ещё|добав|уточн|продолж|также|а если|а теперь|теперь|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 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) {
|
||
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 explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
|
||
if (explicitPeriod && state.focus.period && explicitPeriod !== state.focus.period) {
|
||
const periodLooksLikeFollowupRefinement = hasFollowupMarker(userMessage) || hasReferentialPointer(userMessage);
|
||
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 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 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,
|
||
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;
|
||
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2;
|
||
}
|
||
function looksLikeMojibakeForAddress(value) {
|
||
const source = String(value ?? "");
|
||
if (!source.trim()) {
|
||
return false;
|
||
}
|
||
if (/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/.test(source)) {
|
||
return true;
|
||
}
|
||
return (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2;
|
||
}
|
||
function repairAddressMojibake(value) {
|
||
const source = String(value ?? "");
|
||
if (!looksLikeMojibakeForAddress(source)) {
|
||
return source;
|
||
}
|
||
let candidate = source;
|
||
try {
|
||
const fromWin1251 = iconv_lite_1.default.encode(candidate, "win1251").toString("utf8");
|
||
if (textMojibakeScoreForAddress(fromWin1251) > textMojibakeScoreForAddress(candidate)) {
|
||
candidate = fromWin1251;
|
||
}
|
||
}
|
||
catch (_error) { }
|
||
try {
|
||
const fromLatin1 = Buffer.from(candidate, "latin1").toString("utf8");
|
||
if (textMojibakeScoreForAddress(fromLatin1) > textMojibakeScoreForAddress(candidate)) {
|
||
candidate = fromLatin1;
|
||
}
|
||
}
|
||
catch (_error) { }
|
||
return candidate;
|
||
}
|
||
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 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 (debug.detected_mode === "address_query" || debug.prompt_version === "address_query_runtime_v1") {
|
||
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 (!(debug.detected_mode === "address_query" || debug.prompt_version === "address_query_runtime_v1")) {
|
||
continue;
|
||
}
|
||
const directFilterValue = readAddressFilterString(debug, key);
|
||
if (directFilterValue) {
|
||
return directFilterValue;
|
||
}
|
||
if (key === "contract" && String(debug.anchor_type ?? "").trim() === "contract") {
|
||
const anchorValue = toNonEmptyString(debug.anchor_value_resolved) ?? toNonEmptyString(debug.anchor_value_raw);
|
||
if (anchorValue) {
|
||
return anchorValue;
|
||
}
|
||
}
|
||
if (key === "counterparty" && String(debug.anchor_type ?? "").trim() === "counterparty") {
|
||
const anchorValue = toNonEmptyString(debug.anchor_value_resolved) ?? toNonEmptyString(debug.anchor_value_raw);
|
||
if (anchorValue) {
|
||
return anchorValue;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function hasAddressFollowupContextSignal(userMessage) {
|
||
const repaired = repairAddressMojibake(String(userMessage ?? ""));
|
||
const text = compactWhitespace(repaired.toLowerCase());
|
||
if (!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 (shortFollowup && hasFollowupMarker(text)) {
|
||
return true;
|
||
}
|
||
if (shortFollowup && /(?:^|\s)(?:также|тоже|also|same|again|ещ[её]|теперь|then|now)(?=$|[\s,.;:!?])/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) {
|
||
const hasPrimaryFollowupSignal = hasAddressFollowupContextSignal(userMessage);
|
||
const hasAlternateFollowupSignal = toNonEmptyString(alternateMessage)
|
||
? hasAddressFollowupContextSignal(alternateMessage)
|
||
: false;
|
||
if (!hasPrimaryFollowupSignal && !hasAlternateFollowupSignal) {
|
||
return null;
|
||
}
|
||
const previousAddressDebug = findLastAddressAssistantDebug(items);
|
||
if (!previousAddressDebug) {
|
||
return null;
|
||
}
|
||
const previousIntent = toNonEmptyString(previousAddressDebug.detected_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 (!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
|
||
};
|
||
}
|
||
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 ?? "");
|
||
return {
|
||
...meta,
|
||
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||
sourceMessage: String(sourceMessage ?? ""),
|
||
canonicalMessage
|
||
})
|
||
};
|
||
}
|
||
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 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);
|
||
}
|
||
const counterpartyAnchorSubstitutedByCandidate = sameIntentForAnchorSafety &&
|
||
sourceAnchorQuality.anchorType === "counterparty" &&
|
||
candidateAnchorQuality.anchorType === "counterparty" &&
|
||
sourceAnchorQuality.quality >= 2 &&
|
||
candidateAnchorQuality.quality >= 2 &&
|
||
Boolean(sourceAnchorQuality.anchorValue) &&
|
||
Boolean(candidateAnchorQuality.anchorValue) &&
|
||
hasCounterpartyAnchorSubstitution(sourceAnchorQuality.anchorValue ?? "", candidateAnchorQuality.anchorValue ?? "");
|
||
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);
|
||
}
|
||
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 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) {
|
||
const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? ""));
|
||
const modeDetection = (0, addressQueryClassifier_1.detectAddressQuestionMode)(repairedInputMessage || addressInputMessage);
|
||
const hasClassifierSignal = modeDetection.mode === "address_query";
|
||
const hasLlmCanonicalSignal = Boolean(llmPreDecomposeMeta?.llmCanonicalCandidateDetected);
|
||
const hasMessageSignal = hasClassifierSignal ||
|
||
hasLlmCanonicalSignal ||
|
||
isAddressLlmPreDecomposeCandidate(addressInputMessage) ||
|
||
isAddressLlmPreDecomposeCandidate(repairedInputMessage) ||
|
||
hasAccountingSignal(addressInputMessage) ||
|
||
hasAccountingSignal(repairedInputMessage);
|
||
if (hasMessageSignal) {
|
||
return {
|
||
runAddressLane: true,
|
||
decision: "run_address_lane",
|
||
reason: hasClassifierSignal
|
||
? "address_mode_classifier_detected"
|
||
: hasLlmCanonicalSignal
|
||
? "llm_canonical_candidate_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"
|
||
};
|
||
}
|
||
class AssistantService {
|
||
normalizerService;
|
||
sessions;
|
||
dataLayer;
|
||
sessionLogger;
|
||
addressQueryService;
|
||
constructor(normalizerService, sessions, dataLayer = new assistantDataLayer_1.AssistantDataLayer(), sessionLogger = new assistantSessionLogger_1.AssistantSessionLogger(), addressQueryService = new addressQueryService_1.AddressQueryService()) {
|
||
this.normalizerService = normalizerService;
|
||
this.sessions = sessions;
|
||
this.dataLayer = dataLayer;
|
||
this.sessionLogger = sessionLogger;
|
||
this.addressQueryService = addressQueryService;
|
||
}
|
||
getSession(sessionId) {
|
||
return this.sessions.getSession(sessionId);
|
||
}
|
||
async handleMessage(payload) {
|
||
const session = this.sessions.ensureSession(payload.session_id);
|
||
const sessionId = session.session_id;
|
||
const userMessage = String(payload.user_message ?? payload.message ?? "").trim();
|
||
const userItem = {
|
||
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||
session_id: sessionId,
|
||
role: "user",
|
||
text: userMessage,
|
||
reply_type: null,
|
||
created_at: new Date().toISOString(),
|
||
trace_id: null,
|
||
debug: null
|
||
};
|
||
this.sessions.appendItem(sessionId, userItem);
|
||
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => {
|
||
const safeAddressReply = String((0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(addressLane.reply_text) ?? "").trim() || String(addressLane.reply_text ?? "");
|
||
const debug = buildAddressDebugPayload(addressLane.debug, llmPreDecomposeMeta);
|
||
const assistantItem = {
|
||
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||
session_id: sessionId,
|
||
role: "assistant",
|
||
text: safeAddressReply,
|
||
reply_type: addressLane.reply_type,
|
||
created_at: new Date().toISOString(),
|
||
trace_id: debug.trace_id,
|
||
debug
|
||
};
|
||
this.sessions.appendItem(sessionId, assistantItem);
|
||
const current = this.sessions.getSession(sessionId);
|
||
if (current) {
|
||
this.sessionLogger.persistSession(current);
|
||
}
|
||
const conversation = cloneItems(current?.items ?? []);
|
||
(0, log_1.logJson)({
|
||
timestamp: new Date().toISOString(),
|
||
level: "info",
|
||
service: "assistant_loop",
|
||
message: "assistant_message_processed",
|
||
sessionId,
|
||
eventType: "assistant_message_address",
|
||
details: {
|
||
session_id: sessionId,
|
||
message_id: assistantItem.message_id,
|
||
user_message: userMessage,
|
||
effective_address_user_message: effectiveAddressUserMessage,
|
||
address_followup_context_applied: Boolean(carryoverMeta),
|
||
address_followup_context_previous_intent: carryoverMeta?.previousAddressIntent ?? null,
|
||
address_followup_context_previous_anchor: carryoverMeta?.previousAddressAnchor ?? null,
|
||
address_llm_predecompose_attempted: Boolean(llmPreDecomposeMeta?.attempted),
|
||
address_llm_predecompose_applied: Boolean(llmPreDecomposeMeta?.applied),
|
||
address_llm_predecompose_provider: llmPreDecomposeMeta?.provider ?? null,
|
||
address_llm_predecompose_trace_id: llmPreDecomposeMeta?.traceId ?? null,
|
||
address_llm_predecompose_reason: llmPreDecomposeMeta?.reason ?? null,
|
||
address_fallback_rule_hit: llmPreDecomposeMeta?.fallbackRuleHit ?? null,
|
||
address_sanitized_user_message: llmPreDecomposeMeta?.sanitizedUserMessage ?? null,
|
||
address_tool_gate_decision: llmPreDecomposeMeta?.toolGateDecision ?? null,
|
||
address_tool_gate_reason: llmPreDecomposeMeta?.toolGateReason ?? null,
|
||
address_llm_predecompose_contract_intent: llmPreDecomposeMeta?.predecomposeContract?.intent ?? null,
|
||
address_llm_predecompose_contract_aggregation_profile: llmPreDecomposeMeta?.predecomposeContract?.aggregation_profile ?? null,
|
||
address_llm_predecompose_contract_period_scope: llmPreDecomposeMeta?.predecomposeContract?.period?.scope ?? null,
|
||
detected_mode: addressLane.debug.detected_mode,
|
||
query_shape: addressLane.debug.query_shape,
|
||
detected_intent: addressLane.debug.detected_intent,
|
||
extracted_filters: addressLane.debug.extracted_filters,
|
||
selected_recipe: addressLane.debug.selected_recipe,
|
||
mcp_call_status_legacy: addressLane.debug.mcp_call_status_legacy,
|
||
account_scope_mode: addressLane.debug.account_scope_mode,
|
||
account_scope_fallback_applied: addressLane.debug.account_scope_fallback_applied,
|
||
anchor_type: addressLane.debug.anchor_type,
|
||
resolver_confidence: addressLane.debug.resolver_confidence,
|
||
match_failure_stage: addressLane.debug.match_failure_stage,
|
||
match_failure_reason: addressLane.debug.match_failure_reason,
|
||
mcp_call_status: addressLane.debug.mcp_call_status,
|
||
rows_fetched: addressLane.debug.rows_fetched,
|
||
raw_rows_received: addressLane.debug.raw_rows_received,
|
||
rows_after_account_scope: addressLane.debug.rows_after_account_scope,
|
||
rows_after_recipe_filter: addressLane.debug.rows_after_recipe_filter,
|
||
rows_materialized: addressLane.debug.rows_materialized,
|
||
rows_matched: addressLane.debug.rows_matched,
|
||
materialization_drop_reason: addressLane.debug.materialization_drop_reason,
|
||
account_token_raw: addressLane.debug.account_token_raw,
|
||
account_token_normalized: addressLane.debug.account_token_normalized,
|
||
account_scope_fields_checked: addressLane.debug.account_scope_fields_checked,
|
||
account_scope_match_strategy: addressLane.debug.account_scope_match_strategy,
|
||
account_scope_drop_reason: addressLane.debug.account_scope_drop_reason,
|
||
runtime_readiness: addressLane.debug.runtime_readiness,
|
||
limited_reason_category: addressLane.debug.limited_reason_category,
|
||
response_type: addressLane.debug.response_type,
|
||
limitations: addressLane.debug.limitations,
|
||
assistant_reply: assistantItem.text,
|
||
reply_type: assistantItem.reply_type,
|
||
trace_id: assistantItem.trace_id
|
||
}
|
||
});
|
||
return {
|
||
ok: true,
|
||
session_id: sessionId,
|
||
assistant_reply: assistantItem.text,
|
||
reply_type: assistantItem.reply_type,
|
||
conversation_item: assistantItem,
|
||
debug,
|
||
conversation
|
||
};
|
||
};
|
||
let addressRuntimeMetaForDeep = null;
|
||
if (config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1) {
|
||
const addressPreDecompose = config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1
|
||
? await runAddressLlmPreDecompose(this.normalizerService, payload, userMessage)
|
||
: {
|
||
attempted: false,
|
||
applied: false,
|
||
provider: payload?.llmProvider === "local" ? "local" : payload?.llmProvider === "openai" ? "openai" : null,
|
||
traceId: null,
|
||
effectiveMessage: userMessage,
|
||
reason: "disabled_by_feature_flag",
|
||
llmCanonicalCandidateDetected: false,
|
||
predecomposeContract: (0, predecomposeContract_1.buildAddressLlmPredecomposeContractV1)({
|
||
sourceMessage: userMessage,
|
||
canonicalMessage: userMessage
|
||
}),
|
||
fallbackRuleHit: null,
|
||
sanitizedUserMessage: sanitizeAddressMessageForFallback(userMessage),
|
||
toolGateDecision: null,
|
||
toolGateReason: null
|
||
};
|
||
const addressInputMessage = toNonEmptyString(addressPreDecompose?.effectiveMessage) ?? userMessage;
|
||
const carryover = resolveAddressFollowupCarryoverContext(userMessage, session.items, addressInputMessage);
|
||
const toolGate = resolveAddressToolGateDecision(addressInputMessage, carryover?.followupContext ?? null, addressPreDecompose);
|
||
const addressRuntimeMeta = {
|
||
...addressPreDecompose,
|
||
toolGateDecision: toolGate.decision,
|
||
toolGateReason: toolGate.reason
|
||
};
|
||
addressRuntimeMetaForDeep = addressRuntimeMeta;
|
||
if (!toolGate.runAddressLane) {
|
||
(0, log_1.logJson)({
|
||
timestamp: new Date().toISOString(),
|
||
level: "info",
|
||
service: "assistant_loop",
|
||
message: "assistant_address_tool_gate_skip",
|
||
sessionId,
|
||
details: {
|
||
session_id: sessionId,
|
||
user_message: userMessage,
|
||
effective_address_user_message: addressInputMessage,
|
||
address_llm_predecompose_attempted: Boolean(addressRuntimeMeta?.attempted),
|
||
address_llm_predecompose_applied: Boolean(addressRuntimeMeta?.applied),
|
||
address_llm_predecompose_reason: addressRuntimeMeta?.reason ?? null,
|
||
address_fallback_rule_hit: addressRuntimeMeta?.fallbackRuleHit ?? null,
|
||
address_sanitized_user_message: addressRuntimeMeta?.sanitizedUserMessage ?? null,
|
||
address_tool_gate_decision: addressRuntimeMeta?.toolGateDecision ?? null,
|
||
address_tool_gate_reason: addressRuntimeMeta?.toolGateReason ?? null,
|
||
address_llm_predecompose_contract_intent: addressRuntimeMeta?.predecomposeContract?.intent ?? null,
|
||
address_llm_predecompose_contract_aggregation_profile: addressRuntimeMeta?.predecomposeContract?.aggregation_profile ?? null,
|
||
address_llm_predecompose_contract_period_scope: addressRuntimeMeta?.predecomposeContract?.period?.scope ?? null
|
||
}
|
||
});
|
||
}
|
||
if (toolGate.runAddressLane) {
|
||
const shouldPreferContextualLane = Boolean(carryover?.followupContext);
|
||
if (shouldPreferContextualLane) {
|
||
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, {
|
||
followupContext: carryover.followupContext
|
||
});
|
||
if (contextualAddressLane?.handled) {
|
||
return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressRuntimeMeta);
|
||
}
|
||
}
|
||
const primaryAddressLane = await this.addressQueryService.tryHandle(addressInputMessage);
|
||
if (primaryAddressLane?.handled) {
|
||
return finalizeAddressLaneResponse(primaryAddressLane, addressInputMessage, null, addressRuntimeMeta);
|
||
}
|
||
if (!shouldPreferContextualLane && carryover?.followupContext) {
|
||
const contextualAddressLane = await this.addressQueryService.tryHandle(addressInputMessage, {
|
||
followupContext: carryover.followupContext
|
||
});
|
||
if (contextualAddressLane?.handled) {
|
||
return finalizeAddressLaneResponse(contextualAddressLane, addressInputMessage, carryover, addressRuntimeMeta);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
const followupBinding = config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 &&
|
||
config_1.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 &&
|
||
session.investigation_state
|
||
? buildFollowupStateBinding({
|
||
userMessage,
|
||
payloadContext: payload.context,
|
||
investigationState: session.investigation_state
|
||
})
|
||
: {
|
||
normalizedQuestion: userMessage,
|
||
mergedContext: payload.context,
|
||
usage: null
|
||
};
|
||
const normalizePayload = {
|
||
llmProvider: payload.llmProvider,
|
||
apiKey: payload.apiKey,
|
||
model: payload.model,
|
||
baseUrl: payload.baseUrl,
|
||
temperature: payload.temperature,
|
||
maxOutputTokens: payload.maxOutputTokens,
|
||
promptVersion: payload.promptVersion ?? "address_query_runtime_v1",
|
||
systemPrompt: payload.systemPrompt,
|
||
developerPrompt: payload.developerPrompt,
|
||
domainPrompt: payload.domainPrompt,
|
||
fewShotExamples: payload.fewShotExamples,
|
||
userQuestion: followupBinding.normalizedQuestion,
|
||
context: followupBinding.mergedContext,
|
||
useMock: Boolean(payload.useMock)
|
||
};
|
||
const normalized = await this.normalizerService.normalize(normalizePayload);
|
||
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
|
||
const initialBusinessScopeResolution = resolveBusinessScopeAlignment({
|
||
userMessage,
|
||
companyAnchors,
|
||
normalized: normalized.normalized,
|
||
routeSummary: normalized.route_hint_summary
|
||
});
|
||
const inferredDomainByMessage = inferP0DomainFromMessage(userMessage);
|
||
const focusDomainForGuards = inferredDomainByMessage === "settlements_60_62" ||
|
||
inferredDomainByMessage === "vat_document_register_book" ||
|
||
inferredDomainByMessage === "month_close_costs_20_44" ||
|
||
inferredDomainByMessage === "fixed_asset_amortization"
|
||
? inferredDomainByMessage
|
||
: null;
|
||
const temporalGuard = (0, assistantRuntimeGuards_1.resolveTemporalGuard)({
|
||
userMessage,
|
||
normalized: normalized.normalized,
|
||
companyAnchors
|
||
});
|
||
const domainPolarityGuardInitial = (0, assistantRuntimeGuards_1.resolveDomainPolarityGuard)({
|
||
userMessage,
|
||
companyAnchors,
|
||
focusDomainHint: focusDomainForGuards
|
||
});
|
||
const claimAnchorAudit = (0, assistantClaimBoundEvidence_1.resolveClaimBoundAnchors)({
|
||
userMessage,
|
||
companyAnchors,
|
||
focusDomainHint: focusDomainForGuards,
|
||
primaryPeriod: temporalGuard.effective_primary_period ?? temporalGuard.primary_period_window
|
||
});
|
||
const businessScopeResolution = resolveBusinessScopeFromLiveContext({
|
||
current: initialBusinessScopeResolution,
|
||
temporalGuard,
|
||
claimType: claimAnchorAudit.claim_type,
|
||
focusDomainHint: focusDomainForGuards,
|
||
userMessage,
|
||
companyAnchors,
|
||
followupApplied: Boolean(followupBinding.usage?.applied)
|
||
});
|
||
const resolvedRouteSummary = businessScopeResolution.route_summary_resolved;
|
||
const requirementExtraction = extractRequirements(resolvedRouteSummary, normalized.normalized, userMessage);
|
||
let executionPlan = toExecutionPlan(resolvedRouteSummary, normalized.normalized, userMessage, requirementExtraction.byFragment);
|
||
const rbpRoutePlanEnforcement = enforceRbpLiveRoutePlan({
|
||
executionPlan,
|
||
claimType: claimAnchorAudit.claim_type,
|
||
temporalGuard
|
||
});
|
||
executionPlan = rbpRoutePlanEnforcement.executionPlan;
|
||
const faRoutePlanEnforcement = enforceFaLiveRoutePlan({
|
||
executionPlan,
|
||
claimType: claimAnchorAudit.claim_type,
|
||
temporalGuard
|
||
});
|
||
executionPlan = faRoutePlanEnforcement.executionPlan;
|
||
executionPlan = (0, assistantRuntimeGuards_1.applyTemporalHintToExecutionPlan)(executionPlan, temporalGuard);
|
||
executionPlan = (0, assistantRuntimeGuards_1.applyPolarityHintToExecutionPlan)(executionPlan, domainPolarityGuardInitial);
|
||
const retrievalCalls = [];
|
||
const retrievalResultsRaw = [];
|
||
let retrievalResults = [];
|
||
for (const planItem of executionPlan) {
|
||
if (!planItem.should_execute) {
|
||
retrievalCalls.push({
|
||
fragment_id: planItem.fragment_id,
|
||
requirement_ids: planItem.requirement_ids,
|
||
route: planItem.route,
|
||
status: "skipped",
|
||
query_text: planItem.fragment_text,
|
||
reason: mapNoRouteReason(planItem.no_route_reason)
|
||
});
|
||
retrievalResults.push(buildSkippedResult(planItem));
|
||
continue;
|
||
}
|
||
retrievalCalls.push({
|
||
fragment_id: planItem.fragment_id,
|
||
requirement_ids: planItem.requirement_ids,
|
||
route: planItem.route,
|
||
status: "executed",
|
||
query_text: planItem.fragment_text,
|
||
reason: null
|
||
});
|
||
try {
|
||
const raw = await this.dataLayer.executeRouteRuntime(planItem.route, planItem.fragment_text);
|
||
retrievalResultsRaw.push({
|
||
fragment_id: planItem.fragment_id,
|
||
route: planItem.route,
|
||
raw_result: raw
|
||
});
|
||
retrievalResults.push((0, retrievalResultNormalizer_1.normalizeRetrievalResult)(planItem.fragment_id, planItem.requirement_ids, planItem.route, raw));
|
||
}
|
||
catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
retrievalCalls[retrievalCalls.length - 1].status = "failed";
|
||
retrievalCalls[retrievalCalls.length - 1].reason = message;
|
||
const rawError = {
|
||
status: "error",
|
||
result_type: "summary",
|
||
items: [],
|
||
summary: {
|
||
route: planItem.route
|
||
},
|
||
evidence: [],
|
||
why_included: [],
|
||
selection_reason: [],
|
||
risk_factors: [],
|
||
business_interpretation: [],
|
||
confidence: "low",
|
||
limitations: ["Route executor failed."],
|
||
errors: [message]
|
||
};
|
||
retrievalResultsRaw.push({
|
||
fragment_id: planItem.fragment_id,
|
||
route: planItem.route,
|
||
raw_result: rawError
|
||
});
|
||
retrievalResults.push((0, retrievalResultNormalizer_1.normalizeRetrievalResult)(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError));
|
||
}
|
||
}
|
||
const polarityGuardResult = (0, assistantRuntimeGuards_1.applyDomainPolarityGuardToRetrievalResults)({
|
||
retrievalResults,
|
||
guard: domainPolarityGuardInitial
|
||
});
|
||
retrievalResults = polarityGuardResult.retrievalResults;
|
||
const targetedEvidenceResult = (0, assistantClaimBoundEvidence_1.applyTargetedEvidenceAcquisition)({
|
||
retrievalResults,
|
||
claimAudit: claimAnchorAudit
|
||
});
|
||
retrievalResults = targetedEvidenceResult.retrievalResults;
|
||
const evidenceGateResult = (0, assistantRuntimeGuards_1.applyEvidenceAdmissibilityGate)({
|
||
retrievalResults,
|
||
temporal: temporalGuard,
|
||
focusDomainHint: focusDomainForGuards,
|
||
polarity: polarityGuardResult.audit.polarity,
|
||
companyAnchors,
|
||
userMessage
|
||
});
|
||
retrievalResults = evidenceGateResult.retrievalResults;
|
||
const rbpLiveRouteAudit = collectRbpLiveRouteAudit({
|
||
claimType: claimAnchorAudit.claim_type,
|
||
retrievalResults,
|
||
planAudit: rbpRoutePlanEnforcement.audit
|
||
});
|
||
const faLiveRouteAudit = collectFaLiveRouteAudit({
|
||
claimType: claimAnchorAudit.claim_type,
|
||
retrievalResults,
|
||
planAudit: faRoutePlanEnforcement.audit
|
||
});
|
||
const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
|
||
const groundingCheckBase = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
|
||
const groundedAnswerEligibilityGuard = (0, assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility)({
|
||
temporal: temporalGuard,
|
||
polarity: polarityGuardResult.audit,
|
||
evidence: evidenceGateResult.audit,
|
||
claimAnchors: claimAnchorAudit,
|
||
targetedEvidenceHitRate: targetedEvidenceResult.audit.targeted_evidence_hit_rate,
|
||
businessScopeResolved: businessScopeResolution.business_scope_resolved
|
||
});
|
||
const groundingCheck = (0, assistantRuntimeGuards_1.applyEligibilityToGroundingCheck)(groundingCheckBase, groundedAnswerEligibilityGuard);
|
||
const focusDomainHint = followupBinding.usage?.applied
|
||
? session.investigation_state?.followup_context?.active_domain ?? session.investigation_state?.focus.domain ?? null
|
||
: null;
|
||
const questionTypeClass = (0, questionTypeResolver_1.resolveQuestionType)(userMessage);
|
||
const hasPeriodInCompanyAnchors = (Array.isArray(companyAnchors?.dates) && companyAnchors.dates.some((item) => String(item ?? "").trim().length > 0)) ||
|
||
(Array.isArray(companyAnchors?.periods) && companyAnchors.periods.some((item) => String(item ?? "").trim().length > 0));
|
||
const normalizationPeriodExplicit = hasExplicitPeriodAnchorFromNormalized(normalized.normalized) || hasPeriodInCompanyAnchors;
|
||
const composition = (0, answerComposer_1.composeAssistantAnswer)({
|
||
userMessage,
|
||
routeSummary: resolvedRouteSummary,
|
||
retrievalResults,
|
||
requirements: coverageEvaluation.requirements,
|
||
coverageReport: coverageEvaluation.coverage,
|
||
groundingCheck,
|
||
focusDomainHint,
|
||
questionTypeHint: questionTypeClass,
|
||
companyAnchors,
|
||
normalizationPeriodExplicit,
|
||
enableAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11,
|
||
enableProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1,
|
||
enableLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1
|
||
});
|
||
const safeAssistantReplyBase = (0, answerComposer_1.sanitizeAssistantReplyForUserFacing)(composition.assistant_reply);
|
||
const safeAssistantReply = String(safeAssistantReplyBase ?? "")
|
||
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
|
||
.replace(/\b(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
|
||
.trim();
|
||
const answerStructureV11 = config_1.FEATURE_ASSISTANT_CONTRACTS_V11
|
||
? config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11 && composition.answer_structure_v11
|
||
? composition.answer_structure_v11
|
||
: buildAnswerStructureV11({
|
||
assistantReply: safeAssistantReply,
|
||
coverageReport: coverageEvaluation.coverage,
|
||
groundingCheck,
|
||
retrievalResults
|
||
})
|
||
: null;
|
||
const investigationStateSnapshot = config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && session.investigation_state
|
||
? (0, investigationState_1.updateInvestigationState)({
|
||
previous: session.investigation_state,
|
||
timestamp: new Date().toISOString(),
|
||
questionId: userItem.message_id,
|
||
userMessage,
|
||
routeSummary: resolvedRouteSummary,
|
||
requirements: coverageEvaluation.requirements,
|
||
coverageReport: coverageEvaluation.coverage,
|
||
retrievalResults,
|
||
replyType: composition.reply_type,
|
||
followupApplied: Boolean(followupBinding.usage?.applied)
|
||
})
|
||
: null;
|
||
if (config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && investigationStateSnapshot) {
|
||
this.sessions.setInvestigationState(sessionId, investigationStateSnapshot);
|
||
}
|
||
const debug = {
|
||
trace_id: normalized.trace_id,
|
||
prompt_version: normalized.prompt_version,
|
||
schema_version: normalized.schema_version,
|
||
fallback_type: composition.fallback_type,
|
||
route_summary: resolvedRouteSummary,
|
||
fragments: extractFragments(normalized.normalized),
|
||
requirements_extracted: coverageEvaluation.requirements,
|
||
coverage_report: coverageEvaluation.coverage,
|
||
routes: toDebugRoutes(resolvedRouteSummary),
|
||
retrieval_status: retrievalResults.map((item) => ({
|
||
fragment_id: item.fragment_id,
|
||
requirement_ids: item.requirement_ids,
|
||
route: item.route,
|
||
status: item.status,
|
||
result_type: item.result_type
|
||
})),
|
||
retrieval_results: retrievalResults,
|
||
answer_grounding_check: groundingCheck,
|
||
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
||
question_type_class: questionTypeClass,
|
||
company_anchors: companyAnchors,
|
||
business_scope_raw: businessScopeResolution.business_scope_raw,
|
||
business_scope_resolved: businessScopeResolution.business_scope_resolved,
|
||
company_grounding_applied: businessScopeResolution.company_grounding_applied,
|
||
scope_resolution_reason: businessScopeResolution.scope_resolution_reason,
|
||
company_scope_resolution_reason: businessScopeResolution.scope_resolution_reason,
|
||
raw_time_anchor: temporalGuard.raw_time_anchor,
|
||
raw_time_scope: temporalGuard.raw_time_scope,
|
||
resolved_time_anchor: temporalGuard.resolved_time_anchor,
|
||
resolved_primary_period: temporalGuard.resolved_primary_period,
|
||
effective_primary_period: temporalGuard.effective_primary_period,
|
||
temporal_guard_input: temporalGuard.temporal_guard_input,
|
||
temporal_alignment_status: temporalGuard.temporal_alignment_status,
|
||
temporal_resolution_source: temporalGuard.temporal_resolution_source,
|
||
temporal_guard_basis: temporalGuard.temporal_guard_basis,
|
||
temporal_guard_applied: temporalGuard.temporal_guard_applied,
|
||
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
|
||
temporal_guard: temporalGuard,
|
||
raw_numeric_tokens: polarityGuardResult.audit.raw_numeric_tokens,
|
||
classified_numeric_tokens: polarityGuardResult.audit.classified_numeric_tokens,
|
||
rejected_as_non_accounts: polarityGuardResult.audit.rejected_as_non_accounts,
|
||
resolved_account_anchors: polarityGuardResult.audit.resolved_account_anchors,
|
||
domain_polarity_guard: polarityGuardResult.audit,
|
||
claim_anchor_audit: claimAnchorAudit,
|
||
settlement_role: claimAnchorAudit.settlement_role ?? null,
|
||
settlement_role_resolution_reason: claimAnchorAudit.settlement_role_resolution_reason ?? [],
|
||
polarity_resolution_status: claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
|
||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||
...(faLiveRouteAudit ? { fa_live_route_audit: faLiveRouteAudit } : {}),
|
||
eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
||
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
||
problem_answer_mode: composition.problem_answer_mode ?? "stage1_policy_v11",
|
||
...(Array.isArray(composition.problem_unit_ids_used) && composition.problem_unit_ids_used.length > 0
|
||
? {
|
||
problem_unit_ids_used: composition.problem_unit_ids_used
|
||
}
|
||
: {}),
|
||
address_llm_predecompose_attempted: Boolean(addressRuntimeMetaForDeep?.attempted),
|
||
address_llm_predecompose_applied: Boolean(addressRuntimeMetaForDeep?.applied),
|
||
address_llm_predecompose_reason: addressRuntimeMetaForDeep?.reason ?? null,
|
||
address_llm_predecompose_provider: addressRuntimeMetaForDeep?.provider ?? null,
|
||
address_fallback_rule_hit: addressRuntimeMetaForDeep?.fallbackRuleHit ?? null,
|
||
address_tool_gate_decision: addressRuntimeMetaForDeep?.toolGateDecision ?? null,
|
||
address_tool_gate_reason: addressRuntimeMetaForDeep?.toolGateReason ?? null,
|
||
address_llm_predecompose_contract: addressRuntimeMetaForDeep?.predecomposeContract ?? null,
|
||
answer_structure_v11: answerStructureV11,
|
||
investigation_state_snapshot: investigationStateSnapshot,
|
||
normalized: normalized.normalized
|
||
};
|
||
const assistantItem = {
|
||
message_id: `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||
session_id: sessionId,
|
||
role: "assistant",
|
||
text: safeAssistantReply,
|
||
reply_type: composition.reply_type,
|
||
created_at: new Date().toISOString(),
|
||
trace_id: normalized.trace_id,
|
||
debug
|
||
};
|
||
this.sessions.appendItem(sessionId, assistantItem);
|
||
const current = this.sessions.getSession(sessionId);
|
||
if (current) {
|
||
this.sessionLogger.persistSession(current);
|
||
}
|
||
const conversation = cloneItems(current?.items ?? []);
|
||
(0, log_1.logJson)({
|
||
timestamp: new Date().toISOString(),
|
||
level: "info",
|
||
service: "assistant_loop",
|
||
message: "assistant_message_processed",
|
||
sessionId,
|
||
eventType: "assistant_message",
|
||
details: {
|
||
session_id: sessionId,
|
||
message_id: assistantItem.message_id,
|
||
user_message: userMessage,
|
||
normalizer_output: normalized.normalized,
|
||
execution_plan: executionPlan,
|
||
resolved_execution_state: extractExecutionState(normalized.normalized),
|
||
routes: toDebugRoutes(resolvedRouteSummary),
|
||
retrieval_calls: retrievalCalls,
|
||
retrieval_results_raw: retrievalResultsRaw,
|
||
retrieval_results_normalized: retrievalResults,
|
||
requirements_extracted: coverageEvaluation.requirements,
|
||
requirements_total: coverageEvaluation.coverage.requirements_total,
|
||
requirements_covered: coverageEvaluation.coverage.requirements_covered,
|
||
requirements_uncovered: coverageEvaluation.coverage.requirements_uncovered,
|
||
coverage_status: coverageEvaluation.coverage.requirements_total === coverageEvaluation.coverage.requirements_covered &&
|
||
coverageEvaluation.coverage.requirements_uncovered.length === 0 &&
|
||
coverageEvaluation.coverage.requirements_partially_covered.length === 0
|
||
? "full"
|
||
: "partial_or_limited",
|
||
answer_grounding_status: groundingCheck.status,
|
||
reply_semantic_type: composition.reply_type,
|
||
why_included_summary: groundingCheck.why_included_summary,
|
||
selection_reason_summary: groundingCheck.selection_reason_summary,
|
||
route_subject_match: groundingCheck.route_subject_match,
|
||
clarification_target: coverageEvaluation.coverage.clarification_needed_for,
|
||
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
||
question_type_class: questionTypeClass,
|
||
company_anchors: companyAnchors,
|
||
business_scope_raw: businessScopeResolution.business_scope_raw,
|
||
business_scope_resolved: businessScopeResolution.business_scope_resolved,
|
||
company_grounding_applied: businessScopeResolution.company_grounding_applied,
|
||
scope_resolution_reason: businessScopeResolution.scope_resolution_reason,
|
||
company_scope_resolution_reason: businessScopeResolution.scope_resolution_reason,
|
||
raw_time_anchor: temporalGuard.raw_time_anchor,
|
||
raw_time_scope: temporalGuard.raw_time_scope,
|
||
resolved_time_anchor: temporalGuard.resolved_time_anchor,
|
||
resolved_primary_period: temporalGuard.resolved_primary_period,
|
||
effective_primary_period: temporalGuard.effective_primary_period,
|
||
temporal_guard_input: temporalGuard.temporal_guard_input,
|
||
temporal_alignment_status: temporalGuard.temporal_alignment_status,
|
||
temporal_resolution_source: temporalGuard.temporal_resolution_source,
|
||
temporal_guard_basis: temporalGuard.temporal_guard_basis,
|
||
temporal_guard_applied: temporalGuard.temporal_guard_applied,
|
||
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
|
||
temporal_guard: temporalGuard,
|
||
raw_numeric_tokens: polarityGuardResult.audit.raw_numeric_tokens,
|
||
classified_numeric_tokens: polarityGuardResult.audit.classified_numeric_tokens,
|
||
rejected_as_non_accounts: polarityGuardResult.audit.rejected_as_non_accounts,
|
||
resolved_account_anchors: polarityGuardResult.audit.resolved_account_anchors,
|
||
domain_polarity_guard: polarityGuardResult.audit,
|
||
claim_anchor_audit: claimAnchorAudit,
|
||
settlement_role: claimAnchorAudit.settlement_role ?? null,
|
||
settlement_role_resolution_reason: claimAnchorAudit.settlement_role_resolution_reason ?? [],
|
||
polarity_resolution_status: claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
|
||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||
...(faLiveRouteAudit ? { fa_live_route_audit: faLiveRouteAudit } : {}),
|
||
eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
||
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
||
problem_answer_mode: composition.problem_answer_mode ?? "stage1_policy_v11",
|
||
...(Array.isArray(composition.problem_unit_ids_used) && composition.problem_unit_ids_used.length > 0
|
||
? {
|
||
problem_unit_ids_used: composition.problem_unit_ids_used
|
||
}
|
||
: {}),
|
||
answer_structure_v11: answerStructureV11,
|
||
investigation_state_snapshot: investigationStateSnapshot,
|
||
fallback_type: composition.fallback_type,
|
||
assistant_reply: safeAssistantReply,
|
||
reply_type: composition.reply_type,
|
||
trace_id: normalized.trace_id
|
||
}
|
||
});
|
||
return {
|
||
ok: true,
|
||
session_id: sessionId,
|
||
assistant_reply: safeAssistantReply,
|
||
reply_type: composition.reply_type,
|
||
conversation_item: assistantItem,
|
||
debug,
|
||
conversation
|
||
};
|
||
}
|
||
}
|
||
exports.AssistantService = AssistantService;
|