ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.12.23: декомпозиция deep-turn пайплайна ассистента в runtime-адаптеры
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user