ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.12.23: декомпозиция deep-turn пайплайна ассистента в runtime-адаптеры

This commit is contained in:
2026-04-10 19:22:40 +03:00
parent 19dbaff741
commit 80d108e506
116 changed files with 13949 additions and 2039 deletions
+1
View File
@@ -797,6 +797,7 @@ function buildRunSummary(run) {
llm_provider: llmProvider,
model,
use_mock: toBooleanSafe(run.report.use_mock),
analysis_date: toStringSafe(run.report.analysis_date),
prompt_version: toStringSafe(run.report.prompt_version),
schema_version: toStringSafe(run.report.schema_version),
suite_id: toStringSafe(run.report.suite_id),
+31 -2
View File
@@ -90,7 +90,32 @@ function normalizeCaseIds(value) {
.filter((item) => item.length > 0);
return normalized.length > 0 ? normalized : undefined;
}
function normalizeAnalysisDate(value) {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return undefined;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return undefined;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day) {
return undefined;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function buildEvalPayloadFromBody(body) {
const analysisDate = normalizeAnalysisDate(body.analysis_date) ??
normalizeAnalysisDate(body.analysisDate);
return {
normalizeConfig: (body.normalizeConfig ?? {}),
caseIds: normalizeCaseIds(body.caseIds),
@@ -103,7 +128,8 @@ function buildEvalPayloadFromBody(body) {
? body.compare_with_report_file
: typeof body.comparisonBaselineReportFile === "string"
? body.comparisonBaselineReportFile
: undefined
: undefined,
analysisDate
};
}
function resolveReadablePath(inputPath) {
@@ -245,6 +271,7 @@ function snapshotJob(job) {
eval_target: job.eval_target,
run_id: job.run_id,
case_set_file: job.case_set_file,
analysis_date: job.analysis_date,
total_cases: job.total_cases,
completed_cases: job.completed_cases,
error: job.error,
@@ -258,7 +285,8 @@ function snapshotJob(job) {
: toRecord(job.report.metrics) && typeof toRecord(job.report.metrics)?.score_index === "number"
? Number(toRecord(job.report.metrics)?.score_index)
: null,
cases_total: typeof job.report.cases_total === "number" ? Number(job.report.cases_total) : null
cases_total: typeof job.report.cases_total === "number" ? Number(job.report.cases_total) : null,
analysis_date: toStringSafe(job.report.analysis_date) ?? job.analysis_date
}
: null
};
@@ -310,6 +338,7 @@ function buildEvalRouter(services) {
eval_target: payload.evalTarget,
run_id: runId,
case_set_file: runtimeCaseSetFile,
analysis_date: payload.analysisDate ?? null,
total_cases: caseSeeds.length,
completed_cases: 0,
cases: caseSeeds.map((item) => ({
@@ -427,8 +427,20 @@ function extractLooseByAnchorValue(text) {
}
const lowered = token.toLowerCase();
const stopWords = new Set([
"какой",
"какая",
"какие",
"каких",
"каким",
"какими",
"каком",
"кто",
"что",
"мы",
"видим",
"контрагенту",
"контрагента",
"контрагентам",
"контре",
"компании",
"компанию",
@@ -436,10 +448,14 @@ function extractLooseByAnchorValue(text) {
"организацию",
"поставщику",
"поставщика",
"поставщикам",
"клиенту",
"клиента",
"клиентам",
"покупателю",
"покупателя",
"покупателям",
"заказчикам",
"партнеру",
"партнера",
"договору",
@@ -552,6 +568,9 @@ function isLikelyCounterpartyToken(rawToken) {
"какая",
"какое",
"каких",
"каким",
"какими",
"каком",
"какому",
"какую",
"кто",
@@ -566,6 +585,8 @@ function isLikelyCounterpartyToken(rawToken) {
"чья",
"чей",
"чью",
"мы",
"видим",
"самый",
"самая",
"самое",
@@ -620,10 +641,23 @@ function isLikelyCounterpartyToken(rawToken) {
"контрагент",
"контрагенту",
"контрагента",
"контрагентам",
"компания",
"компании",
"организация",
"организации",
"поставщикам",
"клиентам",
"покупателям",
"заказчикам",
"аванс",
"авансы",
"проблемный",
"проблемные",
"проблемным",
"закрытия",
"закрыть",
"закрыты",
"год",
"года",
"г",
@@ -727,7 +761,7 @@ function isLowQualityCounterpartyAnchorValue(rawValue) {
if (tokens.length === 0) {
return true;
}
const questionCue = /(?:кто|что|какой|какая|какие|какого|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
const questionCue = /(?:кто|что|какой|какая|какие|какого|каких|каким|какими|каком|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
/[?]/u.test(String(rawValue ?? ""));
const rankingCue = /(?:больше|меньше|сам(?:ый|ая|ое|ые)|крупн|жирн|максим|миним)/iu.test(value);
const paymentCue = /(?:плат(?:ит|ят|еж|ёж|ежн|ежей|ежа)|денег|деньг|money|payment)/iu.test(value);
@@ -604,6 +604,10 @@ function hasLifecycleSegmentationSignal(text) {
return /(?:вперв|нов(?:ые|ых|ые\s+контрагент|ые\s+клиент|ые\s+заказчик)|исчез|ушед|ушл|пропал|отвал|только\s+один\s+раз|ровно\s+один\s+раз|однораз|дольше\s+всех|долгожив|самые\s+старые|старые\s+по\s+сотрудничеству|регуляр|эпизодич|разов(?:ые|ой|ые\s+поставщик)|давно\s+не\s+использ|неиспольз|потом\s+перестал)/iu.test(text);
}
function hasCounterpartyActivityLifecycleSignal(text) {
const hasPaymentRiskLexeme = /(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|задерж(?:ива|к)|просроч|долг|задолж)/iu.test(text);
if (hasPaymentRiskLexeme) {
return false;
}
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
return false;
}
@@ -678,6 +682,7 @@ function hasCustomerRevenueAndPaymentsSignal(text) {
/(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн|минимальн)/iu.test(text);
const asksRevenueTotal = /(?:сколько|скока|скок).*(?:денег|выручк|доход|заработ|оборот)/iu.test(text);
const asksOverallTurnover = /(?:общ(?:ий|ие|ая)\s+оборот|общ(?:ая|ий)\s+выручк|total\s+turnover|turnover\s+total)/iu.test(text);
const asksMajorShare = /(?:основн(?:ую|ая|ые|ой)\s+част|больш(?:ую|ая|ие)\s+част|львин(?:ая|ую)\s+дол[яю]|ключев(?:ую|ая)\s+част)/iu.test(text);
const asksValue = /(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(text);
const asksRankOrTop = /(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн)/iu.test(text);
const asksCountOnly = /(?:сколько|скока|скок)\s+/iu.test(text) && !asksValue;
@@ -702,6 +707,9 @@ function hasCustomerRevenueAndPaymentsSignal(text) {
if (asksCounterpartySource && asksValue) {
return true;
}
if (!hasFuzzySupplierLexeme && (asksCustomerGroup || hasCounterpartyLexeme) && asksMajorShare && asksValue) {
return true;
}
if (!hasFuzzySupplierLexeme && asksIncomingFlow && asksRankOrTop) {
return true;
}
@@ -801,6 +809,58 @@ function hasOpenContractsListSignal(text) {
}
return true;
}
function hasSupplierTailRiskSignal(text) {
const hasSupplier = /(?:поставщик|supplier|vendor)/iu.test(text);
const hasTail = /(?:хвост|висят|незакрыт|задолж|долг|просроч)/iu.test(text);
const hasRisk = /(?:систематич|регулярн|проблем|тревог|не\s+разов|больше\s+похож)/iu.test(text);
const hasPeriodCue = /(?:на\s+конец\s+(?:месяц|период)|конец\s+месяц|пару\s+месяц|несколько\s+месяц)/iu.test(text);
return hasSupplier && hasTail && (hasRisk || hasPeriodCue);
}
function hasReceivablesLatencyRiskSignal(text) {
const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text);
const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text);
const hasPayment = /(?:оплат|платеж|платёж|payment)/iu.test(text);
const hasShipment = /(?:отправк|отгруз|реализ|shipment|delivery)/iu.test(text);
const hasDelay = /(?:длинн|долг|просроч|задерж|висят|тревог|too\s+long|late)/iu.test(text);
const hasNonPayment = /(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|неоплач)/iu.test(text);
const hasPeriodOrRiskCue = /(?:за\s+текущ|на\s+конец|тревог|просроч|задерж|долг|длинн)/iu.test(text);
const hasBetweenShipmentAndPayment = /между[\s\S]{0,80}(?:отправк|отгруз|реализ)[\s\S]{0,80}(?:оплат|платеж|платёж|payment)/iu.test(text);
if (hasBuyer && hasPayment && ((hasShipment && hasDelay) || hasBetweenShipmentAndPayment)) {
return true;
}
return (hasBuyer || hasCounterparty) && hasNonPayment && hasPeriodOrRiskCue;
}
function hasSettlementGapSignal(text) {
const hasPayment = /(?:платеж|платёж|оплат|списани|поступлен|payment)/iu.test(text);
const hasDocument = /(?:док(?:и|умент|ументы|ументов)|docs?|documents?)/iu.test(text);
const hasAdvance = /(?:аванс|предоплат)/iu.test(text);
const hasNoDocumentForClosing = /(?:нет|без)\s+(?:док(?:и|умент|ументы|ументов)|закрывающ)/iu.test(text) &&
/(?:закрыти|взаиморасч|акт)/iu.test(text);
const hasNoDocumentForClosingReversed = /(?:док(?:и|умент|ументы|ументов)|закрывающ)[\s\S]{0,48}(?:нет|без)/iu.test(text) &&
/(?:закрыти|взаиморасч|акт)/iu.test(text);
const hasNoPayments = /(?:нет|без)\s+(?:оплат|платеж|платёж|payment)/iu.test(text) ||
/(?:оплат|платеж|платёж|payment)\s+нет/iu.test(text);
const hasDocsWithoutPayments = hasDocument && hasNoPayments;
const hasPaymentsWithoutClosingDocs = hasPayment && (hasNoDocumentForClosing || hasNoDocumentForClosingReversed);
const hasUnclosedAdvanceGap = hasAdvance &&
(/(?:не\s+закрыт|незакрыт|долго\s+не\s+закрыт|давно\s+не\s+закрыт)/iu.test(text) ||
hasNoDocumentForClosing ||
hasNoDocumentForClosingReversed);
return hasPaymentsWithoutClosingDocs || hasDocsWithoutPayments || hasUnclosedAdvanceGap;
}
function hasReconciliationMismatchSignal(text) {
const hasCounterparty = /(?:контрагент|поставщик|клиент|покупател|customer|supplier|counterparty)/iu.test(text);
const hasReconciliationLexeme = /(?:акт(?:а|ом|ах)?\s+свер(?:к|ок)|свер(?:к|ок))/iu.test(text);
const hasMismatchLexeme = /(?:не\s+совпад|несовпад|расхожд|расход|не\s+сход|несход|разъех|разниц|не\s+бь[её]т)/iu.test(text);
const hasBalanceLexeme = /(?:сальд|остат|баланс|saldo|balance)/iu.test(text);
const hasLookupVerb = /(?:покажи|выведи|найд[иь]|show|list)/iu.test(text);
const hasInterrogativeLookup = /(?:по\s+каким|у\s+кого|какие|какой|кто|где)/iu.test(text);
return (hasCounterparty &&
hasReconciliationLexeme &&
hasMismatchLexeme &&
hasBalanceLexeme &&
(hasLookupVerb || hasInterrogativeLookup));
}
function isLikelyCounterpartyToken(rawToken) {
const token = String(rawToken ?? "").trim().toLowerCase();
if (!token || token.length < 2) {
@@ -1115,6 +1175,34 @@ function resolveAddressIntent(userMessage) {
reasons: ["payables_signal_detected"]
};
}
if (hasSettlementGapSignal(text)) {
return {
intent: "list_open_contracts",
confidence: "medium",
reasons: ["settlement_gap_signal_detected"]
};
}
if (hasReconciliationMismatchSignal(text)) {
return {
intent: "list_open_contracts",
confidence: "medium",
reasons: ["reconciliation_mismatch_signal_detected"]
};
}
if (hasReceivablesLatencyRiskSignal(text)) {
return {
intent: "list_receivables_counterparties",
confidence: "medium",
reasons: ["receivables_payment_lag_signal_detected"]
};
}
if (hasSupplierTailRiskSignal(text)) {
return {
intent: "list_payables_counterparties",
confidence: "medium",
reasons: ["supplier_tail_risk_signal_detected"]
};
}
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
return {
intent: "documents_forming_balance",
@@ -1137,7 +1225,7 @@ function resolveAddressIntent(userMessage) {
};
}
if (hasAny(text, OPEN_ITEMS_HINTS) &&
(text.includes("контраг") || text.includes("договор") || text.includes("контракт") || text.includes("counterparty") || text.includes("contract"))) {
/(?:контраг|договор|контракт|counterparty|contract|покупател|клиент|заказчик|customer|client|buyer|supplier|поставщик)/iu.test(text)) {
return {
intent: "open_items_by_counterparty_or_contract",
confidence: "medium",
+105 -12
View File
@@ -86,6 +86,33 @@ function parseFiniteNumber(value) {
}
return null;
}
function normalizeAnalysisDateHint(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
const strictDate = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
const isoPrefix = strictDate ?? trimmed.match(/^(\d{4})-(\d{2})-(\d{2})T/i);
if (!isoPrefix) {
return null;
}
const year = Number(isoPrefix[1]);
const month = Number(isoPrefix[2]);
const day = Number(isoPrefix[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day) {
return null;
}
return `${isoPrefix[1]}-${isoPrefix[2]}-${isoPrefix[3]}`;
}
function valueAsString(value) {
if (value === null || value === undefined) {
return "";
@@ -665,6 +692,50 @@ function runtimeReadinessForLimitedCategory(category) {
}
return "UNKNOWN";
}
function normalizeLimitedReason(reason) {
let normalized = String(reason ?? "").trim();
if (!normalized) {
return "не хватает подтвержденных данных для уверенного вывода";
}
const replacements = [
[/address_query\s*v?1/giu, "текущий адресный режим"],
[/address\s*v1/giu, "текущий адресный режим"],
[/intent-specific\s+recipe/giu, "встроенный фильтр сценария"],
[/live\s+recipe/giu, "текущий сценарий выборки"],
[/materialized\s+live-строках/giu, "доступном срезе данных"],
[/live-выборке/giu, "выборке данных"],
[/live-данных/giu, "данных"],
[/deep-analysis/giu, "режим расширенной проверки"],
[/\blookup\b/giu, "поиск"],
[/\bintent\b/giu, "сценария"],
[/\brecipe\b/giu, "шаблон выборки"],
[/\byakor\b/giu, "ориентир"],
[/\banchor\b/giu, "ориентир"],
[/\s+/gu, " "]
];
for (const [pattern, value] of replacements) {
normalized = normalized.replace(pattern, value);
}
return normalized.trim();
}
function normalizeLimitedNextStep(nextStep) {
let normalized = String(nextStep ?? "").trim();
if (!normalized) {
return "";
}
const replacements = [
[/address_query\s*v?1/giu, "текущий адресный режим"],
[/deep-analysis/giu, "режим расширенной проверки"],
[/\bP0 intent\b/giu, "поддерживаемый сценарий"],
[/\bintent\b/giu, "сценарий"],
[/\blookup\b/giu, "поиск"],
[/\s+/gu, " "]
];
for (const [pattern, value] of replacements) {
normalized = normalized.replace(pattern, value);
}
return normalized.trim();
}
function rowHasNonEmptyField(row, keys) {
return keys.some((key) => String(row[key] ?? "").trim().length > 0);
}
@@ -766,20 +837,27 @@ function toLegacyMcpStatus(status) {
}
function composeLimitedReply(category, reason, nextStep) {
const heading = category === "empty_match"
? "В live-данных по текущему фильтру записи не найдены."
? "По текущим условиям в доступном срезе данных совпадений не нашлось."
: category === "missing_anchor"
? "Для точного адресного поиска не хватает обязательного якоря."
? "Чтобы ответить надежно, нужен более точный ориентир в запросе."
: category === "recipe_visibility_gap"
? "Текущий live recipe не дает нужную видимость данных для этого сценария."
? "Запрос понятен, но текущий режим не дает нужной детализации."
: category === "unsupported"
? "Этот запрос не подходит под address_query V1."
: "Не удалось выполнить адресный live-запрос в V1.";
? "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима."
: "Не удалось завершить проверку в адресном режиме.";
const reasonLine = category === "unsupported"
? "Коротко: этот сценарий пока не поддержан в текущем адресном контуре."
: category === "missing_anchor"
? "Коротко: в запросе не хватает конкретного ориентира (контрагент, договор или период)."
: category === "recipe_visibility_gap"
? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки."
: `Коротко: ${normalizeLimitedReason(reason)}.`;
const lines = [
heading,
`Причина: ${reason}.`
reasonLine
];
if (nextStep) {
lines.push(`Что нужно уточнить: ${nextStep}.`);
lines.push(`Что можно сделать дальше: ${normalizeLimitedNextStep(nextStep)}.`);
}
return lines.join("\n");
}
@@ -842,7 +920,22 @@ class AddressQueryService {
if (!decompose) {
return null;
}
const { mode, shape, intent, filters, baseReasons } = decompose;
const { mode, shape, intent, filters } = decompose;
const baseReasons = [...decompose.baseReasons];
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
if (analysisDate) {
const hasTemporalFilter = Boolean((typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) ||
(typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) ||
(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0));
if (!hasTemporalFilter) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: analysisDate
};
filters.warnings = [...new Set([...(filters.warnings ?? []), "as_of_date_from_analysis_context"])];
baseReasons.push("as_of_date_from_analysis_context");
}
}
const composeOptionsFromFilters = (filterSet) => ({
userMessage,
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
@@ -863,8 +956,8 @@ class AddressQueryService {
rowsFetched: 0,
rowsMatched: 0,
category: "unsupported",
reasonText: "intent пока не поддержан в address V1",
nextStep: "переформулируйте вопрос как адресный lookup по счету/контрагенту/договору",
reasonText: "сценарий пока вне поддерживаемого контура текущего адресного режима",
nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету",
limitations: ["intent_not_supported_in_v1"],
reasons: baseReasons
});
@@ -903,8 +996,8 @@ class AddressQueryService {
rowsFetched: 0,
rowsMatched: 0,
category: "recipe_visibility_gap",
reasonText: "для intent пока нет recipe в address V1",
nextStep: "выберите поддерживаемый P0 intent или переключите запрос в deep-analysis",
reasonText: "для этого сценария пока нет готового шаблона выборки в текущем режиме",
nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки",
limitations: ["recipe_not_available"],
reasons: [...baseReasons, ...recipeSelection.selection_reason]
});
@@ -1172,7 +1172,7 @@ function composeFactualReply(intent, rows, options = {}) {
if (intent === "list_open_contracts") {
const contracts = contractCandidatesFromRows(rows);
const lines = [
"Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).",
"Проверил потенциальные разрывы во взаиморасчетах (платежи без закрытия и документы без оплат).",
`Строк движения: ${rows.length}.`,
`Договорных кандидатов: ${contracts.length}.`
];
@@ -1188,6 +1188,34 @@ function composeFactualReply(intent, rows, options = {}) {
text: lines.join("\n")
};
}
if (intent === "list_payables_counterparties") {
const lines = [
"Проверил поставщиков с признаками незакрытых хвостов по взаиморасчетам (контур 60/76).",
`Строк в выборке: ${rows.length}.`,
...(rows.length > 0
? ["Ниже примеры строк для ручной проверки."]
: ["Явных признаков системной задолженности по доступному срезу не найдено."]),
...formatTopRows(rows, 6)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "list_receivables_counterparties") {
const lines = [
"Проверил покупателей с признаками затянутой оплаты (контур 62/76).",
`Строк в выборке: ${rows.length}.`,
...(rows.length > 0
? ["Ниже примеры строк, которые стоит проверить в первую очередь."]
: ["Явных признаков затяжной дебиторки по доступному срезу не найдено."]),
...formatTopRows(rows, 6)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "open_items_by_counterparty_or_contract") {
const lines = [
"Собраны открытые позиции по указанному фильтру (контрагент/договор).",
@@ -1279,12 +1307,7 @@ function composeFactualReply(intent, rows, options = {}) {
text: lines.join("\n")
};
}
const title = intent === "list_payables_counterparties"
? "Срез обязательств (payables) собран по движениям с account scope 60/76."
: intent === "list_receivables_counterparties"
? "Срез требований (receivables) собран по движениям с account scope 62/76."
: "Срез адресного запроса собран.";
const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
const lines = ["Срез адресного запроса собран.", `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
@@ -0,0 +1,107 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantAnswerStructureV11 = buildAssistantAnswerStructureV11;
const config_1 = require("../config");
const stage1Contracts_1 = require("../types/stage1Contracts");
const EVIDENCE_LIMITATION_REASON_CODE_SET = new Set([
"snapshot_only",
"heuristic_inference",
"missing_mechanism",
"weak_source_mapping",
"insufficient_detail",
"unknown"
]);
function summarizeUnique(values, limit = 6) {
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
}
function isEvidenceLimitationReasonCode(value) {
return EVIDENCE_LIMITATION_REASON_CODE_SET.has(value);
}
function firstNonEmptyLine(text) {
const line = String(text ?? "")
.split("\n")
.map((item) => item.trim())
.find((item) => item.length > 0);
return (line ?? String(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 buildAssistantAnswerStructureV11(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).filter(isEvidenceLimitationReasonCode);
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";
const enableEvidenceEnrichment = input.options?.enableEvidenceEnrichment ?? config_1.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1;
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",
...(enableEvidenceEnrichment && 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
}
};
}
@@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.assembleAssistantContractsBundleV1 = assembleAssistantContractsBundleV1;
const assistantOrchestrationContracts_1 = require("./assistantOrchestrationContracts");
function assembleAssistantContractsBundleV1(input) {
const queryFrameContractV1 = (0, assistantOrchestrationContracts_1.buildAssistantQueryFrameContractV1)({
userMessage: input.userMessage,
normalizedQuestion: input.normalizedQuestion,
normalized: input.normalized,
routeSummary: input.routeSummary,
droppedIntentSegments: input.droppedIntentSegments,
analysisContext: input.analysisContext
});
const executionPlanContractV1 = (0, assistantOrchestrationContracts_1.buildAssistantExecutionPlanContractV1)({
executionPlan: input.executionPlan,
requirements: input.requirements
});
const outcomeClassV1 = (0, assistantOrchestrationContracts_1.classifyAssistantOutcomeClassV1)({
replyType: input.replyType,
coverageReport: input.coverageReport,
grounding: input.grounding,
retrievalResults: input.retrievalResults
});
const coverageContractV1 = (0, assistantOrchestrationContracts_1.buildAssistantCoverageContractV1)({
coverageReport: input.coverageReport,
grounding: input.grounding,
outcomeClass: outcomeClassV1
});
return {
queryFrameContractV1,
executionPlanContractV1,
outcomeClassV1,
coverageContractV1,
assistantOrchestrationContractsV1: {
query_frame: queryFrameContractV1,
execution_plan: executionPlanContractV1,
evidence_bundle: input.evidenceBundleContractV1,
coverage: coverageContractV1
}
};
}
@@ -0,0 +1,344 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractRequirementsForRoute = extractRequirementsForRoute;
exports.evaluateCoverageForRequirements = evaluateCoverageForRequirements;
exports.checkGroundingForRequirements = checkGroundingForRequirements;
function summarizeUnique(values, limit = 6) {
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
}
const SUBJECT_TOKEN_RULES = {
nds: {
critical: true,
patterns: [
"vat",
"accumulationregister",
"ндс",
"книгипокупок",
"книгипродаж",
"налогнадобавленнуюстоимость"
]
},
os: {
critical: true,
patterns: ["fixed_asset", "fixedasset", "основн", "амортиз"]
},
saldo: {
critical: true,
patterns: ["balance", "saldo", "сальдо", "остат"]
},
counterparty: {
critical: false,
patterns: [
"counterparty",
"supplier",
"buyer",
"counterparty_id",
"journal_counterparty",
"document_has_counterparty",
"контрагент",
"поставщик",
"покупател"
],
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",
"документ"
],
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",
"аномал",
"риск"
],
routes: ["store_feature_risk", "batch_refresh_then_store"]
},
chain: {
critical: false,
patterns: ["chain", "cross_entity_chain", "relation_types", "operations_count", "matched_counterparties", "цепоч"],
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 extractRequirementsForRoute(input) {
const byFragment = new Map();
const requirements = [];
const pushRequirement = (item) => {
const subjectTokens = input.extractSubjectTokens(item.requirement_text);
requirements.push({
requirement_id: item.requirement_id,
source_fragment_id: item.source_fragment_id,
requirement_text: item.requirement_text,
subject_tokens: subjectTokens,
status: item.status,
route: item.route
});
if (item.source_fragment_id) {
const current = byFragment.get(item.source_fragment_id) ?? [];
current.push(item.requirement_id);
byFragment.set(item.source_fragment_id, current);
}
};
if (!input.routeSummary) {
pushRequirement({
requirement_id: "R1",
source_fragment_id: null,
requirement_text: input.userMessage,
status: "clarification_needed",
route: null
});
return { requirements, byFragment };
}
if (input.routeSummary.mode === "legacy_v1") {
pushRequirement({
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: input.userMessage,
status: "covered",
route: input.routeSummary.route_hint
});
return { requirements, byFragment };
}
input.routeSummary.decisions.forEach((decision, index) => {
const requirementId = `R${index + 1}`;
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.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 evaluateCoverageForRequirements(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 checkGroundingForRequirements(input) {
const whyIncludedSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.why_included));
const selectionReasonSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.selection_reason));
const hasMaterialResults = input.retrievalResults.some((item) => item.status === "ok" || item.status === "partial");
const subjectTokens = input.extractSubjectTokens(input.userMessage);
const executedRoutes = new Set(input.retrievalResults
.filter((item) => item.status !== "error")
.map((item) => item.route)
.filter(Boolean));
const retrievalCorpus = JSON.stringify(input.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 &&
input.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 (input.coverage.requirements_covered === 0) {
status = "no_grounded_answer";
reasons.push("Ни одно требование не получило подтвержденного покрытия.");
}
else if (input.coverage.requirements_uncovered.length > 0 ||
input.coverage.requirements_partially_covered.length > 0 ||
input.coverage.clarification_needed_for.length > 0 ||
input.coverage.out_of_scope_requirements.length > 0) {
status = "partial";
reasons.push("Вопрос покрыт частично: есть непокрытые или требующие уточнения требования.");
}
if (whyIncludedSummary.length === 0) {
reasons.push("В текущей выборке не хватает явных подтверждений, почему записи попали в ответ.");
}
if (missingSubjectTokens.length > 0 && missingCriticalTokens.length === 0) {
reasons.push(`Часть контекста вопроса не подтверждена напрямую в найденных данных: ${missingSubjectTokens.join(", ")}`);
}
const missingRequirements = [
...input.coverage.requirements_uncovered,
...input.coverage.requirements_partially_covered,
...input.coverage.clarification_needed_for,
...input.coverage.out_of_scope_requirements
];
return {
status,
route_subject_match: routeSubjectMatch,
missing_requirements: missingRequirements,
reasons,
why_included_summary: whyIncludedSummary,
selection_reason_summary: selectionReasonSummary
};
}
+56 -22
View File
@@ -141,6 +141,29 @@ function formatIsoDateUtc(date) {
const day = String(date.getUTCDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function normalizeIsoDate(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function monthEndFromIso(isoDate) {
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
@@ -198,14 +221,25 @@ function hasRbpSignal(text) {
function hasFixedAssetAmortizationSignal(text) {
return /(?:амортиз|основн(?:ые|ых)?\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|depreciat|fixed\s*asset|account\s*0[12]|счет\s*0[12])/i.test(String(text ?? "").toLowerCase());
}
function buildLiveMcpCallPlan(route, fragmentText) {
function buildLiveMcpCallPlan(route, fragmentText, temporalHint) {
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
const preferredDomainHint = (0, investigationState_1.inferP0DomainFromMessage)(fragmentText);
const periodScope = inferPeriodScope(fragmentText);
const primaryFrom = periodScope.from ?? "2020-07-01";
const primaryTo = periodScope.to ?? monthEndFromIso(primaryFrom) ?? "2020-07-31";
const carryFrom = shiftIsoDate(primaryFrom, -31) ?? primaryFrom;
const carryTo = shiftIsoDate(primaryTo, 31) ?? primaryTo;
const hintedAsOfDate = normalizeIsoDate(temporalHint?.as_of_date);
const hintedPeriodFrom = normalizeIsoDate(temporalHint?.period_from);
const hintedPeriodTo = normalizeIsoDate(temporalHint?.period_to);
const primaryFrom = periodScope.from ?? hintedPeriodFrom ?? hintedAsOfDate;
const primaryTo = periodScope.to ??
hintedPeriodTo ??
(!periodScope.from && !hintedPeriodFrom && hintedAsOfDate ? hintedAsOfDate : primaryFrom ? monthEndFromIso(primaryFrom) ?? primaryFrom : null);
const carryFrom = primaryFrom ? shiftIsoDate(primaryFrom, -31) ?? primaryFrom : null;
const carryTo = primaryTo ? shiftIsoDate(primaryTo, 31) ?? primaryTo : null;
const buildPrimaryQuery = (limit) => primaryFrom && primaryTo
? buildLiveRangeQuery(primaryFrom, primaryTo, limit)
: MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(limit));
const buildCarryQuery = (limit) => carryFrom && carryTo
? buildLiveRangeQuery(carryFrom, carryTo, limit)
: buildPrimaryQuery(limit);
const faClaim = preferredDomainHint === "fixed_asset_amortization" ||
hasFixedAssetAmortizationSignal(fragmentText) ||
semanticProfile.query_subject === "fixed_asset_card_mismatch" ||
@@ -219,7 +253,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_amortization_documents_in_period",
purpose: "seed_amortization_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@@ -227,7 +261,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_fixed_asset_movements_accounts_01_02",
purpose: "collect_fa_object_movements",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@@ -235,7 +269,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_fixed_asset_cards_expected_for_period",
purpose: "build_expected_fa_set",
query: buildLiveRangeQuery(carryFrom, primaryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@@ -243,7 +277,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "match_expected_vs_actual_fa_coverage",
purpose: "compare_expected_vs_actual_fa_coverage",
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@@ -265,7 +299,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_vat_source_documents_in_period",
purpose: "seed_vat_source_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@@ -273,7 +307,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_vat_invoice_links_in_period",
purpose: "collect_invoice_links",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@@ -281,7 +315,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_vat_register_entries_in_period",
purpose: "collect_vat_register_entries",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@@ -289,7 +323,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_vat_book_entries_in_period",
purpose: "collect_vat_book_entries",
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@@ -326,7 +360,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_rbp_writeoff_documents_in_period",
purpose: "seed_writeoff_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
@@ -334,7 +368,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_rbp_object_movements_account_97",
purpose: "collect_rbp_object_movements",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97"]
@@ -342,7 +376,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "find_month_close_entries_linked_to_rbp",
purpose: "link_month_close_to_rbp",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
@@ -350,7 +384,7 @@ function buildLiveMcpCallPlan(route, fragmentText) {
{
call_id: "compute_end_period_residual_by_rbp_object",
purpose: "collect_residual_tail_signals",
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
@@ -1410,7 +1444,7 @@ function buildSemanticRetrievalProfile(fragmentText) {
pushMany(entityTypes, ["document", "tax_entry", "posting"]);
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
}
if (/ос|основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(lower) ||
if (/основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(lower) ||
hasFixedAssetAccountScope) {
pushMany(domainScope, ["fixed_assets"]);
pushMany(documentTypes, ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"]);
@@ -2243,7 +2277,7 @@ class AssistantDataLayer {
}
return enforceBroadQueryGuards(route, fragmentText, result);
}
async executeRouteRuntime(route, fragmentText) {
async executeRouteRuntime(route, fragmentText, options) {
const base = this.executeRoute(route, fragmentText);
if (!config_1.FEATURE_ASSISTANT_MCP_RUNTIME_V1) {
return base;
@@ -2251,7 +2285,7 @@ class AssistantDataLayer {
if (route !== "hybrid_store_plus_live" && route !== "live_mcp_drilldown") {
return base;
}
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText);
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText, options?.temporalHint);
return this.mergeWithLiveOverlay(base, liveOverlay);
}
cloneRawResult(base) {
@@ -2301,9 +2335,9 @@ class AssistantDataLayer {
}
return merged;
}
async fetchLiveMcpOverlay(route, fragmentText) {
async fetchLiveMcpOverlay(route, fragmentText, temporalHint) {
const endpoint = this.buildMcpUrl("/api/execute_query");
const livePlan = buildLiveMcpCallPlan(route, fragmentText);
const livePlan = buildLiveMcpCallPlan(route, fragmentText, temporalHint);
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
const accountScope = livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
? ["01", "02", "08"]
@@ -0,0 +1,92 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildDeepAnalysisDebugPayload = buildDeepAnalysisDebugPayload;
function toAnalysisContext(input) {
if (!input.active) {
return null;
}
return {
as_of_date: input.as_of_date,
period_from: input.period_from,
period_to: input.period_to,
source: input.source,
snapshot_mode: input.snapshot_mode
};
}
function buildDeepAnalysisDebugPayload(input) {
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
return {
trace_id: input.traceId,
prompt_version: input.promptVersion,
schema_version: input.schemaVersion,
fallback_type: input.fallbackType,
route_summary: input.routeSummary,
fragments: input.fragments,
requirements_extracted: input.requirementsExtracted,
coverage_report: input.coverageReport,
routes: input.routes,
retrieval_status: input.retrievalStatus,
retrieval_results: input.retrievalResults,
answer_grounding_check: input.groundingCheck,
dropped_intent_segments: input.droppedIntentSegments,
question_type_class: input.questionTypeClass,
company_anchors: input.companyAnchors,
analysis_context_applied: input.runtimeAnalysisContext.active,
analysis_context: analysisContext,
business_scope_raw: input.businessScopeResolution.business_scope_raw,
business_scope_resolved: input.businessScopeResolution.business_scope_resolved,
company_grounding_applied: input.businessScopeResolution.company_grounding_applied,
scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
company_scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
raw_time_anchor: input.temporalGuard.raw_time_anchor,
raw_time_scope: input.temporalGuard.raw_time_scope,
resolved_time_anchor: input.temporalGuard.resolved_time_anchor,
resolved_primary_period: input.temporalGuard.resolved_primary_period,
effective_primary_period: input.temporalGuard.effective_primary_period,
temporal_guard_input: input.temporalGuard.temporal_guard_input,
temporal_alignment_status: input.temporalGuard.temporal_alignment_status,
temporal_resolution_source: input.temporalGuard.temporal_resolution_source,
temporal_guard_basis: input.temporalGuard.temporal_guard_basis,
temporal_guard_applied: input.temporalGuard.temporal_guard_applied,
temporal_guard_outcome: input.temporalGuard.temporal_guard_outcome,
temporal_guard: input.temporalGuard,
raw_numeric_tokens: input.polarityAudit.raw_numeric_tokens,
classified_numeric_tokens: input.polarityAudit.classified_numeric_tokens,
rejected_as_non_accounts: input.polarityAudit.rejected_as_non_accounts,
resolved_account_anchors: input.polarityAudit.resolved_account_anchors,
domain_polarity_guard: input.polarityAudit,
claim_anchor_audit: input.claimAnchorAudit,
settlement_role: input.claimAnchorAudit.settlement_role ?? null,
settlement_role_resolution_reason: input.claimAnchorAudit.settlement_role_resolution_reason ?? [],
polarity_resolution_status: input.claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
targeted_evidence_acquisition: input.targetedEvidenceAudit,
evidence_admissibility_gate: input.evidenceAdmissibilityGateAudit,
...(input.rbpLiveRouteAudit ? { rbp_live_route_audit: input.rbpLiveRouteAudit } : {}),
...(input.faLiveRouteAudit ? { fa_live_route_audit: input.faLiveRouteAudit } : {}),
eligibility_time_basis: input.groundedAnswerEligibilityGuard.eligibility_time_basis,
grounded_answer_eligibility_guard: input.groundedAnswerEligibilityGuard,
...(input.followupStateUsage ? { followup_state_usage: input.followupStateUsage } : {}),
problem_centric_answer_applied: input.compositionDebug.problem_centric_answer_applied ?? false,
problem_units_used_count: input.compositionDebug.problem_units_used_count ?? 0,
problem_answer_mode: input.compositionDebug.problem_answer_mode ?? "stage1_policy_v11",
...(Array.isArray(input.compositionDebug.problem_unit_ids_used) && input.compositionDebug.problem_unit_ids_used.length > 0
? {
problem_unit_ids_used: input.compositionDebug.problem_unit_ids_used
}
: {}),
address_llm_predecompose_attempted: Boolean(input.addressRuntimeMetaForDeep?.attempted),
address_llm_predecompose_applied: Boolean(input.addressRuntimeMetaForDeep?.applied),
address_llm_predecompose_reason: input.addressRuntimeMetaForDeep?.reason ?? null,
address_llm_predecompose_provider: input.addressRuntimeMetaForDeep?.provider ?? null,
address_fallback_rule_hit: input.addressRuntimeMetaForDeep?.fallbackRuleHit ?? null,
address_tool_gate_decision: input.addressRuntimeMetaForDeep?.toolGateDecision ?? null,
address_tool_gate_reason: input.addressRuntimeMetaForDeep?.toolGateReason ?? null,
address_llm_predecompose_contract: input.addressRuntimeMetaForDeep?.predecomposeContract ?? null,
orchestration_contract_v1: input.addressRuntimeMetaForDeep?.orchestrationContract ?? null,
assistant_outcome_class_v1: input.outcomeClassV1,
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
answer_structure_v11: input.answerStructureV11,
investigation_state_snapshot: input.investigationStateSnapshot,
normalized: input.normalizedPayload
};
}
@@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildDeepAnswerArtifacts = buildDeepAnswerArtifacts;
exports.buildAssistantConversationItem = buildAssistantConversationItem;
const assistantAnswerPackageBuilder_1 = require("./assistantAnswerPackageBuilder");
function stripTechnicalTail(text) {
return String(text ?? "")
.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();
}
function buildDeepAnswerArtifacts(input) {
const safeAssistantReply = stripTechnicalTail(input.safeAssistantReplyBase);
const answerStructureV11 = input.featureContractsV11
? input.featureAnswerPolicyV11 && input.compositionAnswerStructureV11
? input.compositionAnswerStructureV11
: (0, assistantAnswerPackageBuilder_1.buildAssistantAnswerStructureV11)({
assistantReply: safeAssistantReply,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
retrievalResults: input.retrievalResults
})
: null;
return {
safeAssistantReply,
answerStructureV11
};
}
function buildAssistantConversationItem(input) {
return {
message_id: input.messageId,
session_id: input.sessionId,
role: "assistant",
text: input.text,
reply_type: input.replyType,
created_at: new Date().toISOString(),
trace_id: input.traceId,
debug: input.debug
};
}
@@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnComposition = buildAssistantDeepTurnComposition;
const questionTypeResolver_1 = require("./questionTypeResolver");
const answerComposer_1 = require("./answerComposer");
function buildAssistantDeepTurnComposition(input) {
const resolveQuestionTypeSafe = input.resolveQuestionTypeFn ?? questionTypeResolver_1.resolveQuestionType;
const composeAssistantAnswerSafe = input.composeAssistantAnswerFn ?? answerComposer_1.composeAssistantAnswer;
const followupApplied = Boolean(input.followupUsage?.applied);
const focusDomainHint = followupApplied
? input.investigationState?.followup_context?.active_domain ?? input.investigationState?.focus.domain ?? null
: null;
const questionTypeClass = resolveQuestionTypeSafe(input.userMessage);
const companyAnchorSet = input.companyAnchors;
const hasPeriodInCompanyAnchors = (Array.isArray(companyAnchorSet?.dates) && companyAnchorSet.dates.some((item) => String(item ?? "").trim().length > 0)) ||
(Array.isArray(companyAnchorSet?.periods) && companyAnchorSet.periods.some((item) => String(item ?? "").trim().length > 0));
const normalizationPeriodExplicit = input.hasExplicitPeriodAnchor(input.normalizedPayload) || hasPeriodInCompanyAnchors;
const composition = composeAssistantAnswerSafe({
userMessage: input.userMessage,
routeSummary: input.routeSummary,
retrievalResults: input.retrievalResults,
requirements: input.requirements,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
focusDomainHint,
questionTypeHint: questionTypeClass,
companyAnchors: input.companyAnchors,
normalizationPeriodExplicit,
enableAnswerPolicyV11: input.featureAnswerPolicyV11,
enableProblemCentricAnswerV1: input.featureProblemCentricAnswerV1,
enableLifecycleAnswerV1: input.featureLifecycleAnswerV1
});
return {
focusDomainHint,
questionTypeClass,
hasPeriodInCompanyAnchors,
normalizationPeriodExplicit,
composition
};
}
@@ -0,0 +1,72 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnRuntimeContext = buildAssistantDeepTurnRuntimeContext;
const KNOWN_P0_DOMAINS = new Set([
"settlements_60_62",
"vat_document_register_book",
"month_close_costs_20_44",
"fixed_asset_amortization"
]);
function toAnalysisContext(runtimeAnalysisContext) {
if (!runtimeAnalysisContext.active) {
return null;
}
return {
as_of_date: runtimeAnalysisContext.as_of_date,
period_from: runtimeAnalysisContext.period_from,
period_to: runtimeAnalysisContext.period_to,
source: runtimeAnalysisContext.source
};
}
function buildAssistantDeepTurnRuntimeContext(input) {
const companyAnchors = input.resolveCompanyAnchors(input.userMessage);
const initialBusinessScopeResolution = input.resolveBusinessScopeAlignment({
userMessage: input.userMessage,
companyAnchors,
normalized: input.normalizedPayload,
routeSummary: input.routeSummary
});
const inferredDomainByMessage = input.inferP0DomainFromMessage(input.userMessage);
const focusDomainForGuards = inferredDomainByMessage && KNOWN_P0_DOMAINS.has(inferredDomainByMessage) ? inferredDomainByMessage : null;
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
const temporalGuard = input.resolveTemporalGuard({
userMessage: input.userMessage,
normalized: input.normalizedPayload,
companyAnchors,
analysisContext
});
const domainPolarityGuardInitial = input.resolveDomainPolarityGuard({
userMessage: input.userMessage,
companyAnchors,
focusDomainHint: focusDomainForGuards
});
const claimAnchorAudit = input.resolveClaimBoundAnchors({
userMessage: input.userMessage,
companyAnchors,
focusDomainHint: focusDomainForGuards,
primaryPeriod: temporalGuard.effective_primary_period ?? temporalGuard.primary_period_window
});
const businessScopeResolution = input.resolveBusinessScopeFromLiveContext({
current: initialBusinessScopeResolution,
temporalGuard,
claimType: claimAnchorAudit.claim_type,
focusDomainHint: focusDomainForGuards,
userMessage: input.userMessage,
companyAnchors,
followupApplied: Boolean(input.followupUsage?.applied)
});
const resolvedRouteSummary = businessScopeResolution.route_summary_resolved;
const liveTemporalHint = toAnalysisContext(input.runtimeAnalysisContext);
return {
companyAnchors,
initialBusinessScopeResolution,
inferredDomainByMessage,
focusDomainForGuards,
temporalGuard,
domainPolarityGuardInitial,
claimAnchorAudit,
businessScopeResolution,
resolvedRouteSummary,
liveTemporalHint
};
}
@@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantDeepTurnGroundingRuntime = runAssistantDeepTurnGroundingRuntime;
const assistantOrchestrationRuntimeAdapter_1 = require("./assistantOrchestrationRuntimeAdapter");
const assistantDeepTurnGuardRuntimeAdapter_1 = require("./assistantDeepTurnGuardRuntimeAdapter");
function runAssistantDeepTurnGroundingRuntime(input) {
const runCoverageGroundingPipelineSafe = input.runCoverageGroundingPipelineFn ?? assistantOrchestrationRuntimeAdapter_1.runAssistantCoverageGroundingPipeline;
const applyGroundingEligibilitySafe = input.applyGroundingEligibilityFn ??
((payload) => (0, assistantDeepTurnGuardRuntimeAdapter_1.applyAssistantDeepTurnGroundingEligibility)(payload));
const rbpLiveRouteAudit = input.collectRbpLiveRouteAudit({
claimType: input.claimType,
retrievalResults: input.retrievalResults,
planAudit: input.rbpPlanAudit
});
const faLiveRouteAudit = input.collectFaLiveRouteAudit({
claimType: input.claimType,
retrievalResults: input.retrievalResults,
planAudit: input.faPlanAudit
});
const orchestrationRuntime = runCoverageGroundingPipelineSafe({
routeSummary: input.routeSummary,
normalized: input.normalizedPayload,
userMessage: input.userMessage,
retrievalResults: input.retrievalResults,
requirementExtraction: input.requirementExtraction,
extractRequirements: input.extractRequirements,
evaluateCoverage: input.evaluateCoverage,
checkGrounding: input.checkGrounding
});
const coverageEvaluation = orchestrationRuntime.coverageEvaluation;
const groundingCheckBase = orchestrationRuntime.groundingCheckBase;
const groundingEligibilityRuntime = applyGroundingEligibilitySafe({
groundingCheckBase,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
evidenceAudit: input.evidenceAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceHitRate: input.targetedEvidenceHitRate,
businessScopeResolved: input.businessScopeResolved
});
return {
rbpLiveRouteAudit,
faLiveRouteAudit,
coverageEvaluation,
groundingCheckBase,
groundedAnswerEligibilityGuard: groundingEligibilityRuntime.groundedAnswerEligibilityGuard,
groundingCheck: groundingEligibilityRuntime.groundingCheck
};
}
@@ -0,0 +1,51 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyAssistantDeepTurnRetrievalGuards = applyAssistantDeepTurnRetrievalGuards;
exports.applyAssistantDeepTurnGroundingEligibility = applyAssistantDeepTurnGroundingEligibility;
const assistantClaimBoundEvidence_1 = require("./assistantClaimBoundEvidence");
const assistantRuntimeGuards_1 = require("./assistantRuntimeGuards");
function applyAssistantDeepTurnRetrievalGuards(input) {
const applyDomainPolarityGuardSafe = input.applyDomainPolarityGuardFn ?? assistantRuntimeGuards_1.applyDomainPolarityGuardToRetrievalResults;
const applyTargetedEvidenceSafe = input.applyTargetedEvidenceFn ?? assistantClaimBoundEvidence_1.applyTargetedEvidenceAcquisition;
const applyEvidenceAdmissibilityGateSafe = input.applyEvidenceAdmissibilityGateFn ?? assistantRuntimeGuards_1.applyEvidenceAdmissibilityGate;
const polarityGuardResult = applyDomainPolarityGuardSafe({
retrievalResults: input.retrievalResults,
guard: input.domainPolarityGuardInitial
});
const targetedEvidenceResult = applyTargetedEvidenceSafe({
retrievalResults: polarityGuardResult.retrievalResults,
claimAudit: input.claimAnchorAudit
});
const evidenceGateResult = applyEvidenceAdmissibilityGateSafe({
retrievalResults: targetedEvidenceResult.retrievalResults,
temporal: input.temporalGuard,
focusDomainHint: input.focusDomainForGuards,
polarity: polarityGuardResult.audit.polarity,
companyAnchors: input.companyAnchors,
userMessage: input.userMessage
});
return {
retrievalResults: evidenceGateResult.retrievalResults,
polarityGuardResult,
targetedEvidenceResult,
evidenceGateResult
};
}
function applyAssistantDeepTurnGroundingEligibility(input) {
const evaluateGroundedAnswerEligibilitySafe = input.evaluateGroundedAnswerEligibilityFn ?? assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility;
const applyEligibilityToGroundingCheckSafe = input.applyEligibilityToGroundingCheckFn ??
((groundingCheck, eligibility) => (0, assistantRuntimeGuards_1.applyEligibilityToGroundingCheck)(groundingCheck, eligibility));
const groundedAnswerEligibilityGuard = evaluateGroundedAnswerEligibilitySafe({
temporal: input.temporalGuard,
polarity: input.polarityAudit,
evidence: input.evidenceAudit,
claimAnchors: input.claimAnchorAudit,
targetedEvidenceHitRate: input.targetedEvidenceHitRate,
businessScopeResolved: input.businessScopeResolved
});
const groundingCheck = applyEligibilityToGroundingCheckSafe(input.groundingCheckBase, groundedAnswerEligibilityGuard);
return {
groundedAnswerEligibilityGuard,
groundingCheck
};
}
@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnPackagingInput = buildAssistantDeepTurnPackagingInput;
function buildAssistantDeepTurnPackagingInput(args) {
return {
...args,
routesForDebug: Array.isArray(args.routesForDebug) ? args.routesForDebug : [],
followupStateUsage: args.followupStateUsage ?? null,
composition: {
reply_type: args.composition.reply_type,
fallback_type: args.composition.fallback_type,
answer_structure_v11: args.composition.answer_structure_v11 ?? null,
problem_centric_answer_applied: args.composition.problem_centric_answer_applied ?? false,
problem_units_used_count: args.composition.problem_units_used_count ?? 0,
problem_answer_mode: args.composition.problem_answer_mode ?? "stage1_policy_v11",
problem_unit_ids_used: Array.isArray(args.composition.problem_unit_ids_used) ? args.composition.problem_unit_ids_used : []
}
};
}
@@ -0,0 +1,139 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.assembleAssistantDeepTurnPackaging = assembleAssistantDeepTurnPackaging;
const assistantEvidenceBundleAssembler_1 = require("./assistantEvidenceBundleAssembler");
const assistantContractsBundleAssembler_1 = require("./assistantContractsBundleAssembler");
const assistantDeepResponseAssembler_1 = require("./assistantDeepResponseAssembler");
const assistantDebugPayloadAssembler_1 = require("./assistantDebugPayloadAssembler");
const assistantMessageLogAssembler_1 = require("./assistantMessageLogAssembler");
function assembleAssistantDeepTurnPackaging(input) {
const normalizedPayload = (input.normalized.normalized ?? null);
const normalizedFragments = Array.isArray(normalizedPayload?.["fragments"]) ? normalizedPayload?.["fragments"] : [];
const evidenceBundleAssembly = (0, assistantEvidenceBundleAssembler_1.assembleAssistantEvidenceBundle)({
retrievalCalls: input.retrievalCalls,
retrievalResults: input.retrievalResults
});
const contractsBundleV1 = (0, assistantContractsBundleAssembler_1.assembleAssistantContractsBundleV1)({
userMessage: input.userMessage,
normalizedQuestion: input.normalizedQuestion,
normalized: input.normalized.normalized,
routeSummary: input.routeSummary,
droppedIntentSegments: input.droppedIntentSegments,
analysisContext: input.analysisContextForContract,
executionPlan: input.executionPlan,
requirements: input.requirementExtractionRequirements,
evidenceBundleContractV1: evidenceBundleAssembly.evidenceBundleContractV1,
replyType: input.composition.reply_type,
coverageReport: input.coverageReport,
grounding: input.groundingCheck,
retrievalResults: input.retrievalResults
});
const deepAnswerArtifacts = (0, assistantDeepResponseAssembler_1.buildDeepAnswerArtifacts)({
safeAssistantReplyBase: input.safeAssistantReplyBase,
featureContractsV11: input.featureContractsV11,
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
compositionAnswerStructureV11: input.composition.answer_structure_v11 ?? null,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
retrievalResults: input.retrievalResults
});
const debug = (0, assistantDebugPayloadAssembler_1.buildDeepAnalysisDebugPayload)({
traceId: input.normalized.trace_id,
promptVersion: input.normalized.prompt_version,
schemaVersion: input.normalized.schema_version,
fallbackType: input.composition.fallback_type,
routeSummary: input.routeSummary,
fragments: normalizedFragments,
requirementsExtracted: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
routes: input.routesForDebug,
retrievalStatus: evidenceBundleAssembly.retrievalStatus,
retrievalResults: input.retrievalResults,
groundingCheck: input.groundingCheck,
droppedIntentSegments: input.droppedIntentSegments,
questionTypeClass: input.questionTypeClass,
companyAnchors: input.companyAnchors,
runtimeAnalysisContext: input.runtimeAnalysisContext,
businessScopeResolution: input.businessScopeResolution,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceAudit: input.targetedEvidenceAudit,
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
faLiveRouteAudit: input.faLiveRouteAudit,
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
followupStateUsage: input.followupStateUsage,
compositionDebug: {
problem_centric_answer_applied: input.composition.problem_centric_answer_applied ?? false,
problem_units_used_count: input.composition.problem_units_used_count ?? 0,
problem_answer_mode: input.composition.problem_answer_mode ?? "stage1_policy_v11",
problem_unit_ids_used: Array.isArray(input.composition.problem_unit_ids_used) ? input.composition.problem_unit_ids_used : []
},
addressRuntimeMetaForDeep: input.addressRuntimeMetaForDeep,
outcomeClassV1: contractsBundleV1.outcomeClassV1,
assistantOrchestrationContractsV1: contractsBundleV1.assistantOrchestrationContractsV1,
answerStructureV11: deepAnswerArtifacts.answerStructureV11,
investigationStateSnapshot: input.investigationStateSnapshot,
normalizedPayload: normalizedPayload
});
const assistantItem = (0, assistantDeepResponseAssembler_1.buildAssistantConversationItem)({
messageId: input.messageId,
sessionId: input.sessionId,
text: deepAnswerArtifacts.safeAssistantReply,
replyType: input.composition.reply_type,
traceId: input.normalized.trace_id,
debug: debug
});
const deepAnalysisLogDetails = (0, assistantMessageLogAssembler_1.buildDeepAnalysisProcessedLogDetails)({
sessionId: input.sessionId,
messageId: input.messageId,
userMessage: input.userMessage,
normalizerOutput: input.normalized.normalized,
executionPlan: input.executionPlan,
resolvedExecutionState: input.resolvedExecutionState,
routes: input.routesForDebug,
retrievalCalls: input.retrievalCalls,
retrievalResultsRaw: input.retrievalResultsRaw,
retrievalResultsNormalized: input.retrievalResults,
requirementsExtracted: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
replyType: input.composition.reply_type,
droppedIntentSegments: input.droppedIntentSegments,
questionTypeClass: input.questionTypeClass,
companyAnchors: input.companyAnchors,
runtimeAnalysisContext: input.runtimeAnalysisContext,
businessScopeResolution: input.businessScopeResolution,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceAudit: input.targetedEvidenceAudit,
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
faLiveRouteAudit: input.faLiveRouteAudit,
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
followupStateUsage: input.followupStateUsage,
compositionDebug: {
problem_centric_answer_applied: input.composition.problem_centric_answer_applied ?? false,
problem_units_used_count: input.composition.problem_units_used_count ?? 0,
problem_answer_mode: input.composition.problem_answer_mode ?? "stage1_policy_v11",
problem_unit_ids_used: Array.isArray(input.composition.problem_unit_ids_used) ? input.composition.problem_unit_ids_used : [],
fallback_type: input.composition.fallback_type
},
outcomeClassV1: contractsBundleV1.outcomeClassV1,
assistantOrchestrationContractsV1: contractsBundleV1.assistantOrchestrationContractsV1,
answerStructureV11: deepAnswerArtifacts.answerStructureV11,
investigationStateSnapshot: input.investigationStateSnapshot,
assistantReply: deepAnswerArtifacts.safeAssistantReply,
traceId: input.normalized.trace_id
});
return {
evidenceBundleAssembly,
contractsBundleV1,
deepAnswerArtifacts,
debug,
assistantItem,
deepAnalysisLogDetails
};
}
@@ -0,0 +1,106 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantDeepTurnPackagingRuntime = runAssistantDeepTurnPackagingRuntime;
const nanoid_1 = require("nanoid");
const assistantDeepTurnInputBuilder_1 = require("./assistantDeepTurnInputBuilder");
const assistantDeepTurnPackaging_1 = require("./assistantDeepTurnPackaging");
const assistantDeepTurnPrePackagingContext_1 = require("./assistantDeepTurnPrePackagingContext");
const assistantInvestigationStateRuntimeAdapter_1 = require("./assistantInvestigationStateRuntimeAdapter");
function runAssistantDeepTurnPackagingRuntime(input) {
const buildPrePackagingContextSafe = input.buildPrePackagingContextFn ?? assistantDeepTurnPrePackagingContext_1.buildAssistantDeepTurnPrePackagingContext;
const buildInvestigationStateSnapshotSafe = input.buildInvestigationStateSnapshotFn ?? assistantInvestigationStateRuntimeAdapter_1.buildAssistantInvestigationStateSnapshot;
const persistInvestigationStateSnapshotSafe = input.persistInvestigationStateSnapshotFn ?? assistantInvestigationStateRuntimeAdapter_1.persistAssistantInvestigationStateSnapshot;
const buildDeepTurnPackagingInputSafe = input.buildDeepTurnPackagingInputFn ?? assistantDeepTurnInputBuilder_1.buildAssistantDeepTurnPackagingInput;
const assembleDeepTurnPackagingSafe = input.assembleDeepTurnPackagingFn ?? assistantDeepTurnPackaging_1.assembleAssistantDeepTurnPackaging;
const deepTurnPrePackagingContext = buildPrePackagingContextSafe({
normalizedPayload: input.normalized.normalized,
routeSummary: input.routeSummary,
runtimeAnalysisContext: input.runtimeAnalysisContext,
assistantReply: input.composition.assistant_reply,
extractDroppedIntentSegments: input.extractDroppedIntentSegments,
buildDebugRoutes: input.buildDebugRoutes,
extractExecutionState: input.extractExecutionState,
sanitizeReply: input.sanitizeReply
});
const investigationStateSnapshot = buildInvestigationStateSnapshotSafe({
featureEnabled: input.featureInvestigationStateV1,
previousState: input.previousInvestigationState,
timestamp: input.nowIso ? input.nowIso() : new Date().toISOString(),
questionId: input.questionId,
userMessage: input.userMessage,
routeSummary: input.routeSummary,
requirements: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
retrievalResults: input.retrievalResults,
replyType: input.composition.reply_type,
followupApplied: input.followupApplied
});
persistInvestigationStateSnapshotSafe({
featureEnabled: input.featureInvestigationStateV1,
sessionId: input.sessionId,
snapshot: investigationStateSnapshot,
persist: input.persistInvestigationState
});
const messageId = input.messageIdFactory ? input.messageIdFactory() : `msg-${(0, nanoid_1.nanoid)(10)}`;
const deepTurnPackagingInput = buildDeepTurnPackagingInputSafe({
sessionId: input.sessionId,
messageId,
userMessage: input.userMessage,
normalized: input.normalized,
normalizedQuestion: input.normalizedQuestion,
routeSummary: input.routeSummary,
droppedIntentSegments: deepTurnPrePackagingContext.droppedIntentSegments,
analysisContextForContract: deepTurnPrePackagingContext.analysisContextForContract,
executionPlan: input.executionPlan,
requirementExtractionRequirements: input.requirementExtractionRequirements,
coverageEvaluationRequirements: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
retrievalCalls: input.retrievalCalls,
retrievalResultsRaw: input.retrievalResultsRaw,
retrievalResults: input.retrievalResults,
routesForDebug: deepTurnPrePackagingContext.routesForDebug,
resolvedExecutionState: deepTurnPrePackagingContext.resolvedExecutionState,
questionTypeClass: input.questionTypeClass,
companyAnchors: input.companyAnchors,
runtimeAnalysisContext: input.runtimeAnalysisContext,
businessScopeResolution: input.businessScopeResolution,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceAudit: input.targetedEvidenceAudit,
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
faLiveRouteAudit: input.faLiveRouteAudit,
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
followupStateUsage: input.followupStateUsage,
composition: {
reply_type: input.composition.reply_type,
fallback_type: input.composition.fallback_type,
answer_structure_v11: input.composition.answer_structure_v11,
problem_centric_answer_applied: input.composition.problem_centric_answer_applied,
problem_units_used_count: input.composition.problem_units_used_count,
problem_answer_mode: input.composition.problem_answer_mode,
problem_unit_ids_used: input.composition.problem_unit_ids_used
},
safeAssistantReplyBase: deepTurnPrePackagingContext.safeAssistantReplyBase,
featureContractsV11: input.featureContractsV11,
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
investigationStateSnapshot,
addressRuntimeMetaForDeep: input.addressRuntimeMetaForDeep
});
const deepTurnPackaging = assembleDeepTurnPackagingSafe(deepTurnPackagingInput);
return {
messageId,
investigationStateSnapshot,
droppedIntentSegments: deepTurnPrePackagingContext.droppedIntentSegments,
analysisContextForContract: deepTurnPrePackagingContext.analysisContextForContract,
routesForDebug: deepTurnPrePackagingContext.routesForDebug,
resolvedExecutionState: deepTurnPrePackagingContext.resolvedExecutionState,
safeAssistantReplyBase: deepTurnPrePackagingContext.safeAssistantReplyBase,
safeAssistantReply: deepTurnPackaging.deepAnswerArtifacts.safeAssistantReply,
debug: deepTurnPackaging.debug,
assistantItem: deepTurnPackaging.assistantItem,
deepAnalysisLogDetails: deepTurnPackaging.deepAnalysisLogDetails
};
}
@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnExecutionPlan = buildAssistantDeepTurnExecutionPlan;
function buildAssistantDeepTurnExecutionPlan(input) {
const requirementExtraction = input.extractRequirements(input.routeSummary, input.normalizedPayload, input.userMessage);
let executionPlan = input.toExecutionPlan(input.routeSummary, input.normalizedPayload, input.userMessage, requirementExtraction.byFragment);
const rbpRoutePlanEnforcement = input.enforceRbpLiveRoutePlan({
executionPlan,
claimType: input.claimType,
temporalGuard: input.temporalGuard
});
executionPlan = rbpRoutePlanEnforcement.executionPlan;
const faRoutePlanEnforcement = input.enforceFaLiveRoutePlan({
executionPlan,
claimType: input.claimType,
temporalGuard: input.temporalGuard
});
executionPlan = faRoutePlanEnforcement.executionPlan;
executionPlan = input.applyTemporalHintToExecutionPlan(executionPlan, input.temporalGuard);
executionPlan = input.applyPolarityHintToExecutionPlan(executionPlan, input.domainPolarityGuardInitial);
return {
requirementExtraction,
executionPlan,
rbpRoutePlanEnforcement,
faRoutePlanEnforcement
};
}
@@ -0,0 +1,20 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnPrePackagingContext = buildAssistantDeepTurnPrePackagingContext;
function buildAssistantDeepTurnPrePackagingContext(input) {
return {
droppedIntentSegments: input.extractDroppedIntentSegments(input.normalizedPayload),
analysisContextForContract: input.runtimeAnalysisContext.active
? {
as_of_date: input.runtimeAnalysisContext.as_of_date,
period_from: input.runtimeAnalysisContext.period_from,
period_to: input.runtimeAnalysisContext.period_to,
source: input.runtimeAnalysisContext.source,
snapshot_mode: input.runtimeAnalysisContext.snapshot_mode
}
: null,
routesForDebug: input.buildDebugRoutes(input.routeSummary),
resolvedExecutionState: input.extractExecutionState(input.normalizedPayload),
safeAssistantReplyBase: input.sanitizeReply(input.assistantReply, "Нужны уточнения для надежного ответа.")
};
}
@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnSuccessResponse = buildAssistantDeepTurnSuccessResponse;
function buildAssistantDeepTurnSuccessResponse(input) {
return {
ok: true,
session_id: input.sessionId,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
conversation_item: input.conversationItem,
debug: input.debug,
conversation: input.conversation
};
}
@@ -0,0 +1,78 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeAssistantDeepTurnRetrievalPlan = executeAssistantDeepTurnRetrievalPlan;
const retrievalResultNormalizer_1 = require("./retrievalResultNormalizer");
function buildRouteExecutorErrorRawResult(route, message) {
return {
status: "error",
result_type: "summary",
items: [],
summary: {
route
},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "low",
limitations: ["Route executor failed."],
errors: [message]
};
}
async function executeAssistantDeepTurnRetrievalPlan(input) {
const normalizeRetrievalResultSafe = input.normalizeRetrievalResultFn ?? retrievalResultNormalizer_1.normalizeRetrievalResult;
const retrievalCalls = [];
const retrievalResultsRaw = [];
const retrievalResults = [];
for (const planItem of input.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: input.mapNoRouteReason(planItem.no_route_reason)
});
retrievalResults.push(input.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 input.executeRouteRuntime(planItem.route, planItem.fragment_text, {
temporalHint: input.liveTemporalHint
});
retrievalResultsRaw.push({
fragment_id: planItem.fragment_id,
route: planItem.route,
raw_result: raw
});
retrievalResults.push(normalizeRetrievalResultSafe(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 = buildRouteExecutorErrorRawResult(planItem.route, message);
retrievalResultsRaw.push({
fragment_id: planItem.fragment_id,
route: planItem.route,
raw_result: rawError
});
retrievalResults.push(normalizeRetrievalResultSafe(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError));
}
}
return {
retrievalCalls,
retrievalResultsRaw,
retrievalResults
};
}
@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.assembleAssistantEvidenceBundle = assembleAssistantEvidenceBundle;
const assistantOrchestrationContracts_1 = require("./assistantOrchestrationContracts");
function buildRetrievalStatus(retrievalResults) {
return retrievalResults.map((item) => ({
fragment_id: item.fragment_id,
requirement_ids: item.requirement_ids,
route: item.route,
status: item.status,
result_type: item.result_type
}));
}
function assembleAssistantEvidenceBundle(input) {
const retrievalResults = Array.isArray(input.retrievalResults) ? input.retrievalResults : [];
return {
evidenceBundleContractV1: (0, assistantOrchestrationContracts_1.buildAssistantEvidenceBundleContractV1)({
retrievalCalls: Array.isArray(input.retrievalCalls) ? input.retrievalCalls : [],
retrievalResults
}),
retrievalStatus: buildRetrievalStatus(retrievalResults)
};
}
@@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantInvestigationStateSnapshot = buildAssistantInvestigationStateSnapshot;
exports.persistAssistantInvestigationStateSnapshot = persistAssistantInvestigationStateSnapshot;
const investigationState_1 = require("./investigationState");
function buildAssistantInvestigationStateSnapshot(input) {
if (!input.featureEnabled || !input.previousState) {
return null;
}
return (0, investigationState_1.updateInvestigationState)({
previous: input.previousState,
timestamp: input.timestamp,
questionId: input.questionId,
userMessage: input.userMessage,
routeSummary: input.routeSummary,
requirements: input.requirements,
coverageReport: input.coverageReport,
retrievalResults: input.retrievalResults,
replyType: input.replyType,
followupApplied: input.followupApplied
});
}
function persistAssistantInvestigationStateSnapshot(input) {
if (!input.featureEnabled || !input.snapshot) {
return false;
}
input.persist(input.sessionId, input.snapshot);
return true;
}
@@ -0,0 +1,102 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildDeepAnalysisProcessedLogDetails = buildDeepAnalysisProcessedLogDetails;
function toAnalysisContext(input) {
if (!input.active) {
return null;
}
return {
as_of_date: input.as_of_date,
period_from: input.period_from,
period_to: input.period_to,
source: input.source,
snapshot_mode: input.snapshot_mode
};
}
function resolveCoverageStatus(coverageReport) {
return coverageReport.requirements_total === coverageReport.requirements_covered &&
coverageReport.requirements_uncovered.length === 0 &&
coverageReport.requirements_partially_covered.length === 0
? "full"
: "partial_or_limited";
}
function buildDeepAnalysisProcessedLogDetails(input) {
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
return {
session_id: input.sessionId,
message_id: input.messageId,
user_message: input.userMessage,
normalizer_output: input.normalizerOutput,
execution_plan: input.executionPlan,
resolved_execution_state: input.resolvedExecutionState,
routes: input.routes,
retrieval_calls: input.retrievalCalls,
retrieval_results_raw: input.retrievalResultsRaw,
retrieval_results_normalized: input.retrievalResultsNormalized,
requirements_extracted: input.requirementsExtracted,
requirements_total: input.coverageReport.requirements_total,
requirements_covered: input.coverageReport.requirements_covered,
requirements_uncovered: input.coverageReport.requirements_uncovered,
coverage_status: resolveCoverageStatus(input.coverageReport),
answer_grounding_status: input.groundingCheck.status,
reply_semantic_type: input.replyType,
why_included_summary: input.groundingCheck.why_included_summary,
selection_reason_summary: input.groundingCheck.selection_reason_summary,
route_subject_match: input.groundingCheck.route_subject_match,
clarification_target: input.coverageReport.clarification_needed_for,
dropped_intent_segments: input.droppedIntentSegments,
question_type_class: input.questionTypeClass,
company_anchors: input.companyAnchors,
analysis_context_applied: input.runtimeAnalysisContext.active,
analysis_context: analysisContext,
business_scope_raw: input.businessScopeResolution.business_scope_raw,
business_scope_resolved: input.businessScopeResolution.business_scope_resolved,
company_grounding_applied: input.businessScopeResolution.company_grounding_applied,
scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
company_scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
raw_time_anchor: input.temporalGuard.raw_time_anchor,
raw_time_scope: input.temporalGuard.raw_time_scope,
resolved_time_anchor: input.temporalGuard.resolved_time_anchor,
resolved_primary_period: input.temporalGuard.resolved_primary_period,
effective_primary_period: input.temporalGuard.effective_primary_period,
temporal_guard_input: input.temporalGuard.temporal_guard_input,
temporal_alignment_status: input.temporalGuard.temporal_alignment_status,
temporal_resolution_source: input.temporalGuard.temporal_resolution_source,
temporal_guard_basis: input.temporalGuard.temporal_guard_basis,
temporal_guard_applied: input.temporalGuard.temporal_guard_applied,
temporal_guard_outcome: input.temporalGuard.temporal_guard_outcome,
temporal_guard: input.temporalGuard,
raw_numeric_tokens: input.polarityAudit.raw_numeric_tokens,
classified_numeric_tokens: input.polarityAudit.classified_numeric_tokens,
rejected_as_non_accounts: input.polarityAudit.rejected_as_non_accounts,
resolved_account_anchors: input.polarityAudit.resolved_account_anchors,
domain_polarity_guard: input.polarityAudit,
claim_anchor_audit: input.claimAnchorAudit,
settlement_role: input.claimAnchorAudit.settlement_role ?? null,
settlement_role_resolution_reason: input.claimAnchorAudit.settlement_role_resolution_reason ?? [],
polarity_resolution_status: input.claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
targeted_evidence_acquisition: input.targetedEvidenceAudit,
evidence_admissibility_gate: input.evidenceAdmissibilityGateAudit,
...(input.rbpLiveRouteAudit ? { rbp_live_route_audit: input.rbpLiveRouteAudit } : {}),
...(input.faLiveRouteAudit ? { fa_live_route_audit: input.faLiveRouteAudit } : {}),
eligibility_time_basis: input.groundedAnswerEligibilityGuard.eligibility_time_basis,
grounded_answer_eligibility_guard: input.groundedAnswerEligibilityGuard,
...(input.followupStateUsage ? { followup_state_usage: input.followupStateUsage } : {}),
problem_centric_answer_applied: input.compositionDebug.problem_centric_answer_applied ?? false,
problem_units_used_count: input.compositionDebug.problem_units_used_count ?? 0,
problem_answer_mode: input.compositionDebug.problem_answer_mode ?? "stage1_policy_v11",
...(Array.isArray(input.compositionDebug.problem_unit_ids_used) && input.compositionDebug.problem_unit_ids_used.length > 0
? {
problem_unit_ids_used: input.compositionDebug.problem_unit_ids_used
}
: {}),
assistant_outcome_class_v1: input.outcomeClassV1,
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
answer_structure_v11: input.answerStructureV11,
investigation_state_snapshot: input.investigationStateSnapshot,
fallback_type: input.compositionDebug.fallback_type,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
trace_id: input.traceId
};
}
@@ -0,0 +1,148 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.classifyAssistantOutcomeClassV1 = classifyAssistantOutcomeClassV1;
exports.buildAssistantQueryFrameContractV1 = buildAssistantQueryFrameContractV1;
exports.buildAssistantExecutionPlanContractV1 = buildAssistantExecutionPlanContractV1;
exports.buildAssistantEvidenceBundleContractV1 = buildAssistantEvidenceBundleContractV1;
exports.buildAssistantCoverageContractV1 = buildAssistantCoverageContractV1;
function normalizeSnapshotMode(value) {
const token = String(value ?? "").trim();
if (token === "force_snapshot" || token === "force_live") {
return token;
}
return "auto";
}
function extractFragmentsTotal(normalized) {
if (!normalized || typeof normalized !== "object") {
return 0;
}
const source = normalized;
const fragments = source.fragments;
return Array.isArray(fragments) ? fragments.length : 0;
}
function collectEvidenceTotals(retrievalResults) {
let evidenceTotal = 0;
const sourceRefs = new Set();
let limitationTotal = 0;
let errorTotal = 0;
for (const result of retrievalResults) {
evidenceTotal += Array.isArray(result.evidence) ? result.evidence.length : 0;
limitationTotal += Array.isArray(result.limitations) ? result.limitations.length : 0;
errorTotal += Array.isArray(result.errors) ? result.errors.length : 0;
for (const evidence of result.evidence ?? []) {
const ref = String(evidence?.source_ref?.canonical_ref ?? "").trim();
if (ref) {
sourceRefs.add(ref);
}
}
}
return {
evidence_total: evidenceTotal,
source_refs_total: sourceRefs.size,
limitation_total: limitationTotal,
error_total: errorTotal
};
}
function classifyAssistantOutcomeClassV1(input) {
const replyType = input.replyType;
const grounding = input.grounding;
const coverage = input.coverageReport;
const hasOnlyErrors = input.retrievalResults.length > 0 &&
input.retrievalResults.every((item) => item.status === "error");
if (replyType === "backend_error" || hasOnlyErrors) {
return "BLOCKED_BY_TOOLING";
}
if (grounding.status === "route_mismatch_blocked" || replyType === "route_mismatch_blocked") {
return "MISROUTED";
}
if (replyType === "clarification_required" || coverage.clarification_needed_for.length > 0) {
return "BLOCKED_BY_AMBIGUITY";
}
if (replyType === "out_of_scope") {
return "BLOCKED_BY_AMBIGUITY";
}
const fullCoverage = coverage.requirements_total > 0 &&
coverage.requirements_total === coverage.requirements_covered &&
coverage.requirements_uncovered.length === 0 &&
coverage.requirements_partially_covered.length === 0 &&
coverage.clarification_needed_for.length === 0 &&
coverage.out_of_scope_requirements.length === 0;
if (fullCoverage && grounding.status === "grounded") {
return "FULLY_ANSWERED";
}
const hasAnyCoverage = coverage.requirements_covered > 0 ||
coverage.requirements_partially_covered.length > 0 ||
grounding.status === "partial";
if (hasAnyCoverage) {
return "PARTIALLY_ANSWERED";
}
const missingRequirementSignal = grounding.missing_requirements.length > 0 ||
coverage.requirements_uncovered.length > 0 ||
coverage.requirements_total > 0;
const possibleBindingFailure = replyType === "no_grounded_answer" &&
missingRequirementSignal &&
grounding.route_subject_match;
if (possibleBindingFailure) {
return "FAILED_TO_BIND_ENTITIES";
}
return "BLOCKED_BY_MISSING_DATA";
}
function buildAssistantQueryFrameContractV1(input) {
const analysis = input.analysisContext
? {
as_of_date: input.analysisContext.as_of_date ?? null,
period_from: input.analysisContext.period_from ?? null,
period_to: input.analysisContext.period_to ?? null,
source: input.analysisContext.source ?? null,
snapshot_mode: normalizeSnapshotMode(input.analysisContext.snapshot_mode)
}
: null;
return {
schema_version: "assistant_query_frame_v1",
original_user_question: String(input.userMessage ?? ""),
normalized_question: String(input.normalizedQuestion ?? ""),
route_summary_mode: input.routeSummary?.mode ?? "none",
fragments_total: extractFragmentsTotal(input.normalized),
dropped_intent_segments: Array.isArray(input.droppedIntentSegments) ? [...input.droppedIntentSegments] : [],
analysis_context: analysis
};
}
function buildAssistantExecutionPlanContractV1(input) {
return {
schema_version: "assistant_execution_plan_v1",
steps: (Array.isArray(input.executionPlan) ? input.executionPlan : []).map((item) => ({
fragment_id: String(item.fragment_id ?? ""),
route: String(item.route ?? ""),
should_execute: Boolean(item.should_execute),
requirement_ids: Array.isArray(item.requirement_ids) ? [...item.requirement_ids] : [],
no_route_reason: item.no_route_reason ?? null,
clarification_reason: item.clarification_reason ?? null
})),
requirements_total: Array.isArray(input.requirements) ? input.requirements.length : 0
};
}
function buildAssistantEvidenceBundleContractV1(input) {
const retrievalResults = Array.isArray(input.retrievalResults) ? input.retrievalResults : [];
const breakdown = {
ok: retrievalResults.filter((item) => item.status === "ok").length,
partial: retrievalResults.filter((item) => item.status === "partial").length,
empty: retrievalResults.filter((item) => item.status === "empty").length,
error: retrievalResults.filter((item) => item.status === "error").length
};
const totals = collectEvidenceTotals(retrievalResults);
return {
schema_version: "assistant_evidence_bundle_v1",
retrieval_calls_total: Array.isArray(input.retrievalCalls) ? input.retrievalCalls.length : 0,
retrieval_results_total: retrievalResults.length,
retrieval_status_breakdown: breakdown,
...totals
};
}
function buildAssistantCoverageContractV1(input) {
return {
schema_version: "assistant_coverage_contract_v1",
coverage_report: input.coverageReport,
grounding: input.grounding,
outcome_class: input.outcomeClass
};
}
@@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantCoverageGroundingPipeline = runAssistantCoverageGroundingPipeline;
function runAssistantCoverageGroundingPipeline(input) {
const requirementExtraction = input.requirementExtraction ?? input.extractRequirements(input.routeSummary, input.normalized, input.userMessage);
const coverageEvaluation = input.evaluateCoverage(requirementExtraction.requirements, input.retrievalResults);
const groundingCheckBase = input.checkGrounding(input.userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, input.retrievalResults);
return {
requirementExtraction,
coverageEvaluation,
groundingCheckBase
};
}
@@ -0,0 +1,106 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildFragmentTextById = buildFragmentTextById;
exports.buildExecutionPlanFromRoute = buildExecutionPlanFromRoute;
exports.buildDebugRoutesFromRoute = buildDebugRoutesFromRoute;
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 buildFragmentTextById(fragments) {
const result = new Map();
for (const item of fragments) {
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 buildExecutionPlanFromRoute(input) {
if (!input.routeSummary) {
return [];
}
if (input.routeSummary.mode === "legacy_v1") {
return [
{
fragment_id: "F1",
requirement_ids: input.requirementByFragment.get("F1") ?? ["R1"],
route: input.routeSummary.route_hint,
should_execute: true,
fragment_text: input.userMessage,
no_route_reason: null,
clarification_reason: null
}
];
}
return input.routeSummary.decisions.map((decision) => {
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.userMessage;
if (decision.route === "no_route") {
return {
fragment_id: decision.fragment_id,
requirement_ids: input.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: input.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 buildDebugRoutesFromRoute(input) {
if (!input.routeSummary) {
return [];
}
if (input.routeSummary.mode === "legacy_v1") {
return [
{
fragment_id: "F1",
route: input.routeSummary.route_hint,
reason: input.resolveLegacyRouteReason(input.routeSummary.route_hint),
confidence: input.routeSummary.confidence,
intent_class: input.routeSummary.intent_class
}
];
}
return input.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
}));
}
@@ -584,7 +584,78 @@ function toTemporalGuardInput(window, fallback) {
const value = String(fallback ?? "").trim();
return value || null;
}
function normalizeIsoDate(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function normalizeTemporalWindow(input) {
const asOfDate = normalizeIsoDate(input.asOfDate);
if (asOfDate) {
return {
from: asOfDate,
to: asOfDate,
granularity: "day"
};
}
const from = normalizeIsoDate(input.periodFrom);
const to = normalizeIsoDate(input.periodTo);
if (!from || !to) {
return null;
}
return {
from,
to,
granularity: from === to ? "day" : "month"
};
}
function resolveTemporalGuard(input) {
const analysisWindow = normalizeTemporalWindow({
asOfDate: input.analysisContext?.as_of_date,
periodFrom: input.analysisContext?.period_from,
periodTo: input.analysisContext?.period_to
});
if (analysisWindow) {
const source = String(input.analysisContext?.source ?? "").trim() || "analysis_context";
const guardInput = toTemporalGuardInput(analysisWindow, analysisWindow.from);
return {
raw_time_anchor: analysisWindow.from,
raw_time_scope: guardInput,
resolved_time_anchor: analysisWindow.granularity === "day" ? analysisWindow.from : null,
resolved_primary_period: analysisWindow,
effective_primary_period: analysisWindow,
temporal_guard_input: guardInput,
temporal_alignment_status: "aligned",
temporal_resolution_source: source,
temporal_guard_basis: "raw_time_scope_unlocked",
temporal_guard_applied: false,
temporal_guard_outcome: "passed",
primary_period_window: null,
allowed_context_window: null,
controlled_temporal_expansion_enabled: false,
context_expansion_reasons_allowed: ["prehistory", "carryover", "post_period_closure", "long_running_contract_context"],
normalized_anchor_drift_detected: false,
reason_codes: ["analysis_context_applied"]
};
}
const rawAnchorText = collectRawTemporalAnchorText(input.userMessage, input.companyAnchors);
const julyAnchor = resolveJulyAnchor(rawAnchorText);
const normalizedAnchor = normalizedAnchorFromFragments(input.normalized);
@@ -654,9 +725,14 @@ function applyTemporalHintToExecutionPlan(executionPlan, temporal) {
return executionPlan;
}
const primaryWindow = temporal.effective_primary_period ?? temporal.primary_period_window;
const periodLabel = primaryWindow
? `${primaryWindow.from}..${primaryWindow.to}`
: temporal.resolved_time_anchor
? temporal.resolved_time_anchor
: "active_period";
const hint = primaryWindow?.granularity === "day" && temporal.resolved_time_anchor
? `primary period ${temporal.resolved_time_anchor}; controlled temporal expansion only for linked entities`
: `primary period July 2020 (${primaryWindow?.from ?? JULY_WINDOW.from}..${primaryWindow?.to ?? JULY_WINDOW.to}); controlled temporal expansion only for linked entities`;
: `primary period ${periodLabel}; controlled temporal expansion only for linked entities`;
return executionPlan.map((item) => {
if (!item.should_execute) {
return item;
@@ -1319,15 +1395,15 @@ function applyEligibilityToGroundingCheck(groundingCheck, eligibility) {
? "no_grounded_answer"
: "partial";
const reasonMap = {
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.",
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
admissible_evidence_count_zero: "Недостаточно подтвержденных данных для уверенного ответа.",
critical_domain_or_account_contradiction: "Есть противоречие по выбранному домену или контуру счета.",
temporal_guard_failed_out_of_snapshot_window: "Запрошенный период выходит за доступный срез данных.",
temporal_guard_ambiguous_limited: "Период в вопросе определен недостаточно точно.",
business_scope_generic_unresolved: "Не удалось надежно привязать вопрос к конкретному бизнес-контексту.",
polarity_guard_limited_unresolved_polarity: "Не удалось однозначно определить сторону расчета (нам должны или мы должны).",
polarity_guard_blocked_conflict: "В данных есть конфликт по стороне расчета.",
claim_anchor_coverage_insufficient: "Не хватает ключевых ориентиров в вопросе (период, объект или контрагент).",
targeted_evidence_hit_rate_zero: "Не хватило целевых подтверждений по выбранному сценарию."
};
const reasons = [
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.commitAssistantTurnAndLog = commitAssistantTurnAndLog;
function commitAssistantTurnAndLog(input) {
input.appendItem(input.sessionId, input.assistantItem);
const currentSession = input.getSession(input.sessionId);
if (currentSession) {
input.persistSession(currentSession);
}
const conversation = input.cloneConversation(currentSession?.items ?? []);
input.logEvent({
timestamp: (input.nowIso ?? (() => new Date().toISOString()))(),
level: "info",
service: "assistant_loop",
message: "assistant_message_processed",
sessionId: input.sessionId,
eventType: input.eventType,
details: input.logDetails
});
return {
currentSession,
conversation
};
}
+64
View File
@@ -169,6 +169,29 @@ function parseRawQuestions(rawQuestions) {
.filter(Boolean);
return byLine.length > 0 ? byLine : [text];
}
function normalizeAnalysisDate(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function executionReadinessOf(fragment) {
return "execution_readiness" in fragment ? fragment.execution_readiness : "executable";
}
@@ -759,6 +782,13 @@ class EvalService {
...payload.normalizeConfig,
userQuestion: item.raw_question,
context: {
period_hint: payload.analysisDate ?? undefined,
analysis_context: payload.analysisDate
? {
as_of_date: payload.analysisDate,
source: "eval_analysis_date"
}
: undefined,
eval_label: runId,
case_id: item.case_id,
eval_mode: payload.mode
@@ -1553,6 +1583,7 @@ class EvalService {
const suite = parseAssistantSuiteFile(payload.caseSetFile);
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage1-${(0, nanoid_1.nanoid)(10)}`;
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
const assistantService = new assistantService_1.AssistantService(this.normalizerService, new assistantSessionStore_1.AssistantSessionStore());
const diagnostics = [];
let requestsTotal = 0;
@@ -1579,6 +1610,15 @@ class EvalService {
developerPrompt: payload.normalizeConfig.developerPrompt,
domainPrompt: payload.normalizeConfig.domainPrompt,
fewShotExamples: payload.normalizeConfig.fewShotExamples,
context: analysisDate
? {
period_hint: analysisDate,
analysis_context: {
as_of_date: analysisDate,
source: "eval_analysis_date"
}
}
: undefined,
useMock: payload.useMock
}));
turnResponses.push(response);
@@ -1820,6 +1860,7 @@ class EvalService {
eval_target: "assistant_stage1",
mode: payload.mode,
use_mock: Boolean(payload.useMock),
analysis_date: analysisDate,
prompt_version: payload.normalizeConfig.promptVersion ?? null,
suite_id: suite.suite_id,
suite_version: suite.suite_version,
@@ -1887,6 +1928,7 @@ class EvalService {
const suite = parseAssistantStage2SuiteFile(payload.caseSetFile);
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage2-${(0, nanoid_1.nanoid)(10)}`;
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
const assistantService = new assistantService_1.AssistantService(this.normalizerService, new assistantSessionStore_1.AssistantSessionStore());
const diagnostics = [];
let requestsTotal = 0;
@@ -1915,6 +1957,15 @@ class EvalService {
developerPrompt: payload.normalizeConfig.developerPrompt,
domainPrompt: payload.normalizeConfig.domainPrompt,
fewShotExamples: payload.normalizeConfig.fewShotExamples,
context: analysisDate
? {
period_hint: analysisDate,
analysis_context: {
as_of_date: analysisDate,
source: "eval_analysis_date"
}
}
: undefined,
useMock: payload.useMock
}));
turnResponses.push(response);
@@ -2090,6 +2141,7 @@ class EvalService {
eval_target: "assistant_stage2",
mode: payload.mode,
use_mock: Boolean(payload.useMock),
analysis_date: analysisDate,
prompt_version: payload.normalizeConfig.promptVersion ?? null,
suite_id: suite.suite_id,
suite_version: suite.suite_version,
@@ -2172,6 +2224,7 @@ class EvalService {
async run(payload) {
const mode = payload.mode ?? "standard";
const evalTarget = payload.evalTarget ?? "normalizer";
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
if (evalTarget === "assistant_stage1") {
return this.runAssistantStage1({
normalizeConfig: payload.normalizeConfig,
@@ -2180,6 +2233,7 @@ class EvalService {
mode,
caseSetFile: payload.caseSetFile,
compareWithReportFile: payload.compareWithReportFile,
analysisDate: analysisDate ?? undefined,
runId: payload.runId
});
}
@@ -2191,6 +2245,7 @@ class EvalService {
mode,
caseSetFile: payload.caseSetFile,
compareWithReportFile: payload.compareWithReportFile,
analysisDate: analysisDate ?? undefined,
runId: payload.runId
});
}
@@ -2231,6 +2286,7 @@ class EvalService {
return this.runV2({
...payload,
mode,
analysisDate: analysisDate ?? undefined,
cases: filtered
});
}
@@ -2256,6 +2312,13 @@ class EvalService {
...payload.normalizeConfig,
userQuestion: item.raw_question,
context: {
period_hint: analysisDate ?? undefined,
analysis_context: analysisDate
? {
as_of_date: analysisDate,
source: "eval_analysis_date"
}
: undefined,
expected_route: item.expected.route_hint,
eval_label: runId,
case_id: item.case_id,
@@ -2366,6 +2429,7 @@ class EvalService {
timestamp: new Date().toISOString(),
mode,
use_mock: Boolean(payload.useMock),
analysis_date: analysisDate,
prompt_version: payload.normalizeConfig.promptVersion ?? null,
dataset: {
source: payload.caseSetFile ? "file" : "data/eval_cases/*.json",
@@ -99,7 +99,7 @@ function resolveQuestionType(input) {
if (bestType !== "unknown") {
return bestType;
}
if (/[?пјџ]/u.test(text)) {
if (/(?:\bwhy\b|почему|из-?за\s+чего|в\s+ч(?:е|ё)м\s+причина)/iu.test(text)) {
return "why_breaks";
}
return "unknown";
@@ -93,6 +93,7 @@ interface RunSummary {
llm_provider: string | null;
model: string | null;
use_mock: boolean | null;
analysis_date: string | null;
prompt_version: string | null;
schema_version: string | null;
suite_id: string | null;
@@ -1012,6 +1013,7 @@ function buildRunSummary(run: IndexedRun): RunSummary {
llm_provider: llmProvider,
model,
use_mock: toBooleanSafe(run.report.use_mock),
analysis_date: toStringSafe(run.report.analysis_date),
prompt_version: toStringSafe(run.report.prompt_version),
schema_version: toStringSafe(run.report.schema_version),
suite_id: toStringSafe(run.report.suite_id),
+37 -2
View File
@@ -35,6 +35,7 @@ interface EvalAsyncJob {
eval_target: EvalTarget;
run_id: string;
case_set_file: string | null;
analysis_date: string | null;
total_cases: number;
completed_cases: number;
cases: EvalAsyncCaseInfo[];
@@ -131,6 +132,32 @@ function normalizeCaseIds(value: unknown): string[] | undefined {
return normalized.length > 0 ? normalized : undefined;
}
function normalizeAnalysisDate(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return undefined;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return undefined;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (
candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day
) {
return undefined;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function buildEvalPayloadFromBody(body: Record<string, unknown>): {
normalizeConfig: Omit<NormalizeRequestPayload, "userQuestion" | "context">;
caseIds?: string[];
@@ -140,7 +167,11 @@ function buildEvalPayloadFromBody(body: Record<string, unknown>): {
rawQuestions?: string;
evalTarget: EvalTarget;
compareWithReportFile?: string;
analysisDate?: string;
} {
const analysisDate =
normalizeAnalysisDate(body.analysis_date) ??
normalizeAnalysisDate(body.analysisDate);
return {
normalizeConfig: (body.normalizeConfig ?? {}) as Omit<NormalizeRequestPayload, "userQuestion" | "context">,
caseIds: normalizeCaseIds(body.caseIds),
@@ -154,7 +185,8 @@ function buildEvalPayloadFromBody(body: Record<string, unknown>): {
? body.compare_with_report_file
: typeof body.comparisonBaselineReportFile === "string"
? body.comparisonBaselineReportFile
: undefined
: undefined,
analysisDate
};
}
@@ -300,6 +332,7 @@ function snapshotJob(job: EvalAsyncJob): Record<string, unknown> {
eval_target: job.eval_target,
run_id: job.run_id,
case_set_file: job.case_set_file,
analysis_date: job.analysis_date,
total_cases: job.total_cases,
completed_cases: job.completed_cases,
error: job.error,
@@ -314,7 +347,8 @@ function snapshotJob(job: EvalAsyncJob): Record<string, unknown> {
: toRecord(job.report.metrics) && typeof toRecord(job.report.metrics)?.score_index === "number"
? Number(toRecord(job.report.metrics)?.score_index)
: null,
cases_total: typeof job.report.cases_total === "number" ? Number(job.report.cases_total) : null
cases_total: typeof job.report.cases_total === "number" ? Number(job.report.cases_total) : null,
analysis_date: toStringSafe(job.report.analysis_date) ?? job.analysis_date
}
: null
};
@@ -377,6 +411,7 @@ export function buildEvalRouter(services: AppServices): Router {
eval_target: payload.evalTarget,
run_id: runId,
case_set_file: runtimeCaseSetFile,
analysis_date: payload.analysisDate ?? null,
total_cases: caseSeeds.length,
completed_cases: 0,
cases: caseSeeds.map((item) => ({
@@ -490,8 +490,20 @@ function extractLooseByAnchorValue(text: string): string | undefined {
}
const lowered = token.toLowerCase();
const stopWords = new Set([
"какой",
"какая",
"какие",
"каких",
"каким",
"какими",
"каком",
"кто",
"что",
"мы",
"видим",
"контрагенту",
"контрагента",
"контрагентам",
"контре",
"компании",
"компанию",
@@ -499,10 +511,14 @@ function extractLooseByAnchorValue(text: string): string | undefined {
"организацию",
"поставщику",
"поставщика",
"поставщикам",
"клиенту",
"клиента",
"клиентам",
"покупателю",
"покупателя",
"покупателям",
"заказчикам",
"партнеру",
"партнера",
"договору",
@@ -618,6 +634,9 @@ function isLikelyCounterpartyToken(rawToken: string): boolean {
"какая",
"какое",
"каких",
"каким",
"какими",
"каком",
"какому",
"какую",
"кто",
@@ -632,6 +651,8 @@ function isLikelyCounterpartyToken(rawToken: string): boolean {
"чья",
"чей",
"чью",
"мы",
"видим",
"самый",
"самая",
"самое",
@@ -686,10 +707,23 @@ function isLikelyCounterpartyToken(rawToken: string): boolean {
"контрагент",
"контрагенту",
"контрагента",
"контрагентам",
"компания",
"компании",
"организация",
"организации",
"поставщикам",
"клиентам",
"покупателям",
"заказчикам",
"аванс",
"авансы",
"проблемный",
"проблемные",
"проблемным",
"закрытия",
"закрыть",
"закрыты",
"год",
"года",
"г",
@@ -795,7 +829,7 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
return true;
}
const questionCue =
/(?:кто|что|какой|какая|какие|какого|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
/(?:кто|что|какой|какая|какие|какого|каких|каким|какими|каком|сколько|где|когда|почему|зачем|which|who|what|how\s+many)/iu.test(value) ||
/[?]/u.test(String(rawValue ?? ""));
const rankingCue = /(?:больше|меньше|сам(?:ый|ая|ое|ые)|крупн|жирн|максим|миним)/iu.test(value);
const paymentCue = /(?:плат(?:ит|ят|еж|ёж|ежн|ежей|ежа)|денег|деньг|money|payment)/iu.test(value);
@@ -667,6 +667,13 @@ function hasLifecycleSegmentationSignal(text: string): boolean {
}
function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
const hasPaymentRiskLexeme =
/(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|задерж(?:ива|к)|просроч|долг|задолж)/iu.test(
text
);
if (hasPaymentRiskLexeme) {
return false;
}
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
return false;
}
@@ -768,6 +775,10 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
);
const asksRevenueTotal = /(?:сколько|скока|скок).*(?:денег|выручк|доход|заработ|оборот)/iu.test(text);
const asksOverallTurnover = /(?:общ(?:ий|ие|ая)\s+оборот|общ(?:ая|ий)\s+выручк|total\s+turnover|turnover\s+total)/iu.test(text);
const asksMajorShare =
/(?:основн(?:ую|ая|ые|ой)\s+част|больш(?:ую|ая|ие)\s+част|львин(?:ая|ую)\s+дол[яю]|ключев(?:ую|ая)\s+част)/iu.test(
text
);
const asksValue =
/(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(
text
@@ -797,6 +808,9 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
if (asksCounterpartySource && asksValue) {
return true;
}
if (!hasFuzzySupplierLexeme && (asksCustomerGroup || hasCounterpartyLexeme) && asksMajorShare && asksValue) {
return true;
}
if (!hasFuzzySupplierLexeme && asksIncomingFlow && asksRankOrTop) {
return true;
}
@@ -920,6 +934,71 @@ function hasOpenContractsListSignal(text: string): boolean {
return true;
}
function hasSupplierTailRiskSignal(text: string): boolean {
const hasSupplier = /(?:поставщик|supplier|vendor)/iu.test(text);
const hasTail = /(?:хвост|висят|незакрыт|задолж|долг|просроч)/iu.test(text);
const hasRisk = /(?:систематич|регулярн|проблем|тревог|не\s+разов|больше\s+похож)/iu.test(text);
const hasPeriodCue = /(?:на\s+конец\s+(?:месяц|период)|конец\s+месяц|пару\s+месяц|несколько\s+месяц)/iu.test(text);
return hasSupplier && hasTail && (hasRisk || hasPeriodCue);
}
function hasReceivablesLatencyRiskSignal(text: string): boolean {
const hasBuyer = /(?:покупател|клиент|заказчик|customer|buyer)/iu.test(text);
const hasCounterparty = /(?:контрагент|counterparty|partner)/iu.test(text);
const hasPayment = /(?:оплат|платеж|платёж|payment)/iu.test(text);
const hasShipment = /(?:отправк|отгруз|реализ|shipment|delivery)/iu.test(text);
const hasDelay = /(?:длинн|долг|просроч|задерж|висят|тревог|too\s+long|late)/iu.test(text);
const hasNonPayment = /(?:не\s+плат(?:ит|ят|ил|или)|без\s+оплат|оплат(?:ы|а)?\s+нет|нет\s+оплат|неоплач)/iu.test(text);
const hasPeriodOrRiskCue = /(?:за\s+текущ|на\s+конец|тревог|просроч|задерж|долг|длинн)/iu.test(text);
const hasBetweenShipmentAndPayment =
/между[\s\S]{0,80}(?:отправк|отгруз|реализ)[\s\S]{0,80}(?:оплат|платеж|платёж|payment)/iu.test(text);
if (hasBuyer && hasPayment && ((hasShipment && hasDelay) || hasBetweenShipmentAndPayment)) {
return true;
}
return (hasBuyer || hasCounterparty) && hasNonPayment && hasPeriodOrRiskCue;
}
function hasSettlementGapSignal(text: string): boolean {
const hasPayment = /(?:платеж|платёж|оплат|списани|поступлен|payment)/iu.test(text);
const hasDocument = /(?:док(?:и|умент|ументы|ументов)|docs?|documents?)/iu.test(text);
const hasAdvance = /(?:аванс|предоплат)/iu.test(text);
const hasNoDocumentForClosing =
/(?:нет|без)\s+(?:док(?:и|умент|ументы|ументов)|закрывающ)/iu.test(text) &&
/(?:закрыти|взаиморасч|акт)/iu.test(text);
const hasNoDocumentForClosingReversed =
/(?:док(?:и|умент|ументы|ументов)|закрывающ)[\s\S]{0,48}(?:нет|без)/iu.test(text) &&
/(?:закрыти|взаиморасч|акт)/iu.test(text);
const hasNoPayments =
/(?:нет|без)\s+(?:оплат|платеж|платёж|payment)/iu.test(text) ||
/(?:оплат|платеж|платёж|payment)\s+нет/iu.test(text);
const hasDocsWithoutPayments = hasDocument && hasNoPayments;
const hasPaymentsWithoutClosingDocs = hasPayment && (hasNoDocumentForClosing || hasNoDocumentForClosingReversed);
const hasUnclosedAdvanceGap =
hasAdvance &&
(/(?:не\s+закрыт|незакрыт|долго\s+не\s+закрыт|давно\s+не\s+закрыт)/iu.test(text) ||
hasNoDocumentForClosing ||
hasNoDocumentForClosingReversed);
return hasPaymentsWithoutClosingDocs || hasDocsWithoutPayments || hasUnclosedAdvanceGap;
}
function hasReconciliationMismatchSignal(text: string): boolean {
const hasCounterparty =
/(?:контрагент|поставщик|клиент|покупател|customer|supplier|counterparty)/iu.test(text);
const hasReconciliationLexeme = /(?:акт(?:а|ом|ах)?\s+свер(?:к|ок)|свер(?:к|ок))/iu.test(text);
const hasMismatchLexeme =
/(?:не\s+совпад|несовпад|расхожд|расход|не\s+сход|несход|разъех|разниц|не\s+бь[её]т)/iu.test(text);
const hasBalanceLexeme = /(?:сальд|остат|баланс|saldo|balance)/iu.test(text);
const hasLookupVerb = /(?:покажи|выведи|найд[иь]|show|list)/iu.test(text);
const hasInterrogativeLookup = /(?:по\s+каким|у\s+кого|какие|какой|кто|где)/iu.test(text);
return (
hasCounterparty &&
hasReconciliationLexeme &&
hasMismatchLexeme &&
hasBalanceLexeme &&
(hasLookupVerb || hasInterrogativeLookup)
);
}
function isLikelyCounterpartyToken(rawToken: string): boolean {
const token = String(rawToken ?? "").trim().toLowerCase();
if (!token || token.length < 2) {
@@ -1273,6 +1352,38 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
};
}
if (hasSettlementGapSignal(text)) {
return {
intent: "list_open_contracts",
confidence: "medium",
reasons: ["settlement_gap_signal_detected"]
};
}
if (hasReconciliationMismatchSignal(text)) {
return {
intent: "list_open_contracts",
confidence: "medium",
reasons: ["reconciliation_mismatch_signal_detected"]
};
}
if (hasReceivablesLatencyRiskSignal(text)) {
return {
intent: "list_receivables_counterparties",
confidence: "medium",
reasons: ["receivables_payment_lag_signal_detected"]
};
}
if (hasSupplierTailRiskSignal(text)) {
return {
intent: "list_payables_counterparties",
confidence: "medium",
reasons: ["supplier_tail_risk_signal_detected"]
};
}
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
return {
intent: "documents_forming_balance",
@@ -1299,7 +1410,9 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
if (
hasAny(text, OPEN_ITEMS_HINTS) &&
(text.includes("контраг") || text.includes("договор") || text.includes("контракт") || text.includes("counterparty") || text.includes("contract"))
/(?:контраг|договор|контракт|counterparty|contract|покупател|клиент|заказчик|customer|client|buyer|supplier|поставщик)/iu.test(
text
)
) {
return {
intent: "open_items_by_counterparty_or_contract",
@@ -30,6 +30,7 @@ interface NormalizedAddressRow {
interface AddressTryHandleOptions {
followupContext?: AddressFollowupContext | null;
analysisDateHint?: string | null;
}
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"] as const;
@@ -121,6 +122,36 @@ function parseFiniteNumber(value: unknown): number | null {
return null;
}
function normalizeAnalysisDateHint(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
const strictDate = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
const isoPrefix = strictDate ?? trimmed.match(/^(\d{4})-(\d{2})-(\d{2})T/i);
if (!isoPrefix) {
return null;
}
const year = Number(isoPrefix[1]);
const month = Number(isoPrefix[2]);
const day = Number(isoPrefix[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (
candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day
) {
return null;
}
return `${isoPrefix[1]}-${isoPrefix[2]}-${isoPrefix[3]}`;
}
function valueAsString(value: unknown): string {
if (value === null || value === undefined) {
return "";
@@ -788,6 +819,58 @@ function runtimeReadinessForLimitedCategory(category: AddressLimitedReasonCatego
return "UNKNOWN";
}
function normalizeLimitedReason(reason: string): string {
let normalized = String(reason ?? "").trim();
if (!normalized) {
return "не хватает подтвержденных данных для уверенного вывода";
}
const replacements: Array<[RegExp, string]> = [
[/address_query\s*v?1/giu, "текущий адресный режим"],
[/address\s*v1/giu, "текущий адресный режим"],
[/intent-specific\s+recipe/giu, "встроенный фильтр сценария"],
[/live\s+recipe/giu, "текущий сценарий выборки"],
[/materialized\s+live-строках/giu, "доступном срезе данных"],
[/live-выборке/giu, "выборке данных"],
[/live-данных/giu, "данных"],
[/deep-analysis/giu, "режим расширенной проверки"],
[/\blookup\b/giu, "поиск"],
[/\bintent\b/giu, "сценария"],
[/\brecipe\b/giu, "шаблон выборки"],
[/\byakor\b/giu, "ориентир"],
[/\banchor\b/giu, "ориентир"],
[/\s+/gu, " "]
];
for (const [pattern, value] of replacements) {
normalized = normalized.replace(pattern, value);
}
return normalized.trim();
}
function normalizeLimitedNextStep(nextStep: string): string {
let normalized = String(nextStep ?? "").trim();
if (!normalized) {
return "";
}
const replacements: Array<[RegExp, string]> = [
[/address_query\s*v?1/giu, "текущий адресный режим"],
[/deep-analysis/giu, "режим расширенной проверки"],
[/\bP0 intent\b/giu, "поддерживаемый сценарий"],
[/\bintent\b/giu, "сценарий"],
[/\blookup\b/giu, "поиск"],
[/\s+/gu, " "]
];
for (const [pattern, value] of replacements) {
normalized = normalized.replace(pattern, value);
}
return normalized.trim();
}
interface RowStageDiagnostics {
rawRowKeysSample: string[];
materializationDropReason:
@@ -945,20 +1028,28 @@ function toLegacyMcpStatus(
function composeLimitedReply(category: AddressLimitedReasonCategory, reason: string, nextStep?: string): string {
const heading =
category === "empty_match"
? "В live-данных по текущему фильтру записи не найдены."
? "По текущим условиям в доступном срезе данных совпадений не нашлось."
: category === "missing_anchor"
? "Для точного адресного поиска не хватает обязательного якоря."
? "Чтобы ответить надежно, нужен более точный ориентир в запросе."
: category === "recipe_visibility_gap"
? "Текущий live recipe не дает нужную видимость данных для этого сценария."
? "Запрос понятен, но текущий режим не дает нужной детализации."
: category === "unsupported"
? "Этот запрос не подходит под address_query V1."
: "Не удалось выполнить адресный live-запрос в V1.";
? "Сейчас этот тип вопроса вне поддерживаемого контура адресного режима."
: "Не удалось завершить проверку в адресном режиме.";
const reasonLine =
category === "unsupported"
? "Коротко: этот сценарий пока не поддержан в текущем адресном контуре."
: category === "missing_anchor"
? "Коротко: в запросе не хватает конкретного ориентира (контрагент, договор или период)."
: category === "recipe_visibility_gap"
? "Коротко: для уверенного ответа нужен более специализированный сценарий выборки."
: `Коротко: ${normalizeLimitedReason(reason)}.`;
const lines = [
heading,
`Причина: ${reason}.`
reasonLine
];
if (nextStep) {
lines.push(`Что нужно уточнить: ${nextStep}.`);
lines.push(`Что можно сделать дальше: ${normalizeLimitedNextStep(nextStep)}.`);
}
return lines.join("\n");
}
@@ -1057,7 +1148,24 @@ export class AddressQueryService {
if (!decompose) {
return null;
}
const { mode, shape, intent, filters, baseReasons } = decompose;
const { mode, shape, intent, filters } = decompose;
const baseReasons = [...decompose.baseReasons];
const analysisDate = normalizeAnalysisDateHint(options.analysisDateHint);
if (analysisDate) {
const hasTemporalFilter = Boolean(
(typeof filters.extracted_filters.period_from === "string" && filters.extracted_filters.period_from.trim().length > 0) ||
(typeof filters.extracted_filters.period_to === "string" && filters.extracted_filters.period_to.trim().length > 0) ||
(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
);
if (!hasTemporalFilter) {
filters.extracted_filters = {
...filters.extracted_filters,
as_of_date: analysisDate
};
filters.warnings = [...new Set([...(filters.warnings ?? []), "as_of_date_from_analysis_context"])];
baseReasons.push("as_of_date_from_analysis_context");
}
}
const composeOptionsFromFilters = (filterSet: AddressFilterSet) => ({
userMessage,
periodFrom: typeof filterSet.period_from === "string" ? filterSet.period_from : undefined,
@@ -1079,8 +1187,8 @@ export class AddressQueryService {
rowsFetched: 0,
rowsMatched: 0,
category: "unsupported",
reasonText: "intent пока не поддержан в address V1",
nextStep: "переформулируйте вопрос как адресный lookup по счету/контрагенту/договору",
reasonText: "сценарий пока вне поддерживаемого контура текущего адресного режима",
nextStep: "могу проверить близкие сценарии: документы/платежи по контрагенту, договоры или остаток по счету",
limitations: ["intent_not_supported_in_v1"],
reasons: baseReasons
});
@@ -1123,8 +1231,8 @@ export class AddressQueryService {
rowsFetched: 0,
rowsMatched: 0,
category: "recipe_visibility_gap",
reasonText: "для intent пока нет recipe в address V1",
nextStep: "выберите поддерживаемый P0 intent или переключите запрос в deep-analysis",
reasonText: "для этого сценария пока нет готового шаблона выборки в текущем режиме",
nextStep: "можно выбрать близкий поддерживаемый сценарий или переключить запрос в режим расширенной проверки",
limitations: ["recipe_not_available"],
reasons: [...baseReasons, ...recipeSelection.selection_reason]
});
@@ -1509,7 +1509,7 @@ export function composeFactualReply(
if (intent === "list_open_contracts") {
const contracts = contractCandidatesFromRows(rows);
const lines = [
"Собраны кандидаты по незакрытым договорным позициям (по live движениям 60/62/76).",
"Проверил потенциальные разрывы во взаиморасчетах (платежи без закрытия и документы без оплат).",
`Строк движения: ${rows.length}.`,
`Договорных кандидатов: ${contracts.length}.`
];
@@ -1525,6 +1525,36 @@ export function composeFactualReply(
};
}
if (intent === "list_payables_counterparties") {
const lines = [
"Проверил поставщиков с признаками незакрытых хвостов по взаиморасчетам (контур 60/76).",
`Строк в выборке: ${rows.length}.`,
...(rows.length > 0
? ["Ниже примеры строк для ручной проверки."]
: ["Явных признаков системной задолженности по доступному срезу не найдено."]),
...formatTopRows(rows, 6)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "list_receivables_counterparties") {
const lines = [
"Проверил покупателей с признаками затянутой оплаты (контур 62/76).",
`Строк в выборке: ${rows.length}.`,
...(rows.length > 0
? ["Ниже примеры строк, которые стоит проверить в первую очередь."]
: ["Явных признаков затяжной дебиторки по доступному срезу не найдено."]),
...formatTopRows(rows, 6)
];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
};
}
if (intent === "open_items_by_counterparty_or_contract") {
const lines = [
"Собраны открытые позиции по указанному фильтру (контрагент/договор).",
@@ -1628,14 +1658,7 @@ export function composeFactualReply(
};
}
const title =
intent === "list_payables_counterparties"
? "Срез обязательств (payables) собран по движениям с account scope 60/76."
: intent === "list_receivables_counterparties"
? "Срез требований (receivables) собран по движениям с account scope 62/76."
: "Срез адресного запроса собран.";
const lines = [title, `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
const lines = ["Срез адресного запроса собран.", `Строк отобрано: ${rows.length}.`, ...formatTopRows(rows, 6)];
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
@@ -0,0 +1,159 @@
import { FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 } from "../config";
import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
import {
ANSWER_STRUCTURE_SCHEMA_VERSION,
type AnswerStructureV11,
type EvidenceLimitationReasonCode
} from "../types/stage1Contracts";
export interface BuildAssistantAnswerStructureV11Input {
assistantReply: string;
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
retrievalResults: UnifiedRetrievalResult[];
options?: {
enableEvidenceEnrichment?: boolean;
};
}
const EVIDENCE_LIMITATION_REASON_CODE_SET: ReadonlySet<EvidenceLimitationReasonCode> = new Set([
"snapshot_only",
"heuristic_inference",
"missing_mechanism",
"weak_source_mapping",
"insufficient_detail",
"unknown"
]);
function summarizeUnique(values: Array<string | null | undefined>, limit = 6): string[] {
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
}
function isEvidenceLimitationReasonCode(value: string): value is EvidenceLimitationReasonCode {
return EVIDENCE_LIMITATION_REASON_CODE_SET.has(value as EvidenceLimitationReasonCode);
}
function firstNonEmptyLine(text: string): string {
const line = String(text ?? "")
.split("\n")
.map((item) => item.trim())
.find((item) => item.length > 0);
return (line ?? String(text ?? "")).slice(0, 220);
}
function buildClaimEvidenceLinks(
retrievalResults: UnifiedRetrievalResult[]
): NonNullable<AnswerStructureV11["evidence_block"]["claim_evidence_links"]> {
const byClaim = new Map<string, string[]>();
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)
}));
}
export function buildAssistantAnswerStructureV11(input: BuildAssistantAnswerStructureV11Input): AnswerStructureV11 {
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): note is string => 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): value is string => typeof value === "string" && value.trim().length > 0)
),
8
);
const limitationReasonCodes: EvidenceLimitationReasonCode[] = 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
).filter(isEvidenceLimitationReasonCode);
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: AnswerStructureV11["mechanism_block"]["status"] =
mechanismNotes.length === 0
? "unresolved"
: limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
? "limited"
: "grounded";
const enableEvidenceEnrichment =
input.options?.enableEvidenceEnrichment ?? FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1;
return {
schema_version: 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",
...(enableEvidenceEnrichment && 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
}
};
}
@@ -0,0 +1,97 @@
import type {
AssistantReplyType,
AssistantRequirement,
AnswerGroundingCheck,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { NormalizedPayload, RouteHintSummary } from "../types/normalizer";
import {
buildAssistantCoverageContractV1,
buildAssistantExecutionPlanContractV1,
buildAssistantQueryFrameContractV1,
classifyAssistantOutcomeClassV1,
type AssistantCoverageContractV1,
type AssistantEvidenceBundleContractV1,
type AssistantExecutionPlanContractV1,
type AssistantOutcomeClassV1,
type AssistantQueryFrameContractV1
} from "./assistantOrchestrationContracts";
export interface AssistantContractsBundleV1 {
queryFrameContractV1: AssistantQueryFrameContractV1;
executionPlanContractV1: AssistantExecutionPlanContractV1;
outcomeClassV1: AssistantOutcomeClassV1;
coverageContractV1: AssistantCoverageContractV1;
assistantOrchestrationContractsV1: {
query_frame: AssistantQueryFrameContractV1;
execution_plan: AssistantExecutionPlanContractV1;
evidence_bundle: AssistantEvidenceBundleContractV1;
coverage: AssistantCoverageContractV1;
};
}
export function assembleAssistantContractsBundleV1(input: {
userMessage: string;
normalizedQuestion: string;
normalized: NormalizedPayload | null;
routeSummary: RouteHintSummary | null;
droppedIntentSegments: string[];
analysisContext: {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
} | null;
executionPlan: Array<{
fragment_id: string;
requirement_ids: string[];
route: string;
should_execute: boolean;
no_route_reason?: string | null;
clarification_reason?: string | null;
}>;
requirements: AssistantRequirement[];
evidenceBundleContractV1: AssistantEvidenceBundleContractV1;
replyType: AssistantReplyType;
coverageReport: RequirementCoverageReport;
grounding: AnswerGroundingCheck;
retrievalResults: UnifiedRetrievalResult[];
}): AssistantContractsBundleV1 {
const queryFrameContractV1 = buildAssistantQueryFrameContractV1({
userMessage: input.userMessage,
normalizedQuestion: input.normalizedQuestion,
normalized: input.normalized,
routeSummary: input.routeSummary,
droppedIntentSegments: input.droppedIntentSegments,
analysisContext: input.analysisContext
});
const executionPlanContractV1 = buildAssistantExecutionPlanContractV1({
executionPlan: input.executionPlan,
requirements: input.requirements
});
const outcomeClassV1 = classifyAssistantOutcomeClassV1({
replyType: input.replyType,
coverageReport: input.coverageReport,
grounding: input.grounding,
retrievalResults: input.retrievalResults
});
const coverageContractV1 = buildAssistantCoverageContractV1({
coverageReport: input.coverageReport,
grounding: input.grounding,
outcomeClass: outcomeClassV1
});
return {
queryFrameContractV1,
executionPlanContractV1,
outcomeClassV1,
coverageContractV1,
assistantOrchestrationContractsV1: {
query_frame: queryFrameContractV1,
execution_plan: executionPlanContractV1,
evidence_bundle: input.evidenceBundleContractV1,
coverage: coverageContractV1
}
};
}
@@ -0,0 +1,421 @@
import type {
AnswerGroundingCheck,
AssistantRequirement,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { RouteHintSummary } from "../types/normalizer";
interface SubjectTokenRule {
critical: boolean;
patterns: string[];
routes?: string[];
}
export interface AssistantRequirementExtractionResult {
requirements: AssistantRequirement[];
byFragment: Map<string, string[]>;
}
function summarizeUnique(values: Array<string | null | undefined>, limit = 6): string[] {
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
}
const SUBJECT_TOKEN_RULES: Record<string, SubjectTokenRule> = {
nds: {
critical: true,
patterns: [
"vat",
"accumulationregister",
"ндс",
"книгипокупок",
"книгипродаж",
"налогнадобавленнуюстоимость"
]
},
os: {
critical: true,
patterns: ["fixed_asset", "fixedasset", "основн", "амортиз"]
},
saldo: {
critical: true,
patterns: ["balance", "saldo", "сальдо", "остат"]
},
counterparty: {
critical: false,
patterns: [
"counterparty",
"supplier",
"buyer",
"counterparty_id",
"journal_counterparty",
"document_has_counterparty",
"контрагент",
"поставщик",
"покупател"
],
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",
"документ"
],
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",
"аномал",
"риск"
],
routes: ["store_feature_risk", "batch_refresh_then_store"]
},
chain: {
critical: false,
patterns: ["chain", "cross_entity_chain", "relation_types", "operations_count", "matched_counterparties", "цепоч"],
routes: ["hybrid_store_plus_live"]
}
};
function hasRegexMatch(corpus: string, pattern: RegExp): boolean {
try {
return pattern.test(corpus);
} catch {
return false;
}
}
function evaluateSubjectTokenMatch(
token: string,
corpus: string,
executedRoutes: Set<string>
): {
matched: boolean;
critical: boolean;
} {
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: string, result: UnifiedRetrievalResult): number {
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: string, result: UnifiedRetrievalResult): boolean {
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;
}
export function extractRequirementsForRoute(input: {
routeSummary: RouteHintSummary | null;
userMessage: string;
fragmentTextById: Map<string, string>;
extractSubjectTokens: (text: string) => string[];
}): AssistantRequirementExtractionResult {
const byFragment = new Map<string, string[]>();
const requirements: AssistantRequirement[] = [];
const pushRequirement = (item: {
requirement_id: string;
source_fragment_id: string | null;
requirement_text: string;
status: AssistantRequirement["status"];
route: string | null;
}): void => {
const subjectTokens = input.extractSubjectTokens(item.requirement_text);
requirements.push({
requirement_id: item.requirement_id,
source_fragment_id: item.source_fragment_id,
requirement_text: item.requirement_text,
subject_tokens: subjectTokens,
status: item.status,
route: item.route
});
if (item.source_fragment_id) {
const current = byFragment.get(item.source_fragment_id) ?? [];
current.push(item.requirement_id);
byFragment.set(item.source_fragment_id, current);
}
};
if (!input.routeSummary) {
pushRequirement({
requirement_id: "R1",
source_fragment_id: null,
requirement_text: input.userMessage,
status: "clarification_needed",
route: null
});
return { requirements, byFragment };
}
if (input.routeSummary.mode === "legacy_v1") {
pushRequirement({
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: input.userMessage,
status: "covered",
route: input.routeSummary.route_hint
});
return { requirements, byFragment };
}
input.routeSummary.decisions.forEach((decision, index) => {
const requirementId = `R${index + 1}`;
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.userMessage;
let status: AssistantRequirement["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 };
}
export function evaluateCoverageForRequirements(
requirements: AssistantRequirement[],
retrievalResults: UnifiedRetrievalResult[]
): {
requirements: AssistantRequirement[];
coverage: RequirementCoverageReport;
} {
const statusByRequirement = new Map<string, Array<{ status: UnifiedRetrievalResult["status"]; substantive: boolean }>>();
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" as const };
}
const hasAnySubstantive = states.some((item) => item.substantive);
if (!hasAnySubstantive) {
return { ...requirement, status: "uncovered" as const };
}
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" as const };
}
if (hasSubstantiveOk || hasSubstantivePartial || hasOk || hasPartial) {
return { ...requirement, status: "partially_covered" as const };
}
return { ...requirement, status: "uncovered" as const };
});
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
}
};
}
export function checkGroundingForRequirements(input: {
userMessage: string;
requirements: AssistantRequirement[];
coverage: RequirementCoverageReport;
retrievalResults: UnifiedRetrievalResult[];
extractSubjectTokens: (text: string) => string[];
}): AnswerGroundingCheck {
const whyIncludedSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.why_included));
const selectionReasonSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.selection_reason));
const hasMaterialResults = input.retrievalResults.some((item) => item.status === "ok" || item.status === "partial");
const subjectTokens = input.extractSubjectTokens(input.userMessage);
const executedRoutes = new Set(
input.retrievalResults
.filter((item) => item.status !== "error")
.map((item) => item.route)
.filter(Boolean)
);
const retrievalCorpus = JSON.stringify(
input.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: string[] = [];
const missingCriticalTokens: string[] = [];
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 &&
input.coverage.requirements_covered > 0 &&
onlyAccountCriticalMissing &&
(whyIncludedSummary.length > 0 || selectionReasonSummary.length > 0);
const routeSubjectMatch =
!hasMaterialResults || missingCriticalTokens.length === 0 || accountOnlyMismatchRecoverable;
let status: AnswerGroundingCheck["status"] = "grounded";
const reasons: string[] = [];
if (!routeSubjectMatch) {
status = "route_mismatch_blocked";
reasons.push(
`Ключевые ориентиры вопроса не подтверждены в найденных данных: ${missingCriticalTokens.join(", ")}`
);
} else if (accountOnlyMismatchRecoverable) {
status = "partial";
reasons.push(
`Часть счетных ориентиров не подтвердилась напрямую (${missingCriticalTokens.join(", ")}), но есть опора для ограниченного вывода.`
);
} else if (input.coverage.requirements_covered === 0) {
status = "no_grounded_answer";
reasons.push("Ни одно требование не получило подтвержденного покрытия.");
} else if (
input.coverage.requirements_uncovered.length > 0 ||
input.coverage.requirements_partially_covered.length > 0 ||
input.coverage.clarification_needed_for.length > 0 ||
input.coverage.out_of_scope_requirements.length > 0
) {
status = "partial";
reasons.push("Вопрос покрыт частично: есть непокрытые или требующие уточнения требования.");
}
if (whyIncludedSummary.length === 0) {
reasons.push("В текущей выборке не хватает явных подтверждений, почему записи попали в ответ.");
}
if (missingSubjectTokens.length > 0 && missingCriticalTokens.length === 0) {
reasons.push(`Часть контекста вопроса не подтверждена напрямую в найденных данных: ${missingSubjectTokens.join(", ")}`);
}
const missingRequirements = [
...input.coverage.requirements_uncovered,
...input.coverage.requirements_partially_covered,
...input.coverage.clarification_needed_for,
...input.coverage.out_of_scope_requirements
];
return {
status,
route_subject_match: routeSubjectMatch,
missing_requirements: missingRequirements,
reasons,
why_included_summary: whyIncludedSummary,
selection_reason_summary: selectionReasonSummary
};
}
@@ -93,6 +93,13 @@ interface LiveMcpCallExecution {
error?: string | null;
}
interface LiveTemporalHint {
as_of_date?: string | null;
period_from?: string | null;
period_to?: string | null;
source?: string | null;
}
type BroadnessLevel = "low" | "medium" | "high";
interface BroadQueryAssessment {
@@ -262,6 +269,32 @@ function formatIsoDateUtc(date: Date): string {
return `${year}-${month}-${day}`;
}
function normalizeIsoDate(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (
candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day
) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function monthEndFromIso(isoDate: string): string | null {
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
@@ -329,14 +362,28 @@ function hasFixedAssetAmortizationSignal(text: string): boolean {
);
}
function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallPlan {
function buildLiveMcpCallPlan(route: string, fragmentText: string, temporalHint?: LiveTemporalHint | null): LiveMcpCallPlan {
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
const preferredDomainHint = inferRuntimeP0DomainHint(fragmentText);
const periodScope = inferPeriodScope(fragmentText);
const primaryFrom = periodScope.from ?? "2020-07-01";
const primaryTo = periodScope.to ?? monthEndFromIso(primaryFrom) ?? "2020-07-31";
const carryFrom = shiftIsoDate(primaryFrom, -31) ?? primaryFrom;
const carryTo = shiftIsoDate(primaryTo, 31) ?? primaryTo;
const hintedAsOfDate = normalizeIsoDate(temporalHint?.as_of_date);
const hintedPeriodFrom = normalizeIsoDate(temporalHint?.period_from);
const hintedPeriodTo = normalizeIsoDate(temporalHint?.period_to);
const primaryFrom = periodScope.from ?? hintedPeriodFrom ?? hintedAsOfDate;
const primaryTo =
periodScope.to ??
hintedPeriodTo ??
(!periodScope.from && !hintedPeriodFrom && hintedAsOfDate ? hintedAsOfDate : primaryFrom ? monthEndFromIso(primaryFrom) ?? primaryFrom : null);
const carryFrom = primaryFrom ? shiftIsoDate(primaryFrom, -31) ?? primaryFrom : null;
const carryTo = primaryTo ? shiftIsoDate(primaryTo, 31) ?? primaryTo : null;
const buildPrimaryQuery = (limit: number): string =>
primaryFrom && primaryTo
? buildLiveRangeQuery(primaryFrom, primaryTo, limit)
: MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(limit));
const buildCarryQuery = (limit: number): string =>
carryFrom && carryTo
? buildLiveRangeQuery(carryFrom, carryTo, limit)
: buildPrimaryQuery(limit);
const faClaim =
preferredDomainHint === "fixed_asset_amortization" ||
@@ -352,7 +399,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_amortization_documents_in_period",
purpose: "seed_amortization_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@@ -360,7 +407,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_fixed_asset_movements_accounts_01_02",
purpose: "collect_fa_object_movements",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@@ -368,7 +415,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_fixed_asset_cards_expected_for_period",
purpose: "build_expected_fa_set",
query: buildLiveRangeQuery(carryFrom, primaryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@@ -376,7 +423,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "match_expected_vs_actual_fa_coverage",
purpose: "compare_expected_vs_actual_fa_coverage",
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["01", "02", "08"]
@@ -400,7 +447,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_vat_source_documents_in_period",
purpose: "seed_vat_source_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@@ -408,7 +455,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_vat_invoice_links_in_period",
purpose: "collect_invoice_links",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@@ -416,7 +463,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_vat_register_entries_in_period",
purpose: "collect_vat_register_entries",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@@ -424,7 +471,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_vat_book_entries_in_period",
purpose: "collect_vat_book_entries",
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["19", "68"]
@@ -464,7 +511,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_rbp_writeoff_documents_in_period",
purpose: "seed_writeoff_documents",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
@@ -472,7 +519,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_rbp_object_movements_account_97",
purpose: "collect_rbp_object_movements",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97"]
@@ -480,7 +527,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "find_month_close_entries_linked_to_rbp",
purpose: "link_month_close_to_rbp",
query: buildLiveRangeQuery(primaryFrom, primaryTo, CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
query: buildPrimaryQuery(CLAIM_BOUND_PRIMARY_LIVE_LIMIT),
limit: CLAIM_BOUND_PRIMARY_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
@@ -488,7 +535,7 @@ function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallP
{
call_id: "compute_end_period_residual_by_rbp_object",
purpose: "collect_residual_tail_signals",
query: buildLiveRangeQuery(carryFrom, carryTo, CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
query: buildCarryQuery(CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT),
limit: CLAIM_BOUND_CARRY_WINDOW_LIVE_LIMIT,
required_for_claim: true,
account_scope_override: ["97", "20", "25", "26", "44"]
@@ -1849,7 +1896,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
}
if (
/ос|основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(
/основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(
lower
) ||
hasFixedAssetAccountScope
@@ -2855,7 +2902,13 @@ export class AssistantDataLayer {
return enforceBroadQueryGuards(route, fragmentText, result);
}
public async executeRouteRuntime(route: string, fragmentText: string): Promise<RawRetrievalResult> {
public async executeRouteRuntime(
route: string,
fragmentText: string,
options?: {
temporalHint?: LiveTemporalHint | null;
}
): Promise<RawRetrievalResult> {
const base = this.executeRoute(route, fragmentText);
if (!FEATURE_ASSISTANT_MCP_RUNTIME_V1) {
return base;
@@ -2864,7 +2917,7 @@ export class AssistantDataLayer {
return base;
}
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText);
const liveOverlay = await this.fetchLiveMcpOverlay(route, fragmentText, options?.temporalHint);
return this.mergeWithLiveOverlay(base, liveOverlay);
}
@@ -2922,9 +2975,13 @@ export class AssistantDataLayer {
return merged;
}
private async fetchLiveMcpOverlay(route: string, fragmentText: string): Promise<LiveMcpOverlay> {
private async fetchLiveMcpOverlay(
route: string,
fragmentText: string,
temporalHint?: LiveTemporalHint | null
): Promise<LiveMcpOverlay> {
const endpoint = this.buildMcpUrl("/api/execute_query");
const livePlan = buildLiveMcpCallPlan(route, fragmentText);
const livePlan = buildLiveMcpCallPlan(route, fragmentText, temporalHint);
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
const accountScope =
livePlan.claim_type === "prove_fixed_asset_amortization_coverage"
@@ -0,0 +1,160 @@
import type { AssistantDebugPayload } from "../types/assistant";
type RetrievalStatusItem = AssistantDebugPayload["retrieval_status"][number];
export interface DeepAnalysisDebugPayloadInput {
traceId: string;
promptVersion: string;
schemaVersion: string;
fallbackType: unknown;
routeSummary: unknown;
fragments: unknown[];
requirementsExtracted: unknown[];
coverageReport: unknown;
routes: Array<Record<string, unknown>>;
retrievalStatus: RetrievalStatusItem[];
retrievalResults: unknown[];
groundingCheck: unknown;
droppedIntentSegments: string[];
questionTypeClass: string;
companyAnchors: unknown;
runtimeAnalysisContext: {
active: boolean;
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
};
businessScopeResolution: {
business_scope_raw?: string[];
business_scope_resolved?: string[];
company_grounding_applied?: boolean;
scope_resolution_reason?: string[];
};
temporalGuard: Record<string, unknown>;
polarityAudit: Record<string, unknown>;
claimAnchorAudit: Record<string, unknown>;
targetedEvidenceAudit: unknown;
evidenceAdmissibilityGateAudit: unknown;
rbpLiveRouteAudit: unknown | null;
faLiveRouteAudit: unknown | null;
groundedAnswerEligibilityGuard: Record<string, unknown>;
followupStateUsage: unknown | null;
compositionDebug: {
problem_centric_answer_applied?: boolean;
problem_units_used_count?: number;
problem_answer_mode?: string;
problem_unit_ids_used?: string[];
};
addressRuntimeMetaForDeep:
| {
attempted?: boolean;
applied?: boolean;
reason?: string | null;
provider?: string | null;
fallbackRuleHit?: string | null;
toolGateDecision?: string | null;
toolGateReason?: string | null;
predecomposeContract?: unknown;
orchestrationContract?: unknown;
}
| null
| undefined;
outcomeClassV1: unknown;
assistantOrchestrationContractsV1: unknown;
answerStructureV11: unknown;
investigationStateSnapshot: unknown;
normalizedPayload: unknown;
}
function toAnalysisContext(input: DeepAnalysisDebugPayloadInput["runtimeAnalysisContext"]): Record<string, unknown> | null {
if (!input.active) {
return null;
}
return {
as_of_date: input.as_of_date,
period_from: input.period_from,
period_to: input.period_to,
source: input.source,
snapshot_mode: input.snapshot_mode
};
}
export function buildDeepAnalysisDebugPayload(input: DeepAnalysisDebugPayloadInput): Record<string, unknown> {
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
return {
trace_id: input.traceId,
prompt_version: input.promptVersion,
schema_version: input.schemaVersion,
fallback_type: input.fallbackType,
route_summary: input.routeSummary,
fragments: input.fragments,
requirements_extracted: input.requirementsExtracted,
coverage_report: input.coverageReport,
routes: input.routes,
retrieval_status: input.retrievalStatus,
retrieval_results: input.retrievalResults,
answer_grounding_check: input.groundingCheck,
dropped_intent_segments: input.droppedIntentSegments,
question_type_class: input.questionTypeClass,
company_anchors: input.companyAnchors,
analysis_context_applied: input.runtimeAnalysisContext.active,
analysis_context: analysisContext,
business_scope_raw: input.businessScopeResolution.business_scope_raw,
business_scope_resolved: input.businessScopeResolution.business_scope_resolved,
company_grounding_applied: input.businessScopeResolution.company_grounding_applied,
scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
company_scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
raw_time_anchor: input.temporalGuard.raw_time_anchor,
raw_time_scope: input.temporalGuard.raw_time_scope,
resolved_time_anchor: input.temporalGuard.resolved_time_anchor,
resolved_primary_period: input.temporalGuard.resolved_primary_period,
effective_primary_period: input.temporalGuard.effective_primary_period,
temporal_guard_input: input.temporalGuard.temporal_guard_input,
temporal_alignment_status: input.temporalGuard.temporal_alignment_status,
temporal_resolution_source: input.temporalGuard.temporal_resolution_source,
temporal_guard_basis: input.temporalGuard.temporal_guard_basis,
temporal_guard_applied: input.temporalGuard.temporal_guard_applied,
temporal_guard_outcome: input.temporalGuard.temporal_guard_outcome,
temporal_guard: input.temporalGuard,
raw_numeric_tokens: input.polarityAudit.raw_numeric_tokens,
classified_numeric_tokens: input.polarityAudit.classified_numeric_tokens,
rejected_as_non_accounts: input.polarityAudit.rejected_as_non_accounts,
resolved_account_anchors: input.polarityAudit.resolved_account_anchors,
domain_polarity_guard: input.polarityAudit,
claim_anchor_audit: input.claimAnchorAudit,
settlement_role: input.claimAnchorAudit.settlement_role ?? null,
settlement_role_resolution_reason: input.claimAnchorAudit.settlement_role_resolution_reason ?? [],
polarity_resolution_status: input.claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
targeted_evidence_acquisition: input.targetedEvidenceAudit,
evidence_admissibility_gate: input.evidenceAdmissibilityGateAudit,
...(input.rbpLiveRouteAudit ? { rbp_live_route_audit: input.rbpLiveRouteAudit } : {}),
...(input.faLiveRouteAudit ? { fa_live_route_audit: input.faLiveRouteAudit } : {}),
eligibility_time_basis: input.groundedAnswerEligibilityGuard.eligibility_time_basis,
grounded_answer_eligibility_guard: input.groundedAnswerEligibilityGuard,
...(input.followupStateUsage ? { followup_state_usage: input.followupStateUsage } : {}),
problem_centric_answer_applied: input.compositionDebug.problem_centric_answer_applied ?? false,
problem_units_used_count: input.compositionDebug.problem_units_used_count ?? 0,
problem_answer_mode: input.compositionDebug.problem_answer_mode ?? "stage1_policy_v11",
...(Array.isArray(input.compositionDebug.problem_unit_ids_used) && input.compositionDebug.problem_unit_ids_used.length > 0
? {
problem_unit_ids_used: input.compositionDebug.problem_unit_ids_used
}
: {}),
address_llm_predecompose_attempted: Boolean(input.addressRuntimeMetaForDeep?.attempted),
address_llm_predecompose_applied: Boolean(input.addressRuntimeMetaForDeep?.applied),
address_llm_predecompose_reason: input.addressRuntimeMetaForDeep?.reason ?? null,
address_llm_predecompose_provider: input.addressRuntimeMetaForDeep?.provider ?? null,
address_fallback_rule_hit: input.addressRuntimeMetaForDeep?.fallbackRuleHit ?? null,
address_tool_gate_decision: input.addressRuntimeMetaForDeep?.toolGateDecision ?? null,
address_tool_gate_reason: input.addressRuntimeMetaForDeep?.toolGateReason ?? null,
address_llm_predecompose_contract: input.addressRuntimeMetaForDeep?.predecomposeContract ?? null,
orchestration_contract_v1: input.addressRuntimeMetaForDeep?.orchestrationContract ?? null,
assistant_outcome_class_v1: input.outcomeClassV1,
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
answer_structure_v11: input.answerStructureV11,
investigation_state_snapshot: input.investigationStateSnapshot,
normalized: input.normalizedPayload
};
}
@@ -0,0 +1,68 @@
import { buildAssistantAnswerStructureV11 } from "./assistantAnswerPackageBuilder";
import type {
AssistantConversationItem,
AssistantDebugPayload,
AssistantReplyType,
AnswerGroundingCheck,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { AnswerStructureV11 } from "../types/stage1Contracts";
export interface DeepAnswerArtifacts {
safeAssistantReply: string;
answerStructureV11: AnswerStructureV11 | null;
}
function stripTechnicalTail(text: string): string {
return String(text ?? "")
.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();
}
export function buildDeepAnswerArtifacts(input: {
safeAssistantReplyBase: string;
featureContractsV11: boolean;
featureAnswerPolicyV11: boolean;
compositionAnswerStructureV11: AnswerStructureV11 | null | undefined;
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
retrievalResults: UnifiedRetrievalResult[];
}): DeepAnswerArtifacts {
const safeAssistantReply = stripTechnicalTail(input.safeAssistantReplyBase);
const answerStructureV11 = input.featureContractsV11
? input.featureAnswerPolicyV11 && input.compositionAnswerStructureV11
? input.compositionAnswerStructureV11
: buildAssistantAnswerStructureV11({
assistantReply: safeAssistantReply,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
retrievalResults: input.retrievalResults
})
: null;
return {
safeAssistantReply,
answerStructureV11
};
}
export function buildAssistantConversationItem(input: {
messageId: string;
sessionId: string;
text: string;
replyType: AssistantReplyType;
traceId: string | null;
debug: AssistantDebugPayload;
}): AssistantConversationItem {
return {
message_id: input.messageId,
session_id: input.sessionId,
role: "assistant",
text: input.text,
reply_type: input.replyType,
created_at: new Date().toISOString(),
trace_id: input.traceId,
debug: input.debug
};
}
@@ -0,0 +1,77 @@
import type { AssistantRequirement, AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
import type { InvestigationStateWithProblemUnits } from "../types/stage2ProblemUnits";
import type { QuestionTypeClass } from "./questionTypeResolver";
import { resolveQuestionType } from "./questionTypeResolver";
import { composeAssistantAnswer } from "./answerComposer";
export interface BuildAssistantDeepTurnCompositionInput {
userMessage: string;
routeSummary: RouteHintSummary | null;
retrievalResults: UnifiedRetrievalResult[];
requirements: AssistantRequirement[];
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
followupUsage: unknown | null | undefined;
investigationState: InvestigationStateWithProblemUnits | null | undefined;
companyAnchors: unknown;
normalizedPayload: NormalizeResponsePayload["normalized"];
featureAnswerPolicyV11: boolean;
featureProblemCentricAnswerV1: boolean;
featureLifecycleAnswerV1: boolean;
hasExplicitPeriodAnchor: (normalizedPayload: NormalizeResponsePayload["normalized"]) => boolean;
resolveQuestionTypeFn?: (input: string) => QuestionTypeClass;
composeAssistantAnswerFn?: typeof composeAssistantAnswer;
}
export interface AssistantDeepTurnCompositionOutput {
focusDomainHint: string | null;
questionTypeClass: QuestionTypeClass;
hasPeriodInCompanyAnchors: boolean;
normalizationPeriodExplicit: boolean;
composition: ReturnType<typeof composeAssistantAnswer>;
}
export function buildAssistantDeepTurnComposition(
input: BuildAssistantDeepTurnCompositionInput
): AssistantDeepTurnCompositionOutput {
const resolveQuestionTypeSafe = input.resolveQuestionTypeFn ?? resolveQuestionType;
const composeAssistantAnswerSafe = input.composeAssistantAnswerFn ?? composeAssistantAnswer;
const followupApplied = Boolean((input.followupUsage as { applied?: unknown } | null)?.applied);
const focusDomainHint = followupApplied
? input.investigationState?.followup_context?.active_domain ?? input.investigationState?.focus.domain ?? null
: null;
const questionTypeClass = resolveQuestionTypeSafe(input.userMessage);
const companyAnchorSet = input.companyAnchors as {
dates?: unknown[];
periods?: unknown[];
} | null;
const hasPeriodInCompanyAnchors =
(Array.isArray(companyAnchorSet?.dates) && companyAnchorSet.dates.some((item) => String(item ?? "").trim().length > 0)) ||
(Array.isArray(companyAnchorSet?.periods) && companyAnchorSet.periods.some((item) => String(item ?? "").trim().length > 0));
const normalizationPeriodExplicit = input.hasExplicitPeriodAnchor(input.normalizedPayload) || hasPeriodInCompanyAnchors;
const composition = composeAssistantAnswerSafe({
userMessage: input.userMessage,
routeSummary: input.routeSummary,
retrievalResults: input.retrievalResults,
requirements: input.requirements,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
focusDomainHint,
questionTypeHint: questionTypeClass,
companyAnchors: input.companyAnchors as any,
normalizationPeriodExplicit,
enableAnswerPolicyV11: input.featureAnswerPolicyV11,
enableProblemCentricAnswerV1: input.featureProblemCentricAnswerV1,
enableLifecycleAnswerV1: input.featureLifecycleAnswerV1
});
return {
focusDomainHint,
questionTypeClass,
hasPeriodInCompanyAnchors,
normalizationPeriodExplicit,
composition
};
}
@@ -0,0 +1,169 @@
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
const KNOWN_P0_DOMAINS = new Set([
"settlements_60_62",
"vat_document_register_book",
"month_close_costs_20_44",
"fixed_asset_amortization"
]);
function toAnalysisContext(
runtimeAnalysisContext: BuildAssistantDeepTurnRuntimeContextInput["runtimeAnalysisContext"]
): Record<string, string | null> | null {
if (!runtimeAnalysisContext.active) {
return null;
}
return {
as_of_date: runtimeAnalysisContext.as_of_date,
period_from: runtimeAnalysisContext.period_from,
period_to: runtimeAnalysisContext.period_to,
source: runtimeAnalysisContext.source
};
}
export interface BuildAssistantDeepTurnRuntimeContextInput {
userMessage: string;
normalizedPayload: NormalizeResponsePayload["normalized"];
routeSummary: RouteHintSummary | null;
runtimeAnalysisContext: {
active: boolean;
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
};
followupUsage: unknown | null | undefined;
resolveCompanyAnchors: (userMessage: string) => unknown;
resolveBusinessScopeAlignment: (input: {
userMessage: string;
companyAnchors: unknown;
normalized: NormalizeResponsePayload["normalized"];
routeSummary: RouteHintSummary | null;
}) => {
route_summary_resolved: RouteHintSummary | null;
[key: string]: unknown;
};
inferP0DomainFromMessage: (userMessage: string) => string | null;
resolveTemporalGuard: (input: {
userMessage: string;
normalized: NormalizeResponsePayload["normalized"];
companyAnchors: unknown;
analysisContext: Record<string, string | null> | null;
}) => {
effective_primary_period?: unknown;
primary_period_window?: unknown;
[key: string]: unknown;
};
resolveDomainPolarityGuard: (input: {
userMessage: string;
companyAnchors: unknown;
focusDomainHint: string | null;
}) => unknown;
resolveClaimBoundAnchors: (input: {
userMessage: string;
companyAnchors: unknown;
focusDomainHint: string | null;
primaryPeriod: unknown;
}) => {
claim_type: string;
[key: string]: unknown;
};
resolveBusinessScopeFromLiveContext: (input: {
current: {
route_summary_resolved: RouteHintSummary | null;
[key: string]: unknown;
};
temporalGuard: unknown;
claimType: string;
focusDomainHint: string | null;
userMessage: string;
companyAnchors: unknown;
followupApplied: boolean;
}) => {
route_summary_resolved: RouteHintSummary | null;
[key: string]: unknown;
};
}
export interface BuildAssistantDeepTurnRuntimeContextOutput {
companyAnchors: unknown;
initialBusinessScopeResolution: {
route_summary_resolved: RouteHintSummary | null;
[key: string]: unknown;
};
inferredDomainByMessage: string | null;
focusDomainForGuards: string | null;
temporalGuard: {
effective_primary_period?: unknown;
primary_period_window?: unknown;
[key: string]: unknown;
};
domainPolarityGuardInitial: unknown;
claimAnchorAudit: {
claim_type: string;
[key: string]: unknown;
};
businessScopeResolution: {
route_summary_resolved: RouteHintSummary | null;
[key: string]: unknown;
};
resolvedRouteSummary: RouteHintSummary | null;
liveTemporalHint: Record<string, string | null> | null;
}
export function buildAssistantDeepTurnRuntimeContext(
input: BuildAssistantDeepTurnRuntimeContextInput
): BuildAssistantDeepTurnRuntimeContextOutput {
const companyAnchors = input.resolveCompanyAnchors(input.userMessage);
const initialBusinessScopeResolution = input.resolveBusinessScopeAlignment({
userMessage: input.userMessage,
companyAnchors,
normalized: input.normalizedPayload,
routeSummary: input.routeSummary
});
const inferredDomainByMessage = input.inferP0DomainFromMessage(input.userMessage);
const focusDomainForGuards =
inferredDomainByMessage && KNOWN_P0_DOMAINS.has(inferredDomainByMessage) ? inferredDomainByMessage : null;
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
const temporalGuard = input.resolveTemporalGuard({
userMessage: input.userMessage,
normalized: input.normalizedPayload,
companyAnchors,
analysisContext
});
const domainPolarityGuardInitial = input.resolveDomainPolarityGuard({
userMessage: input.userMessage,
companyAnchors,
focusDomainHint: focusDomainForGuards
});
const claimAnchorAudit = input.resolveClaimBoundAnchors({
userMessage: input.userMessage,
companyAnchors,
focusDomainHint: focusDomainForGuards,
primaryPeriod: temporalGuard.effective_primary_period ?? temporalGuard.primary_period_window
});
const businessScopeResolution = input.resolveBusinessScopeFromLiveContext({
current: initialBusinessScopeResolution,
temporalGuard,
claimType: claimAnchorAudit.claim_type,
focusDomainHint: focusDomainForGuards,
userMessage: input.userMessage,
companyAnchors,
followupApplied: Boolean((input.followupUsage as { applied?: unknown } | null)?.applied)
});
const resolvedRouteSummary = businessScopeResolution.route_summary_resolved;
const liveTemporalHint = toAnalysisContext(input.runtimeAnalysisContext);
return {
companyAnchors,
initialBusinessScopeResolution,
inferredDomainByMessage,
focusDomainForGuards,
temporalGuard,
domainPolarityGuardInitial,
claimAnchorAudit,
businessScopeResolution,
resolvedRouteSummary,
liveTemporalHint
};
}
@@ -0,0 +1,122 @@
import type {
AnswerGroundingCheck,
AssistantRequirement,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
import type { AssistantRequirementExtractionResult, AssistantCoverageEvaluationResult } from "./assistantOrchestrationRuntimeAdapter";
import { runAssistantCoverageGroundingPipeline } from "./assistantOrchestrationRuntimeAdapter";
import type { AssistantDeepTurnGroundingEligibilityOutput } from "./assistantDeepTurnGuardRuntimeAdapter";
import { applyAssistantDeepTurnGroundingEligibility } from "./assistantDeepTurnGuardRuntimeAdapter";
export interface AssistantDeepTurnGroundingRuntimeInput {
claimType: string;
retrievalResults: UnifiedRetrievalResult[];
rbpPlanAudit: unknown;
faPlanAudit: unknown;
routeSummary: RouteHintSummary | null;
normalizedPayload: NormalizeResponsePayload["normalized"] | null | undefined;
userMessage: string;
requirementExtraction: AssistantRequirementExtractionResult;
extractRequirements: (
routeSummary: RouteHintSummary | null,
normalized: NormalizeResponsePayload["normalized"] | null | undefined,
userMessage: string
) => AssistantRequirementExtractionResult;
evaluateCoverage: (
requirements: AssistantRequirement[],
retrievalResults: UnifiedRetrievalResult[]
) => AssistantCoverageEvaluationResult;
checkGrounding: (
userMessage: string,
requirements: AssistantRequirement[],
coverage: RequirementCoverageReport,
retrievalResults: UnifiedRetrievalResult[]
) => AnswerGroundingCheck;
temporalGuard: unknown;
polarityAudit: unknown;
evidenceAudit: unknown;
claimAnchorAudit: unknown;
targetedEvidenceHitRate?: number | null;
businessScopeResolved?: string[] | null;
collectRbpLiveRouteAudit: (input: {
claimType: string;
retrievalResults: UnifiedRetrievalResult[];
planAudit: unknown;
}) => unknown;
collectFaLiveRouteAudit: (input: {
claimType: string;
retrievalResults: UnifiedRetrievalResult[];
planAudit: unknown;
}) => unknown;
runCoverageGroundingPipelineFn?: typeof runAssistantCoverageGroundingPipeline;
applyGroundingEligibilityFn?: (input: {
groundingCheckBase: AnswerGroundingCheck;
temporalGuard: unknown;
polarityAudit: unknown;
evidenceAudit: unknown;
claimAnchorAudit?: unknown;
targetedEvidenceHitRate?: number | null;
businessScopeResolved?: string[] | null;
}) => AssistantDeepTurnGroundingEligibilityOutput<AnswerGroundingCheck>;
}
export interface AssistantDeepTurnGroundingRuntimeOutput {
rbpLiveRouteAudit: unknown;
faLiveRouteAudit: unknown;
coverageEvaluation: AssistantCoverageEvaluationResult;
groundingCheckBase: AnswerGroundingCheck;
groundedAnswerEligibilityGuard: unknown;
groundingCheck: AnswerGroundingCheck;
}
export function runAssistantDeepTurnGroundingRuntime(
input: AssistantDeepTurnGroundingRuntimeInput
): AssistantDeepTurnGroundingRuntimeOutput {
const runCoverageGroundingPipelineSafe = input.runCoverageGroundingPipelineFn ?? runAssistantCoverageGroundingPipeline;
const applyGroundingEligibilitySafe =
input.applyGroundingEligibilityFn ??
((payload) => applyAssistantDeepTurnGroundingEligibility(payload as any) as any);
const rbpLiveRouteAudit = input.collectRbpLiveRouteAudit({
claimType: input.claimType,
retrievalResults: input.retrievalResults,
planAudit: input.rbpPlanAudit
});
const faLiveRouteAudit = input.collectFaLiveRouteAudit({
claimType: input.claimType,
retrievalResults: input.retrievalResults,
planAudit: input.faPlanAudit
});
const orchestrationRuntime = runCoverageGroundingPipelineSafe({
routeSummary: input.routeSummary,
normalized: input.normalizedPayload,
userMessage: input.userMessage,
retrievalResults: input.retrievalResults,
requirementExtraction: input.requirementExtraction,
extractRequirements: input.extractRequirements,
evaluateCoverage: input.evaluateCoverage,
checkGrounding: input.checkGrounding
});
const coverageEvaluation = orchestrationRuntime.coverageEvaluation;
const groundingCheckBase = orchestrationRuntime.groundingCheckBase;
const groundingEligibilityRuntime = applyGroundingEligibilitySafe({
groundingCheckBase,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
evidenceAudit: input.evidenceAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceHitRate: input.targetedEvidenceHitRate,
businessScopeResolved: input.businessScopeResolved
});
return {
rbpLiveRouteAudit,
faLiveRouteAudit,
coverageEvaluation,
groundingCheckBase,
groundedAnswerEligibilityGuard: groundingEligibilityRuntime.groundedAnswerEligibilityGuard,
groundingCheck: groundingEligibilityRuntime.groundingCheck
};
}
@@ -0,0 +1,112 @@
import type { UnifiedRetrievalResult } from "../types/assistant";
import { applyTargetedEvidenceAcquisition } from "./assistantClaimBoundEvidence";
import {
applyDomainPolarityGuardToRetrievalResults,
applyEvidenceAdmissibilityGate,
applyEligibilityToGroundingCheck,
evaluateGroundedAnswerEligibility
} from "./assistantRuntimeGuards";
type GroundingCheckLike = {
status: string;
reasons: string[];
};
type ApplyEligibilityToGroundingCheckFn = <T extends GroundingCheckLike>(
groundingCheck: T,
eligibility: ReturnType<typeof evaluateGroundedAnswerEligibility>
) => T;
export interface AssistantDeepTurnRetrievalGuardPipelineInput {
retrievalResults: UnifiedRetrievalResult[];
domainPolarityGuardInitial: Parameters<typeof applyDomainPolarityGuardToRetrievalResults>[0]["guard"];
claimAnchorAudit: Parameters<typeof applyTargetedEvidenceAcquisition>[0]["claimAudit"];
temporalGuard: Parameters<typeof applyEvidenceAdmissibilityGate>[0]["temporal"];
focusDomainForGuards: Parameters<typeof applyEvidenceAdmissibilityGate>[0]["focusDomainHint"];
companyAnchors?: Parameters<typeof applyEvidenceAdmissibilityGate>[0]["companyAnchors"];
userMessage: string;
applyDomainPolarityGuardFn?: typeof applyDomainPolarityGuardToRetrievalResults;
applyTargetedEvidenceFn?: typeof applyTargetedEvidenceAcquisition;
applyEvidenceAdmissibilityGateFn?: typeof applyEvidenceAdmissibilityGate;
}
export interface AssistantDeepTurnRetrievalGuardPipelineOutput {
retrievalResults: UnifiedRetrievalResult[];
polarityGuardResult: ReturnType<typeof applyDomainPolarityGuardToRetrievalResults>;
targetedEvidenceResult: ReturnType<typeof applyTargetedEvidenceAcquisition>;
evidenceGateResult: ReturnType<typeof applyEvidenceAdmissibilityGate>;
}
export function applyAssistantDeepTurnRetrievalGuards(
input: AssistantDeepTurnRetrievalGuardPipelineInput
): AssistantDeepTurnRetrievalGuardPipelineOutput {
const applyDomainPolarityGuardSafe = input.applyDomainPolarityGuardFn ?? applyDomainPolarityGuardToRetrievalResults;
const applyTargetedEvidenceSafe = input.applyTargetedEvidenceFn ?? applyTargetedEvidenceAcquisition;
const applyEvidenceAdmissibilityGateSafe = input.applyEvidenceAdmissibilityGateFn ?? applyEvidenceAdmissibilityGate;
const polarityGuardResult = applyDomainPolarityGuardSafe({
retrievalResults: input.retrievalResults,
guard: input.domainPolarityGuardInitial
});
const targetedEvidenceResult = applyTargetedEvidenceSafe({
retrievalResults: polarityGuardResult.retrievalResults,
claimAudit: input.claimAnchorAudit
});
const evidenceGateResult = applyEvidenceAdmissibilityGateSafe({
retrievalResults: targetedEvidenceResult.retrievalResults,
temporal: input.temporalGuard,
focusDomainHint: input.focusDomainForGuards,
polarity: polarityGuardResult.audit.polarity,
companyAnchors: input.companyAnchors,
userMessage: input.userMessage
});
return {
retrievalResults: evidenceGateResult.retrievalResults,
polarityGuardResult,
targetedEvidenceResult,
evidenceGateResult
};
}
export interface AssistantDeepTurnGroundingEligibilityInput<T extends GroundingCheckLike> {
groundingCheckBase: T;
temporalGuard: Parameters<typeof evaluateGroundedAnswerEligibility>[0]["temporal"];
polarityAudit: Parameters<typeof evaluateGroundedAnswerEligibility>[0]["polarity"];
evidenceAudit: Parameters<typeof evaluateGroundedAnswerEligibility>[0]["evidence"];
claimAnchorAudit?: Parameters<typeof evaluateGroundedAnswerEligibility>[0]["claimAnchors"];
targetedEvidenceHitRate?: number | null;
businessScopeResolved?: string[] | null;
evaluateGroundedAnswerEligibilityFn?: typeof evaluateGroundedAnswerEligibility;
applyEligibilityToGroundingCheckFn?: ApplyEligibilityToGroundingCheckFn;
}
export interface AssistantDeepTurnGroundingEligibilityOutput<T extends GroundingCheckLike> {
groundedAnswerEligibilityGuard: ReturnType<typeof evaluateGroundedAnswerEligibility>;
groundingCheck: T;
}
export function applyAssistantDeepTurnGroundingEligibility<T extends GroundingCheckLike>(
input: AssistantDeepTurnGroundingEligibilityInput<T>
): AssistantDeepTurnGroundingEligibilityOutput<T> {
const evaluateGroundedAnswerEligibilitySafe =
input.evaluateGroundedAnswerEligibilityFn ?? evaluateGroundedAnswerEligibility;
const applyEligibilityToGroundingCheckSafe =
input.applyEligibilityToGroundingCheckFn ??
((groundingCheck, eligibility) => applyEligibilityToGroundingCheck(groundingCheck, eligibility));
const groundedAnswerEligibilityGuard = evaluateGroundedAnswerEligibilitySafe({
temporal: input.temporalGuard,
polarity: input.polarityAudit,
evidence: input.evidenceAudit,
claimAnchors: input.claimAnchorAudit,
targetedEvidenceHitRate: input.targetedEvidenceHitRate,
businessScopeResolved: input.businessScopeResolved
});
const groundingCheck = applyEligibilityToGroundingCheckSafe(input.groundingCheckBase, groundedAnswerEligibilityGuard);
return {
groundedAnswerEligibilityGuard,
groundingCheck
};
}
@@ -0,0 +1,112 @@
import type { AssistantReplyType, AssistantRequirement, AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
import type { AnswerStructureV11 } from "../types/stage1Contracts";
import type { AssistantDeepTurnPackagingInput } from "./assistantDeepTurnPackaging";
export interface AssistantDeepTurnInputBuilderArgs {
sessionId: string;
messageId: string;
userMessage: string;
normalized: {
trace_id: string;
prompt_version: string;
schema_version: string;
normalized: NormalizeResponsePayload["normalized"];
};
normalizedQuestion: string;
routeSummary: RouteHintSummary | null;
droppedIntentSegments: string[];
analysisContextForContract: {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
} | null;
executionPlan: Array<{
fragment_id: string;
requirement_ids: string[];
route: string;
should_execute: boolean;
no_route_reason?: string | null;
clarification_reason?: string | null;
}>;
requirementExtractionRequirements: AssistantRequirement[];
coverageEvaluationRequirements: AssistantRequirement[];
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
retrievalCalls: Array<Record<string, unknown>>;
retrievalResultsRaw: unknown[];
retrievalResults: UnifiedRetrievalResult[];
routesForDebug: Array<Record<string, unknown>>;
resolvedExecutionState: unknown;
questionTypeClass: string;
companyAnchors: unknown;
runtimeAnalysisContext: {
active: boolean;
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
};
businessScopeResolution: {
business_scope_raw?: string[];
business_scope_resolved?: string[];
company_grounding_applied?: boolean;
scope_resolution_reason?: string[];
};
temporalGuard: Record<string, unknown>;
polarityAudit: Record<string, unknown>;
claimAnchorAudit: Record<string, unknown>;
targetedEvidenceAudit: unknown;
evidenceAdmissibilityGateAudit: unknown;
rbpLiveRouteAudit: unknown | null;
faLiveRouteAudit: unknown | null;
groundedAnswerEligibilityGuard: Record<string, unknown>;
followupStateUsage?: unknown;
composition: {
reply_type: AssistantReplyType;
fallback_type: unknown;
answer_structure_v11?: AnswerStructureV11 | null;
problem_centric_answer_applied?: boolean;
problem_units_used_count?: number;
problem_answer_mode?: string;
problem_unit_ids_used?: unknown;
};
safeAssistantReplyBase: string;
featureContractsV11: boolean;
featureAnswerPolicyV11: boolean;
investigationStateSnapshot: unknown;
addressRuntimeMetaForDeep:
| {
attempted?: boolean;
applied?: boolean;
reason?: string | null;
provider?: string | null;
fallbackRuleHit?: string | null;
toolGateDecision?: string | null;
toolGateReason?: string | null;
predecomposeContract?: unknown;
orchestrationContract?: unknown;
}
| null
| undefined;
}
export function buildAssistantDeepTurnPackagingInput(args: AssistantDeepTurnInputBuilderArgs): AssistantDeepTurnPackagingInput {
return {
...args,
routesForDebug: Array.isArray(args.routesForDebug) ? args.routesForDebug : [],
followupStateUsage: args.followupStateUsage ?? null,
composition: {
reply_type: args.composition.reply_type,
fallback_type: args.composition.fallback_type,
answer_structure_v11: args.composition.answer_structure_v11 ?? null,
problem_centric_answer_applied: args.composition.problem_centric_answer_applied ?? false,
problem_units_used_count: args.composition.problem_units_used_count ?? 0,
problem_answer_mode: args.composition.problem_answer_mode ?? "stage1_policy_v11",
problem_unit_ids_used: Array.isArray(args.composition.problem_unit_ids_used) ? args.composition.problem_unit_ids_used : []
}
};
}
@@ -0,0 +1,250 @@
import type {
AssistantConversationItem,
AssistantReplyType,
AssistantRequirement,
AnswerGroundingCheck,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
import type { AnswerStructureV11 } from "../types/stage1Contracts";
import {
assembleAssistantEvidenceBundle,
type AssistantEvidenceBundleAssembly
} from "./assistantEvidenceBundleAssembler";
import { assembleAssistantContractsBundleV1, type AssistantContractsBundleV1 } from "./assistantContractsBundleAssembler";
import { buildDeepAnswerArtifacts, buildAssistantConversationItem, type DeepAnswerArtifacts } from "./assistantDeepResponseAssembler";
import { buildDeepAnalysisDebugPayload } from "./assistantDebugPayloadAssembler";
import { buildDeepAnalysisProcessedLogDetails } from "./assistantMessageLogAssembler";
export interface AssistantDeepTurnPackagingInput {
sessionId: string;
messageId: string;
userMessage: string;
normalized: {
trace_id: string;
prompt_version: string;
schema_version: string;
normalized: NormalizeResponsePayload["normalized"];
};
normalizedQuestion: string;
routeSummary: RouteHintSummary | null;
droppedIntentSegments: string[];
analysisContextForContract: {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
} | null;
executionPlan: Array<{
fragment_id: string;
requirement_ids: string[];
route: string;
should_execute: boolean;
no_route_reason?: string | null;
clarification_reason?: string | null;
}>;
requirementExtractionRequirements: AssistantRequirement[];
coverageEvaluationRequirements: AssistantRequirement[];
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
retrievalCalls: Array<Record<string, unknown>>;
retrievalResultsRaw: unknown[];
retrievalResults: UnifiedRetrievalResult[];
routesForDebug: Array<Record<string, unknown>>;
resolvedExecutionState: unknown;
questionTypeClass: string;
companyAnchors: unknown;
runtimeAnalysisContext: {
active: boolean;
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
};
businessScopeResolution: {
business_scope_raw?: string[];
business_scope_resolved?: string[];
company_grounding_applied?: boolean;
scope_resolution_reason?: string[];
};
temporalGuard: Record<string, unknown>;
polarityAudit: Record<string, unknown>;
claimAnchorAudit: Record<string, unknown>;
targetedEvidenceAudit: unknown;
evidenceAdmissibilityGateAudit: unknown;
rbpLiveRouteAudit: unknown | null;
faLiveRouteAudit: unknown | null;
groundedAnswerEligibilityGuard: Record<string, unknown>;
followupStateUsage: unknown | null;
composition: {
reply_type: AssistantReplyType;
fallback_type: unknown;
answer_structure_v11?: AnswerStructureV11 | null;
problem_centric_answer_applied?: boolean;
problem_units_used_count?: number;
problem_answer_mode?: string;
problem_unit_ids_used?: string[];
};
safeAssistantReplyBase: string;
featureContractsV11: boolean;
featureAnswerPolicyV11: boolean;
investigationStateSnapshot: unknown;
addressRuntimeMetaForDeep:
| {
attempted?: boolean;
applied?: boolean;
reason?: string | null;
provider?: string | null;
fallbackRuleHit?: string | null;
toolGateDecision?: string | null;
toolGateReason?: string | null;
predecomposeContract?: unknown;
orchestrationContract?: unknown;
}
| null
| undefined;
}
export interface AssistantDeepTurnPackagingOutput {
evidenceBundleAssembly: AssistantEvidenceBundleAssembly;
contractsBundleV1: AssistantContractsBundleV1;
deepAnswerArtifacts: DeepAnswerArtifacts;
debug: Record<string, unknown>;
assistantItem: AssistantConversationItem;
deepAnalysisLogDetails: Record<string, unknown>;
}
export function assembleAssistantDeepTurnPackaging(input: AssistantDeepTurnPackagingInput): AssistantDeepTurnPackagingOutput {
const normalizedPayload = (input.normalized.normalized ?? null) as Record<string, unknown> | null;
const normalizedFragments = Array.isArray(normalizedPayload?.["fragments"]) ? (normalizedPayload?.["fragments"] as unknown[]) : [];
const evidenceBundleAssembly = assembleAssistantEvidenceBundle({
retrievalCalls: input.retrievalCalls,
retrievalResults: input.retrievalResults
});
const contractsBundleV1 = assembleAssistantContractsBundleV1({
userMessage: input.userMessage,
normalizedQuestion: input.normalizedQuestion,
normalized: input.normalized.normalized,
routeSummary: input.routeSummary,
droppedIntentSegments: input.droppedIntentSegments,
analysisContext: input.analysisContextForContract,
executionPlan: input.executionPlan,
requirements: input.requirementExtractionRequirements,
evidenceBundleContractV1: evidenceBundleAssembly.evidenceBundleContractV1,
replyType: input.composition.reply_type,
coverageReport: input.coverageReport,
grounding: input.groundingCheck,
retrievalResults: input.retrievalResults
});
const deepAnswerArtifacts = buildDeepAnswerArtifacts({
safeAssistantReplyBase: input.safeAssistantReplyBase,
featureContractsV11: input.featureContractsV11,
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
compositionAnswerStructureV11: input.composition.answer_structure_v11 ?? null,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
retrievalResults: input.retrievalResults
});
const debug = buildDeepAnalysisDebugPayload({
traceId: input.normalized.trace_id,
promptVersion: input.normalized.prompt_version,
schemaVersion: input.normalized.schema_version,
fallbackType: input.composition.fallback_type,
routeSummary: input.routeSummary,
fragments: normalizedFragments,
requirementsExtracted: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
routes: input.routesForDebug,
retrievalStatus: evidenceBundleAssembly.retrievalStatus,
retrievalResults: input.retrievalResults,
groundingCheck: input.groundingCheck,
droppedIntentSegments: input.droppedIntentSegments,
questionTypeClass: input.questionTypeClass,
companyAnchors: input.companyAnchors,
runtimeAnalysisContext: input.runtimeAnalysisContext,
businessScopeResolution: input.businessScopeResolution,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceAudit: input.targetedEvidenceAudit,
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
faLiveRouteAudit: input.faLiveRouteAudit,
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
followupStateUsage: input.followupStateUsage,
compositionDebug: {
problem_centric_answer_applied: input.composition.problem_centric_answer_applied ?? false,
problem_units_used_count: input.composition.problem_units_used_count ?? 0,
problem_answer_mode: input.composition.problem_answer_mode ?? "stage1_policy_v11",
problem_unit_ids_used: Array.isArray(input.composition.problem_unit_ids_used) ? input.composition.problem_unit_ids_used : []
},
addressRuntimeMetaForDeep: input.addressRuntimeMetaForDeep,
outcomeClassV1: contractsBundleV1.outcomeClassV1,
assistantOrchestrationContractsV1: contractsBundleV1.assistantOrchestrationContractsV1,
answerStructureV11: deepAnswerArtifacts.answerStructureV11,
investigationStateSnapshot: input.investigationStateSnapshot,
normalizedPayload: normalizedPayload
});
const assistantItem = buildAssistantConversationItem({
messageId: input.messageId,
sessionId: input.sessionId,
text: deepAnswerArtifacts.safeAssistantReply,
replyType: input.composition.reply_type,
traceId: input.normalized.trace_id,
debug: debug as any
});
const deepAnalysisLogDetails = buildDeepAnalysisProcessedLogDetails({
sessionId: input.sessionId,
messageId: input.messageId,
userMessage: input.userMessage,
normalizerOutput: input.normalized.normalized,
executionPlan: input.executionPlan,
resolvedExecutionState: input.resolvedExecutionState,
routes: input.routesForDebug,
retrievalCalls: input.retrievalCalls,
retrievalResultsRaw: input.retrievalResultsRaw,
retrievalResultsNormalized: input.retrievalResults,
requirementsExtracted: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
replyType: input.composition.reply_type,
droppedIntentSegments: input.droppedIntentSegments,
questionTypeClass: input.questionTypeClass,
companyAnchors: input.companyAnchors,
runtimeAnalysisContext: input.runtimeAnalysisContext,
businessScopeResolution: input.businessScopeResolution,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceAudit: input.targetedEvidenceAudit,
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
faLiveRouteAudit: input.faLiveRouteAudit,
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
followupStateUsage: input.followupStateUsage,
compositionDebug: {
problem_centric_answer_applied: input.composition.problem_centric_answer_applied ?? false,
problem_units_used_count: input.composition.problem_units_used_count ?? 0,
problem_answer_mode: input.composition.problem_answer_mode ?? "stage1_policy_v11",
problem_unit_ids_used: Array.isArray(input.composition.problem_unit_ids_used) ? input.composition.problem_unit_ids_used : [],
fallback_type: input.composition.fallback_type as string
},
outcomeClassV1: contractsBundleV1.outcomeClassV1,
assistantOrchestrationContractsV1: contractsBundleV1.assistantOrchestrationContractsV1,
answerStructureV11: deepAnswerArtifacts.answerStructureV11,
investigationStateSnapshot: input.investigationStateSnapshot,
assistantReply: deepAnswerArtifacts.safeAssistantReply,
traceId: input.normalized.trace_id
});
return {
evidenceBundleAssembly,
contractsBundleV1,
deepAnswerArtifacts,
debug,
assistantItem,
deepAnalysisLogDetails
};
}
@@ -0,0 +1,212 @@
import { nanoid } from "nanoid";
import type {
AssistantConversationItem,
AssistantRequirement,
AnswerGroundingCheck,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
import type { InvestigationStateWithProblemUnits } from "../types/stage2ProblemUnits";
import type { AssistantDeepTurnInputBuilderArgs } from "./assistantDeepTurnInputBuilder";
import { buildAssistantDeepTurnPackagingInput } from "./assistantDeepTurnInputBuilder";
import { assembleAssistantDeepTurnPackaging } from "./assistantDeepTurnPackaging";
import type {
AssistantAnalysisContextForContract,
AssistantRuntimeAnalysisContextForPrePackaging
} from "./assistantDeepTurnPrePackagingContext";
import { buildAssistantDeepTurnPrePackagingContext } from "./assistantDeepTurnPrePackagingContext";
import {
buildAssistantInvestigationStateSnapshot,
persistAssistantInvestigationStateSnapshot
} from "./assistantInvestigationStateRuntimeAdapter";
type AssistantDeepTurnCompositionForPackaging = AssistantDeepTurnInputBuilderArgs["composition"] & {
assistant_reply: string;
};
export interface AssistantDeepTurnPackagingRuntimeInput {
featureInvestigationStateV1: boolean;
sessionId: string;
questionId: string;
userMessage: string;
normalized: {
trace_id: string;
prompt_version: string;
schema_version: string;
normalized: NormalizeResponsePayload["normalized"];
};
normalizedQuestion: string;
routeSummary: RouteHintSummary | null;
executionPlan: AssistantDeepTurnInputBuilderArgs["executionPlan"];
requirementExtractionRequirements: AssistantRequirement[];
coverageEvaluationRequirements: AssistantRequirement[];
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
retrievalCalls: Array<Record<string, unknown>>;
retrievalResultsRaw: unknown[];
retrievalResults: UnifiedRetrievalResult[];
questionTypeClass: string;
companyAnchors: unknown;
runtimeAnalysisContext: AssistantDeepTurnInputBuilderArgs["runtimeAnalysisContext"];
businessScopeResolution: AssistantDeepTurnInputBuilderArgs["businessScopeResolution"];
temporalGuard: Record<string, unknown>;
polarityAudit: Record<string, unknown>;
claimAnchorAudit: Record<string, unknown>;
targetedEvidenceAudit: unknown;
evidenceAdmissibilityGateAudit: unknown;
rbpLiveRouteAudit: unknown | null;
faLiveRouteAudit: unknown | null;
groundedAnswerEligibilityGuard: Record<string, unknown>;
followupStateUsage?: unknown;
followupApplied: boolean;
composition: AssistantDeepTurnCompositionForPackaging;
featureContractsV11: boolean;
featureAnswerPolicyV11: boolean;
previousInvestigationState: InvestigationStateWithProblemUnits | null | undefined;
addressRuntimeMetaForDeep:
| {
attempted?: boolean;
applied?: boolean;
reason?: string | null;
provider?: string | null;
fallbackRuleHit?: string | null;
toolGateDecision?: string | null;
toolGateReason?: string | null;
predecomposeContract?: unknown;
orchestrationContract?: unknown;
}
| null
| undefined;
extractDroppedIntentSegments: (normalizedPayload: NormalizeResponsePayload["normalized"]) => string[];
buildDebugRoutes: (routeSummary: RouteHintSummary | null) => Array<Record<string, unknown>>;
extractExecutionState: (normalizedPayload: NormalizeResponsePayload["normalized"]) => unknown;
sanitizeReply: (value: string, fallback?: string) => string;
persistInvestigationState: (sessionId: string, snapshot: InvestigationStateWithProblemUnits) => void;
nowIso?: () => string;
messageIdFactory?: () => string;
buildPrePackagingContextFn?: typeof buildAssistantDeepTurnPrePackagingContext;
buildInvestigationStateSnapshotFn?: typeof buildAssistantInvestigationStateSnapshot;
persistInvestigationStateSnapshotFn?: typeof persistAssistantInvestigationStateSnapshot;
buildDeepTurnPackagingInputFn?: typeof buildAssistantDeepTurnPackagingInput;
assembleDeepTurnPackagingFn?: typeof assembleAssistantDeepTurnPackaging;
}
export interface AssistantDeepTurnPackagingRuntimeOutput {
messageId: string;
investigationStateSnapshot: InvestigationStateWithProblemUnits | null;
droppedIntentSegments: string[];
analysisContextForContract: AssistantAnalysisContextForContract | null;
routesForDebug: Array<Record<string, unknown>>;
resolvedExecutionState: unknown;
safeAssistantReplyBase: string;
safeAssistantReply: string;
debug: Record<string, unknown>;
assistantItem: AssistantConversationItem;
deepAnalysisLogDetails: Record<string, unknown>;
}
export function runAssistantDeepTurnPackagingRuntime(
input: AssistantDeepTurnPackagingRuntimeInput
): AssistantDeepTurnPackagingRuntimeOutput {
const buildPrePackagingContextSafe = input.buildPrePackagingContextFn ?? buildAssistantDeepTurnPrePackagingContext;
const buildInvestigationStateSnapshotSafe =
input.buildInvestigationStateSnapshotFn ?? buildAssistantInvestigationStateSnapshot;
const persistInvestigationStateSnapshotSafe =
input.persistInvestigationStateSnapshotFn ?? persistAssistantInvestigationStateSnapshot;
const buildDeepTurnPackagingInputSafe = input.buildDeepTurnPackagingInputFn ?? buildAssistantDeepTurnPackagingInput;
const assembleDeepTurnPackagingSafe = input.assembleDeepTurnPackagingFn ?? assembleAssistantDeepTurnPackaging;
const deepTurnPrePackagingContext = buildPrePackagingContextSafe({
normalizedPayload: input.normalized.normalized,
routeSummary: input.routeSummary,
runtimeAnalysisContext: input.runtimeAnalysisContext as AssistantRuntimeAnalysisContextForPrePackaging,
assistantReply: input.composition.assistant_reply,
extractDroppedIntentSegments: input.extractDroppedIntentSegments,
buildDebugRoutes: input.buildDebugRoutes,
extractExecutionState: input.extractExecutionState,
sanitizeReply: input.sanitizeReply
});
const investigationStateSnapshot = buildInvestigationStateSnapshotSafe({
featureEnabled: input.featureInvestigationStateV1,
previousState: input.previousInvestigationState,
timestamp: input.nowIso ? input.nowIso() : new Date().toISOString(),
questionId: input.questionId,
userMessage: input.userMessage,
routeSummary: input.routeSummary,
requirements: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
retrievalResults: input.retrievalResults,
replyType: input.composition.reply_type,
followupApplied: input.followupApplied
});
persistInvestigationStateSnapshotSafe({
featureEnabled: input.featureInvestigationStateV1,
sessionId: input.sessionId,
snapshot: investigationStateSnapshot,
persist: input.persistInvestigationState
});
const messageId = input.messageIdFactory ? input.messageIdFactory() : `msg-${nanoid(10)}`;
const deepTurnPackagingInput = buildDeepTurnPackagingInputSafe({
sessionId: input.sessionId,
messageId,
userMessage: input.userMessage,
normalized: input.normalized,
normalizedQuestion: input.normalizedQuestion,
routeSummary: input.routeSummary,
droppedIntentSegments: deepTurnPrePackagingContext.droppedIntentSegments,
analysisContextForContract: deepTurnPrePackagingContext.analysisContextForContract,
executionPlan: input.executionPlan,
requirementExtractionRequirements: input.requirementExtractionRequirements,
coverageEvaluationRequirements: input.coverageEvaluationRequirements,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
retrievalCalls: input.retrievalCalls,
retrievalResultsRaw: input.retrievalResultsRaw,
retrievalResults: input.retrievalResults,
routesForDebug: deepTurnPrePackagingContext.routesForDebug,
resolvedExecutionState: deepTurnPrePackagingContext.resolvedExecutionState,
questionTypeClass: input.questionTypeClass,
companyAnchors: input.companyAnchors,
runtimeAnalysisContext: input.runtimeAnalysisContext,
businessScopeResolution: input.businessScopeResolution,
temporalGuard: input.temporalGuard,
polarityAudit: input.polarityAudit,
claimAnchorAudit: input.claimAnchorAudit,
targetedEvidenceAudit: input.targetedEvidenceAudit,
evidenceAdmissibilityGateAudit: input.evidenceAdmissibilityGateAudit,
rbpLiveRouteAudit: input.rbpLiveRouteAudit,
faLiveRouteAudit: input.faLiveRouteAudit,
groundedAnswerEligibilityGuard: input.groundedAnswerEligibilityGuard,
followupStateUsage: input.followupStateUsage,
composition: {
reply_type: input.composition.reply_type,
fallback_type: input.composition.fallback_type,
answer_structure_v11: input.composition.answer_structure_v11,
problem_centric_answer_applied: input.composition.problem_centric_answer_applied,
problem_units_used_count: input.composition.problem_units_used_count,
problem_answer_mode: input.composition.problem_answer_mode,
problem_unit_ids_used: input.composition.problem_unit_ids_used
},
safeAssistantReplyBase: deepTurnPrePackagingContext.safeAssistantReplyBase,
featureContractsV11: input.featureContractsV11,
featureAnswerPolicyV11: input.featureAnswerPolicyV11,
investigationStateSnapshot,
addressRuntimeMetaForDeep: input.addressRuntimeMetaForDeep
});
const deepTurnPackaging = assembleDeepTurnPackagingSafe(deepTurnPackagingInput);
return {
messageId,
investigationStateSnapshot,
droppedIntentSegments: deepTurnPrePackagingContext.droppedIntentSegments,
analysisContextForContract: deepTurnPrePackagingContext.analysisContextForContract,
routesForDebug: deepTurnPrePackagingContext.routesForDebug,
resolvedExecutionState: deepTurnPrePackagingContext.resolvedExecutionState,
safeAssistantReplyBase: deepTurnPrePackagingContext.safeAssistantReplyBase,
safeAssistantReply: deepTurnPackaging.deepAnswerArtifacts.safeAssistantReply,
debug: deepTurnPackaging.debug,
assistantItem: deepTurnPackaging.assistantItem,
deepAnalysisLogDetails: deepTurnPackaging.deepAnalysisLogDetails
};
}
@@ -0,0 +1,91 @@
import type { AssistantRequirement } from "../types/assistant";
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
import type { AssistantExecutionPlanItem } from "./assistantQueryPlanning";
export interface AssistantRequirementExtractionLike {
requirements: AssistantRequirement[];
byFragment: Map<string, string[]>;
}
export interface AssistantPlanEnforcementAuditLike {
executionPlan: AssistantExecutionPlanItem[];
audit: unknown;
}
export interface BuildAssistantDeepTurnExecutionPlanInput {
routeSummary: RouteHintSummary | null;
normalizedPayload: NormalizeResponsePayload["normalized"];
userMessage: string;
claimType: string;
temporalGuard: unknown;
domainPolarityGuardInitial: unknown;
extractRequirements: (
routeSummary: RouteHintSummary | null,
normalizedPayload: NormalizeResponsePayload["normalized"],
userMessage: string
) => AssistantRequirementExtractionLike;
toExecutionPlan: (
routeSummary: RouteHintSummary | null,
normalizedPayload: NormalizeResponsePayload["normalized"],
userMessage: string,
requirementByFragment: Map<string, string[]>
) => AssistantExecutionPlanItem[];
enforceRbpLiveRoutePlan: (input: {
executionPlan: AssistantExecutionPlanItem[];
claimType: string;
temporalGuard: unknown;
}) => AssistantPlanEnforcementAuditLike;
enforceFaLiveRoutePlan: (input: {
executionPlan: AssistantExecutionPlanItem[];
claimType: string;
temporalGuard: unknown;
}) => AssistantPlanEnforcementAuditLike;
applyTemporalHintToExecutionPlan: (
executionPlan: AssistantExecutionPlanItem[],
temporalGuard: unknown
) => AssistantExecutionPlanItem[];
applyPolarityHintToExecutionPlan: (
executionPlan: AssistantExecutionPlanItem[],
domainPolarityGuardInitial: unknown
) => AssistantExecutionPlanItem[];
}
export interface BuildAssistantDeepTurnExecutionPlanOutput {
requirementExtraction: AssistantRequirementExtractionLike;
executionPlan: AssistantExecutionPlanItem[];
rbpRoutePlanEnforcement: AssistantPlanEnforcementAuditLike;
faRoutePlanEnforcement: AssistantPlanEnforcementAuditLike;
}
export function buildAssistantDeepTurnExecutionPlan(
input: BuildAssistantDeepTurnExecutionPlanInput
): BuildAssistantDeepTurnExecutionPlanOutput {
const requirementExtraction = input.extractRequirements(input.routeSummary, input.normalizedPayload, input.userMessage);
let executionPlan = input.toExecutionPlan(
input.routeSummary,
input.normalizedPayload,
input.userMessage,
requirementExtraction.byFragment
);
const rbpRoutePlanEnforcement = input.enforceRbpLiveRoutePlan({
executionPlan,
claimType: input.claimType,
temporalGuard: input.temporalGuard
});
executionPlan = rbpRoutePlanEnforcement.executionPlan;
const faRoutePlanEnforcement = input.enforceFaLiveRoutePlan({
executionPlan,
claimType: input.claimType,
temporalGuard: input.temporalGuard
});
executionPlan = faRoutePlanEnforcement.executionPlan;
executionPlan = input.applyTemporalHintToExecutionPlan(executionPlan, input.temporalGuard);
executionPlan = input.applyPolarityHintToExecutionPlan(executionPlan, input.domainPolarityGuardInitial);
return {
requirementExtraction,
executionPlan,
rbpRoutePlanEnforcement,
faRoutePlanEnforcement
};
}
@@ -0,0 +1,57 @@
import type { NormalizeResponsePayload, RouteHintSummary } from "../types/normalizer";
export interface AssistantRuntimeAnalysisContextForPrePackaging {
active: boolean;
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
}
export interface AssistantAnalysisContextForContract {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
}
export interface BuildAssistantDeepTurnPrePackagingContextInput {
normalizedPayload: NormalizeResponsePayload["normalized"];
routeSummary: RouteHintSummary | null;
runtimeAnalysisContext: AssistantRuntimeAnalysisContextForPrePackaging;
assistantReply: string;
extractDroppedIntentSegments: (normalizedPayload: NormalizeResponsePayload["normalized"]) => string[];
buildDebugRoutes: (routeSummary: RouteHintSummary | null) => Array<Record<string, unknown>>;
extractExecutionState: (normalizedPayload: NormalizeResponsePayload["normalized"]) => unknown;
sanitizeReply: (value: string, fallback?: string) => string;
}
export interface AssistantDeepTurnPrePackagingContext {
droppedIntentSegments: string[];
analysisContextForContract: AssistantAnalysisContextForContract | null;
routesForDebug: Array<Record<string, unknown>>;
resolvedExecutionState: unknown;
safeAssistantReplyBase: string;
}
export function buildAssistantDeepTurnPrePackagingContext(
input: BuildAssistantDeepTurnPrePackagingContextInput
): AssistantDeepTurnPrePackagingContext {
return {
droppedIntentSegments: input.extractDroppedIntentSegments(input.normalizedPayload),
analysisContextForContract: input.runtimeAnalysisContext.active
? {
as_of_date: input.runtimeAnalysisContext.as_of_date,
period_from: input.runtimeAnalysisContext.period_from,
period_to: input.runtimeAnalysisContext.period_to,
source: input.runtimeAnalysisContext.source,
snapshot_mode: input.runtimeAnalysisContext.snapshot_mode
}
: null,
routesForDebug: input.buildDebugRoutes(input.routeSummary),
resolvedExecutionState: input.extractExecutionState(input.normalizedPayload),
safeAssistantReplyBase: input.sanitizeReply(input.assistantReply, "Нужны уточнения для надежного ответа.")
};
}
@@ -0,0 +1,29 @@
import type {
AssistantConversationItem,
AssistantDebugPayload,
AssistantMessageResponsePayload,
AssistantReplyType
} from "../types/assistant";
export interface BuildAssistantDeepTurnSuccessResponseInput {
sessionId: string;
assistantReply: string;
replyType: AssistantReplyType;
conversationItem: AssistantConversationItem;
debug: AssistantDebugPayload | Record<string, unknown>;
conversation: AssistantConversationItem[];
}
export function buildAssistantDeepTurnSuccessResponse(
input: BuildAssistantDeepTurnSuccessResponseInput
): AssistantMessageResponsePayload {
return {
ok: true,
session_id: input.sessionId,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
conversation_item: input.conversationItem,
debug: input.debug as AssistantDebugPayload,
conversation: input.conversation
};
}
@@ -0,0 +1,131 @@
import type { UnifiedRetrievalResult } from "../types/assistant";
import type { AssistantExecutionPlanItem } from "./assistantQueryPlanning";
import { normalizeRetrievalResult } from "./retrievalResultNormalizer";
export interface AssistantLiveTemporalHint {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
}
export interface AssistantRetrievalCallRecord {
fragment_id: string;
requirement_ids: string[];
route: string;
status: "skipped" | "executed" | "failed";
query_text: string;
reason: string | null;
}
export interface AssistantRetrievalRawResultRecord {
fragment_id: string;
route: string;
raw_result: unknown;
}
export interface AssistantDeepTurnRetrievalExecutionInput {
executionPlan: AssistantExecutionPlanItem[];
liveTemporalHint: AssistantLiveTemporalHint | null;
executeRouteRuntime: (
route: string,
fragmentText: string,
options: {
temporalHint: AssistantLiveTemporalHint | null;
}
) => Promise<unknown>;
mapNoRouteReason: (reason: string | null) => string;
buildSkippedResult: (item: AssistantExecutionPlanItem) => UnifiedRetrievalResult;
normalizeRetrievalResultFn?: typeof normalizeRetrievalResult;
}
export interface AssistantDeepTurnRetrievalExecutionOutput {
retrievalCalls: AssistantRetrievalCallRecord[];
retrievalResultsRaw: AssistantRetrievalRawResultRecord[];
retrievalResults: UnifiedRetrievalResult[];
}
function buildRouteExecutorErrorRawResult(route: string, message: string): Record<string, unknown> {
return {
status: "error",
result_type: "summary",
items: [],
summary: {
route
},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "low",
limitations: ["Route executor failed."],
errors: [message]
};
}
export async function executeAssistantDeepTurnRetrievalPlan(
input: AssistantDeepTurnRetrievalExecutionInput
): Promise<AssistantDeepTurnRetrievalExecutionOutput> {
const normalizeRetrievalResultSafe = input.normalizeRetrievalResultFn ?? normalizeRetrievalResult;
const retrievalCalls: AssistantRetrievalCallRecord[] = [];
const retrievalResultsRaw: AssistantRetrievalRawResultRecord[] = [];
const retrievalResults: UnifiedRetrievalResult[] = [];
for (const planItem of input.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: input.mapNoRouteReason(planItem.no_route_reason)
});
retrievalResults.push(input.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 input.executeRouteRuntime(planItem.route, planItem.fragment_text, {
temporalHint: input.liveTemporalHint
});
retrievalResultsRaw.push({
fragment_id: planItem.fragment_id,
route: planItem.route,
raw_result: raw
});
retrievalResults.push(
normalizeRetrievalResultSafe(planItem.fragment_id, planItem.requirement_ids, planItem.route, raw as any)
);
} 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 = buildRouteExecutorErrorRawResult(planItem.route, message);
retrievalResultsRaw.push({
fragment_id: planItem.fragment_id,
route: planItem.route,
raw_result: rawError
});
retrievalResults.push(
normalizeRetrievalResultSafe(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError as any)
);
}
}
return {
retrievalCalls,
retrievalResultsRaw,
retrievalResults
};
}
@@ -0,0 +1,36 @@
import type { AssistantDebugPayload, UnifiedRetrievalResult } from "../types/assistant";
import {
buildAssistantEvidenceBundleContractV1,
type AssistantEvidenceBundleContractV1
} from "./assistantOrchestrationContracts";
type RetrievalStatusItem = AssistantDebugPayload["retrieval_status"][number];
export interface AssistantEvidenceBundleAssembly {
evidenceBundleContractV1: AssistantEvidenceBundleContractV1;
retrievalStatus: RetrievalStatusItem[];
}
function buildRetrievalStatus(retrievalResults: UnifiedRetrievalResult[]): RetrievalStatusItem[] {
return retrievalResults.map((item) => ({
fragment_id: item.fragment_id,
requirement_ids: item.requirement_ids,
route: item.route,
status: item.status,
result_type: item.result_type
}));
}
export function assembleAssistantEvidenceBundle(input: {
retrievalCalls: Array<Record<string, unknown>>;
retrievalResults: UnifiedRetrievalResult[];
}): AssistantEvidenceBundleAssembly {
const retrievalResults = Array.isArray(input.retrievalResults) ? input.retrievalResults : [];
return {
evidenceBundleContractV1: buildAssistantEvidenceBundleContractV1({
retrievalCalls: Array.isArray(input.retrievalCalls) ? input.retrievalCalls : [],
retrievalResults
}),
retrievalStatus: buildRetrievalStatus(retrievalResults)
};
}
@@ -0,0 +1,55 @@
import type { AssistantReplyType, AssistantRequirement, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
import type { RouteHintSummary } from "../types/normalizer";
import type { InvestigationStateWithProblemUnits } from "../types/stage2ProblemUnits";
import { updateInvestigationState } from "./investigationState";
export interface BuildAssistantInvestigationStateSnapshotInput {
featureEnabled: boolean;
previousState: InvestigationStateWithProblemUnits | null | undefined;
timestamp: string;
questionId: string;
userMessage: string;
routeSummary: RouteHintSummary | null;
requirements: AssistantRequirement[];
coverageReport: RequirementCoverageReport;
retrievalResults: UnifiedRetrievalResult[];
replyType: AssistantReplyType;
followupApplied: boolean;
}
export function buildAssistantInvestigationStateSnapshot(
input: BuildAssistantInvestigationStateSnapshotInput
): InvestigationStateWithProblemUnits | null {
if (!input.featureEnabled || !input.previousState) {
return null;
}
return updateInvestigationState({
previous: input.previousState,
timestamp: input.timestamp,
questionId: input.questionId,
userMessage: input.userMessage,
routeSummary: input.routeSummary,
requirements: input.requirements,
coverageReport: input.coverageReport,
retrievalResults: input.retrievalResults,
replyType: input.replyType,
followupApplied: input.followupApplied
});
}
export interface PersistAssistantInvestigationStateSnapshotInput {
featureEnabled: boolean;
sessionId: string;
snapshot: InvestigationStateWithProblemUnits | null;
persist: (sessionId: string, snapshot: InvestigationStateWithProblemUnits) => void;
}
export function persistAssistantInvestigationStateSnapshot(
input: PersistAssistantInvestigationStateSnapshotInput
): boolean {
if (!input.featureEnabled || !input.snapshot) {
return false;
}
input.persist(input.sessionId, input.snapshot);
return true;
}
@@ -0,0 +1,159 @@
import type { AnswerGroundingCheck, RequirementCoverageReport } from "../types/assistant";
export interface DeepAnalysisMessageLogDetailsInput {
sessionId: string;
messageId: string;
userMessage: string;
normalizerOutput: unknown;
executionPlan: Array<Record<string, unknown>>;
resolvedExecutionState: unknown;
routes: Array<Record<string, unknown>>;
retrievalCalls: Array<Record<string, unknown>>;
retrievalResultsRaw: unknown[];
retrievalResultsNormalized: unknown[];
requirementsExtracted: unknown[];
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
replyType: string;
droppedIntentSegments: string[];
questionTypeClass: string;
companyAnchors: unknown;
runtimeAnalysisContext: {
active: boolean;
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
};
businessScopeResolution: {
business_scope_raw?: string[];
business_scope_resolved?: string[];
company_grounding_applied?: boolean;
scope_resolution_reason?: string[];
};
temporalGuard: Record<string, unknown>;
polarityAudit: Record<string, unknown>;
claimAnchorAudit: Record<string, unknown>;
targetedEvidenceAudit: unknown;
evidenceAdmissibilityGateAudit: unknown;
rbpLiveRouteAudit: unknown | null;
faLiveRouteAudit: unknown | null;
groundedAnswerEligibilityGuard: Record<string, unknown>;
followupStateUsage: unknown | null;
compositionDebug: {
problem_centric_answer_applied?: boolean;
problem_units_used_count?: number;
problem_answer_mode?: string;
problem_unit_ids_used?: string[];
fallback_type?: string;
};
outcomeClassV1: unknown;
assistantOrchestrationContractsV1: unknown;
answerStructureV11: unknown;
investigationStateSnapshot: unknown;
assistantReply: string;
traceId: string;
}
function toAnalysisContext(input: DeepAnalysisMessageLogDetailsInput["runtimeAnalysisContext"]): Record<string, unknown> | null {
if (!input.active) {
return null;
}
return {
as_of_date: input.as_of_date,
period_from: input.period_from,
period_to: input.period_to,
source: input.source,
snapshot_mode: input.snapshot_mode
};
}
function resolveCoverageStatus(coverageReport: RequirementCoverageReport): "full" | "partial_or_limited" {
return coverageReport.requirements_total === coverageReport.requirements_covered &&
coverageReport.requirements_uncovered.length === 0 &&
coverageReport.requirements_partially_covered.length === 0
? "full"
: "partial_or_limited";
}
export function buildDeepAnalysisProcessedLogDetails(input: DeepAnalysisMessageLogDetailsInput): Record<string, unknown> {
const analysisContext = toAnalysisContext(input.runtimeAnalysisContext);
return {
session_id: input.sessionId,
message_id: input.messageId,
user_message: input.userMessage,
normalizer_output: input.normalizerOutput,
execution_plan: input.executionPlan,
resolved_execution_state: input.resolvedExecutionState,
routes: input.routes,
retrieval_calls: input.retrievalCalls,
retrieval_results_raw: input.retrievalResultsRaw,
retrieval_results_normalized: input.retrievalResultsNormalized,
requirements_extracted: input.requirementsExtracted,
requirements_total: input.coverageReport.requirements_total,
requirements_covered: input.coverageReport.requirements_covered,
requirements_uncovered: input.coverageReport.requirements_uncovered,
coverage_status: resolveCoverageStatus(input.coverageReport),
answer_grounding_status: input.groundingCheck.status,
reply_semantic_type: input.replyType,
why_included_summary: input.groundingCheck.why_included_summary,
selection_reason_summary: input.groundingCheck.selection_reason_summary,
route_subject_match: input.groundingCheck.route_subject_match,
clarification_target: input.coverageReport.clarification_needed_for,
dropped_intent_segments: input.droppedIntentSegments,
question_type_class: input.questionTypeClass,
company_anchors: input.companyAnchors,
analysis_context_applied: input.runtimeAnalysisContext.active,
analysis_context: analysisContext,
business_scope_raw: input.businessScopeResolution.business_scope_raw,
business_scope_resolved: input.businessScopeResolution.business_scope_resolved,
company_grounding_applied: input.businessScopeResolution.company_grounding_applied,
scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
company_scope_resolution_reason: input.businessScopeResolution.scope_resolution_reason,
raw_time_anchor: input.temporalGuard.raw_time_anchor,
raw_time_scope: input.temporalGuard.raw_time_scope,
resolved_time_anchor: input.temporalGuard.resolved_time_anchor,
resolved_primary_period: input.temporalGuard.resolved_primary_period,
effective_primary_period: input.temporalGuard.effective_primary_period,
temporal_guard_input: input.temporalGuard.temporal_guard_input,
temporal_alignment_status: input.temporalGuard.temporal_alignment_status,
temporal_resolution_source: input.temporalGuard.temporal_resolution_source,
temporal_guard_basis: input.temporalGuard.temporal_guard_basis,
temporal_guard_applied: input.temporalGuard.temporal_guard_applied,
temporal_guard_outcome: input.temporalGuard.temporal_guard_outcome,
temporal_guard: input.temporalGuard,
raw_numeric_tokens: input.polarityAudit.raw_numeric_tokens,
classified_numeric_tokens: input.polarityAudit.classified_numeric_tokens,
rejected_as_non_accounts: input.polarityAudit.rejected_as_non_accounts,
resolved_account_anchors: input.polarityAudit.resolved_account_anchors,
domain_polarity_guard: input.polarityAudit,
claim_anchor_audit: input.claimAnchorAudit,
settlement_role: input.claimAnchorAudit.settlement_role ?? null,
settlement_role_resolution_reason: input.claimAnchorAudit.settlement_role_resolution_reason ?? [],
polarity_resolution_status: input.claimAnchorAudit.polarity_resolution_status ?? "not_applicable",
targeted_evidence_acquisition: input.targetedEvidenceAudit,
evidence_admissibility_gate: input.evidenceAdmissibilityGateAudit,
...(input.rbpLiveRouteAudit ? { rbp_live_route_audit: input.rbpLiveRouteAudit } : {}),
...(input.faLiveRouteAudit ? { fa_live_route_audit: input.faLiveRouteAudit } : {}),
eligibility_time_basis: input.groundedAnswerEligibilityGuard.eligibility_time_basis,
grounded_answer_eligibility_guard: input.groundedAnswerEligibilityGuard,
...(input.followupStateUsage ? { followup_state_usage: input.followupStateUsage } : {}),
problem_centric_answer_applied: input.compositionDebug.problem_centric_answer_applied ?? false,
problem_units_used_count: input.compositionDebug.problem_units_used_count ?? 0,
problem_answer_mode: input.compositionDebug.problem_answer_mode ?? "stage1_policy_v11",
...(Array.isArray(input.compositionDebug.problem_unit_ids_used) && input.compositionDebug.problem_unit_ids_used.length > 0
? {
problem_unit_ids_used: input.compositionDebug.problem_unit_ids_used
}
: {}),
assistant_outcome_class_v1: input.outcomeClassV1,
assistant_orchestration_contracts_v1: input.assistantOrchestrationContractsV1,
answer_structure_v11: input.answerStructureV11,
investigation_state_snapshot: input.investigationStateSnapshot,
fallback_type: input.compositionDebug.fallback_type,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
trace_id: input.traceId
};
}
@@ -0,0 +1,275 @@
import type {
AssistantReplyType,
AssistantRequirement,
AnswerGroundingCheck,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { NormalizedPayload, RouteHintSummary } from "../types/normalizer";
export type AssistantOutcomeClassV1 =
| "FULLY_ANSWERED"
| "PARTIALLY_ANSWERED"
| "BLOCKED_BY_AMBIGUITY"
| "BLOCKED_BY_MISSING_DATA"
| "BLOCKED_BY_TOOLING"
| "MISROUTED"
| "FAILED_TO_BIND_ENTITIES";
export interface AssistantAnalysisContextContractV1 {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
source: string | null;
snapshot_mode: "auto" | "force_snapshot" | "force_live";
}
export interface AssistantQueryFrameContractV1 {
schema_version: "assistant_query_frame_v1";
original_user_question: string;
normalized_question: string;
route_summary_mode: RouteHintSummary["mode"] | "none";
fragments_total: number;
dropped_intent_segments: string[];
analysis_context: AssistantAnalysisContextContractV1 | null;
}
export interface AssistantExecutionPlanStepContractV1 {
fragment_id: string;
route: string;
should_execute: boolean;
requirement_ids: string[];
no_route_reason: string | null;
clarification_reason: string | null;
}
export interface AssistantExecutionPlanContractV1 {
schema_version: "assistant_execution_plan_v1";
steps: AssistantExecutionPlanStepContractV1[];
requirements_total: number;
}
export interface AssistantEvidenceBundleContractV1 {
schema_version: "assistant_evidence_bundle_v1";
retrieval_calls_total: number;
retrieval_results_total: number;
retrieval_status_breakdown: {
ok: number;
partial: number;
empty: number;
error: number;
};
evidence_total: number;
source_refs_total: number;
limitation_total: number;
error_total: number;
}
export interface AssistantCoverageContractV1 {
schema_version: "assistant_coverage_contract_v1";
coverage_report: RequirementCoverageReport;
grounding: AnswerGroundingCheck;
outcome_class: AssistantOutcomeClassV1;
}
function normalizeSnapshotMode(value: unknown): "auto" | "force_snapshot" | "force_live" {
const token = String(value ?? "").trim();
if (token === "force_snapshot" || token === "force_live") {
return token;
}
return "auto";
}
function extractFragmentsTotal(normalized: NormalizedPayload | null | undefined): number {
if (!normalized || typeof normalized !== "object") {
return 0;
}
const source = normalized as unknown as { fragments?: unknown };
const fragments = source.fragments;
return Array.isArray(fragments) ? fragments.length : 0;
}
function collectEvidenceTotals(retrievalResults: UnifiedRetrievalResult[]): {
evidence_total: number;
source_refs_total: number;
limitation_total: number;
error_total: number;
} {
let evidenceTotal = 0;
const sourceRefs = new Set<string>();
let limitationTotal = 0;
let errorTotal = 0;
for (const result of retrievalResults) {
evidenceTotal += Array.isArray(result.evidence) ? result.evidence.length : 0;
limitationTotal += Array.isArray(result.limitations) ? result.limitations.length : 0;
errorTotal += Array.isArray(result.errors) ? result.errors.length : 0;
for (const evidence of result.evidence ?? []) {
const ref = String(evidence?.source_ref?.canonical_ref ?? "").trim();
if (ref) {
sourceRefs.add(ref);
}
}
}
return {
evidence_total: evidenceTotal,
source_refs_total: sourceRefs.size,
limitation_total: limitationTotal,
error_total: errorTotal
};
}
export function classifyAssistantOutcomeClassV1(input: {
replyType: AssistantReplyType;
coverageReport: RequirementCoverageReport;
grounding: AnswerGroundingCheck;
retrievalResults: UnifiedRetrievalResult[];
}): AssistantOutcomeClassV1 {
const replyType = input.replyType;
const grounding = input.grounding;
const coverage = input.coverageReport;
const hasOnlyErrors =
input.retrievalResults.length > 0 &&
input.retrievalResults.every((item) => item.status === "error");
if (replyType === "backend_error" || hasOnlyErrors) {
return "BLOCKED_BY_TOOLING";
}
if (grounding.status === "route_mismatch_blocked" || replyType === "route_mismatch_blocked") {
return "MISROUTED";
}
if (replyType === "clarification_required" || coverage.clarification_needed_for.length > 0) {
return "BLOCKED_BY_AMBIGUITY";
}
if (replyType === "out_of_scope") {
return "BLOCKED_BY_AMBIGUITY";
}
const fullCoverage =
coverage.requirements_total > 0 &&
coverage.requirements_total === coverage.requirements_covered &&
coverage.requirements_uncovered.length === 0 &&
coverage.requirements_partially_covered.length === 0 &&
coverage.clarification_needed_for.length === 0 &&
coverage.out_of_scope_requirements.length === 0;
if (fullCoverage && grounding.status === "grounded") {
return "FULLY_ANSWERED";
}
const hasAnyCoverage =
coverage.requirements_covered > 0 ||
coverage.requirements_partially_covered.length > 0 ||
grounding.status === "partial";
if (hasAnyCoverage) {
return "PARTIALLY_ANSWERED";
}
const missingRequirementSignal =
grounding.missing_requirements.length > 0 ||
coverage.requirements_uncovered.length > 0 ||
coverage.requirements_total > 0;
const possibleBindingFailure =
replyType === "no_grounded_answer" &&
missingRequirementSignal &&
grounding.route_subject_match;
if (possibleBindingFailure) {
return "FAILED_TO_BIND_ENTITIES";
}
return "BLOCKED_BY_MISSING_DATA";
}
export function buildAssistantQueryFrameContractV1(input: {
userMessage: string;
normalizedQuestion: string;
normalized: NormalizedPayload | null | undefined;
routeSummary: RouteHintSummary | null;
droppedIntentSegments: string[];
analysisContext?: {
as_of_date?: string | null;
period_from?: string | null;
period_to?: string | null;
source?: string | null;
snapshot_mode?: string | null;
} | null;
}): AssistantQueryFrameContractV1 {
const analysis = input.analysisContext
? {
as_of_date: input.analysisContext.as_of_date ?? null,
period_from: input.analysisContext.period_from ?? null,
period_to: input.analysisContext.period_to ?? null,
source: input.analysisContext.source ?? null,
snapshot_mode: normalizeSnapshotMode(input.analysisContext.snapshot_mode)
}
: null;
return {
schema_version: "assistant_query_frame_v1",
original_user_question: String(input.userMessage ?? ""),
normalized_question: String(input.normalizedQuestion ?? ""),
route_summary_mode: input.routeSummary?.mode ?? "none",
fragments_total: extractFragmentsTotal(input.normalized),
dropped_intent_segments: Array.isArray(input.droppedIntentSegments) ? [...input.droppedIntentSegments] : [],
analysis_context: analysis
};
}
export function buildAssistantExecutionPlanContractV1(input: {
executionPlan: Array<{
fragment_id: string;
requirement_ids: string[];
route: string;
should_execute: boolean;
no_route_reason?: string | null;
clarification_reason?: string | null;
}>;
requirements: AssistantRequirement[];
}): AssistantExecutionPlanContractV1 {
return {
schema_version: "assistant_execution_plan_v1",
steps: (Array.isArray(input.executionPlan) ? input.executionPlan : []).map((item) => ({
fragment_id: String(item.fragment_id ?? ""),
route: String(item.route ?? ""),
should_execute: Boolean(item.should_execute),
requirement_ids: Array.isArray(item.requirement_ids) ? [...item.requirement_ids] : [],
no_route_reason: item.no_route_reason ?? null,
clarification_reason: item.clarification_reason ?? null
})),
requirements_total: Array.isArray(input.requirements) ? input.requirements.length : 0
};
}
export function buildAssistantEvidenceBundleContractV1(input: {
retrievalCalls: Array<Record<string, unknown>>;
retrievalResults: UnifiedRetrievalResult[];
}): AssistantEvidenceBundleContractV1 {
const retrievalResults = Array.isArray(input.retrievalResults) ? input.retrievalResults : [];
const breakdown = {
ok: retrievalResults.filter((item) => item.status === "ok").length,
partial: retrievalResults.filter((item) => item.status === "partial").length,
empty: retrievalResults.filter((item) => item.status === "empty").length,
error: retrievalResults.filter((item) => item.status === "error").length
};
const totals = collectEvidenceTotals(retrievalResults);
return {
schema_version: "assistant_evidence_bundle_v1",
retrieval_calls_total: Array.isArray(input.retrievalCalls) ? input.retrievalCalls.length : 0,
retrieval_results_total: retrievalResults.length,
retrieval_status_breakdown: breakdown,
...totals
};
}
export function buildAssistantCoverageContractV1(input: {
coverageReport: RequirementCoverageReport;
grounding: AnswerGroundingCheck;
outcomeClass: AssistantOutcomeClassV1;
}): AssistantCoverageContractV1 {
return {
schema_version: "assistant_coverage_contract_v1",
coverage_report: input.coverageReport,
grounding: input.grounding,
outcome_class: input.outcomeClass
};
}
@@ -0,0 +1,66 @@
import type {
AnswerGroundingCheck,
AssistantRequirement,
RequirementCoverageReport,
UnifiedRetrievalResult
} from "../types/assistant";
import type { NormalizedPayload, RouteHintSummary } from "../types/normalizer";
export interface AssistantRequirementExtractionResult {
requirements: AssistantRequirement[];
byFragment: Map<string, string[]>;
}
export interface AssistantCoverageEvaluationResult {
requirements: AssistantRequirement[];
coverage: RequirementCoverageReport;
}
export interface AssistantCoverageGroundingPipelineInput {
routeSummary: RouteHintSummary | null;
normalized: NormalizedPayload | null | undefined;
userMessage: string;
retrievalResults: UnifiedRetrievalResult[];
requirementExtraction?: AssistantRequirementExtractionResult;
extractRequirements: (
routeSummary: RouteHintSummary | null,
normalized: NormalizedPayload | null | undefined,
userMessage: string
) => AssistantRequirementExtractionResult;
evaluateCoverage: (
requirements: AssistantRequirement[],
retrievalResults: UnifiedRetrievalResult[]
) => AssistantCoverageEvaluationResult;
checkGrounding: (
userMessage: string,
requirements: AssistantRequirement[],
coverage: RequirementCoverageReport,
retrievalResults: UnifiedRetrievalResult[]
) => AnswerGroundingCheck;
}
export interface AssistantCoverageGroundingPipelineOutput {
requirementExtraction: AssistantRequirementExtractionResult;
coverageEvaluation: AssistantCoverageEvaluationResult;
groundingCheckBase: AnswerGroundingCheck;
}
export function runAssistantCoverageGroundingPipeline(
input: AssistantCoverageGroundingPipelineInput
): AssistantCoverageGroundingPipelineOutput {
const requirementExtraction =
input.requirementExtraction ?? input.extractRequirements(input.routeSummary, input.normalized, input.userMessage);
const coverageEvaluation = input.evaluateCoverage(requirementExtraction.requirements, input.retrievalResults);
const groundingCheckBase = input.checkGrounding(
input.userMessage,
coverageEvaluation.requirements,
coverageEvaluation.coverage,
input.retrievalResults
);
return {
requirementExtraction,
coverageEvaluation,
groundingCheckBase
};
}
@@ -0,0 +1,133 @@
import type { RouteHintSummary } from "../types/normalizer";
interface FragmentLike {
fragment_id?: string;
raw_fragment_text?: string;
normalized_fragment_text?: string;
account_hints?: unknown;
}
export interface AssistantExecutionPlanItem {
fragment_id: string;
requirement_ids: string[];
route: string;
should_execute: boolean;
fragment_text: string;
no_route_reason: string | null;
clarification_reason: string | null;
}
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function enrichFragmentTextWithHints(fragment: FragmentLike, text: string): string {
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(", ")}`;
}
export function buildFragmentTextById(fragments: FragmentLike[]): Map<string, string> {
const result = new Map<string, string>();
for (const item of fragments) {
if (!item || typeof item !== "object") {
continue;
}
const fragment = item as FragmentLike;
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;
}
export function buildExecutionPlanFromRoute(input: {
routeSummary: RouteHintSummary | null;
userMessage: string;
fragmentTextById: Map<string, string>;
requirementByFragment: Map<string, string[]>;
}): AssistantExecutionPlanItem[] {
if (!input.routeSummary) {
return [];
}
if (input.routeSummary.mode === "legacy_v1") {
return [
{
fragment_id: "F1",
requirement_ids: input.requirementByFragment.get("F1") ?? ["R1"],
route: input.routeSummary.route_hint,
should_execute: true,
fragment_text: input.userMessage,
no_route_reason: null,
clarification_reason: null
}
];
}
return input.routeSummary.decisions.map((decision) => {
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.userMessage;
if (decision.route === "no_route") {
return {
fragment_id: decision.fragment_id,
requirement_ids: input.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: input.requirementByFragment.get(decision.fragment_id) ?? [],
route: decision.route,
should_execute: true,
fragment_text: text,
no_route_reason: null,
clarification_reason: decision.clarification_reason ?? null
};
});
}
export function buildDebugRoutesFromRoute(input: {
routeSummary: RouteHintSummary | null;
resolveLegacyRouteReason: (route: string) => string;
}): Array<Record<string, unknown>> {
if (!input.routeSummary) {
return [];
}
if (input.routeSummary.mode === "legacy_v1") {
return [
{
fragment_id: "F1",
route: input.routeSummary.route_hint,
reason: input.resolveLegacyRouteReason(input.routeSummary.route_hint),
confidence: input.routeSummary.confidence,
intent_class: input.routeSummary.intent_class
}
];
}
return input.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
}));
}
@@ -683,11 +683,97 @@ function toTemporalGuardInput(window: TemporalWindow | null, fallback: string |
return value || null;
}
function normalizeIsoDate(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (
candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day
) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
function normalizeTemporalWindow(input: {
asOfDate?: unknown;
periodFrom?: unknown;
periodTo?: unknown;
}): TemporalWindow | null {
const asOfDate = normalizeIsoDate(input.asOfDate);
if (asOfDate) {
return {
from: asOfDate,
to: asOfDate,
granularity: "day"
};
}
const from = normalizeIsoDate(input.periodFrom);
const to = normalizeIsoDate(input.periodTo);
if (!from || !to) {
return null;
}
return {
from,
to,
granularity: from === to ? "day" : "month"
};
}
export function resolveTemporalGuard(input: {
userMessage: string;
normalized: NormalizedPayload | null | undefined;
companyAnchors?: CompanyAnchorSet | null;
analysisContext?: {
as_of_date?: string | null;
period_from?: string | null;
period_to?: string | null;
source?: string | null;
} | null;
}): TemporalGuardAudit {
const analysisWindow = normalizeTemporalWindow({
asOfDate: input.analysisContext?.as_of_date,
periodFrom: input.analysisContext?.period_from,
periodTo: input.analysisContext?.period_to
});
if (analysisWindow) {
const source = String(input.analysisContext?.source ?? "").trim() || "analysis_context";
const guardInput = toTemporalGuardInput(analysisWindow, analysisWindow.from);
return {
raw_time_anchor: analysisWindow.from,
raw_time_scope: guardInput,
resolved_time_anchor: analysisWindow.granularity === "day" ? analysisWindow.from : null,
resolved_primary_period: analysisWindow,
effective_primary_period: analysisWindow,
temporal_guard_input: guardInput,
temporal_alignment_status: "aligned",
temporal_resolution_source: source,
temporal_guard_basis: "raw_time_scope_unlocked",
temporal_guard_applied: false,
temporal_guard_outcome: "passed",
primary_period_window: null,
allowed_context_window: null,
controlled_temporal_expansion_enabled: false,
context_expansion_reasons_allowed: ["prehistory", "carryover", "post_period_closure", "long_running_contract_context"],
normalized_anchor_drift_detected: false,
reason_codes: ["analysis_context_applied"]
};
}
const rawAnchorText = collectRawTemporalAnchorText(input.userMessage, input.companyAnchors);
const julyAnchor = resolveJulyAnchor(rawAnchorText);
const normalizedAnchor = normalizedAnchorFromFragments(input.normalized);
@@ -762,10 +848,15 @@ export function applyTemporalHintToExecutionPlan<
return executionPlan;
}
const primaryWindow = temporal.effective_primary_period ?? temporal.primary_period_window;
const periodLabel = primaryWindow
? `${primaryWindow.from}..${primaryWindow.to}`
: temporal.resolved_time_anchor
? temporal.resolved_time_anchor
: "active_period";
const hint =
primaryWindow?.granularity === "day" && temporal.resolved_time_anchor
? `primary period ${temporal.resolved_time_anchor}; controlled temporal expansion only for linked entities`
: `primary period July 2020 (${primaryWindow?.from ?? JULY_WINDOW.from}..${primaryWindow?.to ?? JULY_WINDOW.to}); controlled temporal expansion only for linked entities`;
: `primary period ${periodLabel}; controlled temporal expansion only for linked entities`;
return executionPlan.map((item) => {
if (!item.should_execute) {
return item;
@@ -1590,15 +1681,15 @@ export function applyEligibilityToGroundingCheck<T extends { status: string; rea
? "no_grounded_answer"
: "partial";
const reasonMap: Record<string, string> = {
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.",
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
admissible_evidence_count_zero: "Недостаточно подтвержденных данных для уверенного ответа.",
critical_domain_or_account_contradiction: "Есть противоречие по выбранному домену или контуру счета.",
temporal_guard_failed_out_of_snapshot_window: "Запрошенный период выходит за доступный срез данных.",
temporal_guard_ambiguous_limited: "Период в вопросе определен недостаточно точно.",
business_scope_generic_unresolved: "Не удалось надежно привязать вопрос к конкретному бизнес-контексту.",
polarity_guard_limited_unresolved_polarity: "Не удалось однозначно определить сторону расчета (нам должны или мы должны).",
polarity_guard_blocked_conflict: "В данных есть конфликт по стороне расчета.",
claim_anchor_coverage_insufficient: "Не хватает ключевых ориентиров в вопросе (период, объект или контрагент).",
targeted_evidence_hit_rate_zero: "Не хватило целевых подтверждений по выбранному сценарию."
};
const reasons = [
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
import type { AssistantConversationItem, AssistantSessionState } from "../types/assistant";
export interface CommitAssistantTurnAndLogInput {
sessionId: string;
assistantItem: AssistantConversationItem;
eventType: string;
logDetails: Record<string, unknown>;
appendItem: (sessionId: string, item: AssistantConversationItem) => void;
getSession: (sessionId: string) => AssistantSessionState | null;
persistSession: (session: AssistantSessionState) => void;
cloneConversation: (items: AssistantConversationItem[]) => AssistantConversationItem[];
logEvent: (payload: {
timestamp: string;
level: "info";
service: "assistant_loop";
message: "assistant_message_processed";
sessionId: string;
eventType: string;
details: Record<string, unknown>;
}) => void;
nowIso?: () => string;
}
export interface CommitAssistantTurnAndLogOutput {
currentSession: AssistantSessionState | null;
conversation: AssistantConversationItem[];
}
export function commitAssistantTurnAndLog(input: CommitAssistantTurnAndLogInput): CommitAssistantTurnAndLogOutput {
input.appendItem(input.sessionId, input.assistantItem);
const currentSession = input.getSession(input.sessionId);
if (currentSession) {
input.persistSession(currentSession);
}
const conversation = input.cloneConversation(currentSession?.items ?? []);
input.logEvent({
timestamp: (input.nowIso ?? (() => new Date().toISOString()))(),
level: "info",
service: "assistant_loop",
message: "assistant_message_processed",
sessionId: input.sessionId,
eventType: input.eventType,
details: input.logDetails
});
return {
currentSession,
conversation
};
}
@@ -264,6 +264,32 @@ function parseRawQuestions(rawQuestions: string): string[] {
return byLine.length > 0 ? byLine : [text];
}
function normalizeAnalysisDate(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
return null;
}
const candidate = new Date(Date.UTC(year, month - 1, day));
if (
candidate.getUTCFullYear() !== year ||
candidate.getUTCMonth() + 1 !== month ||
candidate.getUTCDate() !== day
) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]}`;
}
type V2FamilyFragment =
| NormalizedQueryV2["fragments"][number]
| NormalizedQueryV2_0_1["fragments"][number]
@@ -936,6 +962,7 @@ export class EvalService {
mode: EvalRunMode;
caseSetFile?: string;
rawQuestions?: string;
analysisDate?: string;
cases: EvalInputCase[];
}): Promise<Record<string, unknown>> {
const runId = `eval-${nanoid(10)}`;
@@ -976,6 +1003,13 @@ export class EvalService {
...payload.normalizeConfig,
userQuestion: item.raw_question,
context: {
period_hint: payload.analysisDate ?? undefined,
analysis_context: payload.analysisDate
? {
as_of_date: payload.analysisDate,
source: "eval_analysis_date"
}
: undefined,
eval_label: runId,
case_id: item.case_id,
eval_mode: payload.mode
@@ -1876,6 +1910,7 @@ export class EvalService {
mode: EvalRunMode;
caseSetFile?: string;
compareWithReportFile?: string;
analysisDate?: string;
runId?: string;
}): Promise<Record<string, unknown>> {
if (!FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1) {
@@ -1889,6 +1924,7 @@ export class EvalService {
const suite = parseAssistantSuiteFile(payload.caseSetFile);
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage1-${nanoid(10)}`;
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
const assistantService = new AssistantService(this.normalizerService, new AssistantSessionStore());
const diagnostics: AssistantCaseDiagnostics[] = [];
let requestsTotal = 0;
@@ -1917,6 +1953,15 @@ export class EvalService {
developerPrompt: payload.normalizeConfig.developerPrompt,
domainPrompt: payload.normalizeConfig.domainPrompt,
fewShotExamples: payload.normalizeConfig.fewShotExamples,
context: analysisDate
? {
period_hint: analysisDate,
analysis_context: {
as_of_date: analysisDate,
source: "eval_analysis_date"
}
}
: undefined,
useMock: payload.useMock
})) as AssistantMessageResponsePayload;
turnResponses.push(response);
@@ -2153,6 +2198,7 @@ export class EvalService {
eval_target: "assistant_stage1",
mode: payload.mode,
use_mock: Boolean(payload.useMock),
analysis_date: analysisDate,
prompt_version: payload.normalizeConfig.promptVersion ?? null,
suite_id: suite.suite_id,
suite_version: suite.suite_version,
@@ -2225,6 +2271,7 @@ export class EvalService {
mode: EvalRunMode;
caseSetFile?: string;
compareWithReportFile?: string;
analysisDate?: string;
runId?: string;
}): Promise<Record<string, unknown>> {
if (!FEATURE_ASSISTANT_STAGE2_EVAL_V1) {
@@ -2238,6 +2285,7 @@ export class EvalService {
const suite = parseAssistantStage2SuiteFile(payload.caseSetFile);
const suiteCases = suite.cases.filter((item) => !payload.caseIds || payload.caseIds.includes(item.case_id));
const runId = typeof payload.runId === "string" && payload.runId.trim().length > 0 ? payload.runId.trim() : `assistant-stage2-${nanoid(10)}`;
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
const assistantService = new AssistantService(this.normalizerService, new AssistantSessionStore());
const diagnostics: AssistantStage2CaseDiagnostics[] = [];
let requestsTotal = 0;
@@ -2269,6 +2317,15 @@ export class EvalService {
developerPrompt: payload.normalizeConfig.developerPrompt,
domainPrompt: payload.normalizeConfig.domainPrompt,
fewShotExamples: payload.normalizeConfig.fewShotExamples,
context: analysisDate
? {
period_hint: analysisDate,
analysis_context: {
as_of_date: analysisDate,
source: "eval_analysis_date"
}
}
: undefined,
useMock: payload.useMock
})) as AssistantMessageResponsePayload;
turnResponses.push(response);
@@ -2446,6 +2503,7 @@ export class EvalService {
eval_target: "assistant_stage2",
mode: payload.mode,
use_mock: Boolean(payload.useMock),
analysis_date: analysisDate,
prompt_version: payload.normalizeConfig.promptVersion ?? null,
suite_id: suite.suite_id,
suite_version: suite.suite_version,
@@ -2552,10 +2610,12 @@ export class EvalService {
rawQuestions?: string;
evalTarget?: EvalTarget;
compareWithReportFile?: string;
analysisDate?: string;
runId?: string;
}): Promise<Record<string, unknown>> {
const mode = payload.mode ?? "standard";
const evalTarget = payload.evalTarget ?? "normalizer";
const analysisDate = normalizeAnalysisDate(payload.analysisDate);
if (evalTarget === "assistant_stage1") {
return this.runAssistantStage1({
@@ -2565,6 +2625,7 @@ export class EvalService {
mode,
caseSetFile: payload.caseSetFile,
compareWithReportFile: payload.compareWithReportFile,
analysisDate: analysisDate ?? undefined,
runId: payload.runId
});
}
@@ -2577,6 +2638,7 @@ export class EvalService {
mode,
caseSetFile: payload.caseSetFile,
compareWithReportFile: payload.compareWithReportFile,
analysisDate: analysisDate ?? undefined,
runId: payload.runId
});
}
@@ -2622,6 +2684,7 @@ export class EvalService {
return this.runV2({
...payload,
mode,
analysisDate: analysisDate ?? undefined,
cases: filtered
});
}
@@ -2651,6 +2714,13 @@ export class EvalService {
...payload.normalizeConfig,
userQuestion: item.raw_question,
context: {
period_hint: analysisDate ?? undefined,
analysis_context: analysisDate
? {
as_of_date: analysisDate,
source: "eval_analysis_date"
}
: undefined,
expected_route: item.expected.route_hint as NormalizeRequestPayload["context"] extends infer C
? C extends { expected_route?: infer R }
? R
@@ -2779,6 +2849,7 @@ export class EvalService {
timestamp: new Date().toISOString(),
mode,
use_mock: Boolean(payload.useMock),
analysis_date: analysisDate,
prompt_version: payload.normalizeConfig.promptVersion ?? null,
dataset: {
source: payload.caseSetFile ? "file" : "data/eval_cases/*.json",
@@ -125,7 +125,7 @@ export function resolveQuestionType(input: string): QuestionTypeClass {
return bestType;
}
if (/[?пјџ]/u.test(text)) {
if (/(?:\bwhy\b|почему|из-?за\s+чего|в\s+ч(?:е|ё)м\s+причина)/iu.test(text)) {
return "why_breaks";
}
@@ -235,6 +235,14 @@ export interface RouteHintSummaryV2 {
export type RouteHintSummary = RouteHintSummaryV1 | RouteHintSummaryV2;
export type NormalizedPayload = NormalizedQueryV1 | NormalizedQueryV2 | NormalizedQueryV2_0_1 | NormalizedQueryV2_0_2;
export interface AnalysisContextV1 {
as_of_date?: string;
period_from?: string;
period_to?: string;
snapshot_mode?: "auto" | "force_snapshot" | "force_live";
source?: string;
}
export interface NormalizeRequestPayload {
llmProvider?: LlmProvider;
apiKey?: string;
@@ -250,6 +258,7 @@ export interface NormalizeRequestPayload {
userQuestion: string;
context?: {
period_hint?: string;
analysis_context?: AnalysisContextV1;
business_context?: string;
expected_route?: RouteHint;
eval_label?: string;
@@ -1654,6 +1654,11 @@ describe("address intent resolver expansion (M2.3a)", () => {
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves major-share revenue wording into customer revenue intent", () => {
const result = resolveAddressIntent("какие контрагенты принесли основную часть нашей выручки за отчетный период?");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves customer revenue intent from highest inflow slang wording", () => {
const result = resolveAddressIntent("какие приходы самые высокие за все время");
expect(result.intent).toBe("customer_revenue_and_payments");
@@ -1725,6 +1730,74 @@ describe("address intent resolver expansion (M2.3a)", () => {
const result = resolveAddressIntent("покажи документы по этому же договору");
expect(result.intent).toBe("list_documents_by_contract");
});
it("routes supplier tail-risk wording into payables intent", () => {
const result = resolveAddressIntent(
"Кто из поставщиков имеет хвосты с документами на конец месяца, которые уже больше похожи на систематическую проблему, а не на обычную задержку?"
);
expect(result.intent).toBe("list_payables_counterparties");
});
it("keeps out-of-scope supplier control wording as unknown intent", () => {
const result = resolveAddressIntent(
"Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?"
);
expect(result.intent).toBe("unknown");
});
it("routes long shipment-to-payment lag wording into receivables intent", () => {
const result = resolveAddressIntent(
"Где у нас висят покупатели со слишком длинным периодом между отправкой товара и его оплатой, и это уже вызывает тревогу?"
);
expect(result.intent).toBe("list_receivables_counterparties");
});
it("routes non-paying counterparties month-risk wording into receivables intent", () => {
const result = resolveAddressIntent(
"какие контрагенты пока вообще не платят за текущий месяц и это уже тревожный знак для нас?"
);
expect(result.intent).toBe("list_receivables_counterparties");
});
it("routes reconciliation mismatch wording into open contracts intent", () => {
const result = resolveAddressIntent(
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?"
);
expect(result.intent).toBe("list_open_contracts");
});
it("routes reconciliation mismatch wording without explicit lookup verb into open contracts intent", () => {
const result = resolveAddressIntent(
"По каким поставщикам у нас сальдо явно расходится с тем, что они сами указывают в своих актах сверок?"
);
expect(result.intent).toBe("list_open_contracts");
});
it("routes payments-without-closing-docs wording into open contracts intent", () => {
const result = resolveAddressIntent(
"Где у нас есть платежи, но нет документов для закрытия взаиморасчетов? Это уже требует ручной проверки."
);
expect(result.intent).toBe("list_open_contracts");
});
it("routes documents-without-payments wording into open contracts intent", () => {
const result = resolveAddressIntent(
"По каким контрагентам документы есть, а оплат нет. Может, стоит взять на карандаш такие ситуации чтоб не тянуть дальше?"
);
expect(result.intent).toBe("list_open_contracts");
});
it("routes stale advances without closing docs wording into open contracts intent", () => {
const result = resolveAddressIntent(
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?"
);
expect(result.intent).toBe("list_open_contracts");
});
it("routes buyers with open debt wording into open-items intent", () => {
const result = resolveAddressIntent("по каким покупателям у нас есть открытые задолженности на конец месяца?");
expect(result.intent).toBe("open_items_by_counterparty_or_contract");
});
});
describe("address filter extraction for balance drilldown", () => {
@@ -1810,6 +1883,14 @@ describe("address filter extraction for balance drilldown", () => {
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
});
it("does not derive fake counterparty anchor for open-contracts stale-advance wording", () => {
const extracted = extractAddressFilters(
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?",
"list_open_contracts"
);
expect(extracted.extracted_filters.counterparty).toBeUndefined();
});
it("derives VAT forecast quarter-to-date window when plain date phrase is present", () => {
const extracted = extractAddressFilters(
"мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года",
@@ -2250,6 +2331,98 @@ describe("address filter extraction for balance drilldown", () => {
});
describe("address query limited taxonomy and stage diagnostics", () => {
it("injects as_of_date from analysis context when user message has no explicit period", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Покажи контрагентов с незакрытыми хвостами", {
analysisDateHint: "2020-07-31"
});
expect(result?.handled).toBe(true);
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-07-31");
expect(Array.isArray(result?.debug.reasons)).toBe(true);
expect(result?.debug.reasons).toContain("as_of_date_from_analysis_context");
});
it("returns soft out-of-scope reply without technical jargon for unsupported supplier-control wording", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?"
);
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.debug.detected_intent).toBe("unknown");
expect(result?.debug.limited_reason_category).toBe("unsupported");
const reply = String(result?.reply_text ?? "");
expect(reply.toLowerCase()).toContain("вне поддерживаемого контура");
expect(reply).not.toMatch(/address_query|V1|lookup|materialized|якор/iu);
});
it("routes supplier tail-risk wording without forcing missing-anchor fallback", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"Кто из поставщиков имеет хвосты с документами на конец месяца, которые уже больше похожи на систематическую проблему, а не на обычную задержку?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_payables_counterparties");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes shipment-to-payment lag wording into receivables lane without missing-anchor fallback", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"Где у нас висят покупатели со слишком длинным периодом между отправкой товара и его оплатой, и это уже вызывает тревогу?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes payments-without-closing-docs wording into open contracts lane", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"Где у нас есть платежи, но нет документов для закрытия взаиморасчетов? Это уже требует ручной проверки."
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_open_contracts");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes stale advances wording into open contracts lane without missing-anchor fallback", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_open_contracts");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes non-paying counterparties month-risk wording into receivables lane", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"какие контрагенты пока вообще не платят за текущий месяц и это уже тревожный знак для нас?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_receivables_counterparties");
expect(result?.debug.selected_recipe).toBe("address_movements_receivables_v1");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes documents-without-payments wording into open contracts lane", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"По каким контрагентам документы есть, а оплат нет. Может, стоит взять на карандаш такие ситуации чтоб не тянуть дальше?"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_open_contracts");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
});
it("routes period coverage profile question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("За какие годы в базе есть данные?");
@@ -0,0 +1,186 @@
import { describe, expect, it } from "vitest";
import { buildAssistantAnswerStructureV11 } from "../src/services/assistantAnswerPackageBuilder";
function buildRetrieval(input?: Partial<any>): any {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: [],
...input
};
}
describe("assistant answer package builder v11", () => {
it("builds baseline answer structure with unresolved mechanism", () => {
const structure = buildAssistantAnswerStructureV11({
assistantReply: "Первая строка\nВторая строка",
coverageReport: {
requirements_total: 2,
requirements_covered: 1,
requirements_uncovered: ["R2"],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "partial",
route_subject_match: true,
missing_requirements: ["R2"],
reasons: ["limited coverage"],
why_included_summary: [],
selection_reason_summary: []
},
retrievalResults: [buildRetrieval()]
});
expect(structure.schema_version).toBe("answer_structure_v1_1");
expect(structure.answer_summary).toBe("Первая строка");
expect(structure.mechanism_block.status).toBe("unresolved");
expect(structure.evidence_block.coverage_note).toBe("coverage_partial_or_limited");
expect(structure.uncertainty_block.open_uncertainties).toEqual(["R2"]);
});
it("adds claim-evidence links when enrichment is explicitly enabled", () => {
const structure = buildAssistantAnswerStructureV11({
assistantReply: "Ответ",
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalResults: [
buildRetrieval({
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07"
},
locator: {
field_path: "amount",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "trace confirmed",
confidence: "high",
limitation: null,
payload: {}
}
]
})
],
options: {
enableEvidenceEnrichment: true
}
});
expect(Array.isArray(structure.evidence_block.claim_evidence_links)).toBe(true);
expect(structure.evidence_block.claim_evidence_links?.[0]?.claim_ref).toBe("requirement:R1");
expect(structure.evidence_block.claim_evidence_links?.[0]?.evidence_ids).toContain("ev-1");
});
it("omits claim-evidence links when enrichment is disabled", () => {
const structure = buildAssistantAnswerStructureV11({
assistantReply: "Ответ",
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalResults: [
buildRetrieval({
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07"
},
locator: {
field_path: "amount",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "trace confirmed",
confidence: "high",
limitation: null,
payload: {}
}
]
})
],
options: {
enableEvidenceEnrichment: false
}
});
expect(structure.evidence_block.claim_evidence_links).toBeUndefined();
});
});
@@ -0,0 +1,123 @@
import { describe, expect, it } from "vitest";
import type { AssistantRequirement, UnifiedRetrievalResult } from "../src/types/assistant";
import { buildAssistantEvidenceBundleContractV1 } from "../src/services/assistantOrchestrationContracts";
import { assembleAssistantContractsBundleV1 } from "../src/services/assistantContractsBundleAssembler";
function buildRequirement(): AssistantRequirement {
return {
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "req1",
subject_tokens: ["account_60.01"],
status: "covered",
route: "hybrid_store_plus_live"
};
}
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: [],
...input
};
}
describe("assistant contracts bundle assembler", () => {
it("assembles query/execution/coverage contracts with outcome class", () => {
const retrievalResults = [buildRetrieval({ status: "ok" })];
const bundle = assembleAssistantContractsBundleV1({
userMessage: "проверь хвосты по 60.01",
normalizedQuestion: "проверь хвосты по 60.01",
normalized: {
schema_version: "normalized_query_v2_0_2",
user_message_raw: "проверь хвосты по 60.01",
message_in_scope: true,
scope_confidence: "high",
contains_multiple_tasks: false,
fragments: [{ fragment_id: "F1" }],
discarded_fragments: [],
global_notes: {
needs_clarification: false,
clarification_reason: null
}
} as any,
routeSummary: {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 1,
in_scope_fragments: 1,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [],
fallback: {
type: "none",
message: null
}
},
droppedIntentSegments: [],
analysisContext: {
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto"
},
executionPlan: [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
should_execute: true,
no_route_reason: null,
clarification_reason: null
}
],
requirements: [buildRequirement()],
evidenceBundleContractV1: buildAssistantEvidenceBundleContractV1({
retrievalCalls: [{ route: "hybrid_store_plus_live" }],
retrievalResults
}),
replyType: "factual_with_explanation",
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
grounding: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalResults
});
expect(bundle.queryFrameContractV1.schema_version).toBe("assistant_query_frame_v1");
expect(bundle.executionPlanContractV1.schema_version).toBe("assistant_execution_plan_v1");
expect(bundle.coverageContractV1.schema_version).toBe("assistant_coverage_contract_v1");
expect(bundle.outcomeClassV1).toBe("FULLY_ANSWERED");
expect(bundle.assistantOrchestrationContractsV1.evidence_bundle.schema_version).toBe("assistant_evidence_bundle_v1");
});
});
@@ -0,0 +1,199 @@
import { describe, expect, it } from "vitest";
import {
checkGroundingForRequirements,
evaluateCoverageForRequirements,
extractRequirementsForRoute
} from "../src/services/assistantCoverageGrounding";
describe("assistant coverage-grounding module", () => {
it("extracts requirements from deterministic route summary", () => {
const extracted = extractRequirementsForRoute({
routeSummary: {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 2,
in_scope_fragments: 2,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [
{
fragment_id: "F1",
route: "no_route",
no_route_reason: "insufficient_specificity",
reason: "missing anchor"
},
{
fragment_id: "F2",
route: "hybrid_store_plus_live",
reason: "ok route"
}
],
fallback: {
type: "none",
message: null
}
} as any,
userMessage: "base question",
fragmentTextById: new Map([
["F1", "need more details"],
["F2", "check account 60"]
]),
extractSubjectTokens: (text) => (text.includes("60") ? ["account_60"] : ["counterparty"])
});
expect(extracted.requirements).toHaveLength(2);
expect(extracted.requirements[0].status).toBe("clarification_needed");
expect(extracted.requirements[0].route).toBeNull();
expect(extracted.requirements[1].status).toBe("covered");
expect(extracted.requirements[1].route).toBe("hybrid_store_plus_live");
expect(extracted.byFragment.get("F2")).toEqual(["R2"]);
});
it("evaluates coverage from retrieval outcomes", () => {
const requirements = [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "req1",
subject_tokens: ["account_60"],
status: "covered" as const,
route: "hybrid_store_plus_live"
},
{
requirement_id: "R2",
source_fragment_id: "F2",
requirement_text: "req2",
subject_tokens: ["counterparty"],
status: "covered" as const,
route: "store_feature_risk"
}
];
const retrievalResults = [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07"
},
locator: {
field_path: "amount",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "ok",
confidence: "high",
limitation: null,
payload: {}
}
],
why_included: ["why"],
selection_reason: ["sel"],
risk_factors: [],
business_interpretation: [],
confidence: "high",
limitations: [],
errors: []
},
{
fragment_id: "F2",
requirement_ids: ["R2"],
route: "store_feature_risk",
status: "empty",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "low",
limitations: [],
errors: []
}
] as any;
const evaluation = evaluateCoverageForRequirements(requirements as any, retrievalResults);
expect(evaluation.coverage.requirements_total).toBe(2);
expect(evaluation.coverage.requirements_covered).toBe(1);
expect(evaluation.coverage.requirements_uncovered).toContain("R2");
expect(evaluation.requirements.find((item) => item.requirement_id === "R1")?.status).toBe("covered");
});
it("produces route mismatch grounding when critical subject token is absent", () => {
const grounded = checkGroundingForRequirements({
userMessage: "Проверь НДС цепочку",
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "vat chain",
subject_tokens: ["nds"],
status: "covered",
route: "hybrid_store_plus_live"
}
] as any,
coverage: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
retrievalResults: [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: { note: "no tax markers" },
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: []
}
] as any,
extractSubjectTokens: () => ["nds"]
});
expect(grounded.status).toBe("route_mismatch_blocked");
expect(grounded.route_subject_match).toBe(false);
expect(grounded.reasons.some((item) => item.includes("Ключевые ориентиры вопроса"))).toBe(true);
});
});
@@ -0,0 +1,130 @@
import { describe, expect, it } from "vitest";
import { buildDeepAnalysisDebugPayload } from "../src/services/assistantDebugPayloadAssembler";
function baseInput() {
return {
traceId: "trace-1",
promptVersion: "normalizer_v2_0_2",
schemaVersion: "normalized_query_v2_0_2",
fallbackType: "none",
routeSummary: { mode: "deterministic_v2" },
fragments: [{ fragment_id: "F1" }],
requirementsExtracted: [{ requirement_id: "R1", status: "covered" }],
coverageReport: { requirements_total: 1, requirements_covered: 1 },
routes: [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
retrievalStatus: [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary"
}
],
retrievalResults: [{ fragment_id: "F1", status: "ok" }],
groundingCheck: { status: "grounded" },
droppedIntentSegments: [],
questionTypeClass: "factual_lookup",
companyAnchors: { companies: ["demo"] },
runtimeAnalysisContext: {
active: true,
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto" as const
},
businessScopeResolution: {
business_scope_raw: ["company_specific_accounting"],
business_scope_resolved: ["company_specific_accounting"],
company_grounding_applied: true,
scope_resolution_reason: ["resolved"]
},
temporalGuard: {
raw_time_anchor: "2020-07",
raw_time_scope: "month",
resolved_time_anchor: "2020-07",
resolved_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
effective_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
temporal_guard_input: "2020-07",
temporal_alignment_status: "aligned",
temporal_resolution_source: "analysis_context",
temporal_guard_basis: "analysis_context",
temporal_guard_applied: true,
temporal_guard_outcome: "pass"
},
polarityAudit: {
raw_numeric_tokens: ["60.01"],
classified_numeric_tokens: [{ token: "60.01" }],
rejected_as_non_accounts: [],
resolved_account_anchors: ["60.01"]
},
claimAnchorAudit: {
settlement_role: "supplier",
settlement_role_resolution_reason: ["account_60_detected"],
polarity_resolution_status: "resolved"
},
targetedEvidenceAudit: { targeted_evidence_hit_rate: 1 },
evidenceAdmissibilityGateAudit: { admissible_evidence_count: 1 },
rbpLiveRouteAudit: null,
faLiveRouteAudit: null,
groundedAnswerEligibilityGuard: { eligibility_time_basis: "analysis_context", eligible: true },
followupStateUsage: null,
compositionDebug: {
problem_centric_answer_applied: true,
problem_units_used_count: 2,
problem_answer_mode: "stage3_lifecycle_aware_v1",
problem_unit_ids_used: ["pu-1", "pu-2"]
},
addressRuntimeMetaForDeep: {
attempted: true,
applied: true,
reason: "ok",
provider: "openai",
fallbackRuleHit: null,
toolGateDecision: "run_address_lane",
toolGateReason: "detected",
predecomposeContract: { schema_version: "x" },
orchestrationContract: { schema_version: "y" }
},
outcomeClassV1: "FULLY_ANSWERED",
assistantOrchestrationContractsV1: { query_frame: {}, execution_plan: {}, evidence_bundle: {}, coverage: {} },
answerStructureV11: { schema_version: "answer_structure_v1_1" },
investigationStateSnapshot: { status: "active" },
normalizedPayload: { schema_version: "normalized_query_v2_0_2" }
};
}
describe("assistant debug payload assembler", () => {
it("builds deep debug payload with analysis context and optional sections", () => {
const payload = buildDeepAnalysisDebugPayload(baseInput());
expect(payload.trace_id).toBe("trace-1");
expect(payload.analysis_context_applied).toBe(true);
expect(payload.analysis_context).toMatchObject({
as_of_date: "2020-07-31",
source: "eval_analysis_date"
});
expect(payload.problem_unit_ids_used).toEqual(["pu-1", "pu-2"]);
expect(payload.address_llm_predecompose_applied).toBe(true);
expect(payload.assistant_outcome_class_v1).toBe("FULLY_ANSWERED");
});
it("omits optional fields when they are not provided", () => {
const input = baseInput();
input.runtimeAnalysisContext.active = false;
input.followupStateUsage = null;
input.compositionDebug.problem_unit_ids_used = [];
input.rbpLiveRouteAudit = null;
input.faLiveRouteAudit = null;
input.addressRuntimeMetaForDeep = null;
const payload = buildDeepAnalysisDebugPayload(input);
expect(payload.analysis_context).toBeNull();
expect(Object.prototype.hasOwnProperty.call(payload, "followup_state_usage")).toBe(false);
expect(Object.prototype.hasOwnProperty.call(payload, "problem_unit_ids_used")).toBe(false);
expect(payload.address_llm_predecompose_applied).toBe(false);
expect(payload.address_llm_predecompose_contract).toBeNull();
});
});
@@ -0,0 +1,131 @@
import { describe, expect, it } from "vitest";
import { buildAssistantConversationItem, buildDeepAnswerArtifacts } from "../src/services/assistantDeepResponseAssembler";
describe("assistant deep response assembler", () => {
it("strips technical tail and builds fallback answer structure when missing in composition", () => {
const artifacts = buildDeepAnswerArtifacts({
safeAssistantReplyBase: "Короткий ответ\n\ndebug_payload_json: {\"x\":1}",
featureContractsV11: true,
featureAnswerPolicyV11: true,
compositionAnswerStructureV11: null,
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalResults: []
});
expect(artifacts.safeAssistantReply).toBe("Короткий ответ");
expect(artifacts.answerStructureV11?.schema_version).toBe("answer_structure_v1_1");
});
it("uses provided composition answer structure and creates assistant conversation item", () => {
const provided = {
schema_version: "answer_structure_v1_1",
answer_summary: "sum",
direct_answer: "direct",
mechanism_block: {
status: "grounded" as const,
mechanism_notes: [],
limitation_reason_codes: []
},
evidence_block: {
evidence_ids: [],
mechanism_notes: [],
coverage_note: "ok"
},
uncertainty_block: {
open_uncertainties: [],
limitations: []
},
next_step_block: {
recommended_actions: [],
clarification_questions: []
}
};
const artifacts = buildDeepAnswerArtifacts({
safeAssistantReplyBase: "Готово",
featureContractsV11: true,
featureAnswerPolicyV11: true,
compositionAnswerStructureV11: provided as any,
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalResults: []
});
expect(artifacts.answerStructureV11).toEqual(provided);
const item = buildAssistantConversationItem({
messageId: "msg-1",
sessionId: "asst-1",
text: artifacts.safeAssistantReply,
replyType: "factual",
traceId: "trace-1",
debug: {
trace_id: "trace-1",
prompt_version: "normalizer_v2_0_2",
schema_version: "normalized_query_v2_0_2",
fallback_type: "none",
route_summary: null,
fragments: [],
requirements_extracted: [],
coverage_report: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
routes: [],
retrieval_status: [],
retrieval_results: [],
answer_grounding_check: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
dropped_intent_segments: [],
answer_structure_v11: null,
investigation_state_snapshot: null,
normalized: null
} as any
});
expect(item.message_id).toBe("msg-1");
expect(item.session_id).toBe("asst-1");
expect(item.reply_type).toBe("factual");
expect(item.text).toBe("Готово");
expect(typeof item.created_at).toBe("string");
});
});
@@ -0,0 +1,126 @@
import { describe, expect, it } from "vitest";
import { buildAssistantDeepTurnComposition } from "../src/services/assistantDeepTurnCompositionRuntimeAdapter";
describe("assistant deep turn composition runtime adapter", () => {
it("uses followup domain hint and company-anchor period signal", () => {
let capturedInput: Record<string, unknown> | null = null;
const output = buildAssistantDeepTurnComposition({
userMessage: "проверь хвосты по 60.01",
routeSummary: null,
retrievalResults: [],
requirements: [],
coverageReport: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
followupUsage: { applied: true },
investigationState: {
schema_version: "investigation_state_v1",
session_id: "asst-1",
status: "active",
turn_index: 1,
updated_at: "2026-04-10T10:00:00.000Z",
question_id: "msg-1",
question_scope_id: null,
scope_origin: null,
focus: {
domain: "settlements_60_62",
period: null,
primary_accounts: [],
active_query_subject: null
},
narrowing_status: "unknown",
evidence_refs: [],
open_uncertainties: [],
last_answer_mode: null,
followup_context: null,
query_mode_hint: "direct_answer"
} as any,
companyAnchors: {
periods: ["2020-07"],
dates: []
},
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
featureAnswerPolicyV11: true,
featureProblemCentricAnswerV1: true,
featureLifecycleAnswerV1: true,
hasExplicitPeriodAnchor: () => false,
resolveQuestionTypeFn: () => "factual_lookup",
composeAssistantAnswerFn: ((input: Record<string, unknown>) => {
capturedInput = input;
return {
assistant_reply: "ok",
fallback_type: "none",
reply_type: "factual"
};
}) as any
});
expect(output.focusDomainHint).toBe("settlements_60_62");
expect(output.questionTypeClass).toBe("factual_lookup");
expect(output.hasPeriodInCompanyAnchors).toBe(true);
expect(output.normalizationPeriodExplicit).toBe(true);
expect(output.composition.reply_type).toBe("factual");
expect(capturedInput?.focusDomainHint).toBe("settlements_60_62");
expect(capturedInput?.normalizationPeriodExplicit).toBe(true);
});
it("falls back to explicit period from normalized payload when anchors are absent", () => {
const output = buildAssistantDeepTurnComposition({
userMessage: "проверь закрытие",
routeSummary: null,
retrievalResults: [],
requirements: [],
coverageReport: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
followupUsage: { applied: false },
investigationState: null,
companyAnchors: {
periods: [],
dates: []
},
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
featureAnswerPolicyV11: true,
featureProblemCentricAnswerV1: true,
featureLifecycleAnswerV1: true,
hasExplicitPeriodAnchor: () => true,
resolveQuestionTypeFn: () => "verification",
composeAssistantAnswerFn: (() => ({
assistant_reply: "ok",
fallback_type: "none",
reply_type: "factual"
})) as any
});
expect(output.focusDomainHint).toBeNull();
expect(output.questionTypeClass).toBe("verification");
expect(output.hasPeriodInCompanyAnchors).toBe(false);
expect(output.normalizationPeriodExplicit).toBe(true);
});
});
@@ -0,0 +1,106 @@
import { describe, expect, it } from "vitest";
import { buildAssistantDeepTurnRuntimeContext } from "../src/services/assistantDeepTurnContextRuntimeAdapter";
describe("assistant deep turn context runtime adapter", () => {
it("assembles context in deterministic order and propagates followup flag", () => {
const callOrder: string[] = [];
const companyAnchors = { accounts: ["60.01"] };
const temporalGuard = {
effective_primary_period: { from: "2020-07-01", to: "2020-07-31" },
primary_period_window: { from: "2020-07-01", to: "2020-07-31" }
};
const businessScope = { route_summary_resolved: { mode: "deterministic_v2", decisions: [] as any[] } } as any;
const output = buildAssistantDeepTurnRuntimeContext({
userMessage: "почему не закрыт 60.01",
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
routeSummary: { mode: "deterministic_v2", decisions: [] } as any,
runtimeAnalysisContext: {
active: true,
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "analysis_context"
},
followupUsage: { applied: true },
resolveCompanyAnchors: () => {
callOrder.push("anchors");
return companyAnchors;
},
resolveBusinessScopeAlignment: () => {
callOrder.push("scope_align");
return businessScope;
},
inferP0DomainFromMessage: () => {
callOrder.push("infer_domain");
return "settlements_60_62";
},
resolveTemporalGuard: (input) => {
callOrder.push("temporal");
expect(input.analysisContext).toEqual({
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "analysis_context"
});
return temporalGuard as any;
},
resolveDomainPolarityGuard: (input) => {
callOrder.push("polarity");
expect(input.focusDomainHint).toBe("settlements_60_62");
return { polarity: "supplier_payable" };
},
resolveClaimBoundAnchors: (input) => {
callOrder.push("claim");
expect(input.primaryPeriod).toEqual(temporalGuard.effective_primary_period);
return { claim_type: "prove_settlement_closure_state" } as any;
},
resolveBusinessScopeFromLiveContext: (input) => {
callOrder.push("scope_live");
expect(input.followupApplied).toBe(true);
return {
...businessScope,
live_scope_used: true
} as any;
}
});
expect(callOrder).toEqual(["anchors", "scope_align", "infer_domain", "temporal", "polarity", "claim", "scope_live"]);
expect(output.companyAnchors).toBe(companyAnchors);
expect(output.focusDomainForGuards).toBe("settlements_60_62");
expect(output.claimAnchorAudit.claim_type).toBe("prove_settlement_closure_state");
expect(output.liveTemporalHint).toEqual({
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "analysis_context"
});
});
it("drops unknown inferred domain and disables live temporal hint when context is inactive", () => {
const output = buildAssistantDeepTurnRuntimeContext({
userMessage: "какой-нибудь вопрос",
normalizedPayload: null as any,
routeSummary: null,
runtimeAnalysisContext: {
active: false,
as_of_date: null,
period_from: null,
period_to: null,
source: null
},
followupUsage: null,
resolveCompanyAnchors: () => ({}),
resolveBusinessScopeAlignment: () => ({ route_summary_resolved: null }),
inferP0DomainFromMessage: () => "unknown_domain",
resolveTemporalGuard: () => ({ primary_period_window: null }),
resolveDomainPolarityGuard: () => ({ polarity: "not_applicable" }),
resolveClaimBoundAnchors: () => ({ claim_type: "unknown" } as any),
resolveBusinessScopeFromLiveContext: () => ({ route_summary_resolved: null })
});
expect(output.focusDomainForGuards).toBeNull();
expect(output.resolvedRouteSummary).toBeNull();
expect(output.liveTemporalHint).toBeNull();
});
});
@@ -0,0 +1,142 @@
import { describe, expect, it } from "vitest";
import { runAssistantDeepTurnGroundingRuntime } from "../src/services/assistantDeepTurnGroundingRuntimeAdapter";
describe("assistant deep turn grounding runtime adapter", () => {
it("runs audits, coverage-grounding pipeline and eligibility overlay in stable order", () => {
const callOrder: string[] = [];
const retrievalResults = [{ fragment_id: "F1" }] as any[];
const coverageEvaluation = {
requirements: [{ requirement_id: "R1" }],
coverage: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
}
} as any;
const groundingCheckBase = {
status: "grounded_positive",
reasons: [],
route_subject_match: true,
missing_requirements: [],
why_included_summary: [],
selection_reason_summary: []
} as any;
const output = runAssistantDeepTurnGroundingRuntime({
claimType: "prove_settlement_closure_state",
retrievalResults,
rbpPlanAudit: { rbp: true },
faPlanAudit: { fa: true },
routeSummary: { mode: "deterministic_v2", decisions: [] } as any,
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
userMessage: "check",
requirementExtraction: {
requirements: [{ requirement_id: "R1" }] as any,
byFragment: new Map([["F1", ["R1"]]])
} as any,
extractRequirements: (() => {
throw new Error("should not be called when requirementExtraction is provided");
}) as any,
evaluateCoverage: (() => {
throw new Error("should not be called directly from adapter");
}) as any,
checkGrounding: (() => {
throw new Error("should not be called directly from adapter");
}) as any,
temporalGuard: { temporal_guard_outcome: "passed" } as any,
polarityAudit: { outcome: "passed" } as any,
evidenceAudit: { admissible_evidence_count: 2 } as any,
claimAnchorAudit: { claim_type: "prove_settlement_closure_state" } as any,
targetedEvidenceHitRate: 0.5,
businessScopeResolved: ["company_specific_accounting"],
collectRbpLiveRouteAudit: (input) => {
callOrder.push("rbp_audit");
expect(input.planAudit).toEqual({ rbp: true });
return { rbp_live: 1 };
},
collectFaLiveRouteAudit: (input) => {
callOrder.push("fa_audit");
expect(input.planAudit).toEqual({ fa: true });
return { fa_live: 1 };
},
runCoverageGroundingPipelineFn: ((input: Record<string, unknown>) => {
callOrder.push("coverage_pipeline");
expect(input.retrievalResults).toBe(retrievalResults);
return {
requirementExtraction: input.requirementExtraction,
coverageEvaluation,
groundingCheckBase
};
}) as any,
applyGroundingEligibilityFn: ((input: Record<string, unknown>) => {
callOrder.push("eligibility");
expect(input.groundingCheckBase).toBe(groundingCheckBase);
return {
groundedAnswerEligibilityGuard: {
eligible: true
},
groundingCheck: groundingCheckBase
};
}) as any
});
expect(callOrder).toEqual(["rbp_audit", "fa_audit", "coverage_pipeline", "eligibility"]);
expect(output.rbpLiveRouteAudit).toEqual({ rbp_live: 1 });
expect(output.faLiveRouteAudit).toEqual({ fa_live: 1 });
expect(output.coverageEvaluation).toBe(coverageEvaluation);
expect(output.groundedAnswerEligibilityGuard).toEqual({ eligible: true });
expect(output.groundingCheck).toBe(groundingCheckBase);
});
it("threads default pipeline output through without custom hooks", () => {
const output = runAssistantDeepTurnGroundingRuntime({
claimType: "unknown",
retrievalResults: [],
rbpPlanAudit: null,
faPlanAudit: null,
routeSummary: null,
normalizedPayload: null as any,
userMessage: "q",
requirementExtraction: {
requirements: [],
byFragment: new Map()
} as any,
extractRequirements: () => ({
requirements: [],
byFragment: new Map()
}) as any,
evaluateCoverage: () => ({
requirements: [],
coverage: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
}
}),
checkGrounding: () =>
({
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
}) as any,
temporalGuard: { temporal_guard_outcome: "passed", temporal_guard_basis: "none" } as any,
polarityAudit: { applied: false, outcome: "not_applicable" } as any,
evidenceAudit: { admissible_evidence_count: 0 } as any,
claimAnchorAudit: null,
collectRbpLiveRouteAudit: () => null,
collectFaLiveRouteAudit: () => null
});
expect(output.coverageEvaluation.requirements).toEqual([]);
expect(output.groundingCheck.status).toBe("no_grounded_answer");
});
});
@@ -0,0 +1,121 @@
import { describe, expect, it } from "vitest";
import {
applyAssistantDeepTurnGroundingEligibility,
applyAssistantDeepTurnRetrievalGuards
} from "../src/services/assistantDeepTurnGuardRuntimeAdapter";
describe("assistant deep turn guard runtime adapter", () => {
it("runs retrieval guards in expected order and threads outputs", () => {
const callOrder: string[] = [];
const seedResults = [{ fragment_id: "F1" }] as any[];
const afterPolarity = [{ fragment_id: "P1" }] as any[];
const afterTargeted = [{ fragment_id: "T1" }] as any[];
const afterGate = [{ fragment_id: "G1" }] as any[];
const output = applyAssistantDeepTurnRetrievalGuards({
retrievalResults: seedResults as any,
domainPolarityGuardInitial: { applied: true, polarity: "supplier_payable" } as any,
claimAnchorAudit: { claim_type: "prove_settlement_closure_state" } as any,
temporalGuard: { temporal_guard_outcome: "passed", temporal_guard_basis: "none" } as any,
focusDomainForGuards: "settlements_60_62" as any,
companyAnchors: { accounts: ["60.01"] } as any,
userMessage: "check settlements",
applyDomainPolarityGuardFn: ((input: Record<string, unknown>) => {
callOrder.push("polarity");
expect(input.retrievalResults).toBe(seedResults);
return {
retrievalResults: afterPolarity,
audit: {
applied: true,
polarity: "supplier_payable",
outcome: "passed",
reason_codes: []
}
};
}) as any,
applyTargetedEvidenceFn: ((input: Record<string, unknown>) => {
callOrder.push("targeted");
expect(input.retrievalResults).toBe(afterPolarity);
return {
retrievalResults: afterTargeted,
audit: {
targeted_evidence_hit_rate: 0.5,
reason_codes: []
}
};
}) as any,
applyEvidenceAdmissibilityGateFn: ((input: Record<string, unknown>) => {
callOrder.push("gate");
expect(input.retrievalResults).toBe(afterTargeted);
expect(input.polarity).toBe("supplier_payable");
expect(input.userMessage).toBe("check settlements");
return {
retrievalResults: afterGate,
audit: {
admissible_evidence_count: 2,
reason_codes: []
}
};
}) as any
});
expect(callOrder).toEqual(["polarity", "targeted", "gate"]);
expect(output.retrievalResults).toBe(afterGate);
expect(output.polarityGuardResult.retrievalResults).toBe(afterPolarity);
expect(output.targetedEvidenceResult.retrievalResults).toBe(afterTargeted);
expect(output.evidenceGateResult.retrievalResults).toBe(afterGate);
});
it("evaluates grounding eligibility and applies status overlay", () => {
const callOrder: string[] = [];
const groundingCheckBase = {
status: "grounded_positive",
reasons: ["base"],
route_subject_match: true
};
const output = applyAssistantDeepTurnGroundingEligibility({
groundingCheckBase,
temporalGuard: { temporal_guard_outcome: "passed", temporal_guard_basis: "none" } as any,
polarityAudit: { applied: true, outcome: "passed", polarity: "supplier_payable" } as any,
evidenceAudit: { admissible_evidence_count: 0 } as any,
claimAnchorAudit: { claim_anchor_resolution_rate: 1, missing_anchors: [], required_anchors: [] } as any,
targetedEvidenceHitRate: 0,
businessScopeResolved: ["company_specific_accounting"],
evaluateGroundedAnswerEligibilityFn: ((input: Record<string, unknown>) => {
callOrder.push("eligibility");
expect(input.targetedEvidenceHitRate).toBe(0);
return {
eligible: false,
temporal_passed: true,
eligibility_time_basis: "none",
business_scope_passed: true,
polarity_passed: true,
claim_anchors_passed: true,
claim_anchor_resolution_rate: 1,
missing_required_anchors: 0,
admissible_evidence_count: 0,
critical_contradiction: false,
outcome: "limited_or_insufficient_evidence",
grounding_mode: "limited_or_insufficient_evidence",
reason_codes: ["admissible_evidence_count_zero"]
};
}) as any,
applyEligibilityToGroundingCheckFn: ((check: Record<string, unknown>, eligibility: Record<string, unknown>) => {
callOrder.push("overlay");
expect(check.status).toBe("grounded_positive");
expect(eligibility.eligible).toBe(false);
return {
...check,
status: "no_grounded_answer",
reasons: ["base", "not_enough_evidence"]
};
}) as any
});
expect(callOrder).toEqual(["eligibility", "overlay"]);
expect(output.groundedAnswerEligibilityGuard.eligible).toBe(false);
expect(output.groundingCheck.status).toBe("no_grounded_answer");
expect(output.groundingCheck.reasons).toEqual(["base", "not_enough_evidence"]);
});
});
@@ -0,0 +1,140 @@
import { describe, expect, it } from "vitest";
import { buildAssistantDeepTurnPackagingInput, type AssistantDeepTurnInputBuilderArgs } from "../src/services/assistantDeepTurnInputBuilder";
function baseArgs(): AssistantDeepTurnInputBuilderArgs {
return {
sessionId: "asst-1",
messageId: "msg-1",
userMessage: "проверь 60.01",
normalized: {
trace_id: "trace-1",
prompt_version: "normalizer_v2_0_2",
schema_version: "normalized_query_v2_0_2",
normalized: {
schema_version: "normalized_query_v2_0_2",
user_message_raw: "проверь 60.01",
message_in_scope: true,
scope_confidence: "high",
contains_multiple_tasks: false,
fragments: [],
discarded_fragments: [],
global_notes: {
needs_clarification: false,
clarification_reason: null
}
}
},
normalizedQuestion: "проверь 60.01",
routeSummary: null,
droppedIntentSegments: [],
analysisContextForContract: null,
executionPlan: [],
requirementExtractionRequirements: [],
coverageEvaluationRequirements: [],
coverageReport: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalCalls: [],
retrievalResultsRaw: [],
retrievalResults: [],
routesForDebug: [],
resolvedExecutionState: {},
questionTypeClass: "factual_lookup",
companyAnchors: {},
runtimeAnalysisContext: {
active: false,
as_of_date: null,
period_from: null,
period_to: null,
source: null,
snapshot_mode: "auto"
},
businessScopeResolution: {},
temporalGuard: {},
polarityAudit: {},
claimAnchorAudit: {},
targetedEvidenceAudit: null,
evidenceAdmissibilityGateAudit: null,
rbpLiveRouteAudit: null,
faLiveRouteAudit: null,
groundedAnswerEligibilityGuard: {},
followupStateUsage: undefined,
composition: {
reply_type: "factual",
fallback_type: "none"
},
safeAssistantReplyBase: "ok",
featureContractsV11: true,
featureAnswerPolicyV11: true,
investigationStateSnapshot: null,
addressRuntimeMetaForDeep: null
};
}
describe("assistant deep turn input builder", () => {
it("applies stable defaults for optional composition and followup fields", () => {
const built = buildAssistantDeepTurnPackagingInput(baseArgs());
expect(built.followupStateUsage).toBeNull();
expect(built.composition.answer_structure_v11).toBeNull();
expect(built.composition.problem_centric_answer_applied).toBe(false);
expect(built.composition.problem_units_used_count).toBe(0);
expect(built.composition.problem_answer_mode).toBe("stage1_policy_v11");
expect(built.composition.problem_unit_ids_used).toEqual([]);
});
it("preserves explicit composition fields and normalizes unit ids array", () => {
const args = baseArgs();
args.followupStateUsage = { applied: true };
args.composition.answer_structure_v11 = {
schema_version: "answer_structure_v1_1",
answer_summary: "sum",
direct_answer: "direct",
mechanism_block: {
status: "grounded",
mechanism_notes: [],
limitation_reason_codes: []
},
evidence_block: {
evidence_ids: [],
source_refs: [],
mechanism_notes: [],
coverage_note: "ok"
},
uncertainty_block: {
open_uncertainties: [],
limitations: []
},
next_step_block: {
recommended_actions: [],
clarification_questions: []
}
} as any;
args.composition.problem_centric_answer_applied = true;
args.composition.problem_units_used_count = 3;
args.composition.problem_answer_mode = "stage3_lifecycle_aware_v1";
args.composition.problem_unit_ids_used = ["pu-1", "pu-2"];
const built = buildAssistantDeepTurnPackagingInput(args);
expect(built.followupStateUsage).toEqual({ applied: true });
expect(built.composition.answer_structure_v11?.schema_version).toBe("answer_structure_v1_1");
expect(built.composition.problem_centric_answer_applied).toBe(true);
expect(built.composition.problem_units_used_count).toBe(3);
expect(built.composition.problem_answer_mode).toBe("stage3_lifecycle_aware_v1");
expect(built.composition.problem_unit_ids_used).toEqual(["pu-1", "pu-2"]);
});
});
@@ -0,0 +1,179 @@
import { describe, expect, it } from "vitest";
import { assembleAssistantDeepTurnPackaging } from "../src/services/assistantDeepTurnPackaging";
function buildRetrieval() {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: []
};
}
function baseInput() {
return {
sessionId: "asst-1",
messageId: "msg-1",
userMessage: "проверь хвосты по 60.01",
normalized: {
trace_id: "trace-1",
prompt_version: "normalizer_v2_0_2",
schema_version: "normalized_query_v2_0_2",
normalized: {
schema_version: "normalized_query_v2_0_2",
user_message_raw: "проверь хвосты по 60.01",
message_in_scope: true,
scope_confidence: "high",
contains_multiple_tasks: false,
fragments: [{ fragment_id: "F1" }],
discarded_fragments: [],
global_notes: {
needs_clarification: false,
clarification_reason: null
}
}
},
normalizedQuestion: "проверь хвосты по 60.01",
routeSummary: {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 1,
in_scope_fragments: 1,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [],
fallback: {
type: "none",
message: null
}
},
droppedIntentSegments: [],
analysisContextForContract: {
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto" as const
},
executionPlan: [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
should_execute: true,
no_route_reason: null,
clarification_reason: null
}
],
requirementExtractionRequirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "проверить хвосты 60.01",
subject_tokens: ["account_60.01"],
status: "covered",
route: "hybrid_store_plus_live"
}
],
coverageEvaluationRequirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "проверить хвосты 60.01",
subject_tokens: ["account_60.01"],
status: "covered",
route: "hybrid_store_plus_live"
}
],
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalCalls: [{ route: "hybrid_store_plus_live" }],
retrievalResultsRaw: [buildRetrieval()],
retrievalResults: [buildRetrieval()],
routesForDebug: [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
resolvedExecutionState: { executable: 1 },
questionTypeClass: "factual_lookup",
companyAnchors: { companies: ["demo"] },
runtimeAnalysisContext: {
active: true,
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto" as const
},
businessScopeResolution: {
business_scope_raw: ["company_specific_accounting"],
business_scope_resolved: ["company_specific_accounting"],
company_grounding_applied: true,
scope_resolution_reason: ["resolved"]
},
temporalGuard: { temporal_guard_applied: true },
polarityAudit: { resolved_account_anchors: ["60.01"] },
claimAnchorAudit: { settlement_role: "supplier" },
targetedEvidenceAudit: { targeted_evidence_hit_rate: 1 },
evidenceAdmissibilityGateAudit: { admissible_evidence_count: 1 },
rbpLiveRouteAudit: null,
faLiveRouteAudit: null,
groundedAnswerEligibilityGuard: { eligible: true },
followupStateUsage: null,
composition: {
reply_type: "factual" as const,
fallback_type: "none",
answer_structure_v11: null,
problem_centric_answer_applied: true,
problem_units_used_count: 1,
problem_answer_mode: "stage3_lifecycle_aware_v1",
problem_unit_ids_used: ["pu-1"]
},
safeAssistantReplyBase: "Короткий ответ\n\ndebug_payload_json: {\"x\":1}",
featureContractsV11: true,
featureAnswerPolicyV11: true,
investigationStateSnapshot: { status: "active" },
addressRuntimeMetaForDeep: null
};
}
describe("assistant deep turn packaging", () => {
it("assembles deep artifacts, debug payload and processed log in one call", () => {
const input = baseInput();
const output = assembleAssistantDeepTurnPackaging(input as any);
expect(output.deepAnswerArtifacts.safeAssistantReply).toBe("Короткий ответ");
expect(output.contractsBundleV1.outcomeClassV1).toBe("FULLY_ANSWERED");
expect(output.debug.trace_id).toBe("trace-1");
expect(output.assistantItem.message_id).toBe("msg-1");
expect(output.assistantItem.text).toBe("Короткий ответ");
expect(output.deepAnalysisLogDetails.session_id).toBe("asst-1");
expect(output.deepAnalysisLogDetails.message_id).toBe("msg-1");
});
});
@@ -0,0 +1,184 @@
import { describe, expect, it } from "vitest";
import { runAssistantDeepTurnPackagingRuntime } from "../src/services/assistantDeepTurnPackagingRuntimeAdapter";
function buildBaseInput() {
return {
featureInvestigationStateV1: true,
sessionId: "asst-1",
questionId: "msg-user-1",
userMessage: "проверь кейс",
normalized: {
trace_id: "trace-1",
prompt_version: "p1",
schema_version: "s1",
normalized: { schema_version: "normalized_query_v2_0_2" } as any
},
normalizedQuestion: "проверь кейс",
routeSummary: null,
executionPlan: [],
requirementExtractionRequirements: [],
coverageEvaluationRequirements: [],
coverageReport: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
retrievalCalls: [],
retrievalResultsRaw: [],
retrievalResults: [],
questionTypeClass: "factual_lookup",
companyAnchors: {},
runtimeAnalysisContext: {
active: false,
as_of_date: null,
period_from: null,
period_to: null,
source: null,
snapshot_mode: "auto" as const
},
businessScopeResolution: {},
temporalGuard: {},
polarityAudit: {},
claimAnchorAudit: {},
targetedEvidenceAudit: {},
evidenceAdmissibilityGateAudit: {},
rbpLiveRouteAudit: null,
faLiveRouteAudit: null,
groundedAnswerEligibilityGuard: {},
followupStateUsage: null,
followupApplied: false,
composition: {
assistant_reply: "raw-reply",
reply_type: "factual" as const,
fallback_type: "none"
},
featureContractsV11: true,
featureAnswerPolicyV11: true,
previousInvestigationState: null,
addressRuntimeMetaForDeep: null,
extractDroppedIntentSegments: () => [],
buildDebugRoutes: () => [],
extractExecutionState: () => null,
sanitizeReply: (value: string) => value,
persistInvestigationState: () => {},
messageIdFactory: () => "msg-fixed"
};
}
describe("assistant deep turn packaging runtime adapter", () => {
it("executes pre-packaging, snapshot, persist, input-build and assembly in stable order", () => {
const callOrder: string[] = [];
let persistedByCallback = 0;
const output = runAssistantDeepTurnPackagingRuntime({
...buildBaseInput(),
persistInvestigationState: () => {
persistedByCallback += 1;
},
buildPrePackagingContextFn: (() => {
callOrder.push("pre");
return {
droppedIntentSegments: ["F2_dropped"],
analysisContextForContract: null,
routesForDebug: [{ fragment_id: "F1" }],
resolvedExecutionState: [{ fragment_id: "F1", execution_readiness: "ready" }],
safeAssistantReplyBase: "safe-base"
};
}) as any,
buildInvestigationStateSnapshotFn: (() => {
callOrder.push("snapshot");
return { schema_version: "investigation_state_v1" };
}) as any,
persistInvestigationStateSnapshotFn: ((input: Record<string, unknown>) => {
callOrder.push("persist");
(input.persist as Function)(input.sessionId, input.snapshot);
return true;
}) as any,
buildDeepTurnPackagingInputFn: ((input: Record<string, unknown>) => {
callOrder.push("input");
expect(input.messageId).toBe("msg-fixed");
expect(input.droppedIntentSegments).toEqual(["F2_dropped"]);
expect(input.safeAssistantReplyBase).toBe("safe-base");
return input;
}) as any,
assembleDeepTurnPackagingFn: (() => {
callOrder.push("assemble");
return {
deepAnswerArtifacts: {
safeAssistantReply: "assistant-safe"
},
debug: { ok: true },
assistantItem: {
message_id: "msg-fixed",
session_id: "asst-1",
role: "assistant",
text: "assistant-safe",
reply_type: "factual",
created_at: "2026-04-10T10:00:00.000Z",
trace_id: "trace-1",
debug: null
},
deepAnalysisLogDetails: { stage: "deep_analysis" }
};
}) as any
});
expect(callOrder).toEqual(["pre", "snapshot", "persist", "input", "assemble"]);
expect(persistedByCallback).toBe(1);
expect(output.messageId).toBe("msg-fixed");
expect(output.safeAssistantReply).toBe("assistant-safe");
expect(output.debug).toEqual({ ok: true });
expect(output.deepAnalysisLogDetails).toEqual({ stage: "deep_analysis" });
});
it("does not persist investigation snapshot when feature is disabled", () => {
let persistedByCallback = 0;
const output = runAssistantDeepTurnPackagingRuntime({
...buildBaseInput(),
featureInvestigationStateV1: false,
persistInvestigationState: () => {
persistedByCallback += 1;
},
buildPrePackagingContextFn: (() => ({
droppedIntentSegments: [],
analysisContextForContract: null,
routesForDebug: [],
resolvedExecutionState: null,
safeAssistantReplyBase: "safe-base"
})) as any,
buildDeepTurnPackagingInputFn: ((input: Record<string, unknown>) => input) as any,
assembleDeepTurnPackagingFn: (() => ({
deepAnswerArtifacts: {
safeAssistantReply: "assistant-safe"
},
debug: {},
assistantItem: {
message_id: "msg-fixed",
session_id: "asst-1",
role: "assistant",
text: "assistant-safe",
reply_type: "factual",
created_at: "2026-04-10T10:00:00.000Z",
trace_id: "trace-1",
debug: null
},
deepAnalysisLogDetails: {}
})) as any
});
expect(output.investigationStateSnapshot).toBeNull();
expect(persistedByCallback).toBe(0);
});
});
@@ -0,0 +1,96 @@
import { describe, expect, it } from "vitest";
import { buildAssistantDeepTurnExecutionPlan } from "../src/services/assistantDeepTurnPlanRuntimeAdapter";
describe("assistant deep turn plan runtime adapter", () => {
it("builds execution plan through extraction, enforcement and guard hints in stable order", () => {
const callOrder: string[] = [];
const byFragment = new Map<string, string[]>([["F1", ["R1"]]]);
const planInitial = [{ fragment_id: "F1", route: "store_canonical" }] as any[];
const planAfterRbp = [{ fragment_id: "F1", route: "hybrid_store_plus_live" }] as any[];
const planAfterFa = [{ fragment_id: "F1", route: "hybrid_store_plus_live", fa: true }] as any[];
const planAfterTemporal = [{ fragment_id: "F1", route: "hybrid_store_plus_live", temporal: true }] as any[];
const planAfterPolarity = [{ fragment_id: "F1", route: "hybrid_store_plus_live", temporal: true, polarity: true }] as any[];
const output = buildAssistantDeepTurnExecutionPlan({
routeSummary: { mode: "deterministic_v2", decisions: [] } as any,
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
userMessage: "check tails",
claimType: "prove_settlement_closure_state",
temporalGuard: { temporal_guard_outcome: "passed" } as any,
domainPolarityGuardInitial: { polarity: "supplier_payable" } as any,
extractRequirements: () => {
callOrder.push("extract");
return {
requirements: [{ requirement_id: "R1" }] as any[],
byFragment
};
},
toExecutionPlan: (_routeSummary, _normalizedPayload, _userMessage, requirementByFragment) => {
callOrder.push("plan");
expect(requirementByFragment).toBe(byFragment);
return planInitial as any;
},
enforceRbpLiveRoutePlan: ({ executionPlan }) => {
callOrder.push("rbp");
expect(executionPlan).toBe(planInitial);
return {
executionPlan: planAfterRbp as any,
audit: { rbp: true }
};
},
enforceFaLiveRoutePlan: ({ executionPlan }) => {
callOrder.push("fa");
expect(executionPlan).toBe(planAfterRbp);
return {
executionPlan: planAfterFa as any,
audit: { fa: true }
};
},
applyTemporalHintToExecutionPlan: (executionPlan) => {
callOrder.push("temporal");
expect(executionPlan).toBe(planAfterFa);
return planAfterTemporal as any;
},
applyPolarityHintToExecutionPlan: (executionPlan) => {
callOrder.push("polarity");
expect(executionPlan).toBe(planAfterTemporal);
return planAfterPolarity as any;
}
});
expect(callOrder).toEqual(["extract", "plan", "rbp", "fa", "temporal", "polarity"]);
expect(output.requirementExtraction.byFragment).toBe(byFragment);
expect(output.executionPlan).toBe(planAfterPolarity);
expect(output.rbpRoutePlanEnforcement.audit).toEqual({ rbp: true });
expect(output.faRoutePlanEnforcement.audit).toEqual({ fa: true });
});
it("preserves empty execution plan end-to-end", () => {
const output = buildAssistantDeepTurnExecutionPlan({
routeSummary: null,
normalizedPayload: null as any,
userMessage: "noop",
claimType: "unknown",
temporalGuard: null,
domainPolarityGuardInitial: null,
extractRequirements: () => ({
requirements: [],
byFragment: new Map()
}),
toExecutionPlan: () => [],
enforceRbpLiveRoutePlan: ({ executionPlan }) => ({
executionPlan,
audit: { rbp: false }
}),
enforceFaLiveRoutePlan: ({ executionPlan }) => ({
executionPlan,
audit: { fa: false }
}),
applyTemporalHintToExecutionPlan: (executionPlan) => executionPlan,
applyPolarityHintToExecutionPlan: (executionPlan) => executionPlan
});
expect(output.executionPlan).toEqual([]);
expect(output.requirementExtraction.requirements).toEqual([]);
});
});
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { buildAssistantDeepTurnPrePackagingContext } from "../src/services/assistantDeepTurnPrePackagingContext";
describe("assistant deep turn pre-packaging context", () => {
it("builds all pre-packaging fields with active analysis context", () => {
const output = buildAssistantDeepTurnPrePackagingContext({
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
routeSummary: null,
runtimeAnalysisContext: {
active: true,
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto"
},
assistantReply: "raw",
extractDroppedIntentSegments: () => ["segment_1"],
buildDebugRoutes: () => [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
extractExecutionState: () => ({ executable: 1 }),
sanitizeReply: (value, fallback) => `${value}::${fallback}`
});
expect(output.droppedIntentSegments).toEqual(["segment_1"]);
expect(output.analysisContextForContract).toEqual({
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto"
});
expect(output.routesForDebug).toEqual([{ fragment_id: "F1", route: "hybrid_store_plus_live" }]);
expect(output.resolvedExecutionState).toEqual({ executable: 1 });
expect(output.safeAssistantReplyBase).toContain("Нужны уточнения для надежного ответа.");
});
it("returns null analysis context when runtime context is inactive", () => {
const output = buildAssistantDeepTurnPrePackagingContext({
normalizedPayload: { schema_version: "normalized_query_v2_0_2" } as any,
routeSummary: null,
runtimeAnalysisContext: {
active: false,
as_of_date: "2020-07-31",
period_from: "2020-07-01",
period_to: "2020-07-31",
source: "eval_analysis_date",
snapshot_mode: "auto"
},
assistantReply: "ok",
extractDroppedIntentSegments: () => [],
buildDebugRoutes: () => [],
extractExecutionState: () => null,
sanitizeReply: (value) => value
});
expect(output.analysisContextForContract).toBeNull();
expect(output.routesForDebug).toEqual([]);
expect(output.safeAssistantReplyBase).toBe("ok");
});
});
@@ -0,0 +1,69 @@
import { describe, expect, it } from "vitest";
import { buildAssistantDeepTurnSuccessResponse } from "../src/services/assistantDeepTurnResponseBuilder";
import type { AssistantConversationItem } from "../src/types/assistant";
function buildAssistantItem(): AssistantConversationItem {
return {
message_id: "msg-1",
session_id: "asst-1",
role: "assistant",
text: "ok",
reply_type: "factual",
created_at: "2026-04-10T10:00:00.000Z",
trace_id: "trace-1",
debug: null
};
}
describe("assistant deep turn response builder", () => {
it("builds canonical assistant message response envelope", () => {
const assistantItem = buildAssistantItem();
const response = buildAssistantDeepTurnSuccessResponse({
sessionId: "asst-1",
assistantReply: "ответ",
replyType: "factual",
conversationItem: assistantItem,
debug: {
trace_id: "trace-1",
prompt_version: "normalizer_v2_0_2",
schema_version: "normalized_query_v2_0_2",
fallback_type: "none",
route_summary: null,
fragments: [],
requirements_extracted: [],
coverage_report: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
routes: [],
retrieval_status: [],
retrieval_results: [],
answer_grounding_check: {
status: "no_grounded_answer",
route_subject_match: false,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
},
dropped_intent_segments: [],
answer_structure_v11: null,
investigation_state_snapshot: null,
normalized: null
} as any,
conversation: [assistantItem]
});
expect(response.ok).toBe(true);
expect(response.session_id).toBe("asst-1");
expect(response.assistant_reply).toBe("ответ");
expect(response.reply_type).toBe("factual");
expect(response.conversation_item.message_id).toBe("msg-1");
expect(response.conversation).toEqual([assistantItem]);
expect(response.debug.trace_id).toBe("trace-1");
});
});
@@ -0,0 +1,158 @@
import { describe, expect, it } from "vitest";
import type { AssistantExecutionPlanItem } from "../src/services/assistantQueryPlanning";
import { executeAssistantDeepTurnRetrievalPlan } from "../src/services/assistantDeepTurnRetrievalRuntimeAdapter";
describe("assistant deep turn retrieval runtime adapter", () => {
it("handles skipped, executed and failed plan items with stable call records", async () => {
const executionPlan: AssistantExecutionPlanItem[] = [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "no_route",
should_execute: false,
fragment_text: "clarify period",
no_route_reason: "insufficient_specificity",
clarification_reason: "domain_or_scope_unclear"
},
{
fragment_id: "F2",
requirement_ids: ["R2"],
route: "store_canonical",
should_execute: true,
fragment_text: "show balances",
no_route_reason: null,
clarification_reason: null
},
{
fragment_id: "F3",
requirement_ids: ["R3"],
route: "live_mcp_drilldown",
should_execute: true,
fragment_text: "tail check",
no_route_reason: null,
clarification_reason: null
}
];
const normalizeCalls: Array<{ fragmentId: string; route: string; rawStatus: string | null }> = [];
const output = await executeAssistantDeepTurnRetrievalPlan({
executionPlan,
liveTemporalHint: null,
executeRouteRuntime: async (route) => {
if (route === "live_mcp_drilldown") {
throw new Error("route failed");
}
return {
status: "ok",
result_type: "summary",
items: [],
summary: { route },
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "high",
limitations: [],
errors: []
};
},
mapNoRouteReason: (reason) => (reason === "insufficient_specificity" ? "Needs clarification." : "No-route decision."),
buildSkippedResult: () =>
({
fragment_id: "F1",
route: "no_route",
status: "partial"
}) as any,
normalizeRetrievalResultFn: ((fragmentId: string, _requirementIds: string[], route: string, raw: Record<string, unknown>) => {
normalizeCalls.push({
fragmentId,
route,
rawStatus: typeof raw.status === "string" ? raw.status : null
});
return {
fragment_id: fragmentId,
route,
status: raw.status
} as any;
}) as any
});
expect(output.retrievalCalls).toEqual([
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "no_route",
status: "skipped",
query_text: "clarify period",
reason: "Needs clarification."
},
{
fragment_id: "F2",
requirement_ids: ["R2"],
route: "store_canonical",
status: "executed",
query_text: "show balances",
reason: null
},
{
fragment_id: "F3",
requirement_ids: ["R3"],
route: "live_mcp_drilldown",
status: "failed",
query_text: "tail check",
reason: "route failed"
}
]);
expect(output.retrievalResultsRaw).toHaveLength(2);
expect((output.retrievalResultsRaw[0].raw_result as Record<string, unknown>).status).toBe("ok");
expect((output.retrievalResultsRaw[1].raw_result as Record<string, unknown>).status).toBe("error");
expect((output.retrievalResultsRaw[1].raw_result as Record<string, unknown>).limitations).toEqual(["Route executor failed."]);
expect(output.retrievalResults).toHaveLength(3);
expect(output.retrievalResults[0].status).toBe("partial");
expect(normalizeCalls).toEqual([
{ fragmentId: "F2", route: "store_canonical", rawStatus: "ok" },
{ fragmentId: "F3", route: "live_mcp_drilldown", rawStatus: "error" }
]);
});
it("passes live temporal hint into route runtime execution", async () => {
const executionPlan: AssistantExecutionPlanItem[] = [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
should_execute: true,
fragment_text: "check as of date",
no_route_reason: null,
clarification_reason: null
}
];
let capturedTemporalHint: Record<string, unknown> | null = null;
await executeAssistantDeepTurnRetrievalPlan({
executionPlan,
liveTemporalHint: {
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "analysis_context"
},
executeRouteRuntime: async (_route, _fragmentText, options) => {
capturedTemporalHint = options.temporalHint as unknown as Record<string, unknown>;
return { status: "ok" };
},
mapNoRouteReason: () => "No-route decision.",
buildSkippedResult: (() => ({ status: "partial" })) as any,
normalizeRetrievalResultFn: (() => ({ status: "ok" })) as any
});
expect(capturedTemporalHint).toEqual({
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "analysis_context"
});
});
});
@@ -0,0 +1,105 @@
import { describe, expect, it } from "vitest";
import type { UnifiedRetrievalResult } from "../src/types/assistant";
import { assembleAssistantEvidenceBundle } from "../src/services/assistantEvidenceBundleAssembler";
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: [],
...input
};
}
describe("assistant evidence bundle assembler", () => {
it("builds evidence contract and retrieval status from the same retrieval set", () => {
const assembled = assembleAssistantEvidenceBundle({
retrievalCalls: [{ route: "hybrid_store_plus_live" }, { route: "store_canonical" }],
retrievalResults: [
buildRetrieval({
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07"
},
locator: {
field_path: "amount",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "signal",
confidence: "medium",
limitation: null,
payload: {}
}
]
}),
buildRetrieval({
fragment_id: "F2",
requirement_ids: ["R2"],
route: "store_canonical",
status: "error",
result_type: "list",
errors: ["timeout"]
})
]
});
expect(assembled.evidenceBundleContractV1.retrieval_calls_total).toBe(2);
expect(assembled.evidenceBundleContractV1.retrieval_results_total).toBe(2);
expect(assembled.evidenceBundleContractV1.retrieval_status_breakdown.ok).toBe(1);
expect(assembled.evidenceBundleContractV1.retrieval_status_breakdown.error).toBe(1);
expect(assembled.evidenceBundleContractV1.evidence_total).toBe(1);
expect(assembled.evidenceBundleContractV1.source_refs_total).toBe(1);
expect(assembled.retrievalStatus).toEqual([
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary"
},
{
fragment_id: "F2",
requirement_ids: ["R2"],
route: "store_canonical",
status: "error",
result_type: "list"
}
]);
});
});
@@ -0,0 +1,158 @@
import { describe, expect, it } from "vitest";
import type { RouteHintSummary } from "../src/types/normalizer";
import type { UnifiedRetrievalResult } from "../src/types/assistant";
import { createEmptyInvestigationState } from "../src/services/investigationState";
import {
buildAssistantInvestigationStateSnapshot,
persistAssistantInvestigationStateSnapshot
} from "../src/services/assistantInvestigationStateRuntimeAdapter";
function buildRouteSummary(): RouteHintSummary {
return {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 1,
in_scope_fragments: 1,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [
{
fragment_id: "F1",
domain_relevance: "in_scope",
business_scope: "company_specific_accounting",
candidate_labels: ["anomaly_probe"],
decision_flags: {
has_multi_entity_scope: false,
asks_for_chain_explanation: false,
asks_for_ranking_or_top: false,
asks_for_period_summary: false,
asks_for_rule_check: true,
asks_for_anomaly_scan: true,
asks_for_exact_object_trace: false,
asks_for_evidence: true,
mentions_period_close_context: false
},
route: "store_feature_risk",
reason: "test-route"
}
],
fallback: {
type: "none",
message: null
}
};
}
function buildRetrievalResult(): UnifiedRetrievalResult {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "store_feature_risk",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: []
};
}
describe("assistant investigation state runtime adapter", () => {
it("returns null and skips persist when feature disabled", () => {
const snapshot = buildAssistantInvestigationStateSnapshot({
featureEnabled: false,
previousState: createEmptyInvestigationState("asst-1", "2026-04-10T10:00:00.000Z"),
timestamp: "2026-04-10T10:01:00.000Z",
questionId: "msg-1",
userMessage: "проверь 60.01",
routeSummary: buildRouteSummary(),
requirements: [],
coverageReport: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
retrievalResults: [],
replyType: "factual",
followupApplied: false
});
expect(snapshot).toBeNull();
let persistCalled = false;
const persisted = persistAssistantInvestigationStateSnapshot({
featureEnabled: false,
sessionId: "asst-1",
snapshot: null,
persist: () => {
persistCalled = true;
}
});
expect(persisted).toBe(false);
expect(persistCalled).toBe(false);
});
it("builds snapshot and persists it when feature enabled", () => {
const previous = createEmptyInvestigationState("asst-2", "2026-04-10T10:00:00.000Z");
const snapshot = buildAssistantInvestigationStateSnapshot({
featureEnabled: true,
previousState: previous,
timestamp: "2026-04-10T10:01:00.000Z",
questionId: "msg-2",
userMessage: "проверь счет 60.01 за 2020-07",
routeSummary: buildRouteSummary(),
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "проверить счет 60.01",
subject_tokens: ["account_60.01"],
status: "covered",
route: "store_feature_risk"
}
],
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
retrievalResults: [buildRetrievalResult()],
replyType: "factual",
followupApplied: false
});
expect(snapshot).not.toBeNull();
expect(snapshot?.turn_index).toBe(1);
expect(snapshot?.question_id).toBe("msg-2");
let persistedSessionId: string | null = null;
let persistedQuestionId: string | null = null;
const persisted = persistAssistantInvestigationStateSnapshot({
featureEnabled: true,
sessionId: "asst-2",
snapshot: snapshot,
persist: (sessionId, state) => {
persistedSessionId = sessionId;
persistedQuestionId = state.question_id;
}
});
expect(persisted).toBe(true);
expect(persistedSessionId).toBe("asst-2");
expect(persistedQuestionId).toBe("msg-2");
});
});
@@ -209,6 +209,33 @@ describe("assistant orchestration contract", () => {
expect(decision.livingReason).toBe("address_lane_triggered");
});
it("keeps explicit address-mode unknown-intent data query in address lane", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage:
"Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?",
effectiveAddressUserMessage:
"Показать контрагентов с вероятным несогласием между сальдо и актом сверки. Рекомендовать запросить сверку.",
followupContext: null,
llmPreDecomposeMeta: {
applied: true,
llmCanonicalCandidateDetected: true,
predecomposeContract: {
mode: "address_query",
mode_confidence: "high",
intent: "unknown",
intent_confidence: "low"
}
} as any,
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
});
it("does not force address lane for deep-analysis unknown intent query with date-like token", () => {
const decision = resolveAssistantOrchestrationDecision({
rawUserMessage: "найди какие либо ошибки на 21 мая 2022 года",
@@ -0,0 +1,123 @@
import { describe, expect, it } from "vitest";
import { buildDeepAnalysisProcessedLogDetails } from "../src/services/assistantMessageLogAssembler";
function baseInput() {
return {
sessionId: "asst-1",
messageId: "msg-1",
userMessage: "проверь 60.01",
normalizerOutput: { schema_version: "normalized_query_v2_0_2" },
executionPlan: [{ fragment_id: "F1", route: "hybrid_store_plus_live", should_execute: true }],
resolvedExecutionState: { executable: 1 },
routes: [{ fragment_id: "F1", route: "hybrid_store_plus_live" }],
retrievalCalls: [{ route: "hybrid_store_plus_live" }],
retrievalResultsRaw: [{ status: "ok" }],
retrievalResultsNormalized: [{ status: "ok" }],
requirementsExtracted: [{ requirement_id: "R1" }],
coverageReport: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
},
groundingCheck: {
status: "grounded",
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: ["signal"],
selection_reason_summary: ["ranked"]
},
replyType: "factual",
droppedIntentSegments: [],
questionTypeClass: "factual_lookup",
companyAnchors: { companies: ["demo"] },
runtimeAnalysisContext: {
active: true,
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "eval_analysis_date",
snapshot_mode: "auto" as const
},
businessScopeResolution: {
business_scope_raw: ["company_specific_accounting"],
business_scope_resolved: ["company_specific_accounting"],
company_grounding_applied: true,
scope_resolution_reason: ["resolved"]
},
temporalGuard: {
raw_time_anchor: "2020-07",
raw_time_scope: "month",
resolved_time_anchor: "2020-07",
resolved_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
effective_primary_period: { from: "2020-07-01", to: "2020-07-31", granularity: "day" },
temporal_guard_input: "2020-07",
temporal_alignment_status: "aligned",
temporal_resolution_source: "analysis_context",
temporal_guard_basis: "analysis_context",
temporal_guard_applied: true,
temporal_guard_outcome: "pass"
},
polarityAudit: {
raw_numeric_tokens: ["60.01"],
classified_numeric_tokens: [{ token: "60.01" }],
rejected_as_non_accounts: [],
resolved_account_anchors: ["60.01"]
},
claimAnchorAudit: {
settlement_role: "supplier",
settlement_role_resolution_reason: ["account_60_detected"],
polarity_resolution_status: "resolved"
},
targetedEvidenceAudit: { targeted_evidence_hit_rate: 1 },
evidenceAdmissibilityGateAudit: { admissible_evidence_count: 1 },
rbpLiveRouteAudit: null,
faLiveRouteAudit: null,
groundedAnswerEligibilityGuard: { eligibility_time_basis: "analysis_context", eligible: true },
followupStateUsage: null,
compositionDebug: {
problem_centric_answer_applied: true,
problem_units_used_count: 1,
problem_answer_mode: "stage3_lifecycle_aware_v1",
problem_unit_ids_used: ["pu-1"],
fallback_type: "none"
},
outcomeClassV1: "FULLY_ANSWERED",
assistantOrchestrationContractsV1: { query_frame: {}, execution_plan: {}, evidence_bundle: {}, coverage: {} },
answerStructureV11: { schema_version: "answer_structure_v1_1" },
investigationStateSnapshot: { status: "active" },
assistantReply: "ok",
traceId: "trace-1"
};
}
describe("assistant message log assembler", () => {
it("builds deep analysis log details and resolves full coverage status", () => {
const details = buildDeepAnalysisProcessedLogDetails(baseInput());
expect(details.session_id).toBe("asst-1");
expect(details.coverage_status).toBe("full");
expect(details.analysis_context).toMatchObject({
as_of_date: "2020-07-31"
});
expect(details.problem_unit_ids_used).toEqual(["pu-1"]);
expect(details.reply_type).toBe("factual");
});
it("marks partial coverage and omits optional sections when empty", () => {
const input = baseInput();
input.coverageReport.requirements_covered = 0;
input.coverageReport.requirements_uncovered = ["R1"];
input.runtimeAnalysisContext.active = false;
input.followupStateUsage = null;
input.compositionDebug.problem_unit_ids_used = [];
const details = buildDeepAnalysisProcessedLogDetails(input);
expect(details.coverage_status).toBe("partial_or_limited");
expect(details.analysis_context).toBeNull();
expect(Object.prototype.hasOwnProperty.call(details, "followup_state_usage")).toBe(false);
expect(Object.prototype.hasOwnProperty.call(details, "problem_unit_ids_used")).toBe(false);
});
});
@@ -0,0 +1,304 @@
import { describe, expect, it } from "vitest";
import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../src/types/assistant";
import type { RouteHintSummary } from "../src/types/normalizer";
import {
buildAssistantCoverageContractV1,
buildAssistantEvidenceBundleContractV1,
buildAssistantExecutionPlanContractV1,
buildAssistantQueryFrameContractV1,
classifyAssistantOutcomeClassV1
} from "../src/services/assistantOrchestrationContracts";
function buildCoverage(input?: Partial<RequirementCoverageReport>): RequirementCoverageReport {
return {
requirements_total: 1,
requirements_covered: 0,
requirements_uncovered: ["R1"],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: [],
...input
};
}
function buildGrounding(input?: Partial<AnswerGroundingCheck>): AnswerGroundingCheck {
return {
status: "no_grounded_answer",
route_subject_match: true,
missing_requirements: ["R1"],
reasons: [],
why_included_summary: [],
selection_reason_summary: [],
...input
};
}
function buildRetrieval(input?: Partial<UnifiedRetrievalResult>): UnifiedRetrievalResult {
return {
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
status: "ok",
result_type: "summary",
items: [],
summary: {},
evidence: [],
why_included: [],
selection_reason: [],
risk_factors: [],
business_interpretation: [],
confidence: "medium",
limitations: [],
errors: [],
...input
};
}
function buildRouteSummary(): RouteHintSummary {
return {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 2,
in_scope_fragments: 2,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [],
fallback: {
type: "none",
message: null
}
};
}
describe("assistant orchestration contracts v1", () => {
it("builds query frame and execution plan contracts with normalized analysis context", () => {
const queryFrame = buildAssistantQueryFrameContractV1({
userMessage: "Покажи хвосты по счету 60",
normalizedQuestion: "Покажи хвосты по счету 60",
normalized: {
schema_version: "normalized_query_v2_0_2",
user_message_raw: "Покажи хвосты по счету 60",
message_in_scope: true,
scope_confidence: "high",
contains_multiple_tasks: false,
fragments: [{ fragment_id: "F1" }, { fragment_id: "F2" }],
discarded_fragments: [],
global_notes: {
needs_clarification: false,
clarification_reason: null
}
} as any,
routeSummary: buildRouteSummary(),
droppedIntentSegments: ["лишний сегмент"],
analysisContext: {
as_of_date: "2020-07-31",
source: "eval_analysis_date",
snapshot_mode: "unexpected_mode"
}
});
const executionPlan = buildAssistantExecutionPlanContractV1({
executionPlan: [
{
fragment_id: "F1",
requirement_ids: ["R1"],
route: "hybrid_store_plus_live",
should_execute: true,
no_route_reason: null,
clarification_reason: null
},
{
fragment_id: "F2",
requirement_ids: ["R2"],
route: "no_route",
should_execute: false,
no_route_reason: "insufficient_specificity",
clarification_reason: "need_period"
}
],
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "req1",
subject_tokens: [],
status: "covered",
route: "hybrid_store_plus_live"
},
{
requirement_id: "R2",
source_fragment_id: "F2",
requirement_text: "req2",
subject_tokens: [],
status: "clarification_needed",
route: null
}
]
});
expect(queryFrame.schema_version).toBe("assistant_query_frame_v1");
expect(queryFrame.route_summary_mode).toBe("deterministic_v2");
expect(queryFrame.fragments_total).toBe(2);
expect(queryFrame.analysis_context?.as_of_date).toBe("2020-07-31");
expect(queryFrame.analysis_context?.snapshot_mode).toBe("auto");
expect(executionPlan.schema_version).toBe("assistant_execution_plan_v1");
expect(executionPlan.steps).toHaveLength(2);
expect(executionPlan.requirements_total).toBe(2);
});
it("classifies fully answered and misrouted outcomes", () => {
const fullyAnswered = classifyAssistantOutcomeClassV1({
replyType: "factual_with_explanation",
coverageReport: buildCoverage({
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
}),
grounding: buildGrounding({
status: "grounded",
missing_requirements: []
}),
retrievalResults: [buildRetrieval({ status: "ok" })]
});
const misrouted = classifyAssistantOutcomeClassV1({
replyType: "route_mismatch_blocked",
coverageReport: buildCoverage(),
grounding: buildGrounding({
status: "route_mismatch_blocked"
}),
retrievalResults: [buildRetrieval({ status: "partial" })]
});
expect(fullyAnswered).toBe("FULLY_ANSWERED");
expect(misrouted).toBe("MISROUTED");
});
it("classifies tooling and entity-binding failures", () => {
const toolingBlocked = classifyAssistantOutcomeClassV1({
replyType: "factual",
coverageReport: buildCoverage(),
grounding: buildGrounding(),
retrievalResults: [buildRetrieval({ status: "error" }), buildRetrieval({ status: "error" })]
});
const entityBindingFailure = classifyAssistantOutcomeClassV1({
replyType: "no_grounded_answer",
coverageReport: buildCoverage({
requirements_total: 1,
requirements_covered: 0,
requirements_uncovered: ["R1"]
}),
grounding: buildGrounding({
status: "no_grounded_answer",
route_subject_match: true,
missing_requirements: ["R1"]
}),
retrievalResults: [buildRetrieval({ status: "empty" })]
});
expect(toolingBlocked).toBe("BLOCKED_BY_TOOLING");
expect(entityBindingFailure).toBe("FAILED_TO_BIND_ENTITIES");
});
it("builds evidence bundle and coverage contracts", () => {
const evidenceBundle = buildAssistantEvidenceBundleContractV1({
retrievalCalls: [{ id: 1 }, { id: 2 }, { id: 3 }],
retrievalResults: [
buildRetrieval({
status: "ok",
evidence: [
{
evidence_id: "ev-1",
claim_ref: "requirement:R1",
source_type: "retrieval_item",
source_ref: {
schema_version: "evidence_source_ref_v1",
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07",
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
},
pointer: {
fragment_id: "F1",
route: "hybrid_store_plus_live",
source: {
namespace: "snapshot_2020",
entity: "document",
id: "doc-1",
period: "2020-07"
},
locator: {
field_path: "amount",
item_index: 0
}
},
evidence_kind: "mechanism_link",
mechanism_note: "signal",
confidence: "medium",
limitation: null,
payload: {}
}
],
limitations: ["needs_extra_period"]
}),
buildRetrieval({
status: "partial",
evidence: [],
errors: ["timeout"]
}),
buildRetrieval({
status: "error",
evidence: [],
errors: ["mcp_unavailable"]
})
]
});
const outcomeClass = classifyAssistantOutcomeClassV1({
replyType: "partial_coverage",
coverageReport: buildCoverage({
requirements_total: 2,
requirements_covered: 1,
requirements_uncovered: ["R2"]
}),
grounding: buildGrounding({
status: "partial",
route_subject_match: true,
missing_requirements: ["R2"]
}),
retrievalResults: [buildRetrieval({ status: "ok" }), buildRetrieval({ status: "partial" })]
});
const coverageContract = buildAssistantCoverageContractV1({
coverageReport: buildCoverage({
requirements_total: 2,
requirements_covered: 1,
requirements_uncovered: ["R2"]
}),
grounding: buildGrounding({
status: "partial",
missing_requirements: ["R2"]
}),
outcomeClass
});
expect(evidenceBundle.retrieval_calls_total).toBe(3);
expect(evidenceBundle.retrieval_results_total).toBe(3);
expect(evidenceBundle.retrieval_status_breakdown.ok).toBe(1);
expect(evidenceBundle.retrieval_status_breakdown.partial).toBe(1);
expect(evidenceBundle.retrieval_status_breakdown.error).toBe(1);
expect(evidenceBundle.evidence_total).toBe(1);
expect(evidenceBundle.source_refs_total).toBe(1);
expect(evidenceBundle.error_total).toBe(2);
expect(coverageContract.outcome_class).toBe("PARTIALLY_ANSWERED");
});
});
@@ -0,0 +1,104 @@
import { describe, expect, it, vi } from "vitest";
import { runAssistantCoverageGroundingPipeline } from "../src/services/assistantOrchestrationRuntimeAdapter";
describe("assistant orchestration runtime adapter", () => {
it("runs requirement -> coverage -> grounding pipeline in order", () => {
const requirementExtraction = {
requirements: [
{
requirement_id: "R1",
source_fragment_id: "F1",
requirement_text: "req",
subject_tokens: ["account_60"],
status: "covered" as const,
route: "hybrid_store_plus_live"
}
],
byFragment: new Map<string, string[]>([["F1", ["R1"]]])
};
const coverageEvaluation = {
requirements: requirementExtraction.requirements,
coverage: {
requirements_total: 1,
requirements_covered: 1,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
}
};
const groundingCheck = {
status: "grounded" as const,
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: ["why"],
selection_reason_summary: ["selection"]
};
const extractRequirements = vi.fn(() => requirementExtraction);
const evaluateCoverage = vi.fn(() => coverageEvaluation);
const checkGrounding = vi.fn(() => groundingCheck);
const output = runAssistantCoverageGroundingPipeline({
routeSummary: null,
normalized: null,
userMessage: "test",
retrievalResults: [],
extractRequirements,
evaluateCoverage,
checkGrounding
});
expect(extractRequirements).toHaveBeenCalledTimes(1);
expect(evaluateCoverage).toHaveBeenCalledTimes(1);
expect(checkGrounding).toHaveBeenCalledTimes(1);
expect(output.requirementExtraction).toBe(requirementExtraction);
expect(output.coverageEvaluation).toBe(coverageEvaluation);
expect(output.groundingCheckBase).toBe(groundingCheck);
});
it("reuses precomputed requirement extraction when provided", () => {
const precomputed = {
requirements: [],
byFragment: new Map<string, string[]>()
};
const extractRequirements = vi.fn(() => {
throw new Error("extractRequirements should not be called");
});
const evaluateCoverage = vi.fn(() => ({
requirements: [],
coverage: {
requirements_total: 0,
requirements_covered: 0,
requirements_uncovered: [],
requirements_partially_covered: [],
clarification_needed_for: [],
out_of_scope_requirements: []
}
}));
const checkGrounding = vi.fn(() => ({
status: "no_grounded_answer" as const,
route_subject_match: true,
missing_requirements: [],
reasons: [],
why_included_summary: [],
selection_reason_summary: []
}));
const output = runAssistantCoverageGroundingPipeline({
routeSummary: null,
normalized: null,
userMessage: "test",
retrievalResults: [],
requirementExtraction: precomputed,
extractRequirements,
evaluateCoverage,
checkGrounding
});
expect(extractRequirements).not.toHaveBeenCalled();
expect(evaluateCoverage).toHaveBeenCalledWith(precomputed.requirements, []);
expect(output.requirementExtraction).toBe(precomputed);
});
});
@@ -0,0 +1,107 @@
import { describe, expect, it } from "vitest";
import {
buildDebugRoutesFromRoute,
buildExecutionPlanFromRoute,
buildFragmentTextById
} from "../src/services/assistantQueryPlanning";
describe("assistant query planning module", () => {
it("builds fragment text map with account hints enrichment", () => {
const map = buildFragmentTextById([
{
fragment_id: "F1",
raw_fragment_text: "проверить хвосты",
normalized_fragment_text: "",
account_hints: ["60.01", "60.02"]
},
{
fragment_id: "F2",
raw_fragment_text: "проверить 60.01 по поставщику",
normalized_fragment_text: "",
account_hints: ["60.01"]
}
]);
expect(map.get("F1")).toBe("проверить хвосты, по счету 60.01, 60.02");
expect(map.get("F2")).toBe("проверить 60.01 по поставщику");
});
it("builds deterministic execution plan from route summary", () => {
const executionPlan = buildExecutionPlanFromRoute({
routeSummary: {
mode: "deterministic_v2",
message_in_scope: true,
scope_confidence: "high",
planner: {
total_fragments: 2,
in_scope_fragments: 2,
out_of_scope_fragments: 0,
discarded_fragments: 0,
contains_multiple_tasks: false
},
decisions: [
{
fragment_id: "F1",
route: "no_route",
no_route_reason: "insufficient_specificity",
clarification_reason: "missing anchor",
reason: "needs clarification"
},
{
fragment_id: "F2",
route: "hybrid_store_plus_live",
reason: "route selected"
}
],
fallback: {
type: "none",
message: null
}
} as any,
userMessage: "base question",
fragmentTextById: new Map([
["F1", "уточни период"],
["F2", "проверь по 60.01"]
]),
requirementByFragment: new Map([
["F1", ["R1"]],
["F2", ["R2"]]
])
});
expect(executionPlan).toHaveLength(2);
expect(executionPlan[0]).toMatchObject({
fragment_id: "F1",
route: "no_route",
should_execute: false,
no_route_reason: "insufficient_specificity",
clarification_reason: "missing anchor"
});
expect(executionPlan[1]).toMatchObject({
fragment_id: "F2",
route: "hybrid_store_plus_live",
should_execute: true,
no_route_reason: null
});
});
it("builds legacy debug routes via resolver", () => {
const routes = buildDebugRoutesFromRoute({
routeSummary: {
mode: "legacy_v1",
intent_class: "partner_reconciliation",
route_hint: "store_canonical",
confidence: "medium"
} as any,
resolveLegacyRouteReason: (route) => `legacy:${route}`
});
expect(routes).toHaveLength(1);
expect(routes[0]).toMatchObject({
fragment_id: "F1",
route: "store_canonical",
reason: "legacy:store_canonical",
confidence: "medium"
});
});
});
@@ -0,0 +1,93 @@
import { describe, expect, it } from "vitest";
import type { AssistantConversationItem, AssistantSessionState } from "../src/types/assistant";
import { commitAssistantTurnAndLog } from "../src/services/assistantTurnCommitRuntimeAdapter";
function buildAssistantItem(): AssistantConversationItem {
return {
message_id: "msg-1",
session_id: "asst-1",
role: "assistant",
text: "ok",
reply_type: "factual",
created_at: "2026-04-10T10:01:00.000Z",
trace_id: "trace-1",
debug: null
};
}
describe("assistant turn commit runtime adapter", () => {
it("appends item, persists existing session, clones conversation and logs event", () => {
const assistantItem = buildAssistantItem();
const storedSession: AssistantSessionState = {
session_id: "asst-1",
updated_at: "2026-04-10T10:01:00.000Z",
items: [assistantItem],
investigation_state: null
};
const calls = {
append: 0,
persist: 0,
log: 0
};
let loggedPayload: Record<string, unknown> | null = null;
const result = commitAssistantTurnAndLog({
sessionId: "asst-1",
assistantItem,
eventType: "assistant_message",
logDetails: { some: "details" },
appendItem: () => {
calls.append += 1;
},
getSession: () => storedSession,
persistSession: () => {
calls.persist += 1;
},
cloneConversation: (items) => items.map((item) => ({ ...item, debug: item.debug ? { ...item.debug } : null })),
logEvent: (payload) => {
calls.log += 1;
loggedPayload = payload as unknown as Record<string, unknown>;
},
nowIso: () => "2026-04-10T10:02:00.000Z"
});
expect(calls.append).toBe(1);
expect(calls.persist).toBe(1);
expect(calls.log).toBe(1);
expect(result.currentSession?.session_id).toBe("asst-1");
expect(result.conversation).toEqual([assistantItem]);
expect(result.conversation).not.toBe(storedSession.items);
expect(loggedPayload?.["sessionId"]).toBe("asst-1");
expect(loggedPayload?.["eventType"]).toBe("assistant_message");
expect(loggedPayload?.["message"]).toBe("assistant_message_processed");
expect(loggedPayload?.["timestamp"]).toBe("2026-04-10T10:02:00.000Z");
});
it("skips persist when session is missing and still logs with empty conversation", () => {
const assistantItem = buildAssistantItem();
let persistCalled = false;
let logCalled = false;
const result = commitAssistantTurnAndLog({
sessionId: "asst-missing",
assistantItem,
eventType: "assistant_message",
logDetails: { x: 1 },
appendItem: () => {},
getSession: () => null,
persistSession: () => {
persistCalled = true;
},
cloneConversation: (items) => items.map((item) => ({ ...item })),
logEvent: () => {
logCalled = true;
}
});
expect(persistCalled).toBe(false);
expect(logCalled).toBe(true);
expect(result.currentSession).toBeNull();
expect(result.conversation).toEqual([]);
});
});
@@ -11,7 +11,8 @@ const FLAG_KEYS = [
"FEATURE_ASSISTANT_ANSWER_POLICY_V11",
"FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1",
"FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1",
"FEATURE_ASSISTANT_PROBLEM_UNITS_V1"
"FEATURE_ASSISTANT_PROBLEM_UNITS_V1",
"FEATURE_ASSISTANT_ADDRESS_QUERY_V1"
] as const;
const ORIGINAL_FLAGS: Record<string, string | undefined> = Object.fromEntries(
@@ -623,6 +624,7 @@ describe("wave10 settlement corrective regression", () => {
process.env.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = "1";
process.env.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = "1";
process.env.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = "1";
process.env.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = "0";
vi.resetModules();
const { createApp } = await import("../src/server");
@@ -31,4 +31,9 @@ describe("questionTypeResolver", () => {
expect(resolveQuestionType("Почему не сходится 62.01/62.02?"))
.toBe("why_breaks");
});
it("keeps generic non-why questions as unknown", () => {
expect(resolveQuestionType("Какие реализации стоит проверить заранее, чтобы не испортить отчетность за месяц?"))
.toBe("unknown");
});
});
File diff suppressed because one or more lines are too long
@@ -1,4 +1,84 @@
[
{
"generation_id": "gen-mnsolawk-vugqyoc",
"created_at": "2026-04-10T09:06:01.461Z",
"mode": "qwen_seed",
"count": 20,
"domain": null,
"questions": [
"какие контрагенты пока вообще не платят за текущий месяц и это уже тревожный знак для нас?",
"где у нас есть отгрузки без закрытия по ним счетов, и это выглядит как серьезная проблема?",
"какие покупатели пока не оплатили свои товары или услуги, хотя сроки давно прошли?",
"у кого из контрагентов есть авансы, но никаких документов для их закрытия нет уже давно?",
"по каким поставщикам у нас сальдо явно расходится с тем, что они сами указывают в своих актах сверок?",
"какие реализации на конец периода выглядят так, будто будут портить картину по выручке из-за незакрытых счетов или документов?",
"где у нас есть оплаты без закрытия взаиморасчетов, и это уже требует ручной проверки?",
"какие контрагенты принесли основную часть нашей выручки за отчетный период?",
"у кого из покупателей есть открытые задолженности перед нами на конец месяца?",
"по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?",
"какие реализации выглядят так, что их нужно проверить заранее, чтобы они не портили отчетность за месяц?",
"где у нас есть отгрузки без документов для их закрытия и это уже требует внимания?",
"кто из контрагентов пока не оплатил свои товары или услуги хотя сроки давно прошли?",
"у кого из поставщиков сальдо явно расходится с тем, что они сами указывают в своих актах сверок?",
"где у нас есть закрытие счетов без подтверждающих документов и это уже требует ручной проверки?",
"какие контрагенты принесли основную часть наших доходов за отчетный период?",
"по каким покупателям у нас есть открытые задолженности на конец месяца?",
"у кого из поставщиков есть авансы, но никаких документов для их закрытия нет уже давно?",
"где у нас есть реализации, которые выглядят так, будто они зависли и будут портить картину по выручке, если их не проверить заранее?",
"какие контрагенты пока вообще не платят за текущий месяц и это тревожный знак для нас?"
],
"generated_by": "manual_reviewer",
"saved_case_set_file": "assistant_autogen_qwen_seed_20260410090601_gen-mnsolawk-vugqyoc.json",
"context": {
"llm_provider": "local",
"model": "Qwen2.5 14B Instruct 1M",
"assistant_prompt_version": "address_query_runtime_v1",
"decomposition_prompt_version": "normalizer_v2_0_2",
"prompt_fingerprint": "Ты semantic-normalizer для бухгалтерского ассистента NDC.\nТвоя роль: только нормализация запроса пользователя в строгий JSON-контракт.\n\nЖесткие правила:\n1) Не давай бухгалтерский ответ по сути вопроса.\n2) Возвращай только JSON без markdown и пояснений.\n3) JSON обязан соответствовать переданной schema normalized_query_v1.\n4) Если период не указан, не выдумывай его; отмечай ambiguity.\n5) Для цепочек документов/проводок/оплат поднимай causal и cross-entity признаки.\n6) Для точечного object trace (номер/строка/ref) поднимай needs_exact_object_trace=true.\n7) Используй терминологию NDC.\nYou are semantic-normalizer for accounting assistant NDC.\nReturn strict JSON only, no markdown, no comments.\n\nTarget schema: normalized_query_v2_0_2.\n\nCore behavior (v2.0.2):\n1. Decompose message into semantic fragments.\n2. Classify fragment domain relevance and business scope.\n3. Fill route-critical flags and ",
"autogen_personality_id": "general",
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов \\ нсд \\ счета \\ общий ваывод по компании - контрагенты заказчиким скока денег кто принес и тп"
}
},
{
"generation_id": "gen-mnsmzzcr-ffcz48q",
"created_at": "2026-04-10T08:21:27.099Z",
"mode": "qwen_seed",
"count": 20,
"domain": null,
"questions": [
"какие контрагенты больше всего задерживают платежи за последний месяц и это уже выходит за рамки обычной просрочки?",
"есть ли поставщики у которых на конец периода есть непроведенные документы, которые мешают закрыть взаиморасчеты?",
"какие покупатели долгое время не отвечают на наши претензии про несоответствие сальдо в сверке?",
"покажи контрагентов где есть оплаты, но документов для их закрытия нет и это уже требует ручного вмешательства?",
"у кого из поставщиков долгое время висят авансы без каких либо движений по ним?",
"какие реализации на конец месяца выглядят так, будто они зависли и могут повлиять на финальные показатели отчетности?",
"какие покупатели больше всего задерживают оплату за товары/услуги?",
"покажи контрагентов где документы есть но платежей для закрытия взаиморасчётов не видно и это уже требует проверки руками?",
"какие поставщики на конец месяца ещё не закрыли все накладные за текущий период?",
"какие контрагенты имеют большое сальдо, которое скорее всего не будет совпадать в акте сверки если его запросить прямо сейчас?",
"есть ли зависшие авансы, которые уже давно надо было либо закрыть, либо хотя бы перепроверить руками?",
"какие отгрузки выглядят так будто проблема не только в том что клиент не оплатил но и в том что сама связка документов собрана криво?",
"у кого из контрагентов есть накладные которые долго не закрываются по причине того что нет соответствующих платежей?",
"какие покупатели на конец периода уже давно не отвечают на наши претензии про несоответствие сальдо в сверке?",
"какие контрагенты имеют большое сальдо которое скорее всего не будет совпадать в акте сверки если его запросить прямо сейчас?",
"у кого из поставщиков долгое время висят авансы без каких либо движений по ним и это уже требует проверки руками?",
"есть ли зависшие накладные которые могут повлиять на финальные показатели отчетности если их не закрыть заранее?",
"какие поставщики на конец месяца ещё не закрыли все накладные за текущий период и это уже требует проверки руками?",
"какие покупатели долгое время не отвечают на наши претензии про несоответствие сальдо в сверке и это уже требует ручного вмешательства?",
"йо По каким поставщикам у нас на конец месяца остались хвосты, которые уже не похожи на обычную задержку документов, а выглядят как реальная проблема в цепочке?"
],
"generated_by": "manual_reviewer",
"saved_case_set_file": "assistant_autogen_qwen_seed_20260410082127_gen-mnsmzzcr-ffcz48q.json",
"context": {
"llm_provider": "local",
"model": "Qwen2.5 14B Instruct 1M",
"assistant_prompt_version": "address_query_runtime_v1",
"decomposition_prompt_version": "normalizer_v2_0_2",
"prompt_fingerprint": "Ты semantic-normalizer для бухгалтерского ассистента NDC.\nТвоя роль: только нормализация запроса пользователя в строгий JSON-контракт.\n\nЖесткие правила:\n1) Не давай бухгалтерский ответ по сути вопроса.\n2) Возвращай только JSON без markdown и пояснений.\n3) JSON обязан соответствовать переданной schema normalized_query_v1.\n4) Если период не указан, не выдумывай его; отмечай ambiguity.\n5) Для цепочек документов/проводок/оплат поднимай causal и cross-entity признаки.\n6) Для точечного object trace (номер/строка/ref) поднимай needs_exact_object_trace=true.\n7) Используй терминологию NDC.\nYou are semantic-normalizer for accounting assistant NDC.\nReturn strict JSON only, no markdown, no comments.\n\nTarget schema: normalized_query_v2_0_2.\n\nCore behavior (v2.0.2):\n1. Decompose message into semantic fragments.\n2. Classify fragment domain relevance and business scope.\n3. Fill route-critical flags and ",
"autogen_personality_id": "general",
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов \\ нсд \\ счета \\ общий ваывод по компании - контрагенты заказчиким скока денег кто принес и тп"
}
},
{
"generation_id": "gen-mnrvs132-1dewq5r",
"created_at": "2026-04-09T19:39:26.463Z",
@@ -0,0 +1,334 @@
{
"suite_id": "assistant_autogen_gen-mnsmzzcr-ffcz48q",
"suite_version": "0.1.0",
"schema_version": "assistant_autogen_suite_v0_1",
"generated_at": "2026-04-10T08:21:27.099Z",
"generation_id": "gen-mnsmzzcr-ffcz48q",
"mode": "qwen_seed",
"domain": null,
"scenario_count": 20,
"case_ids": [
"AUTO-001",
"AUTO-002",
"AUTO-003",
"AUTO-004",
"AUTO-005",
"AUTO-006",
"AUTO-007",
"AUTO-008",
"AUTO-009",
"AUTO-010",
"AUTO-011",
"AUTO-012",
"AUTO-013",
"AUTO-014",
"AUTO-015",
"AUTO-016",
"AUTO-017",
"AUTO-018",
"AUTO-019",
"AUTO-020"
],
"cases": [
{
"case_id": "AUTO-001",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие контрагенты больше всего задерживают платежи за последний месяц и это уже выходит за рамки обычной просрочки?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-002",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "есть ли поставщики у которых на конец периода есть непроведенные документы, которые мешают закрыть взаиморасчеты?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-003",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие покупатели долгое время не отвечают на наши претензии про несоответствие сальдо в сверке?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-004",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "покажи контрагентов где есть оплаты, но документов для их закрытия нет и это уже требует ручного вмешательства?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-005",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "у кого из поставщиков долгое время висят авансы без каких либо движений по ним?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-006",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие реализации на конец месяца выглядят так, будто они зависли и могут повлиять на финальные показатели отчетности?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-007",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие покупатели больше всего задерживают оплату за товары/услуги?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-008",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "покажи контрагентов где документы есть но платежей для закрытия взаиморасчётов не видно и это уже требует проверки руками?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-009",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие поставщики на конец месяца ещё не закрыли все накладные за текущий период?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-010",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие контрагенты имеют большое сальдо, которое скорее всего не будет совпадать в акте сверки если его запросить прямо сейчас?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-011",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "есть ли зависшие авансы, которые уже давно надо было либо закрыть, либо хотя бы перепроверить руками?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-012",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие отгрузки выглядят так будто проблема не только в том что клиент не оплатил но и в том что сама связка документов собрана криво?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-013",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "у кого из контрагентов есть накладные которые долго не закрываются по причине того что нет соответствующих платежей?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-014",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие покупатели на конец периода уже давно не отвечают на наши претензии про несоответствие сальдо в сверке?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-015",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие контрагенты имеют большое сальдо которое скорее всего не будет совпадать в акте сверки если его запросить прямо сейчас?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-016",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "у кого из поставщиков долгое время висят авансы без каких либо движений по ним и это уже требует проверки руками?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-017",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "есть ли зависшие накладные которые могут повлиять на финальные показатели отчетности если их не закрыть заранее?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-018",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие поставщики на конец месяца ещё не закрыли все накладные за текущий период и это уже требует проверки руками?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-019",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие покупатели долгое время не отвечают на наши претензии про несоответствие сальдо в сверке и это уже требует ручного вмешательства?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-020",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "йо По каким поставщикам у нас на конец месяца остались хвосты, которые уже не похожи на обычную задержку документов, а выглядят как реальная проблема в цепочке?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
}
]
}
@@ -0,0 +1,334 @@
{
"suite_id": "assistant_autogen_gen-mnsolawk-vugqyoc",
"suite_version": "0.1.0",
"schema_version": "assistant_autogen_suite_v0_1",
"generated_at": "2026-04-10T09:06:01.460Z",
"generation_id": "gen-mnsolawk-vugqyoc",
"mode": "qwen_seed",
"domain": null,
"scenario_count": 20,
"case_ids": [
"AUTO-001",
"AUTO-002",
"AUTO-003",
"AUTO-004",
"AUTO-005",
"AUTO-006",
"AUTO-007",
"AUTO-008",
"AUTO-009",
"AUTO-010",
"AUTO-011",
"AUTO-012",
"AUTO-013",
"AUTO-014",
"AUTO-015",
"AUTO-016",
"AUTO-017",
"AUTO-018",
"AUTO-019",
"AUTO-020"
],
"cases": [
{
"case_id": "AUTO-001",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие контрагенты пока вообще не платят за текущий месяц и это уже тревожный знак для нас?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-002",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "где у нас есть отгрузки без закрытия по ним счетов, и это выглядит как серьезная проблема?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-003",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие покупатели пока не оплатили свои товары или услуги, хотя сроки давно прошли?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-004",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "у кого из контрагентов есть авансы, но никаких документов для их закрытия нет уже давно?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-005",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "по каким поставщикам у нас сальдо явно расходится с тем, что они сами указывают в своих актах сверок?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-006",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие реализации на конец периода выглядят так, будто будут портить картину по выручке из-за незакрытых счетов или документов?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-007",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "где у нас есть оплаты без закрытия взаиморасчетов, и это уже требует ручной проверки?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-008",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие контрагенты принесли основную часть нашей выручки за отчетный период?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-009",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "у кого из покупателей есть открытые задолженности перед нами на конец месяца?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-010",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-011",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие реализации выглядят так, что их нужно проверить заранее, чтобы они не портили отчетность за месяц?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-012",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "где у нас есть отгрузки без документов для их закрытия и это уже требует внимания?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-013",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "кто из контрагентов пока не оплатил свои товары или услуги хотя сроки давно прошли?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-014",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "у кого из поставщиков сальдо явно расходится с тем, что они сами указывают в своих актах сверок?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-015",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "где у нас есть закрытие счетов без подтверждающих документов и это уже требует ручной проверки?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-016",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие контрагенты принесли основную часть наших доходов за отчетный период?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-017",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "по каким покупателям у нас есть открытые задолженности на конец месяца?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-018",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "у кого из поставщиков есть авансы, но никаких документов для их закрытия нет уже давно?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-019",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "где у нас есть реализации, которые выглядят так, будто они зависли и будут портить картину по выручке, если их не проверить заранее?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-020",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие контрагенты пока вообще не платят за текущий месяц и это тревожный знак для нас?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
}
]
}

Some files were not shown because too many files have changed in this diff Show More