1956 lines
90 KiB
JavaScript
1956 lines
90 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;
|
||
};
|
||
})();
|
||
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"));
|
||
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") {
|
||
return true;
|
||
}
|
||
return (focusDomainHint === "settlements_60_62" ||
|
||
focusDomainHint === "vat_document_register_book" ||
|
||
focusDomainHint === "month_close_costs_20_44" ||
|
||
focusDomainHint === "fixed_asset_amortization");
|
||
}
|
||
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);
|
||
if (!julyResolved || !p0Signal) {
|
||
return current;
|
||
}
|
||
const reasons = Array.isArray(current.scope_resolution_reason) ? [...current.scope_resolution_reason] : [];
|
||
if (!reasons.includes("temporal_claim_bound_company_scope_recovery")) {
|
||
reasons.push("temporal_claim_bound_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 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 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 spans = [...collectDateSpans(lower), ...collectAmountSpans(lower), ...collectPercentSpans(lower)];
|
||
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;
|
||
}
|
||
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 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)/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)];
|
||
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) {
|
||
const lower = String(text ?? "").toLowerCase();
|
||
const accountTokens = extractAccountTokens(lower);
|
||
const hasVatAccount = accountTokens.some((token) => /^(?:19|68)(?:\.|$)/.test(token));
|
||
const hasSettlementAccount = accountTokens.some((token) => /^(?:51|60|62|76)(?:\.|$)/.test(token));
|
||
const hasMonthCloseAccount = accountTokens.some((token) => /^(?:97|2\d|3\d|4[0-4])(?:\.|$)/.test(token));
|
||
const hasFixedAssetAccount = accountTokens.some((token) => /^(?:01|02|08)(?:\.|$)/.test(token));
|
||
const vatLexical = /(?:ндс|vat|сч[её]т[\s-]?фактур|книг[аи]\s+(?:покуп|продаж)|налогов)/i.test(lower);
|
||
const settlementLexical = /(?:долг|аванс|зач[её]т|взаимозач|расч[её]т|оплат|платеж|платёж|постав|покупател)/i.test(lower);
|
||
const monthCloseLexical = /(?:закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|финансовых\s+результат)/i.test(lower);
|
||
const fixedAssetLexical = /(?:основн(?:ые|ых)?\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|амортиз|depreciat|fixed\s*asset)/i.test(lower);
|
||
if (hasVatAccount || vatLexical) {
|
||
return "vat_document_register_book";
|
||
}
|
||
if (fixedAssetLexical || hasFixedAssetAccount) {
|
||
return "fixed_asset_amortization";
|
||
}
|
||
if (monthCloseLexical || hasMonthCloseAccount) {
|
||
return "month_close_costs_20_44";
|
||
}
|
||
if (hasSettlementAccount || settlementLexical) {
|
||
return "settlements_60_62";
|
||
}
|
||
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
|
||
}));
|
||
}
|
||
class AssistantService {
|
||
normalizerService;
|
||
sessions;
|
||
dataLayer;
|
||
sessionLogger;
|
||
constructor(normalizerService, sessions, dataLayer = new assistantDataLayer_1.AssistantDataLayer(), sessionLogger = new assistantSessionLogger_1.AssistantSessionLogger()) {
|
||
this.normalizerService = normalizerService;
|
||
this.sessions = sessions;
|
||
this.dataLayer = dataLayer;
|
||
this.sessionLogger = sessionLogger;
|
||
}
|
||
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 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 = {
|
||
apiKey: payload.apiKey,
|
||
model: payload.model,
|
||
baseUrl: payload.baseUrl,
|
||
temperature: payload.temperature,
|
||
maxOutputTokens: payload.maxOutputTokens,
|
||
promptVersion: payload.promptVersion ?? "normalizer_v2_0_2",
|
||
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 === "fixed_asset_amortization"
|
||
? "month_close_costs_20_44"
|
||
: inferredDomainByMessage === "settlements_60_62" ||
|
||
inferredDomainByMessage === "vat_document_register_book" ||
|
||
inferredDomainByMessage === "month_close_costs_20_44"
|
||
? 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
|
||
});
|
||
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;
|
||
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 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,
|
||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||
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,
|
||
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,
|
||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||
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;
|