NODEDC_1C/llm_normalizer/backend/dist/services/assistantService.js

1423 lines
64 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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"));
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 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
];
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 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);
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 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);
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 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 periodRefinementFollowup = hasPeriodLiteral(userMessage) && problemContinuityAvailable;
const shouldBind = !smallTalkSignal &&
!strongNewAnchorDetected &&
(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
}
}
};
}
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 requirementExtraction = extractRequirements(normalized.route_hint_summary, normalized.normalized, userMessage);
const executionPlan = toExecutionPlan(normalized.route_hint_summary, normalized.normalized, userMessage, requirementExtraction.byFragment);
const retrievalCalls = [];
const retrievalResultsRaw = [];
const 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 coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
const groundingCheck = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
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 companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(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: normalized.route_hint_summary,
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: normalized.route_hint_summary,
requirements: coverageEvaluation.requirements,
coverageReport: coverageEvaluation.coverage,
retrievalResults,
replyType: composition.reply_type
})
: 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: normalized.route_hint_summary,
fragments: extractFragments(normalized.normalized),
requirements_extracted: coverageEvaluation.requirements,
coverage_report: coverageEvaluation.coverage,
routes: toDebugRoutes(normalized.route_hint_summary),
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,
...(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(normalized.route_hint_summary),
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,
...(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;