Этап 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
|
||||
|
||||
Reference in New Issue
Block a user