1112 lines
52 KiB
JavaScript
1112 lines
52 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.AssistantService = void 0;
|
||
const nanoid_1 = require("nanoid");
|
||
const stage1Contracts_1 = require("../types/stage1Contracts");
|
||
const config_1 = require("../config");
|
||
const log_1 = require("../utils/log");
|
||
const answerComposer_1 = require("./answerComposer");
|
||
const assistantDataLayer_1 = require("./assistantDataLayer");
|
||
const assistantSessionLogger_1 = require("./assistantSessionLogger");
|
||
const investigationState_1 = require("./investigationState");
|
||
const retrievalResultNormalizer_1 = require("./retrievalResultNormalizer");
|
||
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 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 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, 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 datePattern = /\b20\d{2}[-/.](?:0[1-9]|1[0-2])(?:[-/.](?:0[1-9]|[12]\d|3[01]))?\b/g;
|
||
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 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]) {
|
||
explicitAccounts.add(contextual[1]);
|
||
}
|
||
}
|
||
if (explicitAccounts.size > 0) {
|
||
return Array.from(explicitAccounts);
|
||
}
|
||
const spans = collectDateSpans(lower);
|
||
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;
|
||
}
|
||
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 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(result.status);
|
||
statusByRequirement.set(requirementId, list);
|
||
}
|
||
}
|
||
const resolvedRequirements = requirements.map((requirement) => {
|
||
if (requirement.status === "out_of_scope" || requirement.status === "clarification_needed") {
|
||
return requirement;
|
||
}
|
||
const statuses = statusByRequirement.get(requirement.requirement_id) ?? [];
|
||
if (statuses.length === 0) {
|
||
return { ...requirement, status: "uncovered" };
|
||
}
|
||
if (statuses.includes("ok")) {
|
||
return { ...requirement, status: "covered" };
|
||
}
|
||
if (statuses.includes("partial")) {
|
||
return { ...requirement, status: "partially_covered" };
|
||
}
|
||
if (statuses.includes("empty") && !statuses.includes("error")) {
|
||
return { ...requirement, status: "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 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_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 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 explicitAccounts = extractAccountTokens(userMessage);
|
||
if (explicitAccounts.length > 0) {
|
||
const knownAccounts = new Set(state.focus.primary_accounts.map((item) => item.trim()));
|
||
if (knownAccounts.size === 0) {
|
||
return true;
|
||
}
|
||
if (explicitAccounts.some((item) => !knownAccounts.has(item))) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
function routeFromInvestigationState(state) {
|
||
const rawDomain = compactWhitespace(state.focus.domain ?? "");
|
||
if (!rawDomain) {
|
||
return null;
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
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;
|
||
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 (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}`);
|
||
}
|
||
if (problemContinuityApplied && (problemState?.focus_problem_types.length ?? 0) > 0) {
|
||
appendParts.push(`Problem focus types: ${(problemState?.focus_problem_types ?? []).slice(0, 3).join(", ")}`);
|
||
}
|
||
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 = this.dataLayer.executeRoute(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 composition = (0, answerComposer_1.composeAssistantAnswer)({
|
||
userMessage,
|
||
routeSummary: normalized.route_hint_summary,
|
||
retrievalResults,
|
||
requirements: coverageEvaluation.requirements,
|
||
coverageReport: coverageEvaluation.coverage,
|
||
groundingCheck,
|
||
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 answerStructureV11 = config_1.FEATURE_ASSISTANT_CONTRACTS_V11
|
||
? config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11 && composition.answer_structure_v11
|
||
? composition.answer_structure_v11
|
||
: buildAnswerStructureV11({
|
||
assistantReply: composition.assistant_reply,
|
||
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),
|
||
...(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: composition.assistant_reply,
|
||
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),
|
||
...(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: composition.assistant_reply,
|
||
reply_type: composition.reply_type,
|
||
trace_id: normalized.trace_id
|
||
}
|
||
});
|
||
return {
|
||
ok: true,
|
||
session_id: sessionId,
|
||
assistant_reply: composition.assistant_reply,
|
||
reply_type: composition.reply_type,
|
||
conversation_item: assistantItem,
|
||
debug,
|
||
conversation
|
||
};
|
||
}
|
||
}
|
||
exports.AssistantService = AssistantService;
|