Этап 4 / Волна 18 закрытие блокеров по времени, доменной полярности и допуску доказательной базы
This commit is contained in:
@@ -0,0 +1,887 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.resolveTemporalGuard = resolveTemporalGuard;
|
||||
exports.applyTemporalHintToExecutionPlan = applyTemporalHintToExecutionPlan;
|
||||
exports.resolveDomainPolarityGuard = resolveDomainPolarityGuard;
|
||||
exports.applyPolarityHintToExecutionPlan = applyPolarityHintToExecutionPlan;
|
||||
exports.applyDomainPolarityGuardToRetrievalResults = applyDomainPolarityGuardToRetrievalResults;
|
||||
exports.applyEvidenceAdmissibilityGate = applyEvidenceAdmissibilityGate;
|
||||
exports.evaluateGroundedAnswerEligibility = evaluateGroundedAnswerEligibility;
|
||||
exports.applyEligibilityToGroundingCheck = applyEligibilityToGroundingCheck;
|
||||
const JULY_YEAR = "2020";
|
||||
const JULY_MONTH = "07";
|
||||
const JULY_WINDOW = {
|
||||
from: "2020-07-01",
|
||||
to: "2020-07-31",
|
||||
granularity: "month"
|
||||
};
|
||||
const RUS_MONTH_TO_NUMBER = {
|
||||
января: "01",
|
||||
январь: "01",
|
||||
февраля: "02",
|
||||
февраль: "02",
|
||||
марта: "03",
|
||||
март: "03",
|
||||
апреля: "04",
|
||||
апрель: "04",
|
||||
мая: "05",
|
||||
май: "05",
|
||||
июня: "06",
|
||||
июнь: "06",
|
||||
июля: "07",
|
||||
июль: "07",
|
||||
августа: "08",
|
||||
август: "08",
|
||||
сентября: "09",
|
||||
сентябрь: "09",
|
||||
октября: "10",
|
||||
октябрь: "10",
|
||||
ноября: "11",
|
||||
ноябрь: "11",
|
||||
декабря: "12",
|
||||
декабрь: "12"
|
||||
};
|
||||
function uniqueStrings(values) {
|
||||
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean)));
|
||||
}
|
||||
function toObject(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function toObjectArray(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
return value.filter((item) => Boolean(item) && typeof item === "object");
|
||||
}
|
||||
function accountPrefix(value) {
|
||||
const token = String(value ?? "").trim();
|
||||
const match = token.match(/^(\d{2})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
function extractAccountsFromText(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const accounts = new Set();
|
||||
const contextualPattern = /(?:\b(?:сч(?:е|ё)т(?:а|у|ом|ов)?|account|schet)\b\s*(?:№|#|:)?\s*)(\d{2}(?:\.\d{2})?)/giu;
|
||||
let contextualMatch = null;
|
||||
while ((contextualMatch = contextualPattern.exec(lower)) !== null) {
|
||||
const token = String(contextualMatch[1] ?? "").trim();
|
||||
if (token) {
|
||||
accounts.add(token);
|
||||
}
|
||||
}
|
||||
const pairPattern = /\b(\d{2}\.\d{2})\s*\/\s*(\d{2}\.\d{2})\b/g;
|
||||
let pairMatch = null;
|
||||
while ((pairMatch = pairPattern.exec(lower)) !== null) {
|
||||
const left = String(pairMatch[1] ?? "").trim();
|
||||
const right = String(pairMatch[2] ?? "").trim();
|
||||
if (left)
|
||||
accounts.add(left);
|
||||
if (right)
|
||||
accounts.add(right);
|
||||
}
|
||||
return Array.from(accounts);
|
||||
}
|
||||
function extractAccountsFromUnknown(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return uniqueStrings(value.flatMap((item) => extractAccountsFromUnknown(item)));
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return uniqueStrings(Object.values(value).flatMap((item) => extractAccountsFromUnknown(item)));
|
||||
}
|
||||
if (typeof value !== "string" && typeof value !== "number") {
|
||||
return [];
|
||||
}
|
||||
const text = String(value);
|
||||
const matches = text.match(/\b\d{2}(?:\.\d{2})?\b/g) ?? [];
|
||||
return uniqueStrings(matches);
|
||||
}
|
||||
function normalizeTwoDigits(value) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
function parseYear(raw) {
|
||||
const token = String(raw ?? "").trim();
|
||||
if (token.length === 2) {
|
||||
return `20${token}`;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
function normalizeDateIso(input) {
|
||||
const year = String(input.year ?? "").trim();
|
||||
const month = normalizeTwoDigits(input.month ?? "");
|
||||
if (!/^\d{4}$/.test(year) || !/^\d{2}$/.test(month)) {
|
||||
return null;
|
||||
}
|
||||
if (input.day === undefined || input.day === null || String(input.day).trim().length === 0) {
|
||||
return `${year}-${month}`;
|
||||
}
|
||||
const day = normalizeTwoDigits(input.day ?? "");
|
||||
if (!/^\d{2}$/.test(day)) {
|
||||
return null;
|
||||
}
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
function parseDateLike(raw) {
|
||||
const value = String(raw ?? "").trim().toLowerCase();
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const isoDay = value.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01])\b/);
|
||||
if (isoDay) {
|
||||
return normalizeDateIso({ year: isoDay[1], month: isoDay[2], day: isoDay[3] });
|
||||
}
|
||||
const isoMonth = value.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])\b/);
|
||||
if (isoMonth) {
|
||||
return normalizeDateIso({ year: isoMonth[1], month: isoMonth[2] });
|
||||
}
|
||||
const dayMonthYear = value.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?[1-9]|1[0-2])[./-](\d{2}|\d{4})\b/);
|
||||
if (dayMonthYear) {
|
||||
return normalizeDateIso({ year: parseYear(dayMonthYear[3]), month: dayMonthYear[2], day: dayMonthYear[1] });
|
||||
}
|
||||
const rusMonthYear = value.match(/\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i);
|
||||
if (rusMonthYear) {
|
||||
const month = RUS_MONTH_TO_NUMBER[String(rusMonthYear[1] ?? "").toLowerCase()];
|
||||
if (!month)
|
||||
return null;
|
||||
return normalizeDateIso({ year: rusMonthYear[2], month });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function monthStart(iso) {
|
||||
const month = String(iso ?? "").slice(0, 7);
|
||||
if (!/^\d{4}-\d{2}$/.test(month)) {
|
||||
return null;
|
||||
}
|
||||
return `${month}-01`;
|
||||
}
|
||||
function normalizeEvidenceDate(value) {
|
||||
const parsed = parseDateLike(value);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
if (/^\d{4}-\d{2}$/.test(parsed)) {
|
||||
return monthStart(parsed);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function isPeriodWithinWindow(periodIso, window) {
|
||||
const normalized = normalizeEvidenceDate(periodIso);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return normalized >= window.from && normalized <= window.to;
|
||||
}
|
||||
function extractNormalizedFragments(normalized) {
|
||||
if (!normalized || typeof normalized !== "object") {
|
||||
return [];
|
||||
}
|
||||
const source = normalized;
|
||||
return toObjectArray(source.fragments);
|
||||
}
|
||||
function normalizedAnchorFromFragments(normalized) {
|
||||
const fragments = extractNormalizedFragments(normalized);
|
||||
for (const fragment of fragments) {
|
||||
const timeScope = toObject(fragment.time_scope);
|
||||
const type = String(timeScope?.type ?? "").trim().toLowerCase();
|
||||
const value = String(timeScope?.value ?? "").trim();
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
const parsed = parseDateLike(value);
|
||||
if (parsed) {
|
||||
return {
|
||||
value: parsed,
|
||||
source: `normalized_time_scope:${type || "unknown"}`
|
||||
};
|
||||
}
|
||||
if (/(?:июл|july)/i.test(value)) {
|
||||
return {
|
||||
value: `${JULY_YEAR}-${JULY_MONTH}`,
|
||||
source: `normalized_time_scope:${type || "unknown"}`
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
value: null,
|
||||
source: "normalized_time_scope:missing"
|
||||
};
|
||||
}
|
||||
function collectRawTemporalAnchorText(userMessage, companyAnchors) {
|
||||
return [userMessage, ...(companyAnchors?.periods ?? []), ...(companyAnchors?.dates ?? [])]
|
||||
.map((item) => String(item ?? "").trim())
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
function resolveJulyAnchor(rawText) {
|
||||
const raw = String(rawText ?? "");
|
||||
const lower = raw.toLowerCase();
|
||||
const explicitYear = lower.match(/\b(20\d{2})\b/)?.[1] ?? null;
|
||||
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july)(?:\D|$)/i);
|
||||
const dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/);
|
||||
const monthByNamed = /(июл|july)/i.test(lower);
|
||||
const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower);
|
||||
if (!dayByNamedJuly && !dayByNumeric && !monthByNamed && !monthByNumeric) {
|
||||
return {
|
||||
raw: null,
|
||||
resolved: null,
|
||||
source: "no_july_anchor",
|
||||
window: null,
|
||||
applyGuard: false
|
||||
};
|
||||
}
|
||||
const dayValue = dayByNamedJuly?.[1] ?? dayByNumeric?.[1] ?? null;
|
||||
const explicitDayYear = dayByNumeric?.[3] ? parseYear(dayByNumeric[3]) : null;
|
||||
const anchorYear = explicitDayYear ?? explicitYear ?? JULY_YEAR;
|
||||
const applyGuard = anchorYear === JULY_YEAR;
|
||||
if (!applyGuard) {
|
||||
return {
|
||||
raw: dayByNamedJuly?.[0] ?? dayByNumeric?.[0] ?? (monthByNamed ? "июль" : "07"),
|
||||
resolved: normalizeDateIso({
|
||||
year: anchorYear,
|
||||
month: JULY_MONTH,
|
||||
...(dayValue ? { day: dayValue } : {})
|
||||
}),
|
||||
source: "explicit_non_snapshot_year",
|
||||
window: null,
|
||||
applyGuard: false
|
||||
};
|
||||
}
|
||||
if (dayValue) {
|
||||
const dayIso = normalizeDateIso({
|
||||
year: JULY_YEAR,
|
||||
month: JULY_MONTH,
|
||||
day: dayValue
|
||||
});
|
||||
if (dayIso) {
|
||||
return {
|
||||
raw: dayByNamedJuly?.[0] ?? dayByNumeric?.[0] ?? null,
|
||||
resolved: dayIso,
|
||||
source: "company_snapshot_july_day_lock",
|
||||
window: {
|
||||
from: dayIso,
|
||||
to: dayIso,
|
||||
granularity: "day"
|
||||
},
|
||||
applyGuard: true
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
raw: monthByNamed ? "июль" : "2020-07",
|
||||
resolved: `${JULY_YEAR}-${JULY_MONTH}`,
|
||||
source: "company_snapshot_july_month_lock",
|
||||
window: JULY_WINDOW,
|
||||
applyGuard: true
|
||||
};
|
||||
}
|
||||
function resolveTemporalGuard(input) {
|
||||
const rawAnchorText = collectRawTemporalAnchorText(input.userMessage, input.companyAnchors);
|
||||
const julyAnchor = resolveJulyAnchor(rawAnchorText);
|
||||
const normalizedAnchor = normalizedAnchorFromFragments(input.normalized);
|
||||
const reasonCodes = [];
|
||||
if (!julyAnchor.applyGuard) {
|
||||
return {
|
||||
raw_time_anchor: julyAnchor.raw,
|
||||
resolved_time_anchor: normalizedAnchor.value,
|
||||
temporal_resolution_source: normalizedAnchor.source,
|
||||
temporal_guard_applied: false,
|
||||
temporal_guard_outcome: "passed",
|
||||
primary_period_window: null,
|
||||
reason_codes: []
|
||||
};
|
||||
}
|
||||
let outcome = "passed";
|
||||
if (normalizedAnchor.value && julyAnchor.window && !isPeriodWithinWindow(normalizedAnchor.value, julyAnchor.window)) {
|
||||
outcome = "failed_out_of_snapshot_window";
|
||||
reasonCodes.push("normalized_anchor_out_of_snapshot_window");
|
||||
}
|
||||
else if (!normalizedAnchor.value && !julyAnchor.resolved) {
|
||||
outcome = "ambiguous_limited";
|
||||
reasonCodes.push("missing_time_anchor_under_snapshot_lock");
|
||||
}
|
||||
return {
|
||||
raw_time_anchor: julyAnchor.raw,
|
||||
resolved_time_anchor: julyAnchor.resolved ?? normalizedAnchor.value,
|
||||
temporal_resolution_source: julyAnchor.source,
|
||||
temporal_guard_applied: true,
|
||||
temporal_guard_outcome: outcome,
|
||||
primary_period_window: julyAnchor.window,
|
||||
reason_codes: reasonCodes
|
||||
};
|
||||
}
|
||||
function applyTemporalHintToExecutionPlan(executionPlan, temporal) {
|
||||
if (!temporal.temporal_guard_applied) {
|
||||
return executionPlan;
|
||||
}
|
||||
const hint = temporal.primary_period_window?.granularity === "day" && temporal.resolved_time_anchor
|
||||
? `в рамках company snapshot даты ${temporal.resolved_time_anchor}`
|
||||
: `в рамках company snapshot июля 2020 (${JULY_WINDOW.from}..${JULY_WINDOW.to})`;
|
||||
return executionPlan.map((item) => {
|
||||
if (!item.should_execute) {
|
||||
return item;
|
||||
}
|
||||
const text = String(item.fragment_text ?? "").trim();
|
||||
if (/2020-07|июл|july/i.test(text)) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
fragment_text: `${text}; ${hint}`.trim()
|
||||
};
|
||||
});
|
||||
}
|
||||
function resolveDomainPolarityGuard(input) {
|
||||
const lower = String(input.userMessage ?? "").toLowerCase();
|
||||
const accounts = uniqueStrings([...(input.companyAnchors?.accounts ?? []), ...extractAccountsFromText(lower)]);
|
||||
const prefixes = new Set(accounts.map((item) => accountPrefix(item)).filter((item) => Boolean(item)));
|
||||
const settlementSignal = input.focusDomainHint === "settlements_60_62" ||
|
||||
prefixes.has("60") ||
|
||||
prefixes.has("62") ||
|
||||
prefixes.has("51") ||
|
||||
prefixes.has("76") ||
|
||||
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т)/i.test(lower);
|
||||
if (!settlementSignal) {
|
||||
return {
|
||||
applied: false,
|
||||
polarity: "not_applicable",
|
||||
outcome: "not_applicable",
|
||||
supplier_score: 0,
|
||||
customer_score: 0,
|
||||
account_scope: accounts,
|
||||
rejected_problem_units: 0,
|
||||
rejected_evidence: 0,
|
||||
critical_contradiction: false,
|
||||
reason_codes: []
|
||||
};
|
||||
}
|
||||
const supplierScore = (/(?:поставщ|supplier|vendor|кредитор|обязательств|payable)/i.test(lower) ? 2 : 0) +
|
||||
(prefixes.has("60") ? 2 : 0) +
|
||||
(/(?:счет\s*60|по\s*60)/i.test(lower) ? 1 : 0);
|
||||
const customerScore = (/(?:покупат|customer|buyer|дебитор|receivable)/i.test(lower) ? 2 : 0) +
|
||||
(prefixes.has("62") ? 2 : 0) +
|
||||
(/(?:счет\s*62|по\s*62)/i.test(lower) ? 1 : 0);
|
||||
let polarity = "mixed_or_unresolved";
|
||||
if (supplierScore > 0 && customerScore === 0) {
|
||||
polarity = "supplier_payable";
|
||||
}
|
||||
else if (customerScore > 0 && supplierScore === 0) {
|
||||
polarity = "customer_receivable";
|
||||
}
|
||||
const unresolved = polarity === "mixed_or_unresolved";
|
||||
return {
|
||||
applied: true,
|
||||
polarity,
|
||||
outcome: unresolved ? "limited_unresolved_polarity" : "passed",
|
||||
supplier_score: supplierScore,
|
||||
customer_score: customerScore,
|
||||
account_scope: accounts,
|
||||
rejected_problem_units: 0,
|
||||
rejected_evidence: 0,
|
||||
critical_contradiction: unresolved,
|
||||
reason_codes: unresolved ? ["unresolved_supplier_customer_polarity"] : []
|
||||
};
|
||||
}
|
||||
function applyPolarityHintToExecutionPlan(executionPlan, polarity) {
|
||||
if (!polarity.applied || polarity.polarity === "mixed_or_unresolved" || polarity.polarity === "not_applicable") {
|
||||
return executionPlan;
|
||||
}
|
||||
const hint = polarity.polarity === "supplier_payable"
|
||||
? "контекст: расчеты с поставщиком, обязательство, счет 60"
|
||||
: "контекст: расчеты с покупателем, дебиторская задолженность, счет 62";
|
||||
return executionPlan.map((item) => {
|
||||
if (!item.should_execute) {
|
||||
return item;
|
||||
}
|
||||
const text = String(item.fragment_text ?? "").trim();
|
||||
if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|счет\s*60|по\s*60)/i.test(text)) {
|
||||
return item;
|
||||
}
|
||||
if (polarity.polarity === "customer_receivable" && /(покупат|customer|счет\s*62|по\s*62)/i.test(text)) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
fragment_text: `${text}; ${hint}`.trim()
|
||||
};
|
||||
});
|
||||
}
|
||||
function containsReceivableSignal(value) {
|
||||
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
|
||||
}
|
||||
function containsPayableSignal(value) {
|
||||
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
|
||||
}
|
||||
function problemUnitCorpus(unit) {
|
||||
return [
|
||||
unit.lifecycle_domain ?? "",
|
||||
unit.problem_unit_type,
|
||||
unit.business_defect_class ?? "",
|
||||
unit.failed_expected_edge ?? "",
|
||||
unit.invalid_transition ?? "",
|
||||
unit.mechanism_summary ?? "",
|
||||
unit.business_lifecycle_interpretation ?? "",
|
||||
...(unit.affected_accounts ?? [])
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
}
|
||||
function isProblemUnitCompatible(unit, polarity) {
|
||||
if (polarity === "supplier_payable") {
|
||||
return !containsReceivableSignal(problemUnitCorpus(unit));
|
||||
}
|
||||
if (polarity === "customer_receivable") {
|
||||
return !containsPayableSignal(problemUnitCorpus(unit));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function evidenceCorpus(evidence) {
|
||||
return JSON.stringify({
|
||||
limitation: evidence.limitation,
|
||||
payload: evidence.payload,
|
||||
mechanism: evidence.mechanism_note,
|
||||
source_ref: evidence.source_ref,
|
||||
pointer: evidence.pointer
|
||||
}).toLowerCase();
|
||||
}
|
||||
function evidenceAccounts(evidence) {
|
||||
const payload = toObject(evidence.payload);
|
||||
const direct = uniqueStrings([
|
||||
...extractAccountsFromUnknown(payload?.account_context),
|
||||
...extractAccountsFromUnknown(payload?.account_debit),
|
||||
...extractAccountsFromUnknown(payload?.account_credit)
|
||||
]);
|
||||
if (direct.length > 0) {
|
||||
return direct;
|
||||
}
|
||||
return uniqueStrings([
|
||||
...extractAccountsFromUnknown(evidence.payload),
|
||||
...extractAccountsFromUnknown(evidence.pointer ?? null)
|
||||
]);
|
||||
}
|
||||
function isEvidenceCompatibleWithPolarity(evidence, polarity) {
|
||||
const corpus = evidenceCorpus(evidence);
|
||||
const accounts = evidenceAccounts(evidence).map((item) => accountPrefix(item)).filter((item) => Boolean(item));
|
||||
if (polarity === "supplier_payable") {
|
||||
if (containsReceivableSignal(corpus)) {
|
||||
return false;
|
||||
}
|
||||
if (accounts.length > 0 && accounts.every((item) => item === "62")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (polarity === "customer_receivable") {
|
||||
if (containsPayableSignal(corpus)) {
|
||||
return false;
|
||||
}
|
||||
if (accounts.length > 0 && accounts.every((item) => item === "60")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function applyDomainPolarityGuardToRetrievalResults(input) {
|
||||
if (!input.guard.applied || input.guard.polarity === "not_applicable" || input.guard.polarity === "mixed_or_unresolved") {
|
||||
return {
|
||||
retrievalResults: input.retrievalResults,
|
||||
audit: input.guard
|
||||
};
|
||||
}
|
||||
let rejectedProblemUnits = 0;
|
||||
let rejectedEvidence = 0;
|
||||
let criticalContradiction = false;
|
||||
const adjusted = input.retrievalResults.map((result) => {
|
||||
const originalUnits = Array.isArray(result.problem_units) ? result.problem_units : [];
|
||||
const filteredUnits = originalUnits.filter((unit) => isProblemUnitCompatible(unit, input.guard.polarity));
|
||||
rejectedProblemUnits += Math.max(0, originalUnits.length - filteredUnits.length);
|
||||
const originalEvidence = Array.isArray(result.evidence) ? result.evidence : [];
|
||||
const filteredEvidence = originalEvidence.filter((item) => isEvidenceCompatibleWithPolarity(item, input.guard.polarity));
|
||||
rejectedEvidence += Math.max(0, originalEvidence.length - filteredEvidence.length);
|
||||
if (originalUnits.length > 0 && filteredUnits.length === 0 && originalEvidence.length > 0 && filteredEvidence.length === 0) {
|
||||
criticalContradiction = true;
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
evidence: filteredEvidence,
|
||||
...(Array.isArray(result.problem_units)
|
||||
? {
|
||||
problem_units: filteredUnits
|
||||
}
|
||||
: {})
|
||||
};
|
||||
});
|
||||
const reasonCodes = [];
|
||||
if (rejectedProblemUnits > 0) {
|
||||
reasonCodes.push("polarity_problem_unit_filter_applied");
|
||||
}
|
||||
if (rejectedEvidence > 0) {
|
||||
reasonCodes.push("polarity_evidence_filter_applied");
|
||||
}
|
||||
if (criticalContradiction) {
|
||||
reasonCodes.push("critical_domain_polarity_contradiction");
|
||||
}
|
||||
return {
|
||||
retrievalResults: adjusted,
|
||||
audit: {
|
||||
...input.guard,
|
||||
rejected_problem_units: rejectedProblemUnits,
|
||||
rejected_evidence: rejectedEvidence,
|
||||
critical_contradiction: criticalContradiction,
|
||||
outcome: criticalContradiction ? "blocked_conflict" : "passed",
|
||||
reason_codes: uniqueStrings([...(input.guard.reason_codes ?? []), ...reasonCodes])
|
||||
}
|
||||
};
|
||||
}
|
||||
function initRejectBreakdown() {
|
||||
return {
|
||||
wrong_period: 0,
|
||||
wrong_domain: 0,
|
||||
wrong_account_scope: 0,
|
||||
weak_source_mapping: 0,
|
||||
zero_live_match: 0,
|
||||
future_dated_or_out_of_window: 0
|
||||
};
|
||||
}
|
||||
function isVatPrefix(prefix) {
|
||||
return prefix === "19" || prefix === "68";
|
||||
}
|
||||
function isSettlementPrefix(prefix) {
|
||||
return prefix === "51" || prefix === "60" || prefix === "62" || prefix === "76";
|
||||
}
|
||||
function isMonthClosePrefix(prefix) {
|
||||
const numeric = Number(prefix);
|
||||
if (prefix === "97") {
|
||||
return true;
|
||||
}
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return false;
|
||||
}
|
||||
return numeric >= 20 && numeric <= 44;
|
||||
}
|
||||
function expectedAccountPrefixes(input) {
|
||||
const explicit = uniqueStrings([...(input.companyAnchors?.accounts ?? []), ...extractAccountsFromText(input.userMessage)])
|
||||
.map((item) => accountPrefix(item))
|
||||
.filter((item) => Boolean(item));
|
||||
if (explicit.length > 0) {
|
||||
return uniqueStrings(explicit);
|
||||
}
|
||||
if (input.focusDomainHint === "vat_document_register_book") {
|
||||
return ["19", "68"];
|
||||
}
|
||||
if (input.focusDomainHint === "month_close_costs_20_44") {
|
||||
return ["20", "25", "26", "44", "97", "01", "02", "08"];
|
||||
}
|
||||
if (input.focusDomainHint === "settlements_60_62") {
|
||||
if (input.polarity === "supplier_payable") {
|
||||
return ["60", "51", "76"];
|
||||
}
|
||||
if (input.polarity === "customer_receivable") {
|
||||
return ["62", "51"];
|
||||
}
|
||||
return ["60", "62", "51", "76"];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
function isLiveEvidence(evidence) {
|
||||
const payload = toObject(evidence.payload);
|
||||
if (String(payload?.source_layer ?? "").trim().toLowerCase() === "mcp_live_probe") {
|
||||
return true;
|
||||
}
|
||||
const sourceEntity = String(evidence.pointer?.source?.entity ?? "").toLowerCase();
|
||||
return sourceEntity.includes("mcplivemovement");
|
||||
}
|
||||
function extractEvidencePeriod(evidence) {
|
||||
const payload = toObject(evidence.payload);
|
||||
return (String(evidence.source_ref?.period ?? "").trim() ||
|
||||
String(evidence.pointer?.source?.period ?? "").trim() ||
|
||||
String(payload?.period ?? "").trim() ||
|
||||
null);
|
||||
}
|
||||
function isExpectedAccountScopeMatch(accounts, expectedPrefixes) {
|
||||
if (accounts.length === 0 || expectedPrefixes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const prefixes = accounts.map((item) => accountPrefix(item)).filter((item) => Boolean(item));
|
||||
if (prefixes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return prefixes.some((prefix) => expectedPrefixes.includes(prefix));
|
||||
}
|
||||
function hasWrongDomainByAccounts(accounts, focusDomainHint) {
|
||||
if (accounts.length === 0 || !focusDomainHint) {
|
||||
return false;
|
||||
}
|
||||
const prefixes = accounts.map((item) => accountPrefix(item)).filter((item) => Boolean(item));
|
||||
if (prefixes.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (focusDomainHint === "settlements_60_62") {
|
||||
return prefixes.every((prefix) => isVatPrefix(prefix));
|
||||
}
|
||||
if (focusDomainHint === "vat_document_register_book") {
|
||||
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isMonthClosePrefix(prefix));
|
||||
}
|
||||
if (focusDomainHint === "month_close_costs_20_44") {
|
||||
return prefixes.every((prefix) => isSettlementPrefix(prefix) || isVatPrefix(prefix));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function extractLiveMatchedRows(result) {
|
||||
const summary = toObject(result.summary);
|
||||
const live = toObject(summary?.live_mcp);
|
||||
const value = Number(live?.matched_rows);
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
function liveAccountScopeWasApplied(result) {
|
||||
const summary = toObject(result.summary);
|
||||
const live = toObject(summary?.live_mcp);
|
||||
const accountScope = live?.account_scope;
|
||||
return Array.isArray(accountScope) && accountScope.length > 0;
|
||||
}
|
||||
function evidenceAdmissibilityReasons(input) {
|
||||
const reasons = new Set();
|
||||
if (input.evidence.limitation?.reason_code === "weak_source_mapping") {
|
||||
reasons.add("weak_source_mapping");
|
||||
}
|
||||
if (input.zeroLiveMatch && isLiveEvidence(input.evidence)) {
|
||||
reasons.add("zero_live_match");
|
||||
}
|
||||
const period = extractEvidencePeriod(input.evidence);
|
||||
if (period && input.temporal.primary_period_window) {
|
||||
const normalized = normalizeEvidenceDate(period);
|
||||
if (normalized && normalized > input.temporal.primary_period_window.to) {
|
||||
reasons.add("future_dated_or_out_of_window");
|
||||
}
|
||||
else if (normalized && !isPeriodWithinWindow(normalized, input.temporal.primary_period_window)) {
|
||||
reasons.add("wrong_period");
|
||||
}
|
||||
}
|
||||
const accounts = evidenceAccounts(input.evidence);
|
||||
if (!isExpectedAccountScopeMatch(accounts, input.expectedPrefixes)) {
|
||||
reasons.add("wrong_account_scope");
|
||||
}
|
||||
if (hasWrongDomainByAccounts(accounts, input.focusDomainHint)) {
|
||||
reasons.add("wrong_domain");
|
||||
}
|
||||
return Array.from(reasons);
|
||||
}
|
||||
function isLiveItem(item) {
|
||||
return String(item.source_layer ?? "").trim().toLowerCase() === "mcp_live_probe";
|
||||
}
|
||||
function itemPeriod(item) {
|
||||
const value = String(item.period ?? item.Period ?? "").trim();
|
||||
return value || null;
|
||||
}
|
||||
function itemAccounts(item) {
|
||||
const direct = uniqueStrings([
|
||||
...extractAccountsFromUnknown(item.account_context),
|
||||
...extractAccountsFromUnknown(item.account_debit),
|
||||
...extractAccountsFromUnknown(item.account_credit)
|
||||
]);
|
||||
if (direct.length > 0) {
|
||||
return direct;
|
||||
}
|
||||
return uniqueStrings([...extractAccountsFromUnknown(item)]);
|
||||
}
|
||||
function itemRejectReasons(input) {
|
||||
const reasons = new Set();
|
||||
if (input.zeroLiveMatch && isLiveItem(input.item)) {
|
||||
reasons.add("zero_live_match");
|
||||
}
|
||||
const period = itemPeriod(input.item);
|
||||
if (period && input.temporal.primary_period_window) {
|
||||
const normalized = normalizeEvidenceDate(period);
|
||||
if (normalized && normalized > input.temporal.primary_period_window.to) {
|
||||
reasons.add("future_dated_or_out_of_window");
|
||||
}
|
||||
else if (normalized && !isPeriodWithinWindow(normalized, input.temporal.primary_period_window)) {
|
||||
reasons.add("wrong_period");
|
||||
}
|
||||
}
|
||||
const accounts = itemAccounts(input.item);
|
||||
if (!isExpectedAccountScopeMatch(accounts, input.expectedPrefixes)) {
|
||||
reasons.add("wrong_account_scope");
|
||||
}
|
||||
if (hasWrongDomainByAccounts(accounts, input.focusDomainHint)) {
|
||||
reasons.add("wrong_domain");
|
||||
}
|
||||
return Array.from(reasons);
|
||||
}
|
||||
function addRejectReason(target, reason) {
|
||||
target[reason] += 1;
|
||||
}
|
||||
function applyEvidenceAdmissibilityGate(input) {
|
||||
const rejectBreakdown = initRejectBreakdown();
|
||||
const categoryBreakdown = {
|
||||
hard_evidence: 0,
|
||||
supporting_signal: 0,
|
||||
inadmissible_noise: 0
|
||||
};
|
||||
let candidateEvidenceTotal = 0;
|
||||
let admissibleEvidenceCount = 0;
|
||||
let rejectedEvidenceCount = 0;
|
||||
let rejectedItemCount = 0;
|
||||
const expectedPrefixes = expectedAccountPrefixes({
|
||||
focusDomainHint: input.focusDomainHint,
|
||||
polarity: input.polarity,
|
||||
companyAnchors: input.companyAnchors,
|
||||
userMessage: input.userMessage
|
||||
});
|
||||
const adjusted = input.retrievalResults.map((result) => {
|
||||
const matchedRows = extractLiveMatchedRows(result);
|
||||
const zeroLiveMatch = matchedRows === 0 && liveAccountScopeWasApplied(result);
|
||||
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
|
||||
candidateEvidenceTotal += evidence.length;
|
||||
const admissibleEvidence = [];
|
||||
for (const item of evidence) {
|
||||
const reasons = evidenceAdmissibilityReasons({
|
||||
evidence: item,
|
||||
temporal: input.temporal,
|
||||
focusDomainHint: input.focusDomainHint,
|
||||
expectedPrefixes,
|
||||
zeroLiveMatch
|
||||
});
|
||||
if (reasons.length > 0) {
|
||||
rejectedEvidenceCount += 1;
|
||||
categoryBreakdown.inadmissible_noise += 1;
|
||||
for (const reason of reasons) {
|
||||
addRejectReason(rejectBreakdown, reason);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const limitationCode = String(item.limitation?.reason_code ?? "").trim();
|
||||
if (!limitationCode && item.confidence !== "low") {
|
||||
categoryBreakdown.hard_evidence += 1;
|
||||
}
|
||||
else {
|
||||
categoryBreakdown.supporting_signal += 1;
|
||||
}
|
||||
admissibleEvidenceCount += 1;
|
||||
admissibleEvidence.push(item);
|
||||
}
|
||||
const items = Array.isArray(result.items) ? result.items : [];
|
||||
const admissibleItems = [];
|
||||
for (const item of items) {
|
||||
const reasons = itemRejectReasons({
|
||||
item,
|
||||
temporal: input.temporal,
|
||||
focusDomainHint: input.focusDomainHint,
|
||||
expectedPrefixes,
|
||||
zeroLiveMatch
|
||||
});
|
||||
if (reasons.length > 0) {
|
||||
rejectedItemCount += 1;
|
||||
for (const reason of reasons) {
|
||||
addRejectReason(rejectBreakdown, reason);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
admissibleItems.push(item);
|
||||
}
|
||||
const summary = {
|
||||
...(toObject(result.summary) ?? {}),
|
||||
evidence_admissibility_gate: {
|
||||
candidate_evidence: evidence.length,
|
||||
admissible_evidence: admissibleEvidence.length,
|
||||
rejected_evidence: Math.max(0, evidence.length - admissibleEvidence.length),
|
||||
rejected_items: Math.max(0, items.length - admissibleItems.length)
|
||||
}
|
||||
};
|
||||
const limitations = [...(result.limitations ?? [])];
|
||||
if (zeroLiveMatch) {
|
||||
limitations.push("Live probe matched_rows=0; live evidence excluded from grounded answer.");
|
||||
}
|
||||
if (admissibleEvidence.length === 0 && evidence.length > 0) {
|
||||
limitations.push("Admissibility gate removed non-admissible evidence for current scope.");
|
||||
}
|
||||
const normalizedStatus = result.status === "ok" && admissibleEvidence.length === 0 && admissibleItems.length === 0
|
||||
? "partial"
|
||||
: result.status;
|
||||
return {
|
||||
...result,
|
||||
status: normalizedStatus,
|
||||
items: admissibleItems,
|
||||
evidence: admissibleEvidence,
|
||||
summary,
|
||||
limitations: uniqueStrings(limitations)
|
||||
};
|
||||
});
|
||||
const reasonCodes = [];
|
||||
if (rejectedEvidenceCount > 0) {
|
||||
reasonCodes.push("inadmissible_evidence_filtered");
|
||||
}
|
||||
if (admissibleEvidenceCount === 0) {
|
||||
reasonCodes.push("no_admissible_evidence_for_grounded_answer");
|
||||
}
|
||||
if (rejectedItemCount > 0) {
|
||||
reasonCodes.push("inadmissible_items_filtered");
|
||||
}
|
||||
return {
|
||||
retrievalResults: adjusted,
|
||||
audit: {
|
||||
candidate_evidence_total: candidateEvidenceTotal,
|
||||
admissible_evidence_count: admissibleEvidenceCount,
|
||||
rejected_evidence_count: rejectedEvidenceCount,
|
||||
rejected_item_count: rejectedItemCount,
|
||||
reject_breakdown: rejectBreakdown,
|
||||
category_breakdown: categoryBreakdown,
|
||||
reason_codes: uniqueStrings(reasonCodes)
|
||||
}
|
||||
};
|
||||
}
|
||||
function evaluateGroundedAnswerEligibility(input) {
|
||||
const temporalPassed = input.temporal.temporal_guard_outcome === "passed";
|
||||
const polarityPassed = !input.polarity.applied || input.polarity.outcome === "passed" || input.polarity.outcome === "not_applicable";
|
||||
const admissibleEvidenceCount = input.evidence.admissible_evidence_count;
|
||||
const criticalContradiction = Boolean(input.polarity.critical_contradiction);
|
||||
const eligible = temporalPassed && polarityPassed && admissibleEvidenceCount > 0 && !criticalContradiction;
|
||||
const reasonCodes = [];
|
||||
if (!temporalPassed) {
|
||||
reasonCodes.push(`temporal_guard_${input.temporal.temporal_guard_outcome}`);
|
||||
}
|
||||
if (!polarityPassed) {
|
||||
reasonCodes.push(`polarity_guard_${input.polarity.outcome}`);
|
||||
}
|
||||
if (admissibleEvidenceCount <= 0) {
|
||||
reasonCodes.push("admissible_evidence_count_zero");
|
||||
}
|
||||
if (criticalContradiction) {
|
||||
reasonCodes.push("critical_domain_or_account_contradiction");
|
||||
}
|
||||
return {
|
||||
eligible,
|
||||
temporal_passed: temporalPassed,
|
||||
polarity_passed: polarityPassed,
|
||||
admissible_evidence_count: admissibleEvidenceCount,
|
||||
critical_contradiction: criticalContradiction,
|
||||
outcome: eligible ? "grounded_allowed" : "limited_or_insufficient_evidence",
|
||||
reason_codes: uniqueStrings(reasonCodes)
|
||||
};
|
||||
}
|
||||
function applyEligibilityToGroundingCheck(groundingCheck, eligibility) {
|
||||
if (eligibility.eligible) {
|
||||
return groundingCheck;
|
||||
}
|
||||
const status = eligibility.admissible_evidence_count <= 0 || !eligibility.temporal_passed ? "no_grounded_answer" : "partial";
|
||||
const reasonMap = {
|
||||
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
|
||||
critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
|
||||
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
|
||||
temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
|
||||
polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
|
||||
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре."
|
||||
};
|
||||
const reasons = [
|
||||
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),
|
||||
...eligibility.reason_codes.map((code) => reasonMap[code] ?? code)
|
||||
];
|
||||
return {
|
||||
...groundingCheck,
|
||||
status,
|
||||
reasons: uniqueStrings(reasons)
|
||||
};
|
||||
}
|
||||
+106
-6
@@ -48,6 +48,7 @@ const investigationState_1 = __importStar(require("./investigationState"));
|
||||
const retrievalResultNormalizer_1 = __importStar(require("./retrievalResultNormalizer"));
|
||||
const questionTypeResolver_1 = __importStar(require("./questionTypeResolver"));
|
||||
const companyAnchorResolver_1 = __importStar(require("./companyAnchorResolver"));
|
||||
const assistantRuntimeGuards_1 = __importStar(require("./assistantRuntimeGuards"));
|
||||
function retrievalSummaryForRoute(route) {
|
||||
if (route === "store_canonical")
|
||||
return "Canonical accounting data path selected.";
|
||||
@@ -897,6 +898,43 @@ function extractFollowupAccountAnchorsLoose(text) {
|
||||
}
|
||||
return Array.from(new Set(anchors));
|
||||
}
|
||||
function accountPrefixToken(value) {
|
||||
const token = String(value ?? "").trim();
|
||||
const match = token.match(/^(\d{2})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
function hasCrossScopeConflictWithState(userMessage, state) {
|
||||
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
|
||||
const statePeriod = compactWhitespace(state.focus.period ?? "");
|
||||
if (explicitPeriod && statePeriod && explicitPeriod !== statePeriod) {
|
||||
return true;
|
||||
}
|
||||
const inferredDomain = inferP0DomainFromMessage(userMessage);
|
||||
const stateDomain = compactWhitespace(state.followup_context?.active_domain ?? state.focus.domain ?? "");
|
||||
if (inferredDomain && stateDomain && inferredDomain !== stateDomain) {
|
||||
return true;
|
||||
}
|
||||
const explicitAccounts = extractAccountTokens(userMessage);
|
||||
const fallbackAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
|
||||
const knownAccounts = Array.isArray(state.focus.primary_accounts) ? state.focus.primary_accounts : [];
|
||||
if (fallbackAccounts.length > 0 && knownAccounts.length > 0) {
|
||||
const knownPrefixes = new Set(knownAccounts.map((item) => accountPrefixToken(item)).filter(Boolean));
|
||||
const newPrefixes = new Set(fallbackAccounts.map((item) => accountPrefixToken(item)).filter(Boolean));
|
||||
if (newPrefixes.size > 0 && knownPrefixes.size > 0) {
|
||||
let intersects = false;
|
||||
for (const prefix of newPrefixes) {
|
||||
if (knownPrefixes.has(prefix)) {
|
||||
intersects = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!intersects) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function inferP0DomainFromMessage(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const accountTokens = extractAccountTokens(lower);
|
||||
@@ -1009,9 +1047,11 @@ function buildFollowupStateBinding(input) {
|
||||
Boolean(problemState) &&
|
||||
((problemState?.active_problem_units.length ?? 0) > 0 || (problemState?.focus_problem_types.length ?? 0) > 0);
|
||||
const strongNewAnchorDetected = hasStrongFollowupAnchors(userMessage, input.investigationState);
|
||||
const scopeConflictDetected = hasCrossScopeConflictWithState(userMessage, input.investigationState);
|
||||
const periodRefinementFollowup = hasPeriodLiteral(userMessage) && problemContinuityAvailable;
|
||||
const shouldBind = !smallTalkSignal &&
|
||||
!strongNewAnchorDetected &&
|
||||
!scopeConflictDetected &&
|
||||
(followupMarker || referentialPointer || periodRefinementFollowup || (!strongSignal && shortPrompt));
|
||||
if (!shouldBind) {
|
||||
return {
|
||||
@@ -1116,7 +1156,10 @@ function buildFollowupStateBinding(input) {
|
||||
problem_continuity_available: problemContinuityAvailable,
|
||||
problem_continuity_applied: problemContinuityApplied,
|
||||
problem_continuity_skipped_reason: problemContinuityApplied ? null : problemContinuitySkippedReason,
|
||||
strong_new_anchor_detected: strongNewAnchorDetected
|
||||
strong_new_anchor_detected: strongNewAnchorDetected,
|
||||
scope_isolation_applied: true,
|
||||
scope_carryover_allowed: !scopeConflictDetected,
|
||||
scope_reset_reason: scopeConflictDetected ? "cross_scope_conflict" : null
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1185,11 +1228,30 @@ class AssistantService {
|
||||
useMock: Boolean(payload.useMock)
|
||||
};
|
||||
const normalized = await this.normalizerService.normalize(normalizePayload);
|
||||
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
|
||||
const inferredDomainByMessage = inferP0DomainFromMessage(userMessage);
|
||||
const focusDomainForGuards = inferredDomainByMessage === "settlements_60_62" ||
|
||||
inferredDomainByMessage === "vat_document_register_book" ||
|
||||
inferredDomainByMessage === "month_close_costs_20_44"
|
||||
? inferredDomainByMessage
|
||||
: null;
|
||||
const temporalGuard = (0, assistantRuntimeGuards_1.resolveTemporalGuard)({
|
||||
userMessage,
|
||||
normalized: normalized.normalized,
|
||||
companyAnchors
|
||||
});
|
||||
const domainPolarityGuardInitial = (0, assistantRuntimeGuards_1.resolveDomainPolarityGuard)({
|
||||
userMessage,
|
||||
companyAnchors,
|
||||
focusDomainHint: focusDomainForGuards
|
||||
});
|
||||
const requirementExtraction = extractRequirements(normalized.route_hint_summary, normalized.normalized, userMessage);
|
||||
const executionPlan = toExecutionPlan(normalized.route_hint_summary, normalized.normalized, userMessage, requirementExtraction.byFragment);
|
||||
let executionPlan = toExecutionPlan(normalized.route_hint_summary, normalized.normalized, userMessage, requirementExtraction.byFragment);
|
||||
executionPlan = (0, assistantRuntimeGuards_1.applyTemporalHintToExecutionPlan)(executionPlan, temporalGuard);
|
||||
executionPlan = (0, assistantRuntimeGuards_1.applyPolarityHintToExecutionPlan)(executionPlan, domainPolarityGuardInitial);
|
||||
const retrievalCalls = [];
|
||||
const retrievalResultsRaw = [];
|
||||
const retrievalResults = [];
|
||||
let retrievalResults = [];
|
||||
for (const planItem of executionPlan) {
|
||||
if (!planItem.should_execute) {
|
||||
retrievalCalls.push({
|
||||
@@ -1248,13 +1310,32 @@ class AssistantService {
|
||||
retrievalResults.push((0, retrievalResultNormalizer_1.normalizeRetrievalResult)(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError));
|
||||
}
|
||||
}
|
||||
const polarityGuardResult = (0, assistantRuntimeGuards_1.applyDomainPolarityGuardToRetrievalResults)({
|
||||
retrievalResults,
|
||||
guard: domainPolarityGuardInitial
|
||||
});
|
||||
retrievalResults = polarityGuardResult.retrievalResults;
|
||||
const evidenceGateResult = (0, assistantRuntimeGuards_1.applyEvidenceAdmissibilityGate)({
|
||||
retrievalResults,
|
||||
temporal: temporalGuard,
|
||||
focusDomainHint: focusDomainForGuards,
|
||||
polarity: polarityGuardResult.audit.polarity,
|
||||
companyAnchors,
|
||||
userMessage
|
||||
});
|
||||
retrievalResults = evidenceGateResult.retrievalResults;
|
||||
const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
|
||||
const groundingCheck = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
|
||||
const groundingCheckBase = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
|
||||
const groundedAnswerEligibilityGuard = (0, assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility)({
|
||||
temporal: temporalGuard,
|
||||
polarity: polarityGuardResult.audit,
|
||||
evidence: evidenceGateResult.audit
|
||||
});
|
||||
const groundingCheck = (0, assistantRuntimeGuards_1.applyEligibilityToGroundingCheck)(groundingCheckBase, groundedAnswerEligibilityGuard);
|
||||
const focusDomainHint = followupBinding.usage?.applied
|
||||
? session.investigation_state?.followup_context?.active_domain ?? session.investigation_state?.focus.domain ?? null
|
||||
: null;
|
||||
const questionTypeClass = (0, questionTypeResolver_1.resolveQuestionType)(userMessage);
|
||||
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
|
||||
const hasPeriodInCompanyAnchors = (Array.isArray(companyAnchors?.dates) && companyAnchors.dates.some((item) => String(item ?? "").trim().length > 0)) ||
|
||||
(Array.isArray(companyAnchors?.periods) && companyAnchors.periods.some((item) => String(item ?? "").trim().length > 0));
|
||||
const normalizationPeriodExplicit = hasExplicitPeriodAnchorFromNormalized(normalized.normalized) || hasPeriodInCompanyAnchors;
|
||||
@@ -1298,7 +1379,8 @@ class AssistantService {
|
||||
requirements: coverageEvaluation.requirements,
|
||||
coverageReport: coverageEvaluation.coverage,
|
||||
retrievalResults,
|
||||
replyType: composition.reply_type
|
||||
replyType: composition.reply_type,
|
||||
followupApplied: Boolean(followupBinding.usage?.applied)
|
||||
})
|
||||
: null;
|
||||
if (config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && investigationStateSnapshot) {
|
||||
@@ -1326,6 +1408,15 @@ class AssistantService {
|
||||
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
||||
question_type_class: questionTypeClass,
|
||||
company_anchors: companyAnchors,
|
||||
raw_time_anchor: temporalGuard.raw_time_anchor,
|
||||
resolved_time_anchor: temporalGuard.resolved_time_anchor,
|
||||
temporal_resolution_source: temporalGuard.temporal_resolution_source,
|
||||
temporal_guard_applied: temporalGuard.temporal_guard_applied,
|
||||
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
|
||||
temporal_guard: temporalGuard,
|
||||
domain_polarity_guard: polarityGuardResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
||||
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
||||
@@ -1391,6 +1482,15 @@ class AssistantService {
|
||||
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
||||
question_type_class: questionTypeClass,
|
||||
company_anchors: companyAnchors,
|
||||
raw_time_anchor: temporalGuard.raw_time_anchor,
|
||||
resolved_time_anchor: temporalGuard.resolved_time_anchor,
|
||||
temporal_resolution_source: temporalGuard.temporal_resolution_source,
|
||||
temporal_guard_applied: temporalGuard.temporal_guard_applied,
|
||||
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
|
||||
temporal_guard: temporalGuard,
|
||||
domain_polarity_guard: polarityGuardResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
||||
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
||||
|
||||
+95
-8
@@ -23,6 +23,63 @@ function detectPeriod(text) {
|
||||
return yearly[1];
|
||||
return null;
|
||||
}
|
||||
function detectExplicitDomainHint(text) {
|
||||
const messageCorpus = String(text ?? "").toLowerCase();
|
||||
const accounts = detectAccounts(text);
|
||||
const hasSettlementSignal = accounts.some((item) => isSettlementAccount(item)) ||
|
||||
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(messageCorpus);
|
||||
if (hasSettlementSignal) {
|
||||
return "settlements_60_62";
|
||||
}
|
||||
const hasVatSignal = accounts.some((item) => isVatAccount(item)) ||
|
||||
/(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
|
||||
if (hasVatSignal) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
const hasCloseSignal = accounts.some((item) => isCloseCostsAccount(item)) ||
|
||||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
|
||||
if (hasCloseSignal) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
const hasFixedAssetSignal = accounts.some((item) => isFixedAssetAccount(item)) ||
|
||||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(messageCorpus);
|
||||
if (hasFixedAssetSignal) {
|
||||
return "fixed_asset_amortization";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function buildQuestionScopeId(input) {
|
||||
const domainPart = String(input.domain ?? "").trim();
|
||||
const periodPart = String(input.period ?? "").trim();
|
||||
const accountPart = capStrings(input.accounts.map((item) => String(item ?? "").trim()).filter(Boolean), 4).join(",");
|
||||
const subjectPart = String(input.subject ?? "").trim().slice(0, 96).toLowerCase();
|
||||
const parts = [
|
||||
domainPart ? `d:${domainPart}` : "",
|
||||
periodPart ? `p:${periodPart}` : "",
|
||||
accountPart ? `a:${accountPart}` : "",
|
||||
subjectPart ? `s:${subjectPart}` : ""
|
||||
].filter(Boolean);
|
||||
if (parts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return parts.join("|");
|
||||
}
|
||||
function deriveScopeOrigin(input) {
|
||||
if (input.followupApplied) {
|
||||
return "followup_state_carryover";
|
||||
}
|
||||
const hasExplicitPeriod = Boolean(detectPeriod(input.userMessage));
|
||||
const hasExplicitAccounts = detectAccounts(input.userMessage).length > 0;
|
||||
const explicitDomain = detectExplicitDomainHint(input.userMessage);
|
||||
if (hasExplicitPeriod || hasExplicitAccounts || explicitDomain) {
|
||||
return "explicit_from_message";
|
||||
}
|
||||
const routeDomain = deriveDomain(input.routeSummary);
|
||||
if (routeDomain && routeDomain !== "no_route") {
|
||||
return "route_derived";
|
||||
}
|
||||
return "underspecified";
|
||||
}
|
||||
function deriveDomain(routeSummary) {
|
||||
if (!routeSummary)
|
||||
return null;
|
||||
@@ -105,7 +162,9 @@ function isCloseCostsAccount(value) {
|
||||
}
|
||||
function inferFollowupActiveDomain(input) {
|
||||
const messageCorpus = String(input.userMessage ?? "").toLowerCase();
|
||||
const contextualCorpus = `${messageCorpus} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase();
|
||||
const contextualCorpus = input.allowStateCarryover
|
||||
? `${messageCorpus} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase()
|
||||
: messageCorpus;
|
||||
const hasFixedAssetLexicalSignal = /(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(messageCorpus);
|
||||
const hasFixedAssetAccountSignal = input.focusAccounts.some((item) => isFixedAssetAccount(item)) &&
|
||||
/(?:сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|(?:01|02|08)(?:\.\d{2})?\s*\/\s*(?:01|02|08)(?:\.\d{2})?|\b0[128](?:\.\d{2})?\b)/i.test(messageCorpus);
|
||||
@@ -127,7 +186,8 @@ function inferFollowupActiveDomain(input) {
|
||||
if (hasCloseSignal) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
if (/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|аванс|долг|settlement|payment)/i.test(contextualCorpus) &&
|
||||
if (input.allowStateCarryover &&
|
||||
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|аванс|долг|settlement|payment)/i.test(contextualCorpus) &&
|
||||
(input.previous.followup_context?.active_domain === "settlements_60_62" ||
|
||||
input.previous.focus.domain === "settlements_60_62")) {
|
||||
return "settlements_60_62";
|
||||
@@ -136,7 +196,10 @@ function inferFollowupActiveDomain(input) {
|
||||
if (routeDomain && routeDomain !== "no_route") {
|
||||
return routeDomain;
|
||||
}
|
||||
return input.previous.followup_context?.active_domain ?? input.previous.focus.domain ?? null;
|
||||
if (input.allowStateCarryover) {
|
||||
return input.previous.followup_context?.active_domain ?? input.previous.focus.domain ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function collectUncoveredRequirementIds(coverageReport) {
|
||||
return capStrings([
|
||||
@@ -313,6 +376,8 @@ function createEmptyInvestigationState(sessionId, timestamp = new Date().toISOSt
|
||||
turn_index: 0,
|
||||
updated_at: timestamp,
|
||||
question_id: null,
|
||||
question_scope_id: null,
|
||||
scope_origin: null,
|
||||
focus: {
|
||||
domain: null,
|
||||
period: null,
|
||||
@@ -329,22 +394,40 @@ function createEmptyInvestigationState(sessionId, timestamp = new Date().toISOSt
|
||||
}
|
||||
function updateInvestigationState(input) {
|
||||
const previous = input.previous;
|
||||
const followupApplied = input.followupApplied === true;
|
||||
const focusFromMessage = capStrings(detectAccounts(input.userMessage), stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
|
||||
const mergedFocusAccounts = capStrings([...focusFromMessage, ...previous.focus.primary_accounts], stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
|
||||
const mergedFocusAccounts = followupApplied
|
||||
? capStrings([...focusFromMessage, ...previous.focus.primary_accounts], stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS)
|
||||
: capStrings(focusFromMessage, stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
|
||||
const requirementIds = capStrings(input.requirements.map((item) => item.requirement_id), stage1Contracts_1.INVESTIGATION_MAX_REQUIREMENT_LINKS);
|
||||
const mainRequirement = input.requirements[0]?.requirement_text ?? input.userMessage;
|
||||
const problemUnitState = updateProblemUnitState(previous, input.retrievalResults);
|
||||
const uncoveredRequirementIds = collectUncoveredRequirementIds(input.coverageReport);
|
||||
const routeDomain = deriveDomain(input.routeSummary);
|
||||
const activeDomain = inferFollowupActiveDomain({
|
||||
userMessage: input.userMessage,
|
||||
focusAccounts: focusFromMessage,
|
||||
routeSummary: input.routeSummary,
|
||||
previous
|
||||
previous,
|
||||
allowStateCarryover: followupApplied
|
||||
});
|
||||
const focusDomain = activeDomain ?? deriveDomain(input.routeSummary) ?? previous.focus.domain;
|
||||
const focusDomain = activeDomain ?? routeDomain ?? (followupApplied ? previous.focus.domain : null);
|
||||
const detectedPeriod = detectPeriod(input.userMessage);
|
||||
const focusPeriod = detectedPeriod ?? (followupApplied ? previous.focus.period : null);
|
||||
const settlementNextActions = settlementFocusActions(activeDomain);
|
||||
const lastProblemUnitId = problemUnitState?.active_problem_units[0] ?? null;
|
||||
const evidenceSummary = collectEvidenceSummary(input.retrievalResults);
|
||||
const scopeOrigin = deriveScopeOrigin({
|
||||
followupApplied,
|
||||
userMessage: input.userMessage,
|
||||
routeSummary: input.routeSummary
|
||||
});
|
||||
const questionScopeId = buildQuestionScopeId({
|
||||
domain: focusDomain,
|
||||
period: focusPeriod,
|
||||
accounts: mergedFocusAccounts,
|
||||
subject: mainRequirement
|
||||
});
|
||||
return {
|
||||
schema_version: stage1Contracts_1.INVESTIGATION_STATE_SCHEMA_VERSION,
|
||||
session_id: previous.session_id,
|
||||
@@ -352,9 +435,11 @@ function updateInvestigationState(input) {
|
||||
turn_index: previous.turn_index + 1,
|
||||
updated_at: input.timestamp,
|
||||
question_id: input.questionId,
|
||||
question_scope_id: questionScopeId,
|
||||
scope_origin: scopeOrigin,
|
||||
focus: {
|
||||
domain: focusDomain,
|
||||
period: detectPeriod(input.userMessage) ?? previous.focus.period,
|
||||
period: focusPeriod,
|
||||
primary_accounts: mergedFocusAccounts,
|
||||
active_query_subject: mainRequirement.slice(0, 180)
|
||||
},
|
||||
@@ -371,7 +456,9 @@ function updateInvestigationState(input) {
|
||||
uncovered_requirement_ids: uncoveredRequirementIds,
|
||||
last_problem_unit_id: lastProblemUnitId,
|
||||
settlement_next_actions: settlementNextActions,
|
||||
evidence_summary: evidenceSummary
|
||||
evidence_summary: evidenceSummary,
|
||||
question_scope_id: questionScopeId,
|
||||
scope_origin: scopeOrigin
|
||||
},
|
||||
query_mode_hint: deriveQueryModeHint(input.routeSummary),
|
||||
...(problemUnitState
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import * as investigationState_1 from "./investigationState";
|
||||
import * as retrievalResultNormalizer_1 from "./retrievalResultNormalizer";
|
||||
import * as questionTypeResolver_1 from "./questionTypeResolver";
|
||||
import * as companyAnchorResolver_1 from "./companyAnchorResolver";
|
||||
import * as assistantRuntimeGuards_1 from "./assistantRuntimeGuards";
|
||||
function retrievalSummaryForRoute(route) {
|
||||
if (route === "store_canonical")
|
||||
return "Canonical accounting data path selected.";
|
||||
@@ -859,6 +860,43 @@ function extractFollowupAccountAnchorsLoose(text) {
|
||||
}
|
||||
return Array.from(new Set(anchors));
|
||||
}
|
||||
function accountPrefixToken(value) {
|
||||
const token = String(value ?? "").trim();
|
||||
const match = token.match(/^(\d{2})/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
function hasCrossScopeConflictWithState(userMessage, state) {
|
||||
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
|
||||
const statePeriod = compactWhitespace(state.focus.period ?? "");
|
||||
if (explicitPeriod && statePeriod && explicitPeriod !== statePeriod) {
|
||||
return true;
|
||||
}
|
||||
const inferredDomain = inferP0DomainFromMessage(userMessage);
|
||||
const stateDomain = compactWhitespace(state.followup_context?.active_domain ?? state.focus.domain ?? "");
|
||||
if (inferredDomain && stateDomain && inferredDomain !== stateDomain) {
|
||||
return true;
|
||||
}
|
||||
const explicitAccounts = extractAccountTokens(userMessage);
|
||||
const fallbackAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
|
||||
const knownAccounts = Array.isArray(state.focus.primary_accounts) ? state.focus.primary_accounts : [];
|
||||
if (fallbackAccounts.length > 0 && knownAccounts.length > 0) {
|
||||
const knownPrefixes = new Set(knownAccounts.map((item) => accountPrefixToken(item)).filter(Boolean));
|
||||
const newPrefixes = new Set(fallbackAccounts.map((item) => accountPrefixToken(item)).filter(Boolean));
|
||||
if (newPrefixes.size > 0 && knownPrefixes.size > 0) {
|
||||
let intersects = false;
|
||||
for (const prefix of newPrefixes) {
|
||||
if (knownPrefixes.has(prefix)) {
|
||||
intersects = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!intersects) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function inferP0DomainFromMessage(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const accountTokens = extractAccountTokens(lower);
|
||||
@@ -971,9 +1009,11 @@ function buildFollowupStateBinding(input) {
|
||||
Boolean(problemState) &&
|
||||
((problemState?.active_problem_units.length ?? 0) > 0 || (problemState?.focus_problem_types.length ?? 0) > 0);
|
||||
const strongNewAnchorDetected = hasStrongFollowupAnchors(userMessage, input.investigationState);
|
||||
const scopeConflictDetected = hasCrossScopeConflictWithState(userMessage, input.investigationState);
|
||||
const periodRefinementFollowup = hasPeriodLiteral(userMessage) && problemContinuityAvailable;
|
||||
const shouldBind = !smallTalkSignal &&
|
||||
!strongNewAnchorDetected &&
|
||||
!scopeConflictDetected &&
|
||||
(followupMarker || referentialPointer || periodRefinementFollowup || (!strongSignal && shortPrompt));
|
||||
if (!shouldBind) {
|
||||
return {
|
||||
@@ -1078,7 +1118,10 @@ function buildFollowupStateBinding(input) {
|
||||
problem_continuity_available: problemContinuityAvailable,
|
||||
problem_continuity_applied: problemContinuityApplied,
|
||||
problem_continuity_skipped_reason: problemContinuityApplied ? null : problemContinuitySkippedReason,
|
||||
strong_new_anchor_detected: strongNewAnchorDetected
|
||||
strong_new_anchor_detected: strongNewAnchorDetected,
|
||||
scope_isolation_applied: true,
|
||||
scope_carryover_allowed: !scopeConflictDetected,
|
||||
scope_reset_reason: scopeConflictDetected ? "cross_scope_conflict" : null
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1147,11 +1190,30 @@ export class AssistantService {
|
||||
useMock: Boolean(payload.useMock)
|
||||
};
|
||||
const normalized = await this.normalizerService.normalize(normalizePayload);
|
||||
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
|
||||
const inferredDomainByMessage = inferP0DomainFromMessage(userMessage);
|
||||
const focusDomainForGuards = inferredDomainByMessage === "settlements_60_62" ||
|
||||
inferredDomainByMessage === "vat_document_register_book" ||
|
||||
inferredDomainByMessage === "month_close_costs_20_44"
|
||||
? inferredDomainByMessage
|
||||
: null;
|
||||
const temporalGuard = (0, assistantRuntimeGuards_1.resolveTemporalGuard)({
|
||||
userMessage,
|
||||
normalized: normalized.normalized,
|
||||
companyAnchors
|
||||
});
|
||||
const domainPolarityGuardInitial = (0, assistantRuntimeGuards_1.resolveDomainPolarityGuard)({
|
||||
userMessage,
|
||||
companyAnchors,
|
||||
focusDomainHint: focusDomainForGuards
|
||||
});
|
||||
const requirementExtraction = extractRequirements(normalized.route_hint_summary, normalized.normalized, userMessage);
|
||||
const executionPlan = toExecutionPlan(normalized.route_hint_summary, normalized.normalized, userMessage, requirementExtraction.byFragment);
|
||||
let executionPlan = toExecutionPlan(normalized.route_hint_summary, normalized.normalized, userMessage, requirementExtraction.byFragment);
|
||||
executionPlan = (0, assistantRuntimeGuards_1.applyTemporalHintToExecutionPlan)(executionPlan, temporalGuard);
|
||||
executionPlan = (0, assistantRuntimeGuards_1.applyPolarityHintToExecutionPlan)(executionPlan, domainPolarityGuardInitial);
|
||||
const retrievalCalls = [];
|
||||
const retrievalResultsRaw = [];
|
||||
const retrievalResults = [];
|
||||
let retrievalResults = [];
|
||||
for (const planItem of executionPlan) {
|
||||
if (!planItem.should_execute) {
|
||||
retrievalCalls.push({
|
||||
@@ -1210,13 +1272,32 @@ export class AssistantService {
|
||||
retrievalResults.push((0, retrievalResultNormalizer_1.normalizeRetrievalResult)(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError));
|
||||
}
|
||||
}
|
||||
const polarityGuardResult = (0, assistantRuntimeGuards_1.applyDomainPolarityGuardToRetrievalResults)({
|
||||
retrievalResults,
|
||||
guard: domainPolarityGuardInitial
|
||||
});
|
||||
retrievalResults = polarityGuardResult.retrievalResults;
|
||||
const evidenceGateResult = (0, assistantRuntimeGuards_1.applyEvidenceAdmissibilityGate)({
|
||||
retrievalResults,
|
||||
temporal: temporalGuard,
|
||||
focusDomainHint: focusDomainForGuards,
|
||||
polarity: polarityGuardResult.audit.polarity,
|
||||
companyAnchors,
|
||||
userMessage
|
||||
});
|
||||
retrievalResults = evidenceGateResult.retrievalResults;
|
||||
const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
|
||||
const groundingCheck = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
|
||||
const groundingCheckBase = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
|
||||
const groundedAnswerEligibilityGuard = (0, assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility)({
|
||||
temporal: temporalGuard,
|
||||
polarity: polarityGuardResult.audit,
|
||||
evidence: evidenceGateResult.audit
|
||||
});
|
||||
const groundingCheck = (0, assistantRuntimeGuards_1.applyEligibilityToGroundingCheck)(groundingCheckBase, groundedAnswerEligibilityGuard);
|
||||
const focusDomainHint = followupBinding.usage?.applied
|
||||
? session.investigation_state?.followup_context?.active_domain ?? session.investigation_state?.focus.domain ?? null
|
||||
: null;
|
||||
const questionTypeClass = (0, questionTypeResolver_1.resolveQuestionType)(userMessage);
|
||||
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
|
||||
const hasPeriodInCompanyAnchors = (Array.isArray(companyAnchors?.dates) && companyAnchors.dates.some((item) => String(item ?? "").trim().length > 0)) ||
|
||||
(Array.isArray(companyAnchors?.periods) && companyAnchors.periods.some((item) => String(item ?? "").trim().length > 0));
|
||||
const normalizationPeriodExplicit = hasExplicitPeriodAnchorFromNormalized(normalized.normalized) || hasPeriodInCompanyAnchors;
|
||||
@@ -1260,7 +1341,8 @@ export class AssistantService {
|
||||
requirements: coverageEvaluation.requirements,
|
||||
coverageReport: coverageEvaluation.coverage,
|
||||
retrievalResults,
|
||||
replyType: composition.reply_type
|
||||
replyType: composition.reply_type,
|
||||
followupApplied: Boolean(followupBinding.usage?.applied)
|
||||
})
|
||||
: null;
|
||||
if (config_1.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && investigationStateSnapshot) {
|
||||
@@ -1288,6 +1370,15 @@ export class AssistantService {
|
||||
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
||||
question_type_class: questionTypeClass,
|
||||
company_anchors: companyAnchors,
|
||||
raw_time_anchor: temporalGuard.raw_time_anchor,
|
||||
resolved_time_anchor: temporalGuard.resolved_time_anchor,
|
||||
temporal_resolution_source: temporalGuard.temporal_resolution_source,
|
||||
temporal_guard_applied: temporalGuard.temporal_guard_applied,
|
||||
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
|
||||
temporal_guard: temporalGuard,
|
||||
domain_polarity_guard: polarityGuardResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
||||
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
||||
@@ -1353,6 +1444,15 @@ export class AssistantService {
|
||||
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
||||
question_type_class: questionTypeClass,
|
||||
company_anchors: companyAnchors,
|
||||
raw_time_anchor: temporalGuard.raw_time_anchor,
|
||||
resolved_time_anchor: temporalGuard.resolved_time_anchor,
|
||||
temporal_resolution_source: temporalGuard.temporal_resolution_source,
|
||||
temporal_guard_applied: temporalGuard.temporal_guard_applied,
|
||||
temporal_guard_outcome: temporalGuard.temporal_guard_outcome,
|
||||
temporal_guard: temporalGuard,
|
||||
domain_polarity_guard: polarityGuardResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
||||
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { RouteHintSummary } from "../types/normalizer";
|
||||
import type {
|
||||
InvestigationLastAnswerMode,
|
||||
InvestigationNarrowingStatus,
|
||||
InvestigationScopeOrigin,
|
||||
InvestigationState
|
||||
} from "../types/stage1Contracts";
|
||||
import {
|
||||
@@ -39,6 +40,7 @@ interface UpdateInvestigationStateInput {
|
||||
coverageReport: RequirementCoverageReport;
|
||||
retrievalResults: UnifiedRetrievalResult[];
|
||||
replyType: InvestigationLastAnswerMode;
|
||||
followupApplied?: boolean;
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]): string[] {
|
||||
@@ -61,6 +63,83 @@ function detectPeriod(text: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectExplicitDomainHint(text: string): string | null {
|
||||
const messageCorpus = String(text ?? "").toLowerCase();
|
||||
const accounts = detectAccounts(text);
|
||||
const hasSettlementSignal =
|
||||
accounts.some((item) => isSettlementAccount(item)) ||
|
||||
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(
|
||||
messageCorpus
|
||||
);
|
||||
if (hasSettlementSignal) {
|
||||
return "settlements_60_62";
|
||||
}
|
||||
const hasVatSignal =
|
||||
accounts.some((item) => isVatAccount(item)) ||
|
||||
/(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
|
||||
if (hasVatSignal) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
const hasCloseSignal =
|
||||
accounts.some((item) => isCloseCostsAccount(item)) ||
|
||||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
|
||||
if (hasCloseSignal) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
const hasFixedAssetSignal =
|
||||
accounts.some((item) => isFixedAssetAccount(item)) ||
|
||||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(
|
||||
messageCorpus
|
||||
);
|
||||
if (hasFixedAssetSignal) {
|
||||
return "fixed_asset_amortization";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildQuestionScopeId(input: {
|
||||
domain: string | null;
|
||||
period: string | null;
|
||||
accounts: string[];
|
||||
subject: string;
|
||||
}): string | null {
|
||||
const domainPart = String(input.domain ?? "").trim();
|
||||
const periodPart = String(input.period ?? "").trim();
|
||||
const accountPart = capStrings(input.accounts.map((item) => String(item ?? "").trim()).filter(Boolean), 4).join(",");
|
||||
const subjectPart = String(input.subject ?? "").trim().slice(0, 96).toLowerCase();
|
||||
const parts = [
|
||||
domainPart ? `d:${domainPart}` : "",
|
||||
periodPart ? `p:${periodPart}` : "",
|
||||
accountPart ? `a:${accountPart}` : "",
|
||||
subjectPart ? `s:${subjectPart}` : ""
|
||||
].filter(Boolean);
|
||||
if (parts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return parts.join("|");
|
||||
}
|
||||
|
||||
function deriveScopeOrigin(input: {
|
||||
followupApplied: boolean;
|
||||
userMessage: string;
|
||||
routeSummary: RouteHintSummary | null;
|
||||
}): InvestigationScopeOrigin {
|
||||
if (input.followupApplied) {
|
||||
return "followup_state_carryover";
|
||||
}
|
||||
const hasExplicitPeriod = Boolean(detectPeriod(input.userMessage));
|
||||
const hasExplicitAccounts = detectAccounts(input.userMessage).length > 0;
|
||||
const explicitDomain = detectExplicitDomainHint(input.userMessage);
|
||||
if (hasExplicitPeriod || hasExplicitAccounts || explicitDomain) {
|
||||
return "explicit_from_message";
|
||||
}
|
||||
const routeDomain = deriveDomain(input.routeSummary);
|
||||
if (routeDomain && routeDomain !== "no_route") {
|
||||
return "route_derived";
|
||||
}
|
||||
return "underspecified";
|
||||
}
|
||||
|
||||
function deriveDomain(routeSummary: RouteHintSummary | null): string | null {
|
||||
if (!routeSummary) return null;
|
||||
if (routeSummary.mode === "legacy_v1") {
|
||||
@@ -165,9 +244,12 @@ function inferFollowupActiveDomain(input: {
|
||||
focusAccounts: string[];
|
||||
routeSummary: RouteHintSummary | null;
|
||||
previous: InvestigationStateWithProblemUnits;
|
||||
allowStateCarryover: boolean;
|
||||
}): string | null {
|
||||
const messageCorpus = String(input.userMessage ?? "").toLowerCase();
|
||||
const contextualCorpus = `${messageCorpus} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase();
|
||||
const contextualCorpus = input.allowStateCarryover
|
||||
? `${messageCorpus} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase()
|
||||
: messageCorpus;
|
||||
|
||||
const hasFixedAssetLexicalSignal =
|
||||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(
|
||||
@@ -206,6 +288,7 @@ function inferFollowupActiveDomain(input: {
|
||||
}
|
||||
|
||||
if (
|
||||
input.allowStateCarryover &&
|
||||
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|аванс|долг|settlement|payment)/i.test(contextualCorpus) &&
|
||||
(input.previous.followup_context?.active_domain === "settlements_60_62" ||
|
||||
input.previous.focus.domain === "settlements_60_62")
|
||||
@@ -218,7 +301,11 @@ function inferFollowupActiveDomain(input: {
|
||||
return routeDomain;
|
||||
}
|
||||
|
||||
return input.previous.followup_context?.active_domain ?? input.previous.focus.domain ?? null;
|
||||
if (input.allowStateCarryover) {
|
||||
return input.previous.followup_context?.active_domain ?? input.previous.focus.domain ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectUncoveredRequirementIds(coverageReport: RequirementCoverageReport): string[] {
|
||||
@@ -447,6 +534,8 @@ export function createEmptyInvestigationState(
|
||||
turn_index: 0,
|
||||
updated_at: timestamp,
|
||||
question_id: null,
|
||||
question_scope_id: null,
|
||||
scope_origin: null,
|
||||
focus: {
|
||||
domain: null,
|
||||
period: null,
|
||||
@@ -464,11 +553,11 @@ export function createEmptyInvestigationState(
|
||||
|
||||
export function updateInvestigationState(input: UpdateInvestigationStateInput): InvestigationStateWithProblemUnits {
|
||||
const previous = input.previous;
|
||||
const followupApplied = input.followupApplied === true;
|
||||
const focusFromMessage = capStrings(detectAccounts(input.userMessage), INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
|
||||
const mergedFocusAccounts = capStrings(
|
||||
[...focusFromMessage, ...previous.focus.primary_accounts],
|
||||
INVESTIGATION_MAX_PRIMARY_ACCOUNTS
|
||||
);
|
||||
const mergedFocusAccounts = followupApplied
|
||||
? capStrings([...focusFromMessage, ...previous.focus.primary_accounts], INVESTIGATION_MAX_PRIMARY_ACCOUNTS)
|
||||
: capStrings(focusFromMessage, INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
|
||||
const requirementIds = capStrings(
|
||||
input.requirements.map((item) => item.requirement_id),
|
||||
INVESTIGATION_MAX_REQUIREMENT_LINKS
|
||||
@@ -476,16 +565,31 @@ export function updateInvestigationState(input: UpdateInvestigationStateInput):
|
||||
const mainRequirement = input.requirements[0]?.requirement_text ?? input.userMessage;
|
||||
const problemUnitState = updateProblemUnitState(previous, input.retrievalResults);
|
||||
const uncoveredRequirementIds = collectUncoveredRequirementIds(input.coverageReport);
|
||||
const routeDomain = deriveDomain(input.routeSummary);
|
||||
const activeDomain = inferFollowupActiveDomain({
|
||||
userMessage: input.userMessage,
|
||||
focusAccounts: focusFromMessage,
|
||||
routeSummary: input.routeSummary,
|
||||
previous
|
||||
previous,
|
||||
allowStateCarryover: followupApplied
|
||||
});
|
||||
const focusDomain = activeDomain ?? deriveDomain(input.routeSummary) ?? previous.focus.domain;
|
||||
const focusDomain = activeDomain ?? routeDomain ?? (followupApplied ? previous.focus.domain : null);
|
||||
const detectedPeriod = detectPeriod(input.userMessage);
|
||||
const focusPeriod = detectedPeriod ?? (followupApplied ? previous.focus.period : null);
|
||||
const settlementNextActions = settlementFocusActions(activeDomain);
|
||||
const lastProblemUnitId = problemUnitState?.active_problem_units[0] ?? null;
|
||||
const evidenceSummary = collectEvidenceSummary(input.retrievalResults);
|
||||
const scopeOrigin = deriveScopeOrigin({
|
||||
followupApplied,
|
||||
userMessage: input.userMessage,
|
||||
routeSummary: input.routeSummary
|
||||
});
|
||||
const questionScopeId = buildQuestionScopeId({
|
||||
domain: focusDomain,
|
||||
period: focusPeriod,
|
||||
accounts: mergedFocusAccounts,
|
||||
subject: mainRequirement
|
||||
});
|
||||
|
||||
return {
|
||||
schema_version: INVESTIGATION_STATE_SCHEMA_VERSION,
|
||||
@@ -494,9 +598,11 @@ export function updateInvestigationState(input: UpdateInvestigationStateInput):
|
||||
turn_index: previous.turn_index + 1,
|
||||
updated_at: input.timestamp,
|
||||
question_id: input.questionId,
|
||||
question_scope_id: questionScopeId,
|
||||
scope_origin: scopeOrigin,
|
||||
focus: {
|
||||
domain: focusDomain,
|
||||
period: detectPeriod(input.userMessage) ?? previous.focus.period,
|
||||
period: focusPeriod,
|
||||
primary_accounts: mergedFocusAccounts,
|
||||
active_query_subject: mainRequirement.slice(0, 180)
|
||||
},
|
||||
@@ -516,7 +622,9 @@ export function updateInvestigationState(input: UpdateInvestigationStateInput):
|
||||
uncovered_requirement_ids: uncoveredRequirementIds,
|
||||
last_problem_unit_id: lastProblemUnitId,
|
||||
settlement_next_actions: settlementNextActions,
|
||||
evidence_summary: evidenceSummary
|
||||
evidence_summary: evidenceSummary,
|
||||
question_scope_id: questionScopeId,
|
||||
scope_origin: scopeOrigin
|
||||
},
|
||||
query_mode_hint: deriveQueryModeHint(input.routeSummary),
|
||||
...(problemUnitState
|
||||
|
||||
@@ -67,9 +67,66 @@ export interface FollowupStateUsageDebug {
|
||||
problem_continuity_applied?: boolean;
|
||||
problem_continuity_skipped_reason?: string | null;
|
||||
strong_new_anchor_detected?: boolean;
|
||||
scope_isolation_applied?: boolean;
|
||||
scope_carryover_allowed?: boolean;
|
||||
scope_reset_reason?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TemporalGuardDebug {
|
||||
raw_time_anchor: string | null;
|
||||
resolved_time_anchor: string | null;
|
||||
temporal_resolution_source: string;
|
||||
temporal_guard_applied: boolean;
|
||||
temporal_guard_outcome: "passed" | "failed_out_of_snapshot_window" | "ambiguous_limited";
|
||||
primary_period_window: {
|
||||
from: string;
|
||||
to: string;
|
||||
granularity: "day" | "month";
|
||||
} | null;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
export interface DomainPolarityGuardDebug {
|
||||
applied: boolean;
|
||||
polarity: "supplier_payable" | "customer_receivable" | "mixed_or_unresolved" | "not_applicable";
|
||||
outcome: "passed" | "limited_unresolved_polarity" | "blocked_conflict" | "not_applicable";
|
||||
supplier_score: number;
|
||||
customer_score: number;
|
||||
account_scope: string[];
|
||||
rejected_problem_units: number;
|
||||
rejected_evidence: number;
|
||||
critical_contradiction: boolean;
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
export interface EvidenceAdmissibilityGateDebug {
|
||||
candidate_evidence_total: number;
|
||||
admissible_evidence_count: number;
|
||||
rejected_evidence_count: number;
|
||||
rejected_item_count: number;
|
||||
reject_breakdown: Record<
|
||||
"wrong_period" | "wrong_domain" | "wrong_account_scope" | "weak_source_mapping" | "zero_live_match" | "future_dated_or_out_of_window",
|
||||
number
|
||||
>;
|
||||
category_breakdown: {
|
||||
hard_evidence: number;
|
||||
supporting_signal: number;
|
||||
inadmissible_noise: number;
|
||||
};
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
export interface GroundedAnswerEligibilityGuardDebug {
|
||||
eligible: boolean;
|
||||
temporal_passed: boolean;
|
||||
polarity_passed: boolean;
|
||||
admissible_evidence_count: number;
|
||||
critical_contradiction: boolean;
|
||||
outcome: "grounded_allowed" | "limited_or_insufficient_evidence";
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
export interface AssistantMessageRequestPayload {
|
||||
session_id?: string;
|
||||
user_message?: string;
|
||||
@@ -132,6 +189,15 @@ export interface AssistantDebugPayload {
|
||||
retrieval_results: UnifiedRetrievalResult[];
|
||||
answer_grounding_check: AnswerGroundingCheck;
|
||||
dropped_intent_segments: string[];
|
||||
raw_time_anchor?: string | null;
|
||||
resolved_time_anchor?: string | null;
|
||||
temporal_resolution_source?: string;
|
||||
temporal_guard_applied?: boolean;
|
||||
temporal_guard_outcome?: TemporalGuardDebug["temporal_guard_outcome"];
|
||||
temporal_guard?: TemporalGuardDebug;
|
||||
domain_polarity_guard?: DomainPolarityGuardDebug;
|
||||
evidence_admissibility_gate?: EvidenceAdmissibilityGateDebug;
|
||||
grounded_answer_eligibility_guard?: GroundedAnswerEligibilityGuardDebug;
|
||||
followup_state_usage?: FollowupStateUsageDebug;
|
||||
problem_centric_answer_applied?: boolean;
|
||||
problem_units_used_count?: number;
|
||||
|
||||
@@ -10,6 +10,11 @@ export const INVESTIGATION_MAX_REQUIREMENT_LINKS = 8;
|
||||
|
||||
export type InvestigationNarrowingStatus = "unknown" | "not_needed" | "applied" | "needs_clarification" | "broad_guarded";
|
||||
export type InvestigationQueryModeHint = "direct_answer" | "investigation_candidate";
|
||||
export type InvestigationScopeOrigin =
|
||||
| "explicit_from_message"
|
||||
| "followup_state_carryover"
|
||||
| "route_derived"
|
||||
| "underspecified";
|
||||
export type InvestigationLastAnswerMode =
|
||||
| "factual"
|
||||
| "factual_with_explanation"
|
||||
@@ -39,6 +44,8 @@ export interface InvestigationFollowupContext {
|
||||
last_problem_unit_id?: string | null;
|
||||
settlement_next_actions?: string[];
|
||||
evidence_summary?: string[];
|
||||
question_scope_id?: string | null;
|
||||
scope_origin?: InvestigationScopeOrigin | null;
|
||||
}
|
||||
|
||||
export interface InvestigationState {
|
||||
@@ -48,6 +55,8 @@ export interface InvestigationState {
|
||||
turn_index: number;
|
||||
updated_at: string;
|
||||
question_id: string | null;
|
||||
question_scope_id?: string | null;
|
||||
scope_origin?: InvestigationScopeOrigin | null;
|
||||
focus: InvestigationStateFocus;
|
||||
narrowing_status: InvestigationNarrowingStatus;
|
||||
evidence_refs: string[];
|
||||
|
||||
@@ -28,6 +28,11 @@ describe("assistant mode API", () => {
|
||||
expect(typeof response.body.debug?.answer_grounding_check?.status).toBe("string");
|
||||
expect(Array.isArray(response.body.debug?.retrieval_status)).toBe(true);
|
||||
expect(Array.isArray(response.body.debug?.retrieval_results)).toBe(true);
|
||||
expect(typeof response.body.debug?.temporal_guard_applied).toBe("boolean");
|
||||
expect(typeof response.body.debug?.temporal_guard_outcome).toBe("string");
|
||||
expect(response.body.debug?.domain_polarity_guard).toBeTruthy();
|
||||
expect(response.body.debug?.evidence_admissibility_gate).toBeTruthy();
|
||||
expect(response.body.debug?.grounded_answer_eligibility_guard).toBeTruthy();
|
||||
expect(Array.isArray(response.body.conversation)).toBe(true);
|
||||
expect(response.body.conversation.length).toBe(2);
|
||||
});
|
||||
@@ -133,7 +138,7 @@ describe("assistant mode API", () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.reply_type).not.toBe("route_mismatch_blocked");
|
||||
expect(response.body.debug?.answer_grounding_check?.status).not.toBe("route_mismatch_blocked");
|
||||
expect(["partial", "grounded"]).toContain(String(response.body.debug?.answer_grounding_check?.status));
|
||||
expect(["partial", "grounded", "no_grounded_answer"]).toContain(String(response.body.debug?.answer_grounding_check?.status));
|
||||
expect(response.body.reply_type).toBe("partial_coverage");
|
||||
});
|
||||
|
||||
@@ -150,7 +155,7 @@ describe("assistant mode API", () => {
|
||||
expect(["partial_coverage", "clarification_required", "route_mismatch_blocked", "factual_with_explanation"]).toContain(
|
||||
String(response.body.reply_type)
|
||||
);
|
||||
expect(["partial", "grounded", "route_mismatch_blocked"]).toContain(
|
||||
expect(["partial", "grounded", "route_mismatch_blocked", "no_grounded_answer"]).toContain(
|
||||
String(response.body.debug?.answer_grounding_check?.status)
|
||||
);
|
||||
expect(typeof response.body.debug?.answer_grounding_check?.route_subject_match).toBe("boolean");
|
||||
|
||||
@@ -194,6 +194,46 @@ describe.sequential("assistant follow-up state binding", () => {
|
||||
expect(second.body.debug?.investigation_state_snapshot?.turn_index).toBe(2);
|
||||
});
|
||||
|
||||
it("isolates scope for independent cross-domain turn and does not carry stale period/domain", async () => {
|
||||
const app = await createAppWithFlags({
|
||||
state: "1",
|
||||
binding: "1",
|
||||
problemUnits: "1",
|
||||
continuity: "1",
|
||||
answerPolicy: "1",
|
||||
problemCentric: "1"
|
||||
});
|
||||
const sessionId = `asst-wave17-scope-isolation-${Date.now()}`;
|
||||
|
||||
const first = await request(app).post("/api/assistant/message").send({
|
||||
session_id: sessionId,
|
||||
useMock: true,
|
||||
promptVersion: "normalizer_v2_0_2",
|
||||
user_message: "Проверь хвосты по счету 60.01 за 2020-06 и почему не закрылся долг."
|
||||
});
|
||||
expect(first.status).toBe(200);
|
||||
expect(first.body.debug?.investigation_state_snapshot?.question_scope_id).toContain("d:settlements_60_62");
|
||||
expect(first.body.debug?.investigation_state_snapshot?.scope_origin).toBe("explicit_from_message");
|
||||
|
||||
const second = await request(app).post("/api/assistant/message").send({
|
||||
session_id: sessionId,
|
||||
useMock: true,
|
||||
promptVersion: "normalizer_v2_0_2",
|
||||
user_message: "Проверь НДС по счету-фактуре: где разрыв в цепочке документ -> регистр -> книга?"
|
||||
});
|
||||
|
||||
expect(second.status).toBe(200);
|
||||
expect(second.body.debug?.followup_state_usage).toBeUndefined();
|
||||
expect(second.body.debug?.investigation_state_snapshot?.scope_origin).toBe("explicit_from_message");
|
||||
expect(String(second.body.debug?.investigation_state_snapshot?.question_scope_id ?? "")).toContain(
|
||||
"d:vat_document_register_book"
|
||||
);
|
||||
expect(second.body.debug?.investigation_state_snapshot?.focus?.period).not.toBe("2020-06");
|
||||
expect(String(second.body.debug?.investigation_state_snapshot?.focus?.domain ?? "")).not.toContain("settlements_60_62");
|
||||
expect(second.body.debug?.investigation_state_snapshot?.followup_context?.question_scope_id).toBeTruthy();
|
||||
expect(second.body.debug?.investigation_state_snapshot?.followup_context?.scope_origin).toBe("explicit_from_message");
|
||||
});
|
||||
|
||||
it("rebinds follow-up domain away from settlements on fixed-asset amortization query", async () => {
|
||||
const app = await createAppWithFlags({
|
||||
state: "1",
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveCompanyAnchors } from "../src/services/companyAnchorResolver";
|
||||
import {
|
||||
applyDomainPolarityGuardToRetrievalResults,
|
||||
applyEligibilityToGroundingCheck,
|
||||
applyEvidenceAdmissibilityGate,
|
||||
applyPolarityHintToExecutionPlan,
|
||||
applyTemporalHintToExecutionPlan,
|
||||
evaluateGroundedAnswerEligibility,
|
||||
resolveDomainPolarityGuard,
|
||||
resolveTemporalGuard
|
||||
} from "../src/services/assistantRuntimeGuards";
|
||||
|
||||
function buildProblemUnit(input: {
|
||||
id: string;
|
||||
lifecycleDomain: string;
|
||||
defect?: string;
|
||||
account?: string;
|
||||
}): any {
|
||||
return {
|
||||
schema_version: "problem_unit_v0_1",
|
||||
problem_unit_id: input.id,
|
||||
problem_unit_type: "unresolved_settlement_cluster",
|
||||
title: "test unit",
|
||||
mechanism_summary: input.defect ?? "mechanism",
|
||||
business_defect_class: input.defect ?? "mechanism",
|
||||
severity: { score: 0.8, grade: "high" },
|
||||
confidence: { score: 0.7, grade: "medium" },
|
||||
lifecycle_domain: input.lifecycleDomain,
|
||||
affected_entities: ["Document:DOC-1"],
|
||||
affected_documents: ["Document:DOC-1"],
|
||||
affected_postings: ["Posting:POST-1"],
|
||||
affected_accounts: [input.account ?? "60"],
|
||||
affected_counterparties: ["Counterparty:CP-1"],
|
||||
affected_contracts: ["Contract:CTR-1"],
|
||||
failed_expected_edge: "payment_to_settlement",
|
||||
period_impact: { is_period_sensitive: true, impact_class: "close_risk" },
|
||||
evidence_pack: ["cand-1"],
|
||||
entity_backlinks: [{ entity: "Document", id: "DOC-1" }],
|
||||
snapshot_limitations: []
|
||||
};
|
||||
}
|
||||
|
||||
function buildEvidence(input: {
|
||||
id: string;
|
||||
period: string;
|
||||
accountDebit?: string;
|
||||
accountCredit?: string;
|
||||
limitationCode?: string | null;
|
||||
sourceLayer?: string | null;
|
||||
}): any {
|
||||
return {
|
||||
evidence_id: input.id,
|
||||
claim_ref: "requirement:R1",
|
||||
source_type: "retrieval_item",
|
||||
source_ref: {
|
||||
schema_version: "evidence_source_ref_v1",
|
||||
namespace: "unknown",
|
||||
entity: "MCPLiveMovement",
|
||||
id: input.id,
|
||||
period: input.period,
|
||||
canonical_ref: `evidence_source_ref_v1|unknown|mcplivemovement|${String(input.id).toLowerCase()}|${input.period}`
|
||||
},
|
||||
pointer: {
|
||||
fragment_id: "F1",
|
||||
route: "hybrid_store_plus_live",
|
||||
source: {
|
||||
namespace: "unknown",
|
||||
entity: "MCPLiveMovement",
|
||||
id: input.id,
|
||||
period: input.period
|
||||
},
|
||||
locator: {
|
||||
field_path: "amount",
|
||||
item_index: 0
|
||||
}
|
||||
},
|
||||
evidence_kind: "mechanism_link",
|
||||
mechanism_note: "payment_to_settlement",
|
||||
confidence: "medium",
|
||||
limitation: input.limitationCode
|
||||
? {
|
||||
reason_code: input.limitationCode,
|
||||
note: null
|
||||
}
|
||||
: null,
|
||||
payload: {
|
||||
period: input.period,
|
||||
account_debit: input.accountDebit ?? null,
|
||||
account_credit: input.accountCredit ?? null,
|
||||
source_layer: input.sourceLayer ?? null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildRetrieval(input?: Partial<any>): any {
|
||||
return {
|
||||
fragment_id: "F1",
|
||||
requirement_ids: ["R1"],
|
||||
route: "hybrid_store_plus_live",
|
||||
status: "ok",
|
||||
result_type: "chain",
|
||||
items: [],
|
||||
summary: {
|
||||
semantic_profile: {
|
||||
account_scope: ["60"],
|
||||
domain_scope: ["settlements", "customers"],
|
||||
relation_patterns: ["payment_to_settlement"]
|
||||
},
|
||||
live_mcp: {
|
||||
matched_rows: 0,
|
||||
account_scope: ["60"]
|
||||
}
|
||||
},
|
||||
evidence: [],
|
||||
candidate_evidence: [],
|
||||
problem_units: [],
|
||||
problem_unit_summary: null,
|
||||
why_included: ["test"],
|
||||
selection_reason: ["test"],
|
||||
risk_factors: ["test"],
|
||||
business_interpretation: ["test"],
|
||||
confidence: "medium",
|
||||
limitations: [],
|
||||
errors: [],
|
||||
...input
|
||||
};
|
||||
}
|
||||
|
||||
describe("stage4 blocker-pack runtime guards", () => {
|
||||
it("flags temporal anchor drift outside July 2020 snapshot", () => {
|
||||
const userMessage = "Почему по оплате от 6 июля 2020 долг по поставщику остался?";
|
||||
const temporal = resolveTemporalGuard({
|
||||
userMessage,
|
||||
companyAnchors: resolveCompanyAnchors(userMessage),
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
fragments: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
time_scope: {
|
||||
type: "explicit",
|
||||
value: "2023-07-06",
|
||||
confidence: "high"
|
||||
}
|
||||
}
|
||||
]
|
||||
} as any
|
||||
});
|
||||
expect(temporal.temporal_guard_applied).toBe(true);
|
||||
expect(temporal.temporal_guard_outcome).toBe("failed_out_of_snapshot_window");
|
||||
expect(temporal.resolved_time_anchor).toBe("2020-07-06");
|
||||
expect(temporal.reason_codes).toContain("normalized_anchor_out_of_snapshot_window");
|
||||
});
|
||||
|
||||
it("locks July month window when question has month-only anchor", () => {
|
||||
const userMessage = "В июльском срезе почему по счету 60 остался хвост?";
|
||||
const temporal = resolveTemporalGuard({
|
||||
userMessage,
|
||||
companyAnchors: resolveCompanyAnchors(userMessage),
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
fragments: []
|
||||
} as any
|
||||
});
|
||||
const hintedPlan = applyTemporalHintToExecutionPlan(
|
||||
[
|
||||
{
|
||||
should_execute: true,
|
||||
fragment_text: "проверить зависший долг по поставщику"
|
||||
}
|
||||
],
|
||||
temporal
|
||||
);
|
||||
expect(temporal.temporal_guard_applied).toBe(true);
|
||||
expect(temporal.temporal_guard_outcome).toBe("passed");
|
||||
expect(temporal.resolved_time_anchor).toBe("2020-07");
|
||||
expect(hintedPlan[0].fragment_text).toMatch(/июля 2020|2020-07-01/);
|
||||
});
|
||||
|
||||
it("filters customer settlement semantics from supplier/payable case", () => {
|
||||
const guard = resolveDomainPolarityGuard({
|
||||
userMessage: "По поставщику и счету 60 долг не закрылся после оплаты.",
|
||||
focusDomainHint: "settlements_60_62"
|
||||
});
|
||||
const withHint = applyPolarityHintToExecutionPlan(
|
||||
[
|
||||
{
|
||||
should_execute: true,
|
||||
fragment_text: "проверить цепочку закрытия долга"
|
||||
}
|
||||
],
|
||||
guard
|
||||
);
|
||||
const result = applyDomainPolarityGuardToRetrievalResults({
|
||||
guard,
|
||||
retrievalResults: [
|
||||
buildRetrieval({
|
||||
problem_units: [
|
||||
buildProblemUnit({
|
||||
id: "pu-supplier",
|
||||
lifecycleDomain: "bank_settlement",
|
||||
defect: "payment_to_settlement",
|
||||
account: "60"
|
||||
}),
|
||||
buildProblemUnit({
|
||||
id: "pu-customer",
|
||||
lifecycleDomain: "customer_settlement",
|
||||
defect: "stale_receivable",
|
||||
account: "62"
|
||||
})
|
||||
],
|
||||
evidence: [
|
||||
buildEvidence({
|
||||
id: "ev-supplier",
|
||||
period: "2020-07",
|
||||
accountDebit: "60.01"
|
||||
}),
|
||||
buildEvidence({
|
||||
id: "ev-customer",
|
||||
period: "2020-07",
|
||||
accountDebit: "62.01"
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
expect(guard.polarity).toBe("supplier_payable");
|
||||
expect(withHint[0].fragment_text).toMatch(/счет 60|поставщиком/i);
|
||||
expect(result.audit.outcome).toBe("passed");
|
||||
expect(result.audit.rejected_problem_units).toBeGreaterThan(0);
|
||||
expect(result.audit.rejected_evidence).toBeGreaterThan(0);
|
||||
const units = result.retrievalResults[0].problem_units ?? [];
|
||||
expect(units.some((item: any) => item.lifecycle_domain === "customer_settlement")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects inadmissible live evidence on zero matched_rows and wrong account/date", () => {
|
||||
const userMessage = "Почему по поставщику по счету 60 в июле 2020 хвост не закрыт?";
|
||||
const temporal = resolveTemporalGuard({
|
||||
userMessage,
|
||||
companyAnchors: resolveCompanyAnchors(userMessage),
|
||||
normalized: {
|
||||
schema_version: "normalized_query_v2_0_2",
|
||||
fragments: [
|
||||
{
|
||||
fragment_id: "F1",
|
||||
time_scope: {
|
||||
type: "explicit",
|
||||
value: "2020-07",
|
||||
confidence: "high"
|
||||
}
|
||||
}
|
||||
]
|
||||
} as any
|
||||
});
|
||||
const polarity = resolveDomainPolarityGuard({
|
||||
userMessage,
|
||||
focusDomainHint: "settlements_60_62"
|
||||
});
|
||||
const gated = applyEvidenceAdmissibilityGate({
|
||||
retrievalResults: [
|
||||
buildRetrieval({
|
||||
items: [
|
||||
{
|
||||
source_entity: "MCPLiveMovement",
|
||||
source_layer: "mcp_live_probe",
|
||||
account_debit: "68.02",
|
||||
account_credit: "19.04",
|
||||
period: "2026-02-01"
|
||||
}
|
||||
],
|
||||
evidence: [
|
||||
buildEvidence({
|
||||
id: "ev-live-1",
|
||||
period: "2026-02-01",
|
||||
accountDebit: "68.02",
|
||||
accountCredit: "19.04",
|
||||
limitationCode: "weak_source_mapping",
|
||||
sourceLayer: "mcp_live_probe"
|
||||
})
|
||||
]
|
||||
})
|
||||
],
|
||||
temporal,
|
||||
polarity: polarity.polarity,
|
||||
focusDomainHint: "settlements_60_62",
|
||||
userMessage,
|
||||
companyAnchors: resolveCompanyAnchors(userMessage)
|
||||
});
|
||||
expect(gated.audit.admissible_evidence_count).toBe(0);
|
||||
expect(gated.audit.rejected_evidence_count).toBe(1);
|
||||
expect(gated.audit.reject_breakdown.zero_live_match).toBeGreaterThan(0);
|
||||
expect(gated.audit.reject_breakdown.wrong_account_scope).toBeGreaterThan(0);
|
||||
expect(gated.audit.reject_breakdown.future_dated_or_out_of_window).toBeGreaterThan(0);
|
||||
expect(gated.retrievalResults[0].evidence).toHaveLength(0);
|
||||
expect(gated.retrievalResults[0].items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("degrades grounded status when eligibility guard fails", () => {
|
||||
const eligibility = evaluateGroundedAnswerEligibility({
|
||||
temporal: {
|
||||
raw_time_anchor: "6 июля 2020",
|
||||
resolved_time_anchor: "2020-07-06",
|
||||
temporal_resolution_source: "company_snapshot_july_day_lock",
|
||||
temporal_guard_applied: true,
|
||||
temporal_guard_outcome: "failed_out_of_snapshot_window",
|
||||
primary_period_window: {
|
||||
from: "2020-07-06",
|
||||
to: "2020-07-06",
|
||||
granularity: "day"
|
||||
},
|
||||
reason_codes: ["normalized_anchor_out_of_snapshot_window"]
|
||||
},
|
||||
polarity: {
|
||||
applied: true,
|
||||
polarity: "supplier_payable",
|
||||
outcome: "passed",
|
||||
supplier_score: 3,
|
||||
customer_score: 0,
|
||||
account_scope: ["60"],
|
||||
rejected_problem_units: 0,
|
||||
rejected_evidence: 0,
|
||||
critical_contradiction: false,
|
||||
reason_codes: []
|
||||
},
|
||||
evidence: {
|
||||
candidate_evidence_total: 2,
|
||||
admissible_evidence_count: 0,
|
||||
rejected_evidence_count: 2,
|
||||
rejected_item_count: 1,
|
||||
reject_breakdown: {
|
||||
wrong_period: 1,
|
||||
wrong_domain: 0,
|
||||
wrong_account_scope: 1,
|
||||
weak_source_mapping: 0,
|
||||
zero_live_match: 0,
|
||||
future_dated_or_out_of_window: 1
|
||||
},
|
||||
category_breakdown: {
|
||||
hard_evidence: 0,
|
||||
supporting_signal: 0,
|
||||
inadmissible_noise: 2
|
||||
},
|
||||
reason_codes: ["no_admissible_evidence_for_grounded_answer"]
|
||||
}
|
||||
});
|
||||
const grounded = applyEligibilityToGroundingCheck(
|
||||
{
|
||||
status: "grounded",
|
||||
reasons: []
|
||||
},
|
||||
eligibility
|
||||
);
|
||||
expect(eligibility.eligible).toBe(false);
|
||||
expect(eligibility.reason_codes).toContain("admissible_evidence_count_zero");
|
||||
expect(grounded.status).toBe("no_grounded_answer");
|
||||
expect(grounded.reasons.join(" ")).toMatch(/Недостаточно допустимого evidence|Temporal anchor/i);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildConversationExportForCopy,
|
||||
sanitizeConversationExportText,
|
||||
type ConversationExportItem
|
||||
} from "../../frontend/src/utils/conversationExport";
|
||||
|
||||
function buildConversation(): ConversationExportItem[] {
|
||||
return [
|
||||
{
|
||||
message_id: "m-user-1",
|
||||
role: "user",
|
||||
text: "Почему не закрывается РБП за июль?",
|
||||
reply_type: null,
|
||||
created_at: "2026-03-28T18:00:00.000Z",
|
||||
trace_id: null,
|
||||
debug: null
|
||||
},
|
||||
{
|
||||
message_id: "m-asst-1",
|
||||
role: "assistant",
|
||||
text: [
|
||||
"Коротко: есть признаки неполного списания РБП.",
|
||||
"",
|
||||
"### debug_payload_json",
|
||||
"```json",
|
||||
"{\"route_summary\":\"hybrid_store_plus_live\",\"domain_scope\":[\"deferred_expense\"]}",
|
||||
"```"
|
||||
].join("\n"),
|
||||
reply_type: "partial_coverage",
|
||||
created_at: "2026-03-28T18:00:05.000Z",
|
||||
trace_id: "tr-1",
|
||||
debug: {
|
||||
route_summary: "hybrid_store_plus_live",
|
||||
coverage_report: { requirements_covered: 0 }
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
describe("conversation export regression", () => {
|
||||
it("default export excludes debug payload sections and keeps user-facing text", () => {
|
||||
const exportText = buildConversationExportForCopy("sess-1", buildConversation(), "default");
|
||||
|
||||
expect(exportText).toContain("export_mode: default");
|
||||
expect(exportText).toContain("Коротко: есть признаки неполного списания РБП.");
|
||||
expect(exportText).not.toContain("### debug_payload_json");
|
||||
expect(exportText).not.toContain("### technical_debug_payload_json");
|
||||
expect(exportText).not.toContain("coverage_report");
|
||||
expect(exportText).not.toContain("domain_scope");
|
||||
expect(exportText).not.toContain("route_summary");
|
||||
});
|
||||
|
||||
it("technical export includes debug payload in dedicated section only", () => {
|
||||
const exportText = buildConversationExportForCopy("sess-1", buildConversation(), "technical");
|
||||
|
||||
expect(exportText).toContain("export_mode: technical");
|
||||
expect(exportText).toContain("### technical_debug_payload_json");
|
||||
expect(exportText).toContain("\"route_summary\": \"hybrid_store_plus_live\"");
|
||||
expect(exportText).not.toContain("### debug_payload_json");
|
||||
});
|
||||
|
||||
it("sanitizeConversationExportText removes technical tails", () => {
|
||||
const source = [
|
||||
"Ответ для пользователя.",
|
||||
"domain_scope: [vat_flow]",
|
||||
"coverage_report: {...}",
|
||||
"### technical_breakdown_json",
|
||||
"```json",
|
||||
"{\"internal\":\"yes\"}",
|
||||
"```"
|
||||
].join("\n");
|
||||
|
||||
const sanitized = sanitizeConversationExportText(source);
|
||||
expect(sanitized).toContain("Ответ для пользователя.");
|
||||
expect(sanitized).not.toContain("domain_scope");
|
||||
expect(sanitized).not.toContain("coverage_report");
|
||||
expect(sanitized).not.toContain("technical_breakdown_json");
|
||||
expect(sanitized).not.toContain("internal");
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,8 @@ describe("investigation_state flow scaffolding", () => {
|
||||
|
||||
expect(first.status).toBe(200);
|
||||
expect(first.body.debug?.investigation_state_snapshot?.schema_version).toBe("investigation_state_v1");
|
||||
expect(first.body.debug?.investigation_state_snapshot?.question_scope_id).toBeTruthy();
|
||||
expect(first.body.debug?.investigation_state_snapshot?.scope_origin).toBe("explicit_from_message");
|
||||
expect(first.body.debug?.answer_structure_v11?.schema_version).toBe("answer_structure_v1_1");
|
||||
expect(first.body.debug?.followup_state_usage).toBeUndefined();
|
||||
|
||||
@@ -38,6 +40,8 @@ describe("investigation_state flow scaffolding", () => {
|
||||
expect(second.status).toBe(200);
|
||||
expect(second.body.debug?.investigation_state_snapshot?.turn_index).toBe(2);
|
||||
expect(second.body.debug?.followup_state_usage?.applied).toBe(true);
|
||||
expect(second.body.debug?.investigation_state_snapshot?.question_scope_id).toBeTruthy();
|
||||
expect(second.body.debug?.investigation_state_snapshot?.followup_context?.question_scope_id).toBeTruthy();
|
||||
|
||||
const sessionResponse = await request(app).get(`/api/assistant/session/${sessionId}`);
|
||||
expect(sessionResponse.status).toBe(200);
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"run_id": "eval-RvKNH7IkRp",
|
||||
"timestamp": "2026-03-28T18:15:42.762Z",
|
||||
"mode": "single-pass-strict",
|
||||
"use_mock": true,
|
||||
"prompt_version": "normalizer_v2_0_2",
|
||||
"schema_version": "v2_0_2",
|
||||
"dataset": {
|
||||
"source": "inline_raw_questions",
|
||||
"file": null,
|
||||
"raw_questions_count": 2
|
||||
},
|
||||
"cases_total": 2,
|
||||
"metrics": {
|
||||
"schema_validation_pass_rate": 100,
|
||||
"scope_detection_accuracy": null,
|
||||
"scope_in_scope_rate": 100,
|
||||
"multi_intent_detected_rate": 0,
|
||||
"clarification_required_rate": 0,
|
||||
"avg_fragments_per_message": 1,
|
||||
"out_of_scope_fragment_rate": 0,
|
||||
"routed_fragment_rate": 100,
|
||||
"no_route_fragment_rate": 0,
|
||||
"route_resolution_accuracy": null,
|
||||
"no_route_precision": null,
|
||||
"false_no_route_rate": null,
|
||||
"execution_state_consistency_rate": 100,
|
||||
"executable_with_soft_assumptions_rate": 100,
|
||||
"soft_assumption_used_fragment_rate": 100,
|
||||
"clarification_precision": null,
|
||||
"clarification_recall": null,
|
||||
"false_clarification_rate": null
|
||||
},
|
||||
"budget": {
|
||||
"requests_total": 0,
|
||||
"retries_used": 0
|
||||
},
|
||||
"clarification_eval": {
|
||||
"labeled_cases": 0,
|
||||
"true_positive": 0,
|
||||
"false_positive": 0,
|
||||
"false_negative": 0
|
||||
},
|
||||
"route_eval": {
|
||||
"labeled_cases": 0,
|
||||
"correct_cases": 0,
|
||||
"expected_routed_cases": 0,
|
||||
"no_route_true_positive": 0,
|
||||
"no_route_false_positive": 0
|
||||
},
|
||||
"scope_eval": {
|
||||
"labeled_cases": 0,
|
||||
"correct_cases": 0
|
||||
},
|
||||
"execution_state_eval": {
|
||||
"checks_total": 2,
|
||||
"checks_passed": 2
|
||||
},
|
||||
"route_distribution": {
|
||||
"hybrid_store_plus_live": 2
|
||||
},
|
||||
"fallback_distribution": {
|
||||
"none": 2
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"case_id": "BQ-001",
|
||||
"raw_question": "Проверь счет 60 за июнь 2020",
|
||||
"validation_passed": true,
|
||||
"message_in_scope": true,
|
||||
"scope_confidence": "high",
|
||||
"contains_multiple_tasks": false,
|
||||
"fragments_total": 1,
|
||||
"in_scope_fragments": 1,
|
||||
"out_of_scope_fragments": 0,
|
||||
"unclear_fragments": 0,
|
||||
"fallback_type": "none",
|
||||
"predicted_route_status": "routed",
|
||||
"expected_route_status": null,
|
||||
"predicted_no_route_reason": null,
|
||||
"expected_no_route_reason": null,
|
||||
"predicted_clarification_required": false,
|
||||
"expected_clarification_required": null,
|
||||
"executable_with_soft_assumptions_fragments": 1,
|
||||
"trace_id": "KyuLvzYsWE-cqj",
|
||||
"request_count_for_case": 0
|
||||
},
|
||||
{
|
||||
"case_id": "BQ-002",
|
||||
"raw_question": "Покажи риски по НДС и по закрытию",
|
||||
"validation_passed": true,
|
||||
"message_in_scope": true,
|
||||
"scope_confidence": "high",
|
||||
"contains_multiple_tasks": false,
|
||||
"fragments_total": 1,
|
||||
"in_scope_fragments": 1,
|
||||
"out_of_scope_fragments": 0,
|
||||
"unclear_fragments": 0,
|
||||
"fallback_type": "none",
|
||||
"predicted_route_status": "routed",
|
||||
"expected_route_status": null,
|
||||
"predicted_no_route_reason": null,
|
||||
"expected_no_route_reason": null,
|
||||
"predicted_clarification_required": false,
|
||||
"expected_clarification_required": null,
|
||||
"executable_with_soft_assumptions_fragments": 1,
|
||||
"trace_id": "zQ2Z38jRawB_ag",
|
||||
"request_count_for_case": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"run_id": "eval-WZp8orHxtP",
|
||||
"timestamp": "2026-03-28T18:15:42.489Z",
|
||||
"mode": "single-pass-strict",
|
||||
"use_mock": true,
|
||||
"prompt_version": "normalizer_v2_0_2",
|
||||
"schema_version": "v2_0_2",
|
||||
"dataset": {
|
||||
"source": "inline_raw_questions",
|
||||
"file": null,
|
||||
"raw_questions_count": 2
|
||||
},
|
||||
"cases_total": 2,
|
||||
"metrics": {
|
||||
"schema_validation_pass_rate": 100,
|
||||
"scope_detection_accuracy": null,
|
||||
"scope_in_scope_rate": 100,
|
||||
"multi_intent_detected_rate": 0,
|
||||
"clarification_required_rate": 0,
|
||||
"avg_fragments_per_message": 1,
|
||||
"out_of_scope_fragment_rate": 0,
|
||||
"routed_fragment_rate": 100,
|
||||
"no_route_fragment_rate": 0,
|
||||
"route_resolution_accuracy": null,
|
||||
"no_route_precision": null,
|
||||
"false_no_route_rate": null,
|
||||
"execution_state_consistency_rate": 100,
|
||||
"executable_with_soft_assumptions_rate": 100,
|
||||
"soft_assumption_used_fragment_rate": 100,
|
||||
"clarification_precision": null,
|
||||
"clarification_recall": null,
|
||||
"false_clarification_rate": null
|
||||
},
|
||||
"budget": {
|
||||
"requests_total": 0,
|
||||
"retries_used": 0
|
||||
},
|
||||
"clarification_eval": {
|
||||
"labeled_cases": 0,
|
||||
"true_positive": 0,
|
||||
"false_positive": 0,
|
||||
"false_negative": 0
|
||||
},
|
||||
"route_eval": {
|
||||
"labeled_cases": 0,
|
||||
"correct_cases": 0,
|
||||
"expected_routed_cases": 0,
|
||||
"no_route_true_positive": 0,
|
||||
"no_route_false_positive": 0
|
||||
},
|
||||
"scope_eval": {
|
||||
"labeled_cases": 0,
|
||||
"correct_cases": 0
|
||||
},
|
||||
"execution_state_eval": {
|
||||
"checks_total": 2,
|
||||
"checks_passed": 2
|
||||
},
|
||||
"route_distribution": {
|
||||
"hybrid_store_plus_live": 2
|
||||
},
|
||||
"fallback_distribution": {
|
||||
"none": 2
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"case_id": "BQ-001",
|
||||
"raw_question": "Проверь счет 60 за июнь 2020",
|
||||
"validation_passed": true,
|
||||
"message_in_scope": true,
|
||||
"scope_confidence": "high",
|
||||
"contains_multiple_tasks": false,
|
||||
"fragments_total": 1,
|
||||
"in_scope_fragments": 1,
|
||||
"out_of_scope_fragments": 0,
|
||||
"unclear_fragments": 0,
|
||||
"fallback_type": "none",
|
||||
"predicted_route_status": "routed",
|
||||
"expected_route_status": null,
|
||||
"predicted_no_route_reason": null,
|
||||
"expected_no_route_reason": null,
|
||||
"predicted_clarification_required": false,
|
||||
"expected_clarification_required": null,
|
||||
"executable_with_soft_assumptions_fragments": 1,
|
||||
"trace_id": "c9Yyo_5RGfuj70",
|
||||
"request_count_for_case": 0
|
||||
},
|
||||
{
|
||||
"case_id": "BQ-002",
|
||||
"raw_question": "Покажи риски по счету 97",
|
||||
"validation_passed": true,
|
||||
"message_in_scope": true,
|
||||
"scope_confidence": "high",
|
||||
"contains_multiple_tasks": false,
|
||||
"fragments_total": 1,
|
||||
"in_scope_fragments": 1,
|
||||
"out_of_scope_fragments": 0,
|
||||
"unclear_fragments": 0,
|
||||
"fallback_type": "none",
|
||||
"predicted_route_status": "routed",
|
||||
"expected_route_status": null,
|
||||
"predicted_no_route_reason": null,
|
||||
"expected_no_route_reason": null,
|
||||
"predicted_clarification_required": false,
|
||||
"expected_clarification_required": null,
|
||||
"executable_with_soft_assumptions_fragments": 1,
|
||||
"trace_id": "KqN5GqxfU0_LaF",
|
||||
"request_count_for_case": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"run_id": "eval-hboVnYh5xt",
|
||||
"timestamp": "2026-03-28T18:15:40.842Z",
|
||||
"mode": "single-pass-strict",
|
||||
"use_mock": true,
|
||||
"prompt_version": "normalizer_v2_0_2",
|
||||
"schema_version": "v2_0_2",
|
||||
"dataset": {
|
||||
"source": "inline_raw_questions",
|
||||
"file": null,
|
||||
"raw_questions_count": 3
|
||||
},
|
||||
"cases_total": 3,
|
||||
"metrics": {
|
||||
"schema_validation_pass_rate": 100,
|
||||
"scope_detection_accuracy": null,
|
||||
"scope_in_scope_rate": 33.33,
|
||||
"multi_intent_detected_rate": 0,
|
||||
"clarification_required_rate": 0,
|
||||
"avg_fragments_per_message": 1,
|
||||
"out_of_scope_fragment_rate": 33.33,
|
||||
"routed_fragment_rate": 66.67,
|
||||
"no_route_fragment_rate": 33.33,
|
||||
"route_resolution_accuracy": null,
|
||||
"no_route_precision": null,
|
||||
"false_no_route_rate": null,
|
||||
"execution_state_consistency_rate": 66.67,
|
||||
"executable_with_soft_assumptions_rate": 100,
|
||||
"soft_assumption_used_fragment_rate": 100,
|
||||
"clarification_precision": null,
|
||||
"clarification_recall": null,
|
||||
"false_clarification_rate": null
|
||||
},
|
||||
"budget": {
|
||||
"requests_total": 0,
|
||||
"retries_used": 0
|
||||
},
|
||||
"clarification_eval": {
|
||||
"labeled_cases": 0,
|
||||
"true_positive": 0,
|
||||
"false_positive": 0,
|
||||
"false_negative": 0
|
||||
},
|
||||
"route_eval": {
|
||||
"labeled_cases": 0,
|
||||
"correct_cases": 0,
|
||||
"expected_routed_cases": 0,
|
||||
"no_route_true_positive": 0,
|
||||
"no_route_false_positive": 0
|
||||
},
|
||||
"scope_eval": {
|
||||
"labeled_cases": 0,
|
||||
"correct_cases": 0
|
||||
},
|
||||
"execution_state_eval": {
|
||||
"checks_total": 3,
|
||||
"checks_passed": 2
|
||||
},
|
||||
"route_distribution": {
|
||||
"hybrid_store_plus_live": 1,
|
||||
"no_route": 1,
|
||||
"batch_refresh_then_store": 1
|
||||
},
|
||||
"fallback_distribution": {
|
||||
"none": 1,
|
||||
"out_of_scope": 1,
|
||||
"clarification": 1
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"case_id": "BQ-001",
|
||||
"raw_question": "Проверь хвосты по поставщикам и разложи цепочку",
|
||||
"validation_passed": true,
|
||||
"message_in_scope": true,
|
||||
"scope_confidence": "high",
|
||||
"contains_multiple_tasks": false,
|
||||
"fragments_total": 1,
|
||||
"in_scope_fragments": 1,
|
||||
"out_of_scope_fragments": 0,
|
||||
"unclear_fragments": 0,
|
||||
"fallback_type": "none",
|
||||
"predicted_route_status": "routed",
|
||||
"expected_route_status": null,
|
||||
"predicted_no_route_reason": null,
|
||||
"expected_no_route_reason": null,
|
||||
"predicted_clarification_required": false,
|
||||
"expected_clarification_required": null,
|
||||
"executable_with_soft_assumptions_fragments": 1,
|
||||
"trace_id": "x2e-ieEiUJTgy9",
|
||||
"request_count_for_case": 0
|
||||
},
|
||||
{
|
||||
"case_id": "BQ-002",
|
||||
"raw_question": "Как вообще по ФСБУ",
|
||||
"validation_passed": true,
|
||||
"message_in_scope": false,
|
||||
"scope_confidence": "low",
|
||||
"contains_multiple_tasks": false,
|
||||
"fragments_total": 1,
|
||||
"in_scope_fragments": 0,
|
||||
"out_of_scope_fragments": 1,
|
||||
"unclear_fragments": 0,
|
||||
"fallback_type": "out_of_scope",
|
||||
"predicted_route_status": "no_route",
|
||||
"expected_route_status": null,
|
||||
"predicted_no_route_reason": "out_of_scope",
|
||||
"expected_no_route_reason": null,
|
||||
"predicted_clarification_required": false,
|
||||
"expected_clarification_required": null,
|
||||
"executable_with_soft_assumptions_fragments": 0,
|
||||
"trace_id": "bZ2y2dX5butilS",
|
||||
"request_count_for_case": 0
|
||||
},
|
||||
{
|
||||
"case_id": "BQ-003",
|
||||
"raw_question": "Покажи топ рисков за июнь 2020",
|
||||
"validation_passed": true,
|
||||
"message_in_scope": false,
|
||||
"scope_confidence": "low",
|
||||
"contains_multiple_tasks": false,
|
||||
"fragments_total": 1,
|
||||
"in_scope_fragments": 0,
|
||||
"out_of_scope_fragments": 0,
|
||||
"unclear_fragments": 1,
|
||||
"fallback_type": "clarification",
|
||||
"predicted_route_status": "routed",
|
||||
"expected_route_status": null,
|
||||
"predicted_no_route_reason": null,
|
||||
"expected_no_route_reason": null,
|
||||
"predicted_clarification_required": false,
|
||||
"expected_clarification_required": null,
|
||||
"executable_with_soft_assumptions_fragments": 0,
|
||||
"trace_id": "s0PY0rCvUCu9z_",
|
||||
"request_count_for_case": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NDC AI Normalizer Playground</title>
|
||||
<script type="module" crossorigin src="/assets/index-PA_66ng-.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-B5_Zqbf2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Ch7jCAii.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AssistantConversationItem } from "../state/types";
|
||||
import { buildConversationExportForCopy } from "../utils/conversationExport";
|
||||
import { JsonView } from "./JsonView";
|
||||
import { PanelFrame } from "./PanelFrame";
|
||||
|
||||
@@ -33,50 +34,6 @@ function shortTime(iso: string): string {
|
||||
return date.toLocaleTimeString("ru-RU");
|
||||
}
|
||||
|
||||
function stringifyDebug(value: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function buildConversationExport(
|
||||
sessionId: string,
|
||||
conversation: AssistantConversationItem[],
|
||||
includeDebugPayload = false
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("# Assistant conversation export");
|
||||
lines.push(`session_id: ${sessionId || "n/a"}`);
|
||||
lines.push(`exported_at: ${new Date().toISOString()}`);
|
||||
lines.push("");
|
||||
|
||||
for (let index = 0; index < conversation.length; index += 1) {
|
||||
const item = conversation[index];
|
||||
lines.push(`## ${index + 1}. ${item.role}`);
|
||||
lines.push(`message_id: ${item.message_id}`);
|
||||
lines.push(`created_at: ${item.created_at}`);
|
||||
lines.push(`reply_type: ${item.reply_type ?? "n/a"}`);
|
||||
if (item.trace_id) {
|
||||
lines.push(`trace_id: ${item.trace_id}`);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(item.text || "(empty)");
|
||||
lines.push("");
|
||||
|
||||
if (includeDebugPayload && item.role === "assistant" && item.debug) {
|
||||
lines.push("### debug_payload_json");
|
||||
lines.push("```json");
|
||||
lines.push(stringifyDebug(item.debug));
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text: string): Promise<boolean> {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
try {
|
||||
@@ -128,6 +85,7 @@ export function AssistantPanel({
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const copyResetTimerRef = useRef<number | null>(null);
|
||||
const [copyState, setCopyState] = useState<"idle" | "success" | "error">("idle");
|
||||
const [copyModeLabel, setCopyModeLabel] = useState<"чат" | "тех">("чат");
|
||||
|
||||
useEffect(() => {
|
||||
if (listRef.current) {
|
||||
@@ -143,13 +101,14 @@ export function AssistantPanel({
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function handleCopyConversation(): Promise<void> {
|
||||
async function handleCopyConversation(mode: "default" | "technical"): Promise<void> {
|
||||
if (conversation.length === 0) {
|
||||
return;
|
||||
}
|
||||
// Copy full run context for diagnostics (including debug payload blocks).
|
||||
const exportText = buildConversationExport(sessionId, conversation, true);
|
||||
|
||||
const exportText = buildConversationExportForCopy(sessionId, conversation, mode);
|
||||
const copied = await copyTextToClipboard(exportText);
|
||||
setCopyModeLabel(mode === "technical" ? "тех" : "чат");
|
||||
setCopyState(copied ? "success" : "error");
|
||||
|
||||
if (copyResetTimerRef.current !== null) {
|
||||
@@ -170,13 +129,25 @@ export function AssistantPanel({
|
||||
type="button"
|
||||
className="assistant-copy-btn"
|
||||
onClick={() => {
|
||||
void handleCopyConversation();
|
||||
void handleCopyConversation("default");
|
||||
}}
|
||||
disabled={conversation.length === 0}
|
||||
title="Экспорт только user-facing чата"
|
||||
>
|
||||
Скопировать чат
|
||||
</button>
|
||||
{copyState === "success" ? <span className="assistant-copy-feedback success">Скопировано</span> : null}
|
||||
<button
|
||||
type="button"
|
||||
className="assistant-copy-btn"
|
||||
onClick={() => {
|
||||
void handleCopyConversation("technical");
|
||||
}}
|
||||
disabled={conversation.length === 0}
|
||||
title="Технический экспорт с debug payload"
|
||||
>
|
||||
Скопировать техчат
|
||||
</button>
|
||||
{copyState === "success" ? <span className="assistant-copy-feedback success">Скопировано ({copyModeLabel})</span> : null}
|
||||
{copyState === "error" ? <span className="assistant-copy-feedback error">Ошибка копирования</span> : null}
|
||||
<span className="status-chip">{sessionId ? `session: ${sessionId}` : "новая сессия"}</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
export type ConversationExportMode = "default" | "technical";
|
||||
|
||||
export interface ConversationExportItem {
|
||||
message_id: string;
|
||||
role: "user" | "assistant";
|
||||
text: string;
|
||||
reply_type: string | null;
|
||||
created_at: string;
|
||||
trace_id: string | null;
|
||||
debug?: unknown | null;
|
||||
}
|
||||
|
||||
const DEBUG_SECTION_PATTERN =
|
||||
/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json|debug_payload|technical_breakdown)\b/i;
|
||||
|
||||
const INLINE_TECH_LINE_PATTERNS: RegExp[] = [
|
||||
/\b(?:debug_payload_json|technical_breakdown_json)\b/i,
|
||||
/\b(?:route_summary|semantic_profile|domain_scope|relation_patterns|account_scope)\b/i,
|
||||
/\b(?:coverage_report|retrieval_status|problem_unit_state|candidate_evidence)\b/i,
|
||||
/\b(?:graph_domain_scope|graph_runtime|selection_reason|why_included)\b/i
|
||||
];
|
||||
|
||||
function stringifyDebug(value: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeConversationExportText(value: string): string {
|
||||
const raw = String(value ?? "");
|
||||
const cutMatch = raw.match(DEBUG_SECTION_PATTERN);
|
||||
const preCut = cutMatch ? raw.slice(0, cutMatch.index) : raw;
|
||||
const withoutDebugHeadings = preCut
|
||||
.replace(/###\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json)[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
|
||||
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|route_summary_json)\b[\s\S]*$/gi, "");
|
||||
|
||||
const lines = withoutDebugHeadings
|
||||
.split(/\r?\n/g)
|
||||
.map((line) => line.trimEnd())
|
||||
.filter((line) => line.trim().length > 0)
|
||||
.filter((line) => !INLINE_TECH_LINE_PATTERNS.some((pattern) => pattern.test(line)));
|
||||
|
||||
return lines.join("\n").trim();
|
||||
}
|
||||
|
||||
export function buildConversationExportForCopy(
|
||||
sessionId: string,
|
||||
conversation: ConversationExportItem[],
|
||||
mode: ConversationExportMode = "default"
|
||||
): string {
|
||||
const includeDebug = mode === "technical";
|
||||
const lines: string[] = [];
|
||||
lines.push("# Assistant conversation export");
|
||||
lines.push(`session_id: ${sessionId || "n/a"}`);
|
||||
lines.push(`export_mode: ${mode}`);
|
||||
lines.push(`exported_at: ${new Date().toISOString()}`);
|
||||
lines.push("");
|
||||
|
||||
for (let index = 0; index < conversation.length; index += 1) {
|
||||
const item = conversation[index];
|
||||
const safeText = sanitizeConversationExportText(item.text || "");
|
||||
lines.push(`## ${index + 1}. ${item.role}`);
|
||||
lines.push(`message_id: ${item.message_id}`);
|
||||
lines.push(`created_at: ${item.created_at}`);
|
||||
lines.push(`reply_type: ${item.reply_type ?? "n/a"}`);
|
||||
if (item.trace_id) {
|
||||
lines.push(`trace_id: ${item.trace_id}`);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(safeText || "(empty)");
|
||||
lines.push("");
|
||||
|
||||
if (includeDebug && item.role === "assistant" && item.debug) {
|
||||
lines.push("### technical_debug_payload_json");
|
||||
lines.push("```json");
|
||||
lines.push(stringifyDebug(item.debug));
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/components/assistantpanel.tsx","./src/components/connectionpanel.tsx","./src/components/historypanel.tsx","./src/components/jsonview.tsx","./src/components/metricspanel.tsx","./src/components/outputpanel.tsx","./src/components/panelframe.tsx","./src/components/promptpanel.tsx","./src/components/querypanel.tsx","./src/components/runtimepanel.tsx","./src/state/defaults.ts","./src/state/types.ts"],"version":"5.9.3"}
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/components/assistantpanel.tsx","./src/components/connectionpanel.tsx","./src/components/historypanel.tsx","./src/components/jsonview.tsx","./src/components/metricspanel.tsx","./src/components/outputpanel.tsx","./src/components/panelframe.tsx","./src/components/promptpanel.tsx","./src/components/querypanel.tsx","./src/components/runtimepanel.tsx","./src/state/defaults.ts","./src/state/types.ts","./src/utils/conversationexport.ts"],"version":"5.9.3"}
|
||||
Reference in New Issue
Block a user