"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;