Этап 4 / Волна 11: объектный трейс по бизнес-якорям, доменное заземление, cleanup утечки дебага
This commit is contained in:
parent
553a5c407a
commit
8b84f5e989
|
|
@ -10,6 +10,112 @@ function fallbackFromSummary(routeSummary) {
|
||||||
function uniqueStrings(values, limit = 6) {
|
function uniqueStrings(values, limit = 6) {
|
||||||
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
|
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
|
||||||
}
|
}
|
||||||
|
function withUniquePush(target, value) {
|
||||||
|
const normalized = String(value ?? "").trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!target.includes(normalized)) {
|
||||||
|
target.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function normalizeAnchorForMatch(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\p{L}\p{N}.:/-]+/gu, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
function collectCompanyAnchorTokens(anchors) {
|
||||||
|
if (!anchors) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const tokens = [];
|
||||||
|
for (const item of anchors.contract_numbers ?? [])
|
||||||
|
withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.document_numbers ?? [])
|
||||||
|
withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.dates ?? [])
|
||||||
|
withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.amounts ?? [])
|
||||||
|
withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.accounts ?? [])
|
||||||
|
withUniquePush(tokens, `\u0441\u0447\u0435\u0442 ${item}`);
|
||||||
|
for (const item of anchors.accounts ?? [])
|
||||||
|
withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.periods ?? [])
|
||||||
|
withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.document_types ?? [])
|
||||||
|
withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.all ?? [])
|
||||||
|
withUniquePush(tokens, item);
|
||||||
|
return uniqueStrings(tokens, 48);
|
||||||
|
}
|
||||||
|
function collectRetrievalCorpus(results) {
|
||||||
|
const chunks = [];
|
||||||
|
for (const result of results) {
|
||||||
|
chunks.push(JSON.stringify(result.summary ?? {}));
|
||||||
|
for (const item of result.items.slice(0, 10)) {
|
||||||
|
chunks.push(JSON.stringify(item));
|
||||||
|
}
|
||||||
|
for (const evidence of result.evidence.slice(0, 16)) {
|
||||||
|
chunks.push(JSON.stringify(evidence));
|
||||||
|
}
|
||||||
|
chunks.push(...result.why_included.slice(0, 16));
|
||||||
|
chunks.push(...result.selection_reason.slice(0, 16));
|
||||||
|
chunks.push(...result.business_interpretation.slice(0, 16));
|
||||||
|
}
|
||||||
|
return chunks.join(" ").toLowerCase();
|
||||||
|
}
|
||||||
|
function isAnchorMatchedInCorpus(anchor, corpus) {
|
||||||
|
const normalized = normalizeAnchorForMatch(anchor);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (normalized.length < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (corpus.includes(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const withoutPrefix = normalized
|
||||||
|
.replace(/^(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|document|account|period|doc_type)\s*[:№#]?\s*/iu, "")
|
||||||
|
.trim();
|
||||||
|
if (withoutPrefix.length >= 3 && corpus.includes(withoutPrefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/^\d+(?:[.,]\d{2})?$/.test(withoutPrefix)) {
|
||||||
|
const normalizedAmount = withoutPrefix.replace(",", ".");
|
||||||
|
return corpus.includes(withoutPrefix) || corpus.includes(normalizedAmount);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function evaluateCompanyAnchorUsage(anchors, retrievalResults) {
|
||||||
|
const present = collectCompanyAnchorTokens(anchors);
|
||||||
|
if (present.length === 0) {
|
||||||
|
return {
|
||||||
|
present: [],
|
||||||
|
used: [],
|
||||||
|
unused: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const corpus = normalizeAnchorForMatch(collectRetrievalCorpus(retrievalResults));
|
||||||
|
const used = [];
|
||||||
|
const unused = [];
|
||||||
|
for (const anchor of present) {
|
||||||
|
if (isAnchorMatchedInCorpus(anchor, corpus)) {
|
||||||
|
withUniquePush(used, anchor);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
withUniquePush(unused, anchor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
present: uniqueStrings(present, 24),
|
||||||
|
used: uniqueStrings(used, 12),
|
||||||
|
unused: uniqueStrings(unused, 12)
|
||||||
|
};
|
||||||
|
}
|
||||||
const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
||||||
const LONG_HEX_PATTERN = /\b[0-9a-f]{24,}\b/gi;
|
const LONG_HEX_PATTERN = /\b[0-9a-f]{24,}\b/gi;
|
||||||
const RAW_REF_BLOB_PATTERN = /\bevidence_source_ref_v1\|[^\s,;]+/gi;
|
const RAW_REF_BLOB_PATTERN = /\bevidence_source_ref_v1\|[^\s,;]+/gi;
|
||||||
|
|
@ -962,6 +1068,10 @@ function isProblemUnitAlignedWithNarrativeDomain(unit, domain) {
|
||||||
return /(payment_to_settlement|settlement_closed|settlement|аванс|зачет|зачёт|расчет|расч[её]т|оплат)/i.test(corpus);
|
return /(payment_to_settlement|settlement_closed|settlement|аванс|зачет|зачёт|расчет|расч[её]т|оплат)/i.test(corpus);
|
||||||
}
|
}
|
||||||
if (domain === "vat_document_register_book") {
|
if (domain === "vat_document_register_book") {
|
||||||
|
const foreignVatDomain = ["period_close", "deferred_expense", "fixed_asset", "bank_settlement", "customer_settlement"].includes(String(unit.lifecycle_domain ?? ""));
|
||||||
|
if (foreignVatDomain && !hasControlledCrossDomainHandoff(unit)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (unit.lifecycle_domain === "vat_flow") {
|
if (unit.lifecycle_domain === "vat_flow") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -971,6 +1081,10 @@ function isProblemUnitAlignedWithNarrativeDomain(unit, domain) {
|
||||||
return /(vat|ндс|invoice|book_entry|register|книг|счет[\s-]?фактур|сч[её]т[\s-]?фактур)/i.test(corpus);
|
return /(vat|ндс|invoice|book_entry|register|книг|счет[\s-]?фактур|сч[её]т[\s-]?фактур)/i.test(corpus);
|
||||||
}
|
}
|
||||||
if (domain === "month_close_costs_20_44") {
|
if (domain === "month_close_costs_20_44") {
|
||||||
|
const foreignMonthCloseDomain = ["vat_flow", "bank_settlement", "customer_settlement", "fixed_asset"].includes(String(unit.lifecycle_domain ?? ""));
|
||||||
|
if (foreignMonthCloseDomain && !hasControlledCrossDomainHandoff(unit)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (unit.lifecycle_domain === "period_close" ||
|
if (unit.lifecycle_domain === "period_close" ||
|
||||||
unit.lifecycle_domain === "deferred_expense" ||
|
unit.lifecycle_domain === "deferred_expense" ||
|
||||||
unit.lifecycle_domain === "fixed_asset") {
|
unit.lifecycle_domain === "fixed_asset") {
|
||||||
|
|
@ -1514,12 +1628,159 @@ function mapDefectTokenToNarrative(value) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function extractAccountNumbers(values) {
|
const KNOWN_ACCOUNT_PREFIXES = new Set([
|
||||||
const numbers = values.flatMap((value) => {
|
"01",
|
||||||
const matches = String(value ?? "").match(/\b\d{2}(?:\.\d{1,2})?\b/g);
|
"02",
|
||||||
return matches ?? [];
|
"07",
|
||||||
|
"08",
|
||||||
|
"10",
|
||||||
|
"13",
|
||||||
|
"19",
|
||||||
|
"20",
|
||||||
|
"21",
|
||||||
|
"23",
|
||||||
|
"25",
|
||||||
|
"26",
|
||||||
|
"28",
|
||||||
|
"29",
|
||||||
|
"41",
|
||||||
|
"43",
|
||||||
|
"44",
|
||||||
|
"45",
|
||||||
|
"50",
|
||||||
|
"51",
|
||||||
|
"52",
|
||||||
|
"55",
|
||||||
|
"57",
|
||||||
|
"58",
|
||||||
|
"60",
|
||||||
|
"62",
|
||||||
|
"66",
|
||||||
|
"67",
|
||||||
|
"68",
|
||||||
|
"69",
|
||||||
|
"70",
|
||||||
|
"71",
|
||||||
|
"73",
|
||||||
|
"76",
|
||||||
|
"90",
|
||||||
|
"91",
|
||||||
|
"94",
|
||||||
|
"96",
|
||||||
|
"97"
|
||||||
|
]);
|
||||||
|
function collectDateLikeSpansForNarrative(text) {
|
||||||
|
const spans = [];
|
||||||
|
const patterns = [
|
||||||
|
/\b20\d{2}[./-](?:0[1-9]|1[0-2])(?:[./-](?:0[1-9]|[12]\d|3[01]))?\b/g,
|
||||||
|
/\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b/g,
|
||||||
|
/\b(?:0?[1-9]|[12]\d|3[01])\s+(?:января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)\b/giu
|
||||||
|
];
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
let match = null;
|
||||||
|
while ((match = pattern.exec(text)) !== null) {
|
||||||
|
spans.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + match[0].length
|
||||||
});
|
});
|
||||||
return uniqueStrings(numbers, 12);
|
}
|
||||||
|
}
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
function collectAmountLikeSpansForNarrative(text) {
|
||||||
|
const spans = [];
|
||||||
|
const pattern = /\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g;
|
||||||
|
let match = null;
|
||||||
|
while ((match = pattern.exec(text)) !== null) {
|
||||||
|
spans.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + match[0].length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
function intersectsNarrativeSpan(start, end, spans) {
|
||||||
|
return spans.some((span) => start < span.end && end > span.start);
|
||||||
|
}
|
||||||
|
function hasAccountContextMarker(text, start, end) {
|
||||||
|
const left = text.slice(Math.max(0, start - 24), start);
|
||||||
|
const right = text.slice(end, Math.min(text.length, end + 24));
|
||||||
|
return /(?:счет|сч\.?|account|schet|по\s+60|по\s+62|по\s+19|по\s+68|по\s+20|по\s+25|по\s+26|по\s+44|расчет|ндс|закрыти|рбп|амортиз|settlement|vat|close)/iu.test(`${left} ${right}`);
|
||||||
|
}
|
||||||
|
function toKnownAccountToken(value) {
|
||||||
|
const token = String(value ?? "").trim();
|
||||||
|
const prefix = token.match(/^(\d{2})/)?.[1];
|
||||||
|
if (!prefix || !KNOWN_ACCOUNT_PREFIXES.has(prefix)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
function extractAccountNumbers(values) {
|
||||||
|
const tokens = [];
|
||||||
|
for (const value of values) {
|
||||||
|
const raw = String(value ?? "");
|
||||||
|
const matches = raw.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
|
||||||
|
for (const match of matches) {
|
||||||
|
const account = toKnownAccountToken(match);
|
||||||
|
if (account) {
|
||||||
|
tokens.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueStrings(tokens, 16);
|
||||||
|
}
|
||||||
|
function extractAccountNumbersFromNarrativeText(value) {
|
||||||
|
const text = String(value ?? "").toLowerCase();
|
||||||
|
if (!text.trim()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const result = [];
|
||||||
|
const dateSpans = collectDateLikeSpansForNarrative(text);
|
||||||
|
const amountSpans = collectAmountLikeSpansForNarrative(text);
|
||||||
|
const blockedSpans = [...dateSpans, ...amountSpans];
|
||||||
|
const contextualPattern = /(?:\b(?:счет(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)\b)\s*(?:№|#|:)?\s*([0-9./,\sиand]{2,96})/giu;
|
||||||
|
let contextualMatch = null;
|
||||||
|
while ((contextualMatch = contextualPattern.exec(text)) !== null) {
|
||||||
|
const chunk = String(contextualMatch[1] ?? "");
|
||||||
|
const chunkTokens = chunk.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
|
||||||
|
for (const token of chunkTokens) {
|
||||||
|
const account = toKnownAccountToken(token);
|
||||||
|
if (account) {
|
||||||
|
result.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const accountPairPattern = /\b(\d{2}(?:\.\d{1,2})?)\s*\/\s*(\d{2}(?:\.\d{1,2})?)\b/g;
|
||||||
|
let pairMatch = null;
|
||||||
|
while ((pairMatch = accountPairPattern.exec(text)) !== null) {
|
||||||
|
const left = toKnownAccountToken(String(pairMatch[1] ?? ""));
|
||||||
|
const right = toKnownAccountToken(String(pairMatch[2] ?? ""));
|
||||||
|
if (left) {
|
||||||
|
result.push(left);
|
||||||
|
}
|
||||||
|
if (right) {
|
||||||
|
result.push(right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const explicitPattern = /\b\d{2}(?:\.\d{1,2})?\b/g;
|
||||||
|
let explicitMatch = null;
|
||||||
|
while ((explicitMatch = explicitPattern.exec(text)) !== null) {
|
||||||
|
const token = String(explicitMatch[0] ?? "");
|
||||||
|
const account = toKnownAccountToken(token);
|
||||||
|
if (!account) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const start = explicitMatch.index;
|
||||||
|
const end = start + token.length;
|
||||||
|
if (intersectsNarrativeSpan(start, end, blockedSpans)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!hasAccountContextMarker(text, start, end)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push(account);
|
||||||
|
}
|
||||||
|
return uniqueStrings(result, 16);
|
||||||
}
|
}
|
||||||
function inferP0NarrativeDomain(units) {
|
function inferP0NarrativeDomain(units) {
|
||||||
const allAccounts = extractAccountNumbers(units.flatMap((unit) => unit.affected_accounts ?? []));
|
const allAccounts = extractAccountNumbers(units.flatMap((unit) => unit.affected_accounts ?? []));
|
||||||
|
|
@ -1642,9 +1903,21 @@ function hasControlledCrossDomainHandoffInResult(result) {
|
||||||
function isSettlementDomainToken(value) {
|
function isSettlementDomainToken(value) {
|
||||||
return /(?:bank_settlement|customer_settlement|settlements?|supplier_payments|suppliers?|customers?)/i.test(String(value ?? ""));
|
return /(?:bank_settlement|customer_settlement|settlements?|supplier_payments|suppliers?|customers?)/i.test(String(value ?? ""));
|
||||||
}
|
}
|
||||||
|
function isVatDomainToken(value) {
|
||||||
|
return /(?:vat_flow|vat|nds|taxes?|purchase_book|sales_book|invoice|book_entry|register)/i.test(String(value ?? ""));
|
||||||
|
}
|
||||||
|
function isMonthCloseDomainToken(value) {
|
||||||
|
return /(?:period_close|month_close|close_operation|cost_close|cost_allocation|deferred_expense)/i.test(String(value ?? ""));
|
||||||
|
}
|
||||||
function isForeignToSettlementDomainToken(value) {
|
function isForeignToSettlementDomainToken(value) {
|
||||||
return /(?:vat_flow|vat|deferred_expense|period_close|fixed_asset|fixed_assets|taxes?)/i.test(String(value ?? ""));
|
return /(?:vat_flow|vat|deferred_expense|period_close|fixed_asset|fixed_assets|taxes?)/i.test(String(value ?? ""));
|
||||||
}
|
}
|
||||||
|
function isForeignToVatDomainToken(value) {
|
||||||
|
return /(?:bank_settlement|customer_settlement|settlements?|period_close|deferred_expense|fixed_asset|fixed_assets|month_close)/i.test(String(value ?? ""));
|
||||||
|
}
|
||||||
|
function isForeignToMonthCloseDomainToken(value) {
|
||||||
|
return /(?:bank_settlement|customer_settlement|settlements?|vat_flow|vat|fixed_asset|fixed_assets)/i.test(String(value ?? ""));
|
||||||
|
}
|
||||||
function collectResultAccounts(result) {
|
function collectResultAccounts(result) {
|
||||||
const accounts = [];
|
const accounts = [];
|
||||||
const semanticProfile = summaryValue(result, "semantic_profile");
|
const semanticProfile = summaryValue(result, "semantic_profile");
|
||||||
|
|
@ -1687,11 +1960,19 @@ function isSubstantiveResult(result) {
|
||||||
}
|
}
|
||||||
return result.items.length > 0 || result.evidence.length > 0;
|
return result.items.length > 0 || result.evidence.length > 0;
|
||||||
}
|
}
|
||||||
function evaluateSettlementEvidenceGrounding(results) {
|
function evaluateP0DomainEvidenceGrounding(results, focusDomain) {
|
||||||
|
if (!focusDomain) {
|
||||||
|
return {
|
||||||
|
has_primary: false,
|
||||||
|
has_foreign_primary: false,
|
||||||
|
foreign_primary_domains: [],
|
||||||
|
blocked: false
|
||||||
|
};
|
||||||
|
}
|
||||||
const substantive = results.filter((item) => isSubstantiveResult(item));
|
const substantive = results.filter((item) => isSubstantiveResult(item));
|
||||||
if (substantive.length === 0) {
|
if (substantive.length === 0) {
|
||||||
return {
|
return {
|
||||||
has_settlement_primary: false,
|
has_primary: false,
|
||||||
has_foreign_primary: false,
|
has_foreign_primary: false,
|
||||||
foreign_primary_domains: [],
|
foreign_primary_domains: [],
|
||||||
blocked: false
|
blocked: false
|
||||||
|
|
@ -1701,42 +1982,91 @@ function evaluateSettlementEvidenceGrounding(results) {
|
||||||
const accounts = collectResultAccounts(result);
|
const accounts = collectResultAccounts(result);
|
||||||
const domains = collectResultDomains(result);
|
const domains = collectResultDomains(result);
|
||||||
const relations = collectResultRelations(result);
|
const relations = collectResultRelations(result);
|
||||||
const settlement = accounts.some((item) => isSettlementAccountToken(item) || /^(?:51|76)(?:\.|$)/.test(item)) ||
|
let inDomain = false;
|
||||||
|
let foreignDomains = [];
|
||||||
|
if (focusDomain === "settlements_60_62") {
|
||||||
|
inDomain =
|
||||||
|
accounts.some((item) => isSettlementAccountToken(item) || /^(?:51|76)(?:\.|$)/.test(item)) ||
|
||||||
domains.some((item) => isSettlementDomainToken(item)) ||
|
domains.some((item) => isSettlementDomainToken(item)) ||
|
||||||
relations.some((item) => /payment_to_settlement|statement_to_document|contract_to_documents/.test(item));
|
relations.some((item) => /payment_to_settlement|statement_to_document|contract_to_documents|linked_to_settlement|settlement_closed/.test(item));
|
||||||
const foreignDomains = domains.filter((item) => isForeignToSettlementDomainToken(item));
|
foreignDomains = domains.filter((item) => isForeignToSettlementDomainToken(item));
|
||||||
|
}
|
||||||
|
else if (focusDomain === "vat_document_register_book") {
|
||||||
|
inDomain =
|
||||||
|
accounts.some((item) => isVatAccountToken(item)) ||
|
||||||
|
domains.some((item) => isVatDomainToken(item)) ||
|
||||||
|
relations.some((item) => /invoice_to_vat|source_doc_present|invoice_linked|book_entry_generated|deduction_posted|register_to_book|vat_/i.test(item));
|
||||||
|
foreignDomains = domains.filter((item) => isForeignToVatDomainToken(item));
|
||||||
|
}
|
||||||
|
else if (focusDomain === "month_close_costs_20_44") {
|
||||||
|
inDomain =
|
||||||
|
accounts.some((item) => isCloseCostsAccountToken(item)) ||
|
||||||
|
domains.some((item) => isMonthCloseDomainToken(item)) ||
|
||||||
|
relations.some((item) => /costs_accumulated|allocation_rules_resolved|close_operation_runs|residuals_zero|close_operation|period_close|allocation|writeoff/i.test(item));
|
||||||
|
foreignDomains = domains.filter((item) => isForeignToMonthCloseDomainToken(item));
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
settlement,
|
inDomain,
|
||||||
foreignDomains: uniqueStrings(foreignDomains, 8)
|
foreignDomains: uniqueStrings(foreignDomains, 8)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const top = substantive[0];
|
const top = substantive[0];
|
||||||
const topClass = classify(top);
|
const topClass = classify(top);
|
||||||
const hasAnySettlement = substantive.some((item) => classify(item).settlement);
|
const hasAnyPrimary = substantive.some((item) => classify(item).inDomain);
|
||||||
const hasForeignPrimary = topClass.foreignDomains.length > 0 && !topClass.settlement;
|
const hasForeignPrimary = topClass.foreignDomains.length > 0 && !topClass.inDomain;
|
||||||
const blocked = hasForeignPrimary && !hasAnySettlement && !hasControlledCrossDomainHandoffInResult(top);
|
const blocked = hasForeignPrimary && !hasAnyPrimary && !hasControlledCrossDomainHandoffInResult(top);
|
||||||
return {
|
return {
|
||||||
has_settlement_primary: hasAnySettlement,
|
has_primary: hasAnyPrimary,
|
||||||
has_foreign_primary: hasForeignPrimary,
|
has_foreign_primary: hasForeignPrimary,
|
||||||
foreign_primary_domains: topClass.foreignDomains,
|
foreign_primary_domains: topClass.foreignDomains,
|
||||||
blocked
|
blocked
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function hasStrongNarrativeDomainSignalInText(userMessage, domain) {
|
||||||
|
if (!domain) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const text = String(userMessage ?? "").toLowerCase();
|
||||||
|
const accountTokens = extractAccountNumbersFromNarrativeText(text);
|
||||||
|
if (domain === "settlements_60_62") {
|
||||||
|
return (accountTokens.some((item) => isSettlementAccountToken(item)) ||
|
||||||
|
/(60\.0[12]|62\.0[12]|долг|аванс|зач[её]т|взаимозач|расч[её]т)/i.test(text));
|
||||||
|
}
|
||||||
|
if (domain === "vat_document_register_book") {
|
||||||
|
return (accountTokens.some((item) => isVatAccountToken(item)) ||
|
||||||
|
/(ндс|vat|счет[-\s]?фактур|сч[её]т[-\s]?фактур|книг[аи]|регистр)/i.test(text));
|
||||||
|
}
|
||||||
|
if (domain === "month_close_costs_20_44") {
|
||||||
|
return (accountTokens.some((item) => isCloseCostsAccountToken(item)) ||
|
||||||
|
/(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(text));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
function inferP0FocusNarrativeDomain(userMessage, results, units, focusDomainHint) {
|
function inferP0FocusNarrativeDomain(userMessage, results, units, focusDomainHint) {
|
||||||
const fromHint = p0NarrativeDomainFromHint(focusDomainHint);
|
const fromHint = p0NarrativeDomainFromHint(focusDomainHint);
|
||||||
|
const fromMessage = inferNarrativeDomainFromText(userMessage);
|
||||||
|
const strongFromMessage = Boolean(fromMessage && hasStrongNarrativeDomainSignalInText(userMessage, fromMessage));
|
||||||
|
const fromDomainGuard = inferP0NarrativeDomainFromDomainGuards(results);
|
||||||
|
if (fromHint && fromMessage && fromHint !== fromMessage) {
|
||||||
|
return strongFromMessage ? fromMessage : fromHint;
|
||||||
|
}
|
||||||
if (fromHint) {
|
if (fromHint) {
|
||||||
return fromHint;
|
return fromHint;
|
||||||
}
|
}
|
||||||
const fromDomainGuard = inferP0NarrativeDomainFromDomainGuards(results);
|
if (fromDomainGuard && fromMessage && fromDomainGuard !== fromMessage) {
|
||||||
|
return strongFromMessage ? fromMessage : fromDomainGuard;
|
||||||
|
}
|
||||||
if (fromDomainGuard) {
|
if (fromDomainGuard) {
|
||||||
return fromDomainGuard;
|
return fromDomainGuard;
|
||||||
}
|
}
|
||||||
const fromMessage = inferNarrativeDomainFromText(userMessage);
|
if (strongFromMessage) {
|
||||||
|
return fromMessage;
|
||||||
|
}
|
||||||
if (fromMessage) {
|
if (fromMessage) {
|
||||||
return fromMessage;
|
return fromMessage;
|
||||||
}
|
}
|
||||||
const semanticScopes = collectSemanticProfileScopes(results);
|
const semanticScopes = collectSemanticProfileScopes(results);
|
||||||
const messageAccounts = extractAccountNumbers([userMessage]);
|
const messageAccounts = extractAccountNumbersFromNarrativeText(userMessage);
|
||||||
const hasExplicitP0AccountSignal = [...messageAccounts, ...semanticScopes.accounts].some((item) => isSettlementAccountToken(item) || isVatAccountToken(item) || isCloseCostsAccountToken(item));
|
const hasExplicitP0AccountSignal = [...messageAccounts, ...semanticScopes.accounts].some((item) => isSettlementAccountToken(item) || isVatAccountToken(item) || isCloseCostsAccountToken(item));
|
||||||
// Domain lock is only applied when we have an explicit P0 signal from the query/profile.
|
// Domain lock is only applied when we have an explicit P0 signal from the query/profile.
|
||||||
if (!hasExplicitP0AccountSignal) {
|
if (!hasExplicitP0AccountSignal) {
|
||||||
|
|
@ -1887,12 +2217,19 @@ function humanizeFactForDirectAnswer(value) {
|
||||||
}
|
}
|
||||||
function buildDirectAnswer(input) {
|
function buildDirectAnswer(input) {
|
||||||
const topFact = humanizeFactForDirectAnswer(firstMeaningfulFact(input.retrievalResults));
|
const topFact = humanizeFactForDirectAnswer(firstMeaningfulFact(input.retrievalResults));
|
||||||
|
const domainAnchor = domainNarrativeAnchor(input.focusDomain);
|
||||||
|
const topFactDomain = topFact ? inferNarrativeDomainFromText(topFact) : null;
|
||||||
|
const topFactAligned = Boolean(topFact) && (!input.focusDomain || topFactDomain === input.focusDomain);
|
||||||
|
const preferredFact = topFactAligned ? topFact : null;
|
||||||
if (input.mode === "focused_grounded") {
|
if (input.mode === "focused_grounded") {
|
||||||
return topFact ?? "Проблема подтверждена на текущей опоре и готова к точечной проверке.";
|
return preferredFact ?? domainAnchor ?? "Проблема подтверждена на текущей опоре и готова к точечной проверке.";
|
||||||
}
|
}
|
||||||
if (input.mode === "broad_partial") {
|
if (input.mode === "broad_partial") {
|
||||||
if (topFact) {
|
if (preferredFact) {
|
||||||
return `${topFact.replace(/[.!?]+$/u, "")}; подтверждение пока частичное.`;
|
return `${preferredFact.replace(/[.!?]+$/u, "")}; подтверждение пока частичное.`;
|
||||||
|
}
|
||||||
|
if (domainAnchor) {
|
||||||
|
return `${domainAnchor.replace(/[.!?]+$/u, "")}; подтверждение пока частичное.`;
|
||||||
}
|
}
|
||||||
return "Есть признаки проблемы, но опора частичная и вывод ограничен.";
|
return "Есть признаки проблемы, но опора частичная и вывод ограничен.";
|
||||||
}
|
}
|
||||||
|
|
@ -1962,9 +2299,19 @@ function buildProblemCentricAnswerStructure(input) {
|
||||||
.map((item) => item.source_ref?.canonical_ref)
|
.map((item) => item.source_ref?.canonical_ref)
|
||||||
.filter((item) => typeof item === "string" && item.trim().length > 0), 6);
|
.filter((item) => typeof item === "string" && item.trim().length > 0), 6);
|
||||||
const evidenceIds = uniqueStrings(input.evidenceItems.map((item) => item.evidence_id), 10);
|
const evidenceIds = uniqueStrings(input.evidenceItems.map((item) => item.evidence_id), 10);
|
||||||
|
const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, input.evidenceItems);
|
||||||
|
const hasCriticalEvidenceLimitation = input.limitationReasonCodes.includes("weak_source_mapping") ||
|
||||||
|
input.limitationReasonCodes.includes("insufficient_detail");
|
||||||
|
const confidenceLimited = input.mode !== "focused_grounded" ||
|
||||||
|
weakUnits ||
|
||||||
|
input.domainLockMiss ||
|
||||||
|
input.limitationReasonCodes.includes("missing_mechanism") ||
|
||||||
|
input.limitationReasonCodes.includes("heuristic_inference") ||
|
||||||
|
hasCriticalEvidenceLimitation ||
|
||||||
|
aggregateEvidenceConfidence === "low";
|
||||||
const mechanismStatus = unitMechanismNotes.length === 0
|
const mechanismStatus = unitMechanismNotes.length === 0
|
||||||
? "unresolved"
|
? "unresolved"
|
||||||
: weakUnits || input.limitationReasonCodes.includes("missing_mechanism")
|
: confidenceLimited
|
||||||
? "limited"
|
? "limited"
|
||||||
: "grounded";
|
: "grounded";
|
||||||
const problemSpecificLimitations = [];
|
const problemSpecificLimitations = [];
|
||||||
|
|
@ -2067,18 +2414,42 @@ function limitationReasonToUserText(code) {
|
||||||
}
|
}
|
||||||
function inferNarrativeDomainFromText(value) {
|
function inferNarrativeDomainFromText(value) {
|
||||||
const text = String(value ?? "").toLowerCase();
|
const text = String(value ?? "").toLowerCase();
|
||||||
const accountTokens = extractAccountNumbers([text]);
|
const accountTokens = extractAccountNumbersFromNarrativeText(text);
|
||||||
const hasSettlementLexicalSignal = /(оплат|долг|аванс|взаимозач|зачет|зачёт|поставщ|покупат|не\s+сход)/i.test(text);
|
let settlementScore = 0;
|
||||||
if (accountTokens.some((token) => isSettlementAccountToken(token)) || hasSettlementLexicalSignal) {
|
let vatScore = 0;
|
||||||
return "settlements_60_62";
|
let monthCloseScore = 0;
|
||||||
|
if (accountTokens.some((token) => isSettlementAccountToken(token))) {
|
||||||
|
settlementScore += 3;
|
||||||
}
|
}
|
||||||
if (accountTokens.some((token) => isVatAccountToken(token)) || /(ндс|счет[-\s]?фактур|регистр|книг)/i.test(text)) {
|
if (accountTokens.some((token) => isVatAccountToken(token))) {
|
||||||
|
vatScore += 3;
|
||||||
|
}
|
||||||
|
if (accountTokens.some((token) => isCloseCostsAccountToken(token))) {
|
||||||
|
monthCloseScore += 3;
|
||||||
|
}
|
||||||
|
if (/(долг|аванс|взаимозач|зачет|зачёт|62\.01|62\.02|60\.01|60\.02|не\s+сход)/i.test(text)) {
|
||||||
|
settlementScore += 2;
|
||||||
|
}
|
||||||
|
if (/(ндс|vat|счет[-\s]?фактур|сч[её]т[-\s]?фактур|книг[аи]|регистр)/i.test(text)) {
|
||||||
|
vatScore += 3;
|
||||||
|
}
|
||||||
|
if (/(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(text)) {
|
||||||
|
monthCloseScore += 3;
|
||||||
|
}
|
||||||
|
const maxScore = Math.max(settlementScore, vatScore, monthCloseScore);
|
||||||
|
if (maxScore <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Tie-break prioritizes explicit VAT and month-close lexical markers over broad settlement wording.
|
||||||
|
if (vatScore === maxScore) {
|
||||||
return "vat_document_register_book";
|
return "vat_document_register_book";
|
||||||
}
|
}
|
||||||
if (accountTokens.some((token) => isCloseCostsAccountToken(token)) ||
|
if (monthCloseScore === maxScore) {
|
||||||
/(закрыти[ея]\s+месяц|затрат|распределени|списан)/i.test(text)) {
|
|
||||||
return "month_close_costs_20_44";
|
return "month_close_costs_20_44";
|
||||||
}
|
}
|
||||||
|
if (settlementScore === maxScore) {
|
||||||
|
return "settlements_60_62";
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function isIncompleteEvidence(structure) {
|
function isIncompleteEvidence(structure) {
|
||||||
|
|
@ -2171,6 +2542,10 @@ function buildEvidenceSectionLines(structure) {
|
||||||
const claimLinks = Array.isArray(structure.evidence_block.claim_evidence_links)
|
const claimLinks = Array.isArray(structure.evidence_block.claim_evidence_links)
|
||||||
? structure.evidence_block.claim_evidence_links.length
|
? structure.evidence_block.claim_evidence_links.length
|
||||||
: 0;
|
: 0;
|
||||||
|
const reliabilityLimited = structure.mechanism_block.status !== "grounded" ||
|
||||||
|
structure.uncertainty_block.limitations.length > 0 ||
|
||||||
|
structure.uncertainty_block.open_uncertainties.length > 0 ||
|
||||||
|
structure.evidence_block.coverage_note === "coverage_partial_or_limited";
|
||||||
const lines = [];
|
const lines = [];
|
||||||
const coverageSplitLines = buildCoverageSplitLines(structure);
|
const coverageSplitLines = buildCoverageSplitLines(structure);
|
||||||
if (evidenceCount > 0) {
|
if (evidenceCount > 0) {
|
||||||
|
|
@ -2186,7 +2561,7 @@ function buildEvidenceSectionLines(structure) {
|
||||||
lines.push("Опора частичная: часть требований покрыта не полностью.");
|
lines.push("Опора частичная: часть требований покрыта не полностью.");
|
||||||
}
|
}
|
||||||
else if (evidenceCount > 0) {
|
else if (evidenceCount > 0) {
|
||||||
lines.push("Опора достаточна для первичного вывода.");
|
lines.push(reliabilityLimited ? "Опора есть, но достаточна только для предварительного вывода." : "Опора достаточна для первичного вывода.");
|
||||||
}
|
}
|
||||||
if (lines.length === 0) {
|
if (lines.length === 0) {
|
||||||
lines.push("Использована доступная выборка документов и проводок в текущем snapshot.");
|
lines.push("Использована доступная выборка документов и проводок в текущем snapshot.");
|
||||||
|
|
@ -2267,6 +2642,8 @@ function humanizeLimitationToken(value) {
|
||||||
return "Не указан документ или объект для трассировки.";
|
return "Не указан документ или объект для трассировки.";
|
||||||
if (normalized === "missing_anchor:counterparty")
|
if (normalized === "missing_anchor:counterparty")
|
||||||
return "Не указан контрагент или договор.";
|
return "Не указан контрагент или договор.";
|
||||||
|
if (normalized === "primary_domain_evidence_not_confirmed")
|
||||||
|
return "Целевой механизм активного домена подтвержден частично; вывод ограничен.";
|
||||||
if (normalized === "settlement_primary_evidence_not_confirmed")
|
if (normalized === "settlement_primary_evidence_not_confirmed")
|
||||||
return "Опора по расчетному контуру не подтверждена: в приоритете были сигналы из смежных доменов.";
|
return "Опора по расчетному контуру не подтверждена: в приоритете были сигналы из смежных доменов.";
|
||||||
if (normalized.includes("snapshot"))
|
if (normalized.includes("snapshot"))
|
||||||
|
|
@ -2330,26 +2707,146 @@ function buildLimitationsSectionLines(structure) {
|
||||||
}
|
}
|
||||||
return ["Существенных ограничений в текущем срезе не выявлено."];
|
return ["Существенных ограничений в текущем срезе не выявлено."];
|
||||||
}
|
}
|
||||||
function renderPolicyReply(structure) {
|
function domainNameForQuestionType(domain) {
|
||||||
|
if (domain === "settlements_60_62")
|
||||||
|
return "\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0443\u0440\u0430";
|
||||||
|
if (domain === "vat_document_register_book")
|
||||||
|
return "\u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u041d\u0414\u0421";
|
||||||
|
if (domain === "month_close_costs_20_44")
|
||||||
|
return "\u043a\u043e\u043d\u0442\u0443\u0440\u0430 \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f \u043c\u0435\u0441\u044f\u0446\u0430";
|
||||||
|
return "\u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0443\u0447\u0430\u0441\u0442\u043a\u0430";
|
||||||
|
}
|
||||||
|
function buildQuestionTypeShortLine(context) {
|
||||||
|
const domainName = domainNameForQuestionType(context.focusDomain);
|
||||||
|
if (context.questionType === "where_break_is") {
|
||||||
|
return `\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u0440\u0430\u0437\u0440\u044b\u0432 \u0432\u043d\u0443\u0442\u0440\u0438 ${domainName}.`;
|
||||||
|
}
|
||||||
|
if (context.questionType === "prove_or_guess") {
|
||||||
|
return "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u0440\u0430\u0437\u0432\u0435\u0441\u0442\u0438 \u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043e \u0438 \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u0443.";
|
||||||
|
}
|
||||||
|
if (context.questionType === "what_is_it_grounded_on") {
|
||||||
|
return "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u043e \u0434\u0430\u043d\u043d\u044b\u043c.";
|
||||||
|
}
|
||||||
|
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
|
||||||
|
return "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u0438 \u043d\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u0446\u0435\u043f\u043e\u0447\u043a\u0438.";
|
||||||
|
}
|
||||||
|
if (context.questionType === "what_to_check_first") {
|
||||||
|
return "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u0434\u0430\u0442\u044c \u043f\u0435\u0440\u0432\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function buildQuestionTypeBrokenLine(context) {
|
||||||
|
if (context.questionType !== "where_break_is") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (context.focusDomain === "settlements_60_62") {
|
||||||
|
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430: \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0430 \u043e\u043f\u043b\u0430\u0442\u044b \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0443 \u0440\u0430\u0441\u0447\u0435\u0442\u043e\u0432 \u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0443 \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f.";
|
||||||
|
}
|
||||||
|
if (context.focusDomain === "vat_document_register_book") {
|
||||||
|
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430: \u0441\u0432\u044f\u0437\u043a\u0430 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430, \u0441\u0447\u0435\u0442\u0430-\u0444\u0430\u043a\u0442\u0443\u0440\u044b \u0438 \u0437\u0430\u043f\u0438\u0441\u0438 \u043a\u043d\u0438\u0433\u0438.";
|
||||||
|
}
|
||||||
|
if (context.focusDomain === "month_close_costs_20_44") {
|
||||||
|
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430: \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u043e\u0442 \u043d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f \u0437\u0430\u0442\u0440\u0430\u0442 \u043a \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044e/\u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044e.";
|
||||||
|
}
|
||||||
|
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430 \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u0447\u0430\u0441\u0442\u0438\u0447\u043d\u043e; \u043d\u0443\u0436\u043d\u0430 \u0442\u043e\u0447\u0435\u0447\u043d\u0430\u044f \u0441\u0432\u0435\u0440\u043a\u0430.";
|
||||||
|
}
|
||||||
|
function buildQuestionTypeWhyLine(context) {
|
||||||
|
if (context.questionType === "prove_or_guess") {
|
||||||
|
return "\u0417\u0434\u0435\u0441\u044c \u0447\u0435\u0441\u0442\u043d\u043e \u0440\u0430\u0437\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u0447\u0442\u043e \u0443\u0436\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e \u0438 \u0447\u0442\u043e \u043f\u043e\u043a\u0430 \u043e\u0441\u0442\u0430\u0435\u0442\u0441\u044f \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u043e\u0439.";
|
||||||
|
}
|
||||||
|
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
|
||||||
|
return "\u0426\u0435\u043f\u043e\u0447\u043a\u0438 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u044b \u043d\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u0438 \u043d\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u043f\u043e \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u043e\u043f\u043e\u0440\u0435.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function buildQuestionTypeEvidenceLine(context) {
|
||||||
|
if (context.questionType === "what_is_it_grounded_on") {
|
||||||
|
return "\u0412 \u044d\u0442\u043e\u043c \u043e\u0442\u0432\u0435\u0442\u0435 \u0432 \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u0438\u043c\u0435\u043d\u043d\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u044f \u0432\u044b\u0432\u043e\u0434\u0430.";
|
||||||
|
}
|
||||||
|
if (context.questionType === "prove_or_guess") {
|
||||||
|
return "\u0421\u0438\u043b\u0430 \u0432\u044b\u0432\u043e\u0434\u0430 \u043e\u0446\u0435\u043d\u0435\u043d\u0430 \u043f\u043e \u043f\u0440\u044f\u043c\u043e\u0439 \u043e\u043f\u043e\u0440\u0435, \u0430 \u043d\u0435 \u043f\u043e \u0434\u043e\u0433\u0430\u0434\u043a\u0430\u043c.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function formatAnchorList(anchors, prefix) {
|
||||||
|
if (anchors.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${prefix}: ${anchors.join(", ")}.`;
|
||||||
|
}
|
||||||
|
function buildQuestionTypeCheckLine(context) {
|
||||||
|
if (context.questionType === "what_to_check_first") {
|
||||||
|
return "\u041d\u0430\u0447\u043d\u0438\u0442\u0435 \u0441 \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u043f\u0443\u043d\u043a\u0442\u0430 \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u0431\u0435\u0437 \u043f\u0435\u0440\u0435\u0441\u043a\u043e\u043a\u0430.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function buildQuestionTypeLimitationLine(context) {
|
||||||
|
if (context.questionType === "prove_or_guess") {
|
||||||
|
return "\u0414\u043b\u044f \u0444\u043e\u0440\u043c\u0430\u0442\u0430 \u00ab\u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043e \u0438\u043b\u0438 \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u0430\u00bb \u0432\u0441\u0435 \u043d\u0435\u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0447\u0430\u0441\u0442\u0438 \u043e\u0442\u0434\u0435\u043b\u0435\u043d\u044b \u0432 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f.";
|
||||||
|
}
|
||||||
|
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
|
||||||
|
return "\u0414\u0435\u043b\u0435\u043d\u0438\u0435 \u043d\u0430 \u00abcomplete/incomplete\u00bb \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043f\u043e\u043b\u043d\u043e\u0442\u044b \u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c \u0441\u0440\u0435\u0437\u0435.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function applyQuestionTypeAndAnchorPolicy(input) {
|
||||||
|
const nextShort = buildQuestionTypeShortLine(input.context) ?? input.shortLine;
|
||||||
|
const nextBroken = dedupeNarrativeLines([buildQuestionTypeBrokenLine(input.context), ...input.brokenLines].filter((item) => Boolean(item)), 4);
|
||||||
|
const nextWhy = dedupeNarrativeLines([buildQuestionTypeWhyLine(input.context), ...input.whyLines].filter((item) => Boolean(item)), 4);
|
||||||
|
const anchorUsedLine = formatAnchorList(input.context.anchors.used, "\u0412 \u043e\u043f\u043e\u0440\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u044b \u044f\u043a\u043e\u0440\u044f \u0432\u043e\u043f\u0440\u043e\u0441\u0430");
|
||||||
|
const anchorUnusedLine = formatAnchorList(input.context.anchors.unused, "\u042f\u043a\u043e\u0440\u044f \u0438\u0437 \u0432\u043e\u043f\u0440\u043e\u0441\u0430 \u0431\u0435\u0437 \u043f\u0440\u044f\u043c\u043e\u0433\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f");
|
||||||
|
const nextEvidence = dedupeNarrativeLines([buildQuestionTypeEvidenceLine(input.context), ...input.evidenceLines, anchorUsedLine].filter((item) => Boolean(item)), 7);
|
||||||
|
const nextChecks = dedupeNarrativeLines([buildQuestionTypeCheckLine(input.context), ...input.checkLines].filter((item) => Boolean(item)), 5);
|
||||||
|
const nextLimitations = dedupeNarrativeLines([buildQuestionTypeLimitationLine(input.context), anchorUnusedLine, ...input.limitationLines].filter((item) => Boolean(item)), 6);
|
||||||
|
return {
|
||||||
|
shortLine: ensureSentence(nextShort),
|
||||||
|
brokenLines: nextBroken,
|
||||||
|
whyLines: nextWhy,
|
||||||
|
evidenceLines: nextEvidence,
|
||||||
|
checkLines: nextChecks,
|
||||||
|
limitationLines: nextLimitations
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function renderPolicyReply(structure, context) {
|
||||||
const shortLine = ensureSentence(buildShortSectionLine(structure));
|
const shortLine = ensureSentence(buildShortSectionLine(structure));
|
||||||
const brokenLines = buildBrokenSectionLines(structure);
|
const brokenLines = buildBrokenSectionLines(structure);
|
||||||
const whyLines = buildWhySectionLines(structure);
|
const whyLines = buildWhySectionLines(structure);
|
||||||
const evidenceLines = buildEvidenceSectionLines(structure);
|
const evidenceLines = buildEvidenceSectionLines(structure);
|
||||||
const checkLines = buildChecksSectionLines(structure);
|
const checkLines = buildChecksSectionLines(structure);
|
||||||
const limitationLines = buildLimitationsSectionLines(structure);
|
const limitationLines = buildLimitationsSectionLines(structure);
|
||||||
|
const enriched = context
|
||||||
|
? applyQuestionTypeAndAnchorPolicy({
|
||||||
|
shortLine,
|
||||||
|
brokenLines,
|
||||||
|
whyLines,
|
||||||
|
evidenceLines,
|
||||||
|
checkLines,
|
||||||
|
limitationLines,
|
||||||
|
context
|
||||||
|
})
|
||||||
|
: {
|
||||||
|
shortLine,
|
||||||
|
brokenLines,
|
||||||
|
whyLines,
|
||||||
|
evidenceLines,
|
||||||
|
checkLines,
|
||||||
|
limitationLines
|
||||||
|
};
|
||||||
return sanitizeUserFacingReply([
|
return sanitizeUserFacingReply([
|
||||||
`Коротко: ${shortLine}`,
|
`Коротко: ${enriched.shortLine}`,
|
||||||
`Что сломано:\n${formatList(brokenLines)}`,
|
`Что сломано:\n${formatList(enriched.brokenLines)}`,
|
||||||
`Почему это похоже на проблему:\n${formatList(whyLines)}`,
|
`Почему это похоже на проблему:\n${formatList(enriched.whyLines)}`,
|
||||||
`На чем это основано:\n${formatList(evidenceLines)}`,
|
`На чем это основано:\n${formatList(enriched.evidenceLines)}`,
|
||||||
`Что проверить первым:\n${formatList(checkLines)}`,
|
`Что проверить первым:\n${formatList(enriched.checkLines)}`,
|
||||||
`Ограничения:\n${formatList(limitationLines)}`
|
`Ограничения:\n${formatList(enriched.limitationLines)}`
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n"));
|
.join("\n\n"));
|
||||||
}
|
}
|
||||||
function composeAssistantAnswerV11(input) {
|
function composeAssistantAnswerV11(input) {
|
||||||
const fallbackType = fallbackFromSummary(input.routeSummary);
|
const fallbackType = fallbackFromSummary(input.routeSummary);
|
||||||
|
const questionType = input.questionTypeHint ?? "unknown";
|
||||||
|
const anchorUsage = evaluateCompanyAnchorUsage(input.companyAnchors, input.retrievalResults);
|
||||||
const okResults = input.retrievalResults.filter((item) => item.status === "ok");
|
const okResults = input.retrievalResults.filter((item) => item.status === "ok");
|
||||||
const partialResults = input.retrievalResults.filter((item) => item.status === "partial");
|
const partialResults = input.retrievalResults.filter((item) => item.status === "partial");
|
||||||
const emptyResults = input.retrievalResults.filter((item) => item.status === "empty");
|
const emptyResults = input.retrievalResults.filter((item) => item.status === "empty");
|
||||||
|
|
@ -2368,15 +2865,8 @@ function composeAssistantAnswerV11(input) {
|
||||||
const problemUnitSummary = selectProblemUnitSummary(input.retrievalResults);
|
const problemUnitSummary = selectProblemUnitSummary(input.retrievalResults);
|
||||||
const problemHeavyUnits = problemUnits.filter((item) => PROBLEM_HEAVY_TYPES.has(item.problem_unit_type));
|
const problemHeavyUnits = problemUnits.filter((item) => PROBLEM_HEAVY_TYPES.has(item.problem_unit_type));
|
||||||
const focusNarrativeDomain = inferP0FocusNarrativeDomain(input.userMessage, input.retrievalResults, problemHeavyUnits, input.focusDomainHint);
|
const focusNarrativeDomain = inferP0FocusNarrativeDomain(input.userMessage, input.retrievalResults, problemHeavyUnits, input.focusDomainHint);
|
||||||
const settlementGrounding = focusNarrativeDomain === "settlements_60_62"
|
const focusDomainGrounding = evaluateP0DomainEvidenceGrounding(input.retrievalResults, focusNarrativeDomain);
|
||||||
? evaluateSettlementEvidenceGrounding(input.retrievalResults)
|
const focusDomainGroundingBlocked = Boolean(focusNarrativeDomain && focusDomainGrounding.blocked);
|
||||||
: {
|
|
||||||
has_settlement_primary: false,
|
|
||||||
has_foreign_primary: false,
|
|
||||||
foreign_primary_domains: [],
|
|
||||||
blocked: false
|
|
||||||
};
|
|
||||||
const settlementGroundingBlocked = focusNarrativeDomain === "settlements_60_62" && settlementGrounding.blocked;
|
|
||||||
const rankedProblemUnits = rankProblemUnitsForAnswer(problemHeavyUnits, lifecycleAnswerEnabled, focusNarrativeDomain);
|
const rankedProblemUnits = rankProblemUnitsForAnswer(problemHeavyUnits, lifecycleAnswerEnabled, focusNarrativeDomain);
|
||||||
const domainAlignedProblemUnits = focusNarrativeDomain === null
|
const domainAlignedProblemUnits = focusNarrativeDomain === null
|
||||||
? rankedProblemUnits
|
? rankedProblemUnits
|
||||||
|
|
@ -2384,7 +2874,7 @@ function composeAssistantAnswerV11(input) {
|
||||||
const domainLockMissBase = Boolean(focusNarrativeDomain &&
|
const domainLockMissBase = Boolean(focusNarrativeDomain &&
|
||||||
rankedProblemUnits.length > 0 &&
|
rankedProblemUnits.length > 0 &&
|
||||||
domainAlignedProblemUnits.length === 0);
|
domainAlignedProblemUnits.length === 0);
|
||||||
const domainLockMiss = domainLockMissBase || settlementGroundingBlocked;
|
const domainLockMiss = domainLockMissBase || focusDomainGroundingBlocked;
|
||||||
const selectedProblemUnits = (focusNarrativeDomain === null ? rankedProblemUnits : domainAlignedProblemUnits).slice(0, 4);
|
const selectedProblemUnits = (focusNarrativeDomain === null ? rankedProblemUnits : domainAlignedProblemUnits).slice(0, 4);
|
||||||
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
|
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
|
||||||
const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, evidenceItems);
|
const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, evidenceItems);
|
||||||
|
|
@ -2422,7 +2912,7 @@ function composeAssistantAnswerV11(input) {
|
||||||
focusedStrong,
|
focusedStrong,
|
||||||
policySignals
|
policySignals
|
||||||
});
|
});
|
||||||
const guardedDecision = settlementGroundingBlocked &&
|
const guardedDecision = focusDomainGroundingBlocked &&
|
||||||
decision.mode !== "out_of_scope" &&
|
decision.mode !== "out_of_scope" &&
|
||||||
decision.mode !== "route_mismatch" &&
|
decision.mode !== "route_mismatch" &&
|
||||||
decision.mode !== "backend_error"
|
decision.mode !== "backend_error"
|
||||||
|
|
@ -2437,7 +2927,9 @@ function composeAssistantAnswerV11(input) {
|
||||||
policySignals.minimum_evidence_failed ||
|
policySignals.minimum_evidence_failed ||
|
||||||
limitationReasonCodes.includes("missing_mechanism") ||
|
limitationReasonCodes.includes("missing_mechanism") ||
|
||||||
limitationReasonCodes.includes("weak_source_mapping") ||
|
limitationReasonCodes.includes("weak_source_mapping") ||
|
||||||
|
limitationReasonCodes.includes("insufficient_detail") ||
|
||||||
aggregateEvidenceConfidence === "low" ||
|
aggregateEvidenceConfidence === "low" ||
|
||||||
|
domainLockMiss ||
|
||||||
lowConfidenceConcentration;
|
lowConfidenceConcentration;
|
||||||
const hardBlockedMode = guardedDecision.mode === "out_of_scope" ||
|
const hardBlockedMode = guardedDecision.mode === "out_of_scope" ||
|
||||||
guardedDecision.mode === "route_mismatch" ||
|
guardedDecision.mode === "route_mismatch" ||
|
||||||
|
|
@ -2468,7 +2960,11 @@ function composeAssistantAnswerV11(input) {
|
||||||
});
|
});
|
||||||
const lifecycleModeActive = lifecycleAnswerEnabled && selectedProblemUnits.length > 0 && hasLifecycleResolution(selectedProblemUnits);
|
const lifecycleModeActive = lifecycleAnswerEnabled && selectedProblemUnits.length > 0 && hasLifecycleResolution(selectedProblemUnits);
|
||||||
return {
|
return {
|
||||||
assistant_reply: renderPolicyReply(problemCentricStructure),
|
assistant_reply: renderPolicyReply(problemCentricStructure, {
|
||||||
|
questionType,
|
||||||
|
focusDomain: focusNarrativeDomain,
|
||||||
|
anchors: anchorUsage
|
||||||
|
}),
|
||||||
fallback_type: guardedDecision.fallback_type,
|
fallback_type: guardedDecision.fallback_type,
|
||||||
reply_type: guardedDecision.reply_type,
|
reply_type: guardedDecision.reply_type,
|
||||||
answer_structure_v11: problemCentricStructure,
|
answer_structure_v11: problemCentricStructure,
|
||||||
|
|
@ -2495,9 +2991,12 @@ function composeAssistantAnswerV11(input) {
|
||||||
...limitationReasonCodes.map((code) => limitationReasonToText(code)),
|
...limitationReasonCodes.map((code) => limitationReasonToText(code)),
|
||||||
...extractLimitations(input.retrievalResults),
|
...extractLimitations(input.retrievalResults),
|
||||||
...input.groundingCheck.reasons,
|
...input.groundingCheck.reasons,
|
||||||
...(settlementGroundingBlocked
|
...(focusDomainGroundingBlocked
|
||||||
|
? ["Целевой механизм активного домена подтвержден частично; часть первичной опоры пришла из смежного контура."]
|
||||||
|
: []),
|
||||||
|
...(anchorUsage.unused.length > 0
|
||||||
? [
|
? [
|
||||||
`Primary settlement evidence is not confirmed; foreign domains dominate: ${settlementGrounding.foreign_primary_domains.join(", ") || "unknown"}.`
|
`Часть якорей запроса пока не подтверждена в опоре: ${anchorUsage.unused.slice(0, 5).join(", ")}.`
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(policySignals.minimum_evidence_failed ? ["Minimum evidence gate failed for current scope."] : []),
|
...(policySignals.minimum_evidence_failed ? ["Minimum evidence gate failed for current scope."] : []),
|
||||||
|
|
@ -2511,11 +3010,18 @@ function composeAssistantAnswerV11(input) {
|
||||||
...(guardedDecision.mode === "clarification_required" && missingAnchors.account ? ["missing_anchor:account"] : []),
|
...(guardedDecision.mode === "clarification_required" && missingAnchors.account ? ["missing_anchor:account"] : []),
|
||||||
...(guardedDecision.mode === "clarification_required" && missingAnchors.documentOrObject ? ["missing_anchor:document_or_object"] : []),
|
...(guardedDecision.mode === "clarification_required" && missingAnchors.documentOrObject ? ["missing_anchor:document_or_object"] : []),
|
||||||
...(guardedDecision.mode === "clarification_required" && missingAnchors.counterparty ? ["missing_anchor:counterparty"] : []),
|
...(guardedDecision.mode === "clarification_required" && missingAnchors.counterparty ? ["missing_anchor:counterparty"] : []),
|
||||||
...(settlementGroundingBlocked ? ["settlement_primary_evidence_not_confirmed"] : [])
|
...(focusDomainGroundingBlocked ? ["primary_domain_evidence_not_confirmed"] : [])
|
||||||
], 8);
|
], 8);
|
||||||
|
const confidenceLimited = guardedDecision.mode !== "focused_grounded" ||
|
||||||
|
limitationReasonCodes.includes("missing_mechanism") ||
|
||||||
|
limitationReasonCodes.includes("heuristic_inference") ||
|
||||||
|
limitationReasonCodes.includes("weak_source_mapping") ||
|
||||||
|
limitationReasonCodes.includes("insufficient_detail") ||
|
||||||
|
aggregateEvidenceConfidence === "low" ||
|
||||||
|
focusDomainGroundingBlocked;
|
||||||
const mechanismStatus = mechanismNotes.length === 0
|
const mechanismStatus = mechanismNotes.length === 0
|
||||||
? "unresolved"
|
? "unresolved"
|
||||||
: limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
|
: confidenceLimited
|
||||||
? "limited"
|
? "limited"
|
||||||
: "grounded";
|
: "grounded";
|
||||||
const answerStructure = {
|
const answerStructure = {
|
||||||
|
|
@ -2524,7 +3030,8 @@ function composeAssistantAnswerV11(input) {
|
||||||
direct_answer: buildDirectAnswer({
|
direct_answer: buildDirectAnswer({
|
||||||
mode: guardedDecision.mode,
|
mode: guardedDecision.mode,
|
||||||
retrievalResults: input.retrievalResults,
|
retrievalResults: input.retrievalResults,
|
||||||
policySignals
|
policySignals,
|
||||||
|
focusDomain: focusNarrativeDomain
|
||||||
}),
|
}),
|
||||||
mechanism_block: {
|
mechanism_block: {
|
||||||
status: mechanismStatus,
|
status: mechanismStatus,
|
||||||
|
|
@ -2557,7 +3064,11 @@ function composeAssistantAnswerV11(input) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
assistant_reply: renderPolicyReply(answerStructure),
|
assistant_reply: renderPolicyReply(answerStructure, {
|
||||||
|
questionType,
|
||||||
|
focusDomain: focusNarrativeDomain,
|
||||||
|
anchors: anchorUsage
|
||||||
|
}),
|
||||||
fallback_type: guardedDecision.fallback_type,
|
fallback_type: guardedDecision.fallback_type,
|
||||||
reply_type: guardedDecision.reply_type,
|
reply_type: guardedDecision.reply_type,
|
||||||
answer_structure_v11: answerStructure,
|
answer_structure_v11: answerStructure,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ const ACCOUNT_SPECIFIC_MARKERS = /(?:\u0441\u0447\u0435\u0442(?:\u0430|\u0443|\u
|
||||||
const PERIOD_MARKERS = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/;
|
const PERIOD_MARKERS = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/;
|
||||||
const ENTITY_SPECIFIC_MARKERS = /(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|supplier|buyer|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|invoice|posting|register|guid|id[:=\s])/iu;
|
const ENTITY_SPECIFIC_MARKERS = /(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|supplier|buyer|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|invoice|posting|register|guid|id[:=\s])/iu;
|
||||||
const EXACT_OBJECT_MARKERS = /(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\s*(?:#|\u2116)|\bref\b|\bid\b|trx-\d+|inv-\d+)/iu;
|
const EXACT_OBJECT_MARKERS = /(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\s*(?:#|\u2116)|\bref\b|\bid\b|trx-\d+|inv-\d+)/iu;
|
||||||
|
const CONTRACT_MARKERS = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440(?:\u0430|\u0443|\u043e\u043c|\u0435)?\s*(?:№|#|n)\s*[a-z\u0430-\u044f0-9./_-]+)/iu;
|
||||||
|
const DOCUMENT_NUMBER_MARKERS = /(?:(?:\u0441\u0447(?:\u0435|\u0451)\u0442(?:-\u0444\u0430\u043a\u0442\u0443\u0440(?:\u0430|\u044b))?|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446(?:\u0438\u044f|\u0438\u0438)|\u0430\u043a\u0442)\s*(?:№|#|n)\s*[a-z\u0430-\u044f0-9./_-]+)/iu;
|
||||||
|
const AMOUNT_MARKERS = /\b(?:\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?|\d+[.,]\d{2})\b/u;
|
||||||
const ROUTE_MIN_EVIDENCE_GATE = {
|
const ROUTE_MIN_EVIDENCE_GATE = {
|
||||||
hybrid_store_plus_live: {
|
hybrid_store_plus_live: {
|
||||||
min_evidence_items: 3,
|
min_evidence_items: 3,
|
||||||
|
|
@ -101,6 +104,9 @@ function detectBroadQuery(fragmentText, route) {
|
||||||
const hasEntityAnchor = ENTITY_SPECIFIC_MARKERS.test(lower);
|
const hasEntityAnchor = ENTITY_SPECIFIC_MARKERS.test(lower);
|
||||||
const hasExactObjectAnchor = EXACT_OBJECT_MARKERS.test(lower);
|
const hasExactObjectAnchor = EXACT_OBJECT_MARKERS.test(lower);
|
||||||
const hasGuidAnchor = extractGuids(lower).length > 0;
|
const hasGuidAnchor = extractGuids(lower).length > 0;
|
||||||
|
const hasContractAnchor = CONTRACT_MARKERS.test(lower);
|
||||||
|
const hasDocumentNumberAnchor = DOCUMENT_NUMBER_MARKERS.test(lower);
|
||||||
|
const hasAmountAnchor = AMOUNT_MARKERS.test(lower);
|
||||||
let anchorScore = 0;
|
let anchorScore = 0;
|
||||||
if (hasGuidAnchor)
|
if (hasGuidAnchor)
|
||||||
anchorScore += 3;
|
anchorScore += 3;
|
||||||
|
|
@ -112,8 +118,17 @@ function detectBroadQuery(fragmentText, route) {
|
||||||
anchorScore += 1;
|
anchorScore += 1;
|
||||||
if (hasExactObjectAnchor)
|
if (hasExactObjectAnchor)
|
||||||
anchorScore += 1;
|
anchorScore += 1;
|
||||||
|
if (hasContractAnchor)
|
||||||
|
anchorScore += 2;
|
||||||
|
if (hasDocumentNumberAnchor)
|
||||||
|
anchorScore += 2;
|
||||||
|
if (hasAmountAnchor)
|
||||||
|
anchorScore += 1;
|
||||||
const weakAnchors = anchorScore <= 1;
|
const weakAnchors = anchorScore <= 1;
|
||||||
const strongFocus = hasGuidAnchor || (hasAccountAnchor && hasPeriodAnchor) || anchorScore >= 4;
|
const strongFocus = hasGuidAnchor ||
|
||||||
|
(hasAccountAnchor && hasPeriodAnchor) ||
|
||||||
|
(hasContractAnchor && hasDocumentNumberAnchor) ||
|
||||||
|
anchorScore >= 4;
|
||||||
const routeSensitiveBroad = route === "batch_refresh_then_store" || route === "hybrid_store_plus_live";
|
const routeSensitiveBroad = route === "batch_refresh_then_store" || route === "hybrid_store_plus_live";
|
||||||
let broadnessLevel = "low";
|
let broadnessLevel = "low";
|
||||||
if (hasGenericMarker && !strongFocus && (weakAnchors || routeSensitiveBroad)) {
|
if (hasGenericMarker && !strongFocus && (weakAnchors || routeSensitiveBroad)) {
|
||||||
|
|
@ -233,9 +248,7 @@ const P0_DOMAIN_CARDS = [
|
||||||
/\u0441\u0447[её]т.?фактур/i,
|
/\u0441\u0447[её]т.?фактур/i,
|
||||||
/\u043a\u043d\u0438\u0433[аи]\s+\u043f\u043e\u043a\u0443\u043f/i,
|
/\u043a\u043d\u0438\u0433[аи]\s+\u043f\u043e\u043a\u0443\u043f/i,
|
||||||
/\u043a\u043d\u0438\u0433[аи]\s+\u043f\u0440\u043e\u0434\u0430\u0436/i,
|
/\u043a\u043d\u0438\u0433[аи]\s+\u043f\u0440\u043e\u0434\u0430\u0436/i,
|
||||||
/\u0432\u044b\u0447\u0435\u0442/i,
|
/\u0432\u044b\u0447\u0435\u0442/i
|
||||||
/\b19\b/,
|
|
||||||
/\b68\b/
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -251,19 +264,20 @@ const P0_DOMAIN_CARDS = [
|
||||||
expected_edges: ["document_to_posting", "deferred_expense_to_writeoff", "contract_to_documents"],
|
expected_edges: ["document_to_posting", "deferred_expense_to_writeoff", "contract_to_documents"],
|
||||||
forbidden_cross_domain_leakage: ["vat", "taxes", "bank", "settlements", "suppliers", "customers", "fixed_assets"],
|
forbidden_cross_domain_leakage: ["vat", "taxes", "bank", "settlements", "suppliers", "customers", "fixed_assets"],
|
||||||
symptom_markers: [
|
symptom_markers: [
|
||||||
/\b20\b/,
|
|
||||||
/\b21\b/,
|
|
||||||
/\b23\b/,
|
|
||||||
/\b25\b/,
|
|
||||||
/\b26\b/,
|
|
||||||
/\b28\b/,
|
|
||||||
/\b29\b/,
|
|
||||||
/\b44\b/,
|
|
||||||
/period\s*close/i,
|
/period\s*close/i,
|
||||||
/\u0437\u0430\u043a\u0440\u044b\u0442/i,
|
/month\s*close/i,
|
||||||
|
/close\s+period/i,
|
||||||
|
/закрыт[а-яё]*\s+период/i,
|
||||||
|
/close\s+operation/i,
|
||||||
|
/allocation/i,
|
||||||
|
/закр/i,
|
||||||
|
/перио/i,
|
||||||
|
/\u0437\u0430\u043a\u0440\u044b\u0442(?:\u0438|\u0438\u0435|\u044b|)\s*(?:\u043c\u0435\u0441\u044f\u0446|\u0441\u0447\u0435\u0442)/i,
|
||||||
|
/\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442/i,
|
||||||
/\u0437\u0430\u0442\u0440\u0430\u0442/i,
|
/\u0437\u0430\u0442\u0440\u0430\u0442/i,
|
||||||
/\u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b/i,
|
/\u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b/i,
|
||||||
/\u043e\u0441\u0442\u0430\u0442\u043a/i
|
/\u0440\u0431\u043f/i,
|
||||||
|
/\u0430\u043c\u043e\u0440\u0442\u0438\u0437/i
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
@ -883,6 +897,26 @@ function extractAccountScopeFromText(text) {
|
||||||
pushAccount(account);
|
pushAccount(account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const closePairPattern = /\b(?:20|21|23|25|26|28|29|44)\s*[-/]\s*(?:20|21|23|25|26|28|29|44)\b/g;
|
||||||
|
let closePairMatch = null;
|
||||||
|
while ((closePairMatch = closePairPattern.exec(lower)) !== null) {
|
||||||
|
const pair = closePairMatch[0];
|
||||||
|
const pairAccounts = pair.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
|
||||||
|
for (const account of pairAccounts) {
|
||||||
|
pushAccount(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const suffixAnchorPattern = /\b(?:51|60|62|68|76|97)(?:\.\d{1,2})?(?:-(?:му|й|го|м|х))?\b/giu;
|
||||||
|
let suffixAnchorMatch = null;
|
||||||
|
while ((suffixAnchorMatch = suffixAnchorPattern.exec(lower)) !== null) {
|
||||||
|
const token = suffixAnchorMatch[0];
|
||||||
|
const start = suffixAnchorMatch.index;
|
||||||
|
const end = start + token.length;
|
||||||
|
if (intersectsSpan(start, end, dateSpans)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pushAccount(token);
|
||||||
|
}
|
||||||
const explicitPattern = /\b\d{2}(?:\.\d{1,2})?\b/g;
|
const explicitPattern = /\b\d{2}(?:\.\d{1,2})?\b/g;
|
||||||
let explicitMatch = null;
|
let explicitMatch = null;
|
||||||
const settlementLexicalAnchor = /(оплат|расчет|расч[её]т|аванс|долг|постав|покуп|settlement|payment|supplier|customer)/i.test(lower);
|
const settlementLexicalAnchor = /(оплат|расчет|расч[её]т|аванс|долг|постав|покуп|settlement|payment|supplier|customer)/i.test(lower);
|
||||||
|
|
@ -1037,31 +1071,41 @@ function buildSemanticRetrievalProfile(fragmentText) {
|
||||||
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
||||||
pushMany(relationPatterns, ["payment_to_settlement", "statement_to_document", "document_to_posting"]);
|
pushMany(relationPatterns, ["payment_to_settlement", "statement_to_document", "document_to_posting"]);
|
||||||
}
|
}
|
||||||
if (/постав|постав|supplier|vendor|60\b/i.test(lower)) {
|
const hasSettlementAccountScope = accountScope.some((item) => item === "51" || item === "60" || item === "62" || item === "76");
|
||||||
|
const hasVatAccountScope = accountScope.some((item) => item === "19" || item === "68");
|
||||||
|
const hasFixedAssetAccountScope = accountScope.some((item) => item === "01" || item === "02" || item === "08");
|
||||||
|
const hasDeferredExpenseAccountScope = accountScope.some((item) => item === "97");
|
||||||
|
const hasMonthCloseCostsAccountScope = accountScope.some((item) => CLOSE_COST_ACCOUNTS.includes(item));
|
||||||
|
const hasExplicitMonthCloseLexicalMarker = /(?:закрыти[ея]\s+месяц|закрыт[а-яё]*\s+период|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+period|close\s+operation)/i.test(lower) ||
|
||||||
|
(/закр/i.test(lower) && /перио/i.test(lower));
|
||||||
|
if (/постав|постав|supplier|vendor/i.test(lower) || hasSettlementAccountScope) {
|
||||||
pushMany(domainScope, ["suppliers", "settlements"]);
|
pushMany(domainScope, ["suppliers", "settlements"]);
|
||||||
pushMany(documentTypes, ["supplier_receipt", "settlement_document"]);
|
pushMany(documentTypes, ["supplier_receipt", "settlement_document"]);
|
||||||
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
||||||
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
|
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
|
||||||
}
|
}
|
||||||
if (/покупат|покупат|customer|buyer|62\b/i.test(lower)) {
|
if (/покупат|покупат|customer|buyer/i.test(lower) || hasSettlementAccountScope) {
|
||||||
pushMany(domainScope, ["customers", "settlements"]);
|
pushMany(domainScope, ["customers", "settlements"]);
|
||||||
pushMany(documentTypes, ["sales_document", "settlement_document"]);
|
pushMany(documentTypes, ["sales_document", "settlement_document"]);
|
||||||
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
||||||
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
|
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
|
||||||
}
|
}
|
||||||
if (/РЅРґСЃ|ндс|vat|РєРЅРёРіР° РїРѕРєСѓРїРѕРє|РєРЅРёРіР° продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т.?фактур|19\b|68\b/i.test(lower)) {
|
if (/РЅРґСЃ|ндс|vat|РєРЅРёРіР° РїРѕРєСѓРїРѕРє|РєРЅРёРіР° продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т.?фактур/i.test(lower) ||
|
||||||
|
hasVatAccountScope) {
|
||||||
pushMany(domainScope, ["vat", "taxes"]);
|
pushMany(domainScope, ["vat", "taxes"]);
|
||||||
pushMany(documentTypes, ["invoice", "vat_document"]);
|
pushMany(documentTypes, ["invoice", "vat_document"]);
|
||||||
pushMany(entityTypes, ["document", "tax_entry", "posting"]);
|
pushMany(entityTypes, ["document", "tax_entry", "posting"]);
|
||||||
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
|
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
|
||||||
}
|
}
|
||||||
if (/РѕСЃ|РѕСЃРЅРѕРІРЅ(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз|01\b|02\b|08\b/i.test(lower)) {
|
if (/РѕСЃ|РѕСЃРЅРѕРІРЅ(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(lower) ||
|
||||||
|
hasFixedAssetAccountScope) {
|
||||||
pushMany(domainScope, ["fixed_assets"]);
|
pushMany(domainScope, ["fixed_assets"]);
|
||||||
pushMany(documentTypes, ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"]);
|
pushMany(documentTypes, ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"]);
|
||||||
pushMany(entityTypes, ["fixed_asset", "document", "posting"]);
|
pushMany(entityTypes, ["fixed_asset", "document", "posting"]);
|
||||||
pushMany(relationPatterns, ["asset_card_to_depreciation", "document_to_posting"]);
|
pushMany(relationPatterns, ["asset_card_to_depreciation", "document_to_posting"]);
|
||||||
}
|
}
|
||||||
if (/СЂР±Рї|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff|97\b/i.test(lower)) {
|
if (/СЂР±Рї|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) ||
|
||||||
|
hasDeferredExpenseAccountScope) {
|
||||||
pushMany(domainScope, ["deferred_expense", "period_close"]);
|
pushMany(domainScope, ["deferred_expense", "period_close"]);
|
||||||
pushMany(documentTypes, ["deferred_expense_document", "period_close_document"]);
|
pushMany(documentTypes, ["deferred_expense_document", "period_close_document"]);
|
||||||
pushMany(entityTypes, ["document", "posting"]);
|
pushMany(entityTypes, ["document", "posting"]);
|
||||||
|
|
@ -1084,7 +1128,7 @@ function buildSemanticRetrievalProfile(fragmentText) {
|
||||||
pushMany(anomalyPatterns, ["repeated_anomaly"]);
|
pushMany(anomalyPatterns, ["repeated_anomaly"]);
|
||||||
pushMany(rankingBasis, ["repeatability"]);
|
pushMany(rankingBasis, ["repeatability"]);
|
||||||
}
|
}
|
||||||
if (/закрыт|закрытие|период|закрыт|закрытие|период|month close|period close|closure/i.test(lower)) {
|
if (hasExplicitMonthCloseLexicalMarker || hasMonthCloseCostsAccountScope || hasDeferredExpenseAccountScope) {
|
||||||
pushMany(domainScope, ["period_close"]);
|
pushMany(domainScope, ["period_close"]);
|
||||||
pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]);
|
pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]);
|
||||||
pushMany(documentTypes, ["period_close_document"]);
|
pushMany(documentTypes, ["period_close_document"]);
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ const assistantDataLayer_1 = __importStar(require("./assistantDataLayer"));
|
||||||
const assistantSessionLogger_1 = __importStar(require("./assistantSessionLogger"));
|
const assistantSessionLogger_1 = __importStar(require("./assistantSessionLogger"));
|
||||||
const investigationState_1 = __importStar(require("./investigationState"));
|
const investigationState_1 = __importStar(require("./investigationState"));
|
||||||
const retrievalResultNormalizer_1 = __importStar(require("./retrievalResultNormalizer"));
|
const retrievalResultNormalizer_1 = __importStar(require("./retrievalResultNormalizer"));
|
||||||
|
const questionTypeResolver_1 = __importStar(require("./questionTypeResolver"));
|
||||||
|
const companyAnchorResolver_1 = __importStar(require("./companyAnchorResolver"));
|
||||||
function retrievalSummaryForRoute(route) {
|
function retrievalSummaryForRoute(route) {
|
||||||
if (route === "store_canonical")
|
if (route === "store_canonical")
|
||||||
return "Canonical accounting data path selected.";
|
return "Canonical accounting data path selected.";
|
||||||
|
|
@ -870,6 +872,26 @@ function extractFollowupAccountAnchorsLoose(text) {
|
||||||
}
|
}
|
||||||
return Array.from(new Set(anchors));
|
return Array.from(new Set(anchors));
|
||||||
}
|
}
|
||||||
|
function inferP0DomainFromMessage(text) {
|
||||||
|
const lower = String(text ?? "").toLowerCase();
|
||||||
|
const accountTokens = extractAccountTokens(lower);
|
||||||
|
const hasVatAccount = accountTokens.some((token) => /^(?:19|68)(?:\.|$)/.test(token));
|
||||||
|
const hasSettlementAccount = accountTokens.some((token) => /^(?:51|60|62|76)(?:\.|$)/.test(token));
|
||||||
|
const hasMonthCloseAccount = accountTokens.some((token) => /^(?:97|2\d|3\d|4[0-4])(?:\.|$)/.test(token));
|
||||||
|
const vatLexical = /(?:ндс|vat|счет[\s-]?фактур|сч[её]т[\s-]?фактур|книг[аи]\s+(?:покуп|продаж)|налогов)/i.test(lower);
|
||||||
|
const settlementLexical = /(?:долг|аванс|зач[её]т|взаимозач|расч[её]т|оплат|платеж|платёж|постав|покупател)/i.test(lower);
|
||||||
|
const monthCloseLexical = /(?:закрыти[ея]\s+месяц|закрытие счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых результат)/i.test(lower);
|
||||||
|
if (hasVatAccount || vatLexical) {
|
||||||
|
return "vat_document_register_book";
|
||||||
|
}
|
||||||
|
if (monthCloseLexical || hasMonthCloseAccount) {
|
||||||
|
return "month_close_costs_20_44";
|
||||||
|
}
|
||||||
|
if (hasSettlementAccount || settlementLexical) {
|
||||||
|
return "settlements_60_62";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function hasStrongFollowupAnchors(userMessage, state) {
|
function hasStrongFollowupAnchors(userMessage, state) {
|
||||||
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
|
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
|
||||||
if (explicitPeriod && state.focus.period && explicitPeriod !== state.focus.period) {
|
if (explicitPeriod && state.focus.period && explicitPeriod !== state.focus.period) {
|
||||||
|
|
@ -878,6 +900,14 @@ function hasStrongFollowupAnchors(userMessage, state) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const inferredDomain = inferP0DomainFromMessage(userMessage);
|
||||||
|
const activeDomain = compactWhitespace(state.followup_context?.active_domain ?? state.focus.domain ?? "");
|
||||||
|
if (inferredDomain && activeDomain && inferredDomain !== activeDomain) {
|
||||||
|
const domainLooksLikeFollowupRefinement = hasFollowupMarker(userMessage) && hasReferentialPointer(userMessage);
|
||||||
|
if (!domainLooksLikeFollowupRefinement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
const explicitAccounts = extractAccountTokens(userMessage);
|
const explicitAccounts = extractAccountTokens(userMessage);
|
||||||
const followupAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
|
const followupAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
|
||||||
if (followupAccounts.length > 0) {
|
if (followupAccounts.length > 0) {
|
||||||
|
|
@ -1193,6 +1223,8 @@ class AssistantService {
|
||||||
const focusDomainHint = followupBinding.usage?.applied
|
const focusDomainHint = followupBinding.usage?.applied
|
||||||
? session.investigation_state?.followup_context?.active_domain ?? session.investigation_state?.focus.domain ?? null
|
? session.investigation_state?.followup_context?.active_domain ?? session.investigation_state?.focus.domain ?? null
|
||||||
: null;
|
: null;
|
||||||
|
const questionTypeClass = (0, questionTypeResolver_1.resolveQuestionType)(userMessage);
|
||||||
|
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
|
||||||
const composition = (0, answerComposer_1.composeAssistantAnswer)({
|
const composition = (0, answerComposer_1.composeAssistantAnswer)({
|
||||||
userMessage,
|
userMessage,
|
||||||
routeSummary: normalized.route_hint_summary,
|
routeSummary: normalized.route_hint_summary,
|
||||||
|
|
@ -1201,6 +1233,8 @@ class AssistantService {
|
||||||
coverageReport: coverageEvaluation.coverage,
|
coverageReport: coverageEvaluation.coverage,
|
||||||
groundingCheck,
|
groundingCheck,
|
||||||
focusDomainHint,
|
focusDomainHint,
|
||||||
|
questionTypeHint: questionTypeClass,
|
||||||
|
companyAnchors,
|
||||||
enableAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11,
|
enableAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11,
|
||||||
enableProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1,
|
enableProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1,
|
||||||
enableLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1
|
enableLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1
|
||||||
|
|
@ -1251,6 +1285,8 @@ class AssistantService {
|
||||||
retrieval_results: retrievalResults,
|
retrieval_results: retrievalResults,
|
||||||
answer_grounding_check: groundingCheck,
|
answer_grounding_check: groundingCheck,
|
||||||
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
||||||
|
question_type_class: questionTypeClass,
|
||||||
|
company_anchors: companyAnchors,
|
||||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||||
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
||||||
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
||||||
|
|
@ -1314,6 +1350,8 @@ class AssistantService {
|
||||||
route_subject_match: groundingCheck.route_subject_match,
|
route_subject_match: groundingCheck.route_subject_match,
|
||||||
clarification_target: coverageEvaluation.coverage.clarification_needed_for,
|
clarification_target: coverageEvaluation.coverage.clarification_needed_for,
|
||||||
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
||||||
|
question_type_class: questionTypeClass,
|
||||||
|
company_anchors: companyAnchors,
|
||||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||||
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
||||||
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.resolveCompanyAnchors = resolveCompanyAnchors;
|
||||||
|
const CONTRACT_PATTERN = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440(?:\u0430|\u0443|ом|е)?\s*(?:№|#|n)?\s*([a-zа-я0-9./_-]+))/giu;
|
||||||
|
const DOCUMENT_NUMBER_PATTERN = /(?:(?:\u0441\u0447(?:\u0435|\u0451)\u0442(?:-\u0444\u0430\u043a\u0442\u0443\u0440(?:а|ы))?|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446(?:ия|ии)|\u0430\u043a\u0442)\s*(?:№|#|n)\s*([a-zа-я0-9./_-]+))/giu;
|
||||||
|
const DATE_PATTERN = /\b(?:\d{1,2}[./]\d{1,2}[./]\d{2,4}|\d{1,2}\s+(?:\u044f\u043d\u0432\u0430\u0440\u044f|\u0444\u0435\u0432\u0440\u0430\u043b\u044f|\u043c\u0430\u0440\u0442\u0430|\u0430\u043f\u0440\u0435\u043b\u044f|\u043c\u0430\u044f|\u0438\u044e\u043d\u044f|\u0438\u044e\u043b\u044f|\u0430\u0432\u0433\u0443\u0441\u0442\u0430|\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f|\u043e\u043a\u0442\u044f\u0431\u0440\u044f|\u043d\u043e\u044f\u0431\u0440\u044f|\u0434\u0435\u043a\u0430\u0431\u0440\u044f))\b/giu;
|
||||||
|
const AMOUNT_PATTERN = /\b(?:\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?|\d+[.,]\d{2})\b/gu;
|
||||||
|
const CONTEXTUAL_ACCOUNT_PATTERN = /(?:\b(?:\u0441\u0447(?:\u0435|\u0451)\u0442(?:а|у|ом|ов)?|account|schet)\b\s*(?:№|#|:)?\s*)(\d{2}(?:\.\d{2})?)/giu;
|
||||||
|
const ACCOUNT_PAIR_PATTERN = /\b(\d{2}\.\d{2})\s*\/\s*(\d{2}\.\d{2})\b/gu;
|
||||||
|
const PERIOD_PATTERN = /\b(?:20\d{2}(?:[-./](?:0?[1-9]|1[0-2]))?|(?:\u0438\u044e\u043b\u044c|\u0438\u044e\u043d\u044c|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c|\u043e\u043a\u0442\u044f\u0431\u0440\u044c|\u043d\u043e\u044f\u0431\u0440\u044c|\u0434\u0435\u043a\u0430\u0431\u0440\u044c|\u044f\u043d\u0432\u0430\u0440\u044c|\u0444\u0435\u0432\u0440\u0430\u043b\u044c|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b\u044c|\u043c\u0430\u0439)\s+20\d{2})\b/giu;
|
||||||
|
const DOCUMENT_TYPE_PATTERNS = [
|
||||||
|
{ name: "invoice", pattern: /\b(?:\u0441\u0447(?:\u0435|\u0451)\u0442-\u0444\u0430\u043a\u0442\u0443\u0440|invoice)\b/iu },
|
||||||
|
{ name: "realization", pattern: /\b(?:\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446|realization)\b/iu },
|
||||||
|
{ name: "payment", pattern: /\b(?:\u043e\u043f\u043b\u0430\u0442|payment|\u043f\u043b\u0430\u0442\u0435\u0436)\b/iu },
|
||||||
|
{ name: "receipt", pattern: /\b(?:\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|receipt)\b/iu },
|
||||||
|
{ name: "close", pattern: /\b(?:\u0437\u0430\u043a\u0440\u044b\u0442\u0438|\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442)\b/iu },
|
||||||
|
{ name: "rbp_writeoff", pattern: /\b(?:\u0440\u0431\u043f|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435)\b/iu },
|
||||||
|
{ name: "amortization", pattern: /\b(?:\u0430\u043c\u043e\u0440\u0442\u0438\u0437|amortization)\b/iu }
|
||||||
|
];
|
||||||
|
const KNOWN_ACCOUNT_PREFIXES = new Set([
|
||||||
|
"01",
|
||||||
|
"02",
|
||||||
|
"07",
|
||||||
|
"08",
|
||||||
|
"10",
|
||||||
|
"13",
|
||||||
|
"19",
|
||||||
|
"20",
|
||||||
|
"21",
|
||||||
|
"23",
|
||||||
|
"25",
|
||||||
|
"26",
|
||||||
|
"41",
|
||||||
|
"43",
|
||||||
|
"44",
|
||||||
|
"45",
|
||||||
|
"50",
|
||||||
|
"51",
|
||||||
|
"52",
|
||||||
|
"55",
|
||||||
|
"57",
|
||||||
|
"58",
|
||||||
|
"60",
|
||||||
|
"62",
|
||||||
|
"66",
|
||||||
|
"67",
|
||||||
|
"68",
|
||||||
|
"69",
|
||||||
|
"70",
|
||||||
|
"71",
|
||||||
|
"73",
|
||||||
|
"76",
|
||||||
|
"90",
|
||||||
|
"91",
|
||||||
|
"94",
|
||||||
|
"96",
|
||||||
|
"97"
|
||||||
|
]);
|
||||||
|
function uniqueStrings(values, limit = 48) {
|
||||||
|
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
|
||||||
|
}
|
||||||
|
function normalizeAnchorToken(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
function collectMatches(text, pattern, useCaptures = true) {
|
||||||
|
const values = [];
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
for (const match of text.matchAll(pattern)) {
|
||||||
|
if (!match)
|
||||||
|
continue;
|
||||||
|
if (useCaptures && match.length > 1) {
|
||||||
|
for (let i = 1; i < match.length; i += 1) {
|
||||||
|
const token = normalizeAnchorToken(match[i] ?? "");
|
||||||
|
if (token)
|
||||||
|
values.push(token);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const token = normalizeAnchorToken(match[0] ?? "");
|
||||||
|
if (token)
|
||||||
|
values.push(token);
|
||||||
|
}
|
||||||
|
return uniqueStrings(values);
|
||||||
|
}
|
||||||
|
function isKnownAccount(value) {
|
||||||
|
const token = String(value ?? "").trim();
|
||||||
|
const match = token.match(/^(\d{2})/);
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return KNOWN_ACCOUNT_PREFIXES.has(match[1]);
|
||||||
|
}
|
||||||
|
function collectAccountAnchors(text) {
|
||||||
|
const tokens = new Set();
|
||||||
|
for (const token of collectMatches(text, CONTEXTUAL_ACCOUNT_PATTERN, true)) {
|
||||||
|
if (isKnownAccount(token)) {
|
||||||
|
tokens.add(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ACCOUNT_PAIR_PATTERN.lastIndex = 0;
|
||||||
|
for (const match of text.matchAll(ACCOUNT_PAIR_PATTERN)) {
|
||||||
|
const left = normalizeAnchorToken(match[1] ?? "");
|
||||||
|
const right = normalizeAnchorToken(match[2] ?? "");
|
||||||
|
if (left && isKnownAccount(left)) {
|
||||||
|
tokens.add(left);
|
||||||
|
}
|
||||||
|
if (right && isKnownAccount(right)) {
|
||||||
|
tokens.add(right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(tokens).slice(0, 24);
|
||||||
|
}
|
||||||
|
function collectDocumentTypeAnchors(text) {
|
||||||
|
return uniqueStrings(DOCUMENT_TYPE_PATTERNS.filter((entry) => entry.pattern.test(text)).map((entry) => entry.name), 12);
|
||||||
|
}
|
||||||
|
function flattenAnchors(input) {
|
||||||
|
return uniqueStrings([
|
||||||
|
...input.contract_numbers,
|
||||||
|
...input.document_numbers,
|
||||||
|
...input.dates,
|
||||||
|
...input.amounts,
|
||||||
|
...input.accounts.map((item) => `account:${item}`),
|
||||||
|
...input.periods.map((item) => `period:${item}`),
|
||||||
|
...input.document_types.map((item) => `doc_type:${item}`)
|
||||||
|
], 64);
|
||||||
|
}
|
||||||
|
function resolveCompanyAnchors(input) {
|
||||||
|
const text = String(input ?? "");
|
||||||
|
const contractNumbers = collectMatches(text, CONTRACT_PATTERN, true).map((item) => `\u0434\u043e\u0433\u043e\u0432\u043e\u0440 № ${item}`);
|
||||||
|
const documentNumbers = collectMatches(text, DOCUMENT_NUMBER_PATTERN, true).map((item) => `\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 № ${item}`);
|
||||||
|
const dates = collectMatches(text, DATE_PATTERN, false);
|
||||||
|
const amounts = collectMatches(text, AMOUNT_PATTERN, false);
|
||||||
|
const accounts = collectAccountAnchors(text);
|
||||||
|
const periods = collectMatches(text, PERIOD_PATTERN, false);
|
||||||
|
const documentTypes = collectDocumentTypeAnchors(text);
|
||||||
|
const resultBase = {
|
||||||
|
contract_numbers: uniqueStrings(contractNumbers, 12),
|
||||||
|
document_numbers: uniqueStrings(documentNumbers, 16),
|
||||||
|
dates: uniqueStrings(dates, 16),
|
||||||
|
amounts: uniqueStrings(amounts, 16),
|
||||||
|
accounts: uniqueStrings(accounts, 24),
|
||||||
|
periods: uniqueStrings(periods, 12),
|
||||||
|
document_types: documentTypes
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...resultBase,
|
||||||
|
all: flattenAnchors(resultBase)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -595,8 +595,13 @@ function inferLifecycleDomain(input) {
|
||||||
]
|
]
|
||||||
.join(" ")
|
.join(" ")
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
const hasExplicitVatHint = includesAny(unitTokens, [/domain_hint:vat_flow/]);
|
||||||
|
const hasExplicitDeferredHint = includesAny(unitTokens, [/domain_hint:deferred_expense/]);
|
||||||
|
const hasExplicitFixedAssetHint = includesAny(unitTokens, [/domain_hint:fixed_asset/]);
|
||||||
|
const hasExplicitPeriodCloseHint = includesAny(unitTokens, [/domain_hint:period_close/]);
|
||||||
|
const hasCustomerSettlementHint = includesAny(unitTokens, [/domain_hint:customer_settlement/]);
|
||||||
|
const hasBankSettlementHint = includesAny(unitTokens, [/domain_hint:bank_settlement/]);
|
||||||
const hasVatMarkers = includesAny(unitTokens, [
|
const hasVatMarkers = includesAny(unitTokens, [
|
||||||
/domain_hint:vat_flow/,
|
|
||||||
/\binvoice_to_vat\b/,
|
/\binvoice_to_vat\b/,
|
||||||
/\bvat_chain_conflict\b/,
|
/\bvat_chain_conflict\b/,
|
||||||
/(^|[^a-z0-9])nds([^a-z0-9]|$)/,
|
/(^|[^a-z0-9])nds([^a-z0-9]|$)/,
|
||||||
|
|
@ -605,7 +610,6 @@ function inferLifecycleDomain(input) {
|
||||||
/\baccount[_:\s-]?(19|68)\b/
|
/\baccount[_:\s-]?(19|68)\b/
|
||||||
]);
|
]);
|
||||||
const hasDeferredMarkers = includesAny(unitTokens, [
|
const hasDeferredMarkers = includesAny(unitTokens, [
|
||||||
/domain_hint:deferred_expense/,
|
|
||||||
/\bdeferred(?:_expense)?\b/,
|
/\bdeferred(?:_expense)?\b/,
|
||||||
/\bdeferred_expense_to_writeoff\b/,
|
/\bdeferred_expense_to_writeoff\b/,
|
||||||
/\bwriteoff\b/,
|
/\bwriteoff\b/,
|
||||||
|
|
@ -614,7 +618,6 @@ function inferLifecycleDomain(input) {
|
||||||
/\baccount[_:\s-]?97\b/
|
/\baccount[_:\s-]?97\b/
|
||||||
]);
|
]);
|
||||||
const hasFixedAssetMarkers = includesAny(unitTokens, [
|
const hasFixedAssetMarkers = includesAny(unitTokens, [
|
||||||
/domain_hint:fixed_asset/,
|
|
||||||
/\bfixed[_\s-]?asset(?:s)?\b/,
|
/\bfixed[_\s-]?asset(?:s)?\b/,
|
||||||
/\basset_card_to_depreciation\b/,
|
/\basset_card_to_depreciation\b/,
|
||||||
/\bdepreciation(?:_active)?\b/,
|
/\bdepreciation(?:_active)?\b/,
|
||||||
|
|
@ -623,7 +626,6 @@ function inferLifecycleDomain(input) {
|
||||||
/\baccount[_:\s-]?(01|02|08)\b/
|
/\baccount[_:\s-]?(01|02|08)\b/
|
||||||
]);
|
]);
|
||||||
const hasPeriodCloseMarkers = includesAny(unitTokens, [
|
const hasPeriodCloseMarkers = includesAny(unitTokens, [
|
||||||
/domain_hint:period_close/,
|
|
||||||
/\bperiod[_\s-]?close\b/,
|
/\bperiod[_\s-]?close\b/,
|
||||||
/\bperiod_close_risk\b/,
|
/\bperiod_close_risk\b/,
|
||||||
/\bclose[_\s-]?risk\b/,
|
/\bclose[_\s-]?risk\b/,
|
||||||
|
|
@ -632,6 +634,24 @@ function inferLifecycleDomain(input) {
|
||||||
/\bmonth[_\s-]?close\b/,
|
/\bmonth[_\s-]?close\b/,
|
||||||
/\bperiod_risk\b/
|
/\bperiod_risk\b/
|
||||||
]);
|
]);
|
||||||
|
if (hasExplicitDeferredHint) {
|
||||||
|
return "deferred_expense";
|
||||||
|
}
|
||||||
|
if (hasExplicitFixedAssetHint) {
|
||||||
|
return "fixed_asset";
|
||||||
|
}
|
||||||
|
if (hasExplicitVatHint) {
|
||||||
|
return "vat_flow";
|
||||||
|
}
|
||||||
|
if (hasExplicitPeriodCloseHint) {
|
||||||
|
return "period_close";
|
||||||
|
}
|
||||||
|
if (hasCustomerSettlementHint) {
|
||||||
|
return "customer_settlement";
|
||||||
|
}
|
||||||
|
if (hasBankSettlementHint) {
|
||||||
|
return "bank_settlement";
|
||||||
|
}
|
||||||
if (hasDeferredMarkers) {
|
if (hasDeferredMarkers) {
|
||||||
return "deferred_expense";
|
return "deferred_expense";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,59 @@ function stringArrayFromUnknown(value) {
|
||||||
function stringArrayFromPayload(item, key) {
|
function stringArrayFromPayload(item, key) {
|
||||||
return stringArrayFromUnknown(item.payload[key]);
|
return stringArrayFromUnknown(item.payload[key]);
|
||||||
}
|
}
|
||||||
|
function domainHintsFromSummary(summary) {
|
||||||
|
const hints = [];
|
||||||
|
const purityGuard = toObject(summary.domain_purity_guard);
|
||||||
|
const domainCardId = String(purityGuard?.domain_card_id ?? "").trim();
|
||||||
|
if (domainCardId === "settlements_60_62") {
|
||||||
|
return ["bank_settlement", "customer_settlement"];
|
||||||
|
}
|
||||||
|
if (domainCardId === "vat_document_register_book") {
|
||||||
|
return ["vat_flow"];
|
||||||
|
}
|
||||||
|
if (domainCardId === "month_close_costs_20_44") {
|
||||||
|
return ["period_close"];
|
||||||
|
}
|
||||||
|
const semanticProfile = toObject(summary.semantic_profile);
|
||||||
|
const domainScope = stringArrayFromUnknown(semanticProfile?.domain_scope);
|
||||||
|
for (const domain of domainScope) {
|
||||||
|
const normalized = domain.toLowerCase();
|
||||||
|
if (normalized === "bank" ||
|
||||||
|
normalized === "settlements" ||
|
||||||
|
normalized === "suppliers" ||
|
||||||
|
normalized === "supplier_payments" ||
|
||||||
|
normalized === "other_settlements") {
|
||||||
|
hints.push("bank_settlement");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (normalized === "customers") {
|
||||||
|
hints.push("customer_settlement");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (normalized === "vat" || normalized === "taxes") {
|
||||||
|
hints.push("vat_flow");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (normalized === "period_close") {
|
||||||
|
hints.push("period_close");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (normalized === "deferred_expense") {
|
||||||
|
hints.push("deferred_expense");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (normalized === "fixed_assets") {
|
||||||
|
hints.push("fixed_asset");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueStrings(hints);
|
||||||
|
}
|
||||||
function extractSemanticProfile(summary) {
|
function extractSemanticProfile(summary) {
|
||||||
const semanticProfile = toObject(summary.semantic_profile);
|
const semanticProfile = toObject(summary.semantic_profile);
|
||||||
|
const domainHints = domainHintsFromSummary(summary).map((item) => `domain_hint:${item}`);
|
||||||
return {
|
return {
|
||||||
relation_patterns: stringArrayFromUnknown(semanticProfile?.relation_patterns),
|
relation_patterns: uniqueStrings([...stringArrayFromUnknown(semanticProfile?.relation_patterns), ...domainHints]),
|
||||||
anomaly_patterns: stringArrayFromUnknown(semanticProfile?.anomaly_patterns)
|
anomaly_patterns: uniqueStrings([...stringArrayFromUnknown(semanticProfile?.anomaly_patterns), ...domainHints])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function resolveEntityOverlay(item, rawEntities) {
|
function resolveEntityOverlay(item, rawEntities) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.resolveQuestionType = resolveQuestionType;
|
||||||
|
const QUESTION_TYPE_RULES = [
|
||||||
|
{
|
||||||
|
type: "what_to_check_first",
|
||||||
|
pattern: /(?:\bwhat\s+to\s+check\s+first\b|\bfirst\s+check\b|\bcheck\s+first\b|\u0441\s+\u0447\u0435\u0433\u043e\s+\u043d\u0430\u0447\u0430\u0442\u044c\s+\u043f\u0440\u043e\u0432\u0435\u0440\u043a|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c\s+\u043f\u0435\u0440\u0432)/iu
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "what_is_it_grounded_on",
|
||||||
|
pattern: /(?:\bwhat\s+is\s+it\s+grounded\s+on\b|\bgrounded\s+on\b|\bbased\s+on\b|\bwhat\s+evidence\b|\u043d\u0430\s+\u0447(?:\u0435|\u0451)\u043c\s+\u044d\u0442\u043e\s+\u043e\u0441\u043d\u043e\u0432\u0430\u043d|\u0447\u0435\u043c\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434)/iu
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "prove_or_guess",
|
||||||
|
pattern: /(?:\bprove\b|\bguess\b|\bprove\s+or\s+guess\b|\bis\s+it\s+proven\b|\u044d\u0442\u043e\s+\u0434\u043e\u043a\u0430\u0437\u0430\u043d|\u0438\u043b\u0438\s+\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0433\u0438\u043f\u043e\u0442\u0435\u0437|\u0434\u043e\u043a\u0430\u0437\u0430\u043d|\u0434\u043e\u0433\u0430\u0434|\u0435\u0441\u0442\u044c\s+\u043b\u0438|\u043c\u043e\u0436\u0435\u0442\s+\u043b\u0438|\u044d\u0442\u043e\s+\u0443\u0436\u0435.*\u0438\u043b\u0438)/iu
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "which_chains_are_complete_vs_incomplete",
|
||||||
|
pattern: /(?:\bcomplete(?:d)?\b.*\bincomplete\b|\bwhich\s+chains?\b|\bcomplete\s+vs\s+incomplete\b|\u043a\u0430\u043a\u0438\u0435\s+\u0446\u0435\u043f\u043e\u0447\u043a[аи]\s+.*\u0437\u0430\u0432\u0435\u0440\u0448|\u0447\u0442\u043e\s+\u0437\u0430\u043a\u0440\u044b\u0442\u043e.*\u0447\u0442\u043e\s+\u043d\u0435\u0442)/iu
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "where_break_is",
|
||||||
|
pattern: /(?:\bwhere\s+is\s+the\s+break\b|\bwhere\s+exactly\b|\blocate\b|\u0433\u0434\u0435\s+\u0438\u043c\u0435\u043d\u043d\u043e|\u0433\u0434\u0435\s+\u0440\u0430\u0437\u0440\u044b\u0432|\u0432\s+\u043a\u0430\u043a\u043e\u043c\s+\u043c\u0435\u0441\u0442\u0435)/iu
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "why_breaks",
|
||||||
|
pattern: /(?:\bwhy\b|\bwhy\s+does\s+it\s+break\b|\u043f\u043e\u0447\u0435\u043c\u0443|\u0432\s+\u0447(?:\u0435|\u0451)\u043c\s+\u043f\u0440\u0438\u0447\u0438\u043d\u0430|\u0438\u0437-\u0437\u0430\s+\u0447\u0435\u0433\u043e)/iu
|
||||||
|
}
|
||||||
|
];
|
||||||
|
function resolveQuestionType(input) {
|
||||||
|
const text = String(input ?? "").trim();
|
||||||
|
if (!text) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
for (const rule of QUESTION_TYPE_RULES) {
|
||||||
|
if (rule.pattern.test(text)) {
|
||||||
|
return rule.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (/[??]/u.test(text)) {
|
||||||
|
return "why_breaks";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,614 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
const EXPECTED_QUESTION_TYPES = [
|
||||||
|
"why_breaks",
|
||||||
|
"prove_or_guess",
|
||||||
|
"prove_or_guess",
|
||||||
|
"why_breaks",
|
||||||
|
"where_break_is",
|
||||||
|
"prove_or_guess",
|
||||||
|
"why_breaks",
|
||||||
|
"which_chains_are_complete_vs_incomplete",
|
||||||
|
"which_chains_are_complete_vs_incomplete",
|
||||||
|
"prove_or_guess",
|
||||||
|
"why_breaks",
|
||||||
|
"prove_or_guess",
|
||||||
|
"why_breaks",
|
||||||
|
"what_is_it_grounded_on",
|
||||||
|
"why_breaks",
|
||||||
|
"which_chains_are_complete_vs_incomplete",
|
||||||
|
"prove_or_guess",
|
||||||
|
"what_is_it_grounded_on",
|
||||||
|
"why_breaks",
|
||||||
|
"prove_or_guess"
|
||||||
|
];
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = {
|
||||||
|
rawFile: "",
|
||||||
|
outputDir: "",
|
||||||
|
caseMatrixFile: "wave13_chat20_case_matrix_updated.md",
|
||||||
|
metricsFile: "wave13_chat20_metrics.json",
|
||||||
|
reportFile: "wave13_regression_report.md",
|
||||||
|
baselineMetricsFile: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
|
const token = argv[i];
|
||||||
|
if (token === "--raw-file") {
|
||||||
|
args.rawFile = String(argv[i + 1] ?? "");
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--output-dir") {
|
||||||
|
args.outputDir = String(argv[i + 1] ?? "");
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--case-matrix-file") {
|
||||||
|
args.caseMatrixFile = String(argv[i + 1] ?? args.caseMatrixFile);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--metrics-file") {
|
||||||
|
args.metricsFile = String(argv[i + 1] ?? args.metricsFile);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--report-file") {
|
||||||
|
args.reportFile = String(argv[i + 1] ?? args.reportFile);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--baseline-metrics-file") {
|
||||||
|
args.baselineMetricsFile = String(argv[i + 1] ?? "");
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDir(dirPath) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJson(filePath) {
|
||||||
|
const raw = fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, "");
|
||||||
|
return JSON.parse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeUtf8Bom(filePath, content) {
|
||||||
|
ensureDir(path.dirname(filePath));
|
||||||
|
fs.writeFileSync(filePath, `\uFEFF${content}`, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function text(value) {
|
||||||
|
return value == null ? "" : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lower(value) {
|
||||||
|
return text(value).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectedDomainByIndex(index) {
|
||||||
|
const caseNo = index + 1;
|
||||||
|
if (caseNo <= 8) {
|
||||||
|
return "settlements_60_62";
|
||||||
|
}
|
||||||
|
if (caseNo <= 16) {
|
||||||
|
return "vat_document_register_book";
|
||||||
|
}
|
||||||
|
return "month_close_costs_20_44";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeInternalDomain(domainName) {
|
||||||
|
const d = lower(domainName);
|
||||||
|
if (!d) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
d.includes("settlement") ||
|
||||||
|
d.includes("supplier") ||
|
||||||
|
d.includes("customer") ||
|
||||||
|
d.includes("bank")
|
||||||
|
) {
|
||||||
|
return "settlements_60_62";
|
||||||
|
}
|
||||||
|
if (d.includes("vat") || d.includes("nds")) {
|
||||||
|
return "vat_document_register_book";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
d.includes("period_close") ||
|
||||||
|
d.includes("month_close") ||
|
||||||
|
d.includes("deferred_expense") ||
|
||||||
|
d.includes("fixed_asset") ||
|
||||||
|
d.includes("close")
|
||||||
|
) {
|
||||||
|
return "month_close_costs_20_44";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeCountMap(target, source) {
|
||||||
|
if (!source || typeof source !== "object") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [key, value] of Object.entries(source)) {
|
||||||
|
const name = text(key);
|
||||||
|
if (!name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const count = Number(value) || 0;
|
||||||
|
if (!target[name]) {
|
||||||
|
target[name] = 0;
|
||||||
|
}
|
||||||
|
target[name] += count > 0 ? count : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectDomainScores(row) {
|
||||||
|
const scores = {};
|
||||||
|
const retrieval = Array.isArray(row?.debug?.retrieval_results) ? row.debug.retrieval_results : [];
|
||||||
|
for (const item of retrieval) {
|
||||||
|
mergeCountMap(scores, item?.problem_unit_summary?.lifecycle_domain_distribution);
|
||||||
|
mergeCountMap(scores, item?.problem_unit_summary?.graph_summary?.domain_distribution);
|
||||||
|
const domainCard = text(item?.summary?.domain_purity_guard?.domain_card_id);
|
||||||
|
if (domainCard) {
|
||||||
|
if (!scores[domainCard]) {
|
||||||
|
scores[domainCard] = 0;
|
||||||
|
}
|
||||||
|
scores[domainCard] += 2;
|
||||||
|
}
|
||||||
|
const resultItems = Array.isArray(item?.items) ? item.items : [];
|
||||||
|
for (const resultItem of resultItems) {
|
||||||
|
const scopes = Array.isArray(resultItem?.graph_domain_scope) ? resultItem.graph_domain_scope : [];
|
||||||
|
for (const scope of scopes) {
|
||||||
|
const name = text(scope);
|
||||||
|
if (!name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!scores[name]) {
|
||||||
|
scores[name] = 0;
|
||||||
|
}
|
||||||
|
scores[name] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const activeDomain = text(row?.debug?.investigation_state_snapshot?.focus?.active_domain);
|
||||||
|
if (activeDomain) {
|
||||||
|
if (!scores[activeDomain]) {
|
||||||
|
scores[activeDomain] = 0;
|
||||||
|
}
|
||||||
|
scores[activeDomain] += 1;
|
||||||
|
}
|
||||||
|
return scores;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickActualDomain(row) {
|
||||||
|
const scores = collectDomainScores(row);
|
||||||
|
const sorted = Object.entries(scores).sort((a, b) => {
|
||||||
|
if (b[1] !== a[1]) {
|
||||||
|
return b[1] - a[1];
|
||||||
|
}
|
||||||
|
return String(a[0]).localeCompare(String(b[0]));
|
||||||
|
});
|
||||||
|
if (!sorted.length) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
return normalizeInternalDomain(sorted[0][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickActualQuestionType(row) {
|
||||||
|
const qType = text(row?.debug?.question_type_class);
|
||||||
|
return qType || "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractCompanyAnchors(row) {
|
||||||
|
const all = row?.debug?.company_anchors?.all;
|
||||||
|
if (!Array.isArray(all)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return all.map((v) => text(v).trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAnchorUsageInAnswer(row, anchors) {
|
||||||
|
if (!anchors.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const reply = lower(row?.assistant_reply);
|
||||||
|
if (!reply) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (reply.includes("в опоре использованы якоря вопроса")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (const anchor of anchors) {
|
||||||
|
const value = lower(anchor);
|
||||||
|
if (value.length < 3) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (reply.includes(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateEvidenceStrength(row) {
|
||||||
|
const status = lower(row?.debug?.answer_grounding_check?.status);
|
||||||
|
if (status === "grounded") {
|
||||||
|
return "strong";
|
||||||
|
}
|
||||||
|
if (status === "partial") {
|
||||||
|
return "weak";
|
||||||
|
}
|
||||||
|
if (status === "no_grounded_answer") {
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
return "limited";
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateConfidenceStyle(row) {
|
||||||
|
const reply = lower(row?.assistant_reply);
|
||||||
|
if (!reply) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
const hasLimitation =
|
||||||
|
reply.includes("ограничени") ||
|
||||||
|
reply.includes("частично") ||
|
||||||
|
reply.includes("низкая") ||
|
||||||
|
reply.includes("не подтвержден");
|
||||||
|
const hasConfident =
|
||||||
|
reply.includes("подтверждено") ||
|
||||||
|
reply.includes("доказ") ||
|
||||||
|
reply.includes("подтверждается");
|
||||||
|
if (hasLimitation && hasConfident) {
|
||||||
|
return "mixed";
|
||||||
|
}
|
||||||
|
if (hasLimitation) {
|
||||||
|
return "limited";
|
||||||
|
}
|
||||||
|
if (hasConfident) {
|
||||||
|
return "confident";
|
||||||
|
}
|
||||||
|
return "neutral";
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsAny(textValue, needles) {
|
||||||
|
const body = lower(textValue);
|
||||||
|
return needles.some((needle) => body.includes(lower(needle)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateFirstCheckRelevance(row, expectedDomain) {
|
||||||
|
const reply = text(row?.assistant_reply);
|
||||||
|
if (!reply) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (expectedDomain === "settlements_60_62") {
|
||||||
|
return containsAny(reply, [
|
||||||
|
"договор",
|
||||||
|
"объект расчет",
|
||||||
|
"регистр расчет",
|
||||||
|
"зачет аванс",
|
||||||
|
"взаимозачет",
|
||||||
|
"60/62/76"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (expectedDomain === "vat_document_register_book") {
|
||||||
|
return containsAny(reply, [
|
||||||
|
"ндс",
|
||||||
|
"счет-фактур",
|
||||||
|
"книга покуп",
|
||||||
|
"книга продаж",
|
||||||
|
"регистр",
|
||||||
|
"19"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (expectedDomain === "month_close_costs_20_44") {
|
||||||
|
return containsAny(reply, [
|
||||||
|
"закрыти",
|
||||||
|
"рбп",
|
||||||
|
"амортизац",
|
||||||
|
"косвен",
|
||||||
|
"20",
|
||||||
|
"25",
|
||||||
|
"26",
|
||||||
|
"44"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateGenericAnswer(row) {
|
||||||
|
const reply = lower(row?.assistant_reply);
|
||||||
|
if (!reply) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const genericPatterns = [
|
||||||
|
"коротко: проблема с закрытием расчета подтверждается частично",
|
||||||
|
"сигнал проблемы есть, но механизм подтвержден не полностью",
|
||||||
|
"вывод сделан по snapshot",
|
||||||
|
"проверьте договор, объект расчетов, регистр расчетов",
|
||||||
|
"проверьте договор и объект расчетов"
|
||||||
|
];
|
||||||
|
const hits = genericPatterns.filter((pattern) => reply.includes(pattern)).length;
|
||||||
|
return hits >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortQuestion(value, maxLength = 130) {
|
||||||
|
const q = text(value).replace(/\s+/g, " ").trim();
|
||||||
|
if (q.length <= maxLength) {
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
return `${q.slice(0, maxLength - 3)}...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPercent(value) {
|
||||||
|
return Number(value.toFixed(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCaseRow(index, row) {
|
||||||
|
const expectedDomain = expectedDomainByIndex(index);
|
||||||
|
const actualDomain = pickActualDomain(row);
|
||||||
|
const expectedQuestionType = EXPECTED_QUESTION_TYPES[index] || "unknown";
|
||||||
|
const actualQuestionType = pickActualQuestionType(row);
|
||||||
|
const anchors = extractCompanyAnchors(row);
|
||||||
|
const anchorsPresent = anchors.length > 0;
|
||||||
|
const anchorsUsed = hasAnchorUsageInAnswer(row, anchors);
|
||||||
|
const evidenceStrength = evaluateEvidenceStrength(row);
|
||||||
|
const confidenceStyle = evaluateConfidenceStyle(row);
|
||||||
|
const firstCheckRelevant = evaluateFirstCheckRelevance(row, expectedDomain);
|
||||||
|
const genericAnswer = evaluateGenericAnswer(row);
|
||||||
|
|
||||||
|
const reasons = [];
|
||||||
|
if (actualDomain !== expectedDomain) {
|
||||||
|
reasons.push("wrong_domain");
|
||||||
|
}
|
||||||
|
if (actualQuestionType !== expectedQuestionType) {
|
||||||
|
reasons.push("wrong_question_type");
|
||||||
|
}
|
||||||
|
if (anchorsPresent && !anchorsUsed) {
|
||||||
|
reasons.push("weak_company_anchor_usage");
|
||||||
|
}
|
||||||
|
if (!firstCheckRelevant) {
|
||||||
|
reasons.push("wrong_first_check");
|
||||||
|
}
|
||||||
|
if (genericAnswer) {
|
||||||
|
reasons.push("generic_answer");
|
||||||
|
}
|
||||||
|
|
||||||
|
let verdict = "PASS";
|
||||||
|
if (reasons.length > 0) {
|
||||||
|
const hardFail = reasons.includes("wrong_domain") || reasons.includes("wrong_first_check");
|
||||||
|
verdict = hardFail || reasons.length >= 3 ? "FAIL" : "SOFT_PASS";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
case_id: text(row?.case_id) || `q${String(index + 1).padStart(2, "0")}`,
|
||||||
|
question_short: shortQuestion(row?.user_message),
|
||||||
|
expected_domain: expectedDomain,
|
||||||
|
actual_domain: actualDomain,
|
||||||
|
expected_question_type: expectedQuestionType,
|
||||||
|
actual_question_type: actualQuestionType,
|
||||||
|
company_anchors_present: anchorsPresent,
|
||||||
|
company_anchors_used_in_answer: anchorsUsed,
|
||||||
|
evidence_strength: evidenceStrength,
|
||||||
|
answer_confidence_style: confidenceStyle,
|
||||||
|
first_check_relevance: firstCheckRelevant,
|
||||||
|
verdict,
|
||||||
|
failure_reason_short: reasons.length ? reasons.join(", ") : "none",
|
||||||
|
is_generic_answer: genericAnswer,
|
||||||
|
failure_reasons: reasons
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function markdownCell(value) {
|
||||||
|
return text(value).replace(/\|/g, "\\|");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCaseMatrixMarkdown(rows) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push("# Wave 13 Chat20 Case Matrix (Updated)");
|
||||||
|
lines.push("");
|
||||||
|
lines.push("| case_id | question_short | expected_domain | actual_domain | expected_question_type | actual_question_type | company_anchors_present | company_anchors_used_in_answer | evidence_strength | answer_confidence_style | first_check_relevance | verdict | failure_reason_short |");
|
||||||
|
lines.push("|---|---|---|---|---|---|---|---|---|---|---|---|---|");
|
||||||
|
for (const row of rows) {
|
||||||
|
lines.push(
|
||||||
|
`| ${markdownCell(row.case_id)} | ${markdownCell(row.question_short)} | ${markdownCell(row.expected_domain)} | ${markdownCell(row.actual_domain)} | ${markdownCell(row.expected_question_type)} | ${markdownCell(row.actual_question_type)} | ${markdownCell(row.company_anchors_present)} | ${markdownCell(row.company_anchors_used_in_answer)} | ${markdownCell(row.evidence_strength)} | ${markdownCell(row.answer_confidence_style)} | ${markdownCell(row.first_check_relevance)} | ${markdownCell(row.verdict)} | ${markdownCell(row.failure_reason_short)} |`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
return `${lines.join("\n")}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countBy(rows, selector) {
|
||||||
|
const result = {};
|
||||||
|
for (const row of rows) {
|
||||||
|
const key = selector(row);
|
||||||
|
if (!result[key]) {
|
||||||
|
result[key] = 0;
|
||||||
|
}
|
||||||
|
result[key] += 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRegressionReport(rows, metrics, baselineMetrics) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push("# Wave 13 Regression Report");
|
||||||
|
lines.push("");
|
||||||
|
lines.push(`- Cases: ${rows.length}`);
|
||||||
|
lines.push(`- PASS: ${metrics.totals.pass}`);
|
||||||
|
lines.push(`- SOFT_PASS: ${metrics.totals.soft_pass}`);
|
||||||
|
lines.push(`- FAIL: ${metrics.totals.fail}`);
|
||||||
|
lines.push("");
|
||||||
|
lines.push("## Metric Snapshot");
|
||||||
|
lines.push(`- domain_correctness_rate: ${metrics.domain_correctness_rate}`);
|
||||||
|
lines.push(`- question_type_fit_rate: ${metrics.question_type_fit_rate}`);
|
||||||
|
lines.push(`- company_anchor_usage_rate: ${metrics.company_anchor_usage_rate}`);
|
||||||
|
lines.push(`- generic_answer_rate: ${metrics.generic_answer_rate}`);
|
||||||
|
lines.push(`- first_check_relevance_rate: ${metrics.first_check_relevance_rate}`);
|
||||||
|
lines.push("");
|
||||||
|
if (baselineMetrics) {
|
||||||
|
lines.push("## Delta vs Baseline");
|
||||||
|
for (const key of [
|
||||||
|
"domain_correctness_rate",
|
||||||
|
"question_type_fit_rate",
|
||||||
|
"company_anchor_usage_rate",
|
||||||
|
"generic_answer_rate",
|
||||||
|
"first_check_relevance_rate"
|
||||||
|
]) {
|
||||||
|
const current = Number(metrics[key] ?? 0);
|
||||||
|
const baseline = Number(baselineMetrics[key] ?? 0);
|
||||||
|
const delta = Number((current - baseline).toFixed(4));
|
||||||
|
lines.push(`- ${key}: ${baseline} -> ${current} (delta ${delta >= 0 ? "+" : ""}${delta})`);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const failures = rows.filter((row) => row.verdict !== "PASS");
|
||||||
|
const reasonCounts = {};
|
||||||
|
for (const row of failures) {
|
||||||
|
for (const reason of row.failure_reasons) {
|
||||||
|
if (!reasonCounts[reason]) {
|
||||||
|
reasonCounts[reason] = 0;
|
||||||
|
}
|
||||||
|
reasonCounts[reason] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const topReasons = Object.entries(reasonCounts).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
||||||
|
lines.push("## Top Defects");
|
||||||
|
if (!topReasons.length) {
|
||||||
|
lines.push("- No defects detected.");
|
||||||
|
} else {
|
||||||
|
for (const [reason, count] of topReasons) {
|
||||||
|
lines.push(`- ${reason}: ${count}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
lines.push("## FAIL Cases");
|
||||||
|
for (const row of rows.filter((item) => item.verdict === "FAIL")) {
|
||||||
|
lines.push(`- ${row.case_id}: ${row.failure_reason_short}`);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
return `${lines.join("\n")}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
if (!args.rawFile) {
|
||||||
|
throw new Error("Missing required argument --raw-file");
|
||||||
|
}
|
||||||
|
if (!args.outputDir) {
|
||||||
|
throw new Error("Missing required argument --output-dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawPath = path.resolve(args.rawFile);
|
||||||
|
const outputDir = path.resolve(args.outputDir);
|
||||||
|
|
||||||
|
const raw = readJson(rawPath);
|
||||||
|
const rows = Array.isArray(raw?.rows) ? raw.rows : [];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
throw new Error("Raw file contains no rows.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const caseRows = rows.map((row, index) => buildCaseRow(index, row));
|
||||||
|
const totalsByVerdict = countBy(caseRows, (row) => row.verdict);
|
||||||
|
const domainCorrect = caseRows.filter((row) => row.expected_domain === row.actual_domain).length;
|
||||||
|
const qTypeFit = caseRows.filter((row) => row.expected_question_type === row.actual_question_type).length;
|
||||||
|
const anchorsPresentCount = caseRows.filter((row) => row.company_anchors_present).length;
|
||||||
|
const anchorsUsedCount = caseRows.filter(
|
||||||
|
(row) => row.company_anchors_present && row.company_anchors_used_in_answer
|
||||||
|
).length;
|
||||||
|
const genericCount = caseRows.filter((row) => row.is_generic_answer).length;
|
||||||
|
const firstCheckRelevantCount = caseRows.filter((row) => row.first_check_relevance).length;
|
||||||
|
|
||||||
|
const metrics = {
|
||||||
|
schema_version: "wave13_chat20_metrics_v2",
|
||||||
|
run_id: path.basename(outputDir),
|
||||||
|
source_session_id: text(raw?.session_id),
|
||||||
|
totals: {
|
||||||
|
cases: caseRows.length,
|
||||||
|
pass: totalsByVerdict.PASS || 0,
|
||||||
|
soft_pass: totalsByVerdict.SOFT_PASS || 0,
|
||||||
|
fail: totalsByVerdict.FAIL || 0
|
||||||
|
},
|
||||||
|
domain_correctness_rate: toPercent(domainCorrect / caseRows.length),
|
||||||
|
question_type_fit_rate: toPercent(qTypeFit / caseRows.length),
|
||||||
|
company_anchor_usage_rate: toPercent(
|
||||||
|
anchorsPresentCount > 0 ? anchorsUsedCount / anchorsPresentCount : 0
|
||||||
|
),
|
||||||
|
company_anchor_usage_rate_global: toPercent(anchorsUsedCount / caseRows.length),
|
||||||
|
generic_answer_rate: toPercent(genericCount / caseRows.length),
|
||||||
|
first_check_relevance_rate: toPercent(firstCheckRelevantCount / caseRows.length),
|
||||||
|
anchors_present_cases: anchorsPresentCount,
|
||||||
|
anchors_used_cases: anchorsUsedCount
|
||||||
|
};
|
||||||
|
|
||||||
|
let baselineMetrics = null;
|
||||||
|
if (args.baselineMetricsFile) {
|
||||||
|
const baselinePath = path.resolve(args.baselineMetricsFile);
|
||||||
|
if (fs.existsSync(baselinePath)) {
|
||||||
|
baselineMetrics = readJson(baselinePath);
|
||||||
|
metrics.baseline_reference = path.basename(baselinePath);
|
||||||
|
metrics.baseline_metrics = {
|
||||||
|
domain_correctness_rate: baselineMetrics.domain_correctness_rate,
|
||||||
|
question_type_fit_rate: baselineMetrics.question_type_fit_rate,
|
||||||
|
company_anchor_usage_rate: baselineMetrics.company_anchor_usage_rate,
|
||||||
|
generic_answer_rate: baselineMetrics.generic_answer_rate,
|
||||||
|
first_check_relevance_rate: baselineMetrics.first_check_relevance_rate
|
||||||
|
};
|
||||||
|
metrics.delta_vs_baseline = {
|
||||||
|
domain_correctness_rate_delta: toPercent(
|
||||||
|
Number(metrics.domain_correctness_rate) -
|
||||||
|
Number(metrics.baseline_metrics.domain_correctness_rate || 0)
|
||||||
|
),
|
||||||
|
question_type_fit_rate_delta: toPercent(
|
||||||
|
Number(metrics.question_type_fit_rate) -
|
||||||
|
Number(metrics.baseline_metrics.question_type_fit_rate || 0)
|
||||||
|
),
|
||||||
|
company_anchor_usage_rate_delta: toPercent(
|
||||||
|
Number(metrics.company_anchor_usage_rate) -
|
||||||
|
Number(metrics.baseline_metrics.company_anchor_usage_rate || 0)
|
||||||
|
),
|
||||||
|
generic_answer_rate_delta: toPercent(
|
||||||
|
Number(metrics.generic_answer_rate) -
|
||||||
|
Number(metrics.baseline_metrics.generic_answer_rate || 0)
|
||||||
|
),
|
||||||
|
first_check_relevance_rate_delta: toPercent(
|
||||||
|
Number(metrics.first_check_relevance_rate) -
|
||||||
|
Number(metrics.baseline_metrics.first_check_relevance_rate || 0)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const matrixPath = path.join(outputDir, args.caseMatrixFile);
|
||||||
|
const metricsPath = path.join(outputDir, args.metricsFile);
|
||||||
|
const reportPath = path.join(outputDir, args.reportFile);
|
||||||
|
|
||||||
|
writeUtf8Bom(matrixPath, buildCaseMatrixMarkdown(caseRows));
|
||||||
|
writeUtf8Bom(metricsPath, `${JSON.stringify(metrics, null, 2)}\n`);
|
||||||
|
writeUtf8Bom(reportPath, buildRegressionReport(caseRows, metrics, baselineMetrics));
|
||||||
|
|
||||||
|
process.stdout.write(
|
||||||
|
[
|
||||||
|
`rows=${caseRows.length}`,
|
||||||
|
`matrix=${matrixPath}`,
|
||||||
|
`metrics=${metricsPath}`,
|
||||||
|
`report=${reportPath}`
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
process.stderr.write(`${error?.stack || error}\n`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,233 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const request = require("supertest");
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = {
|
||||||
|
questionsFile: "",
|
||||||
|
runDir: "",
|
||||||
|
rawFileName: "chat20_wave13_raw.json",
|
||||||
|
chatFileName: "Chat20.txt",
|
||||||
|
chatRuFileName: "Чат20.txt",
|
||||||
|
promptsFileName: path.join("prompt_dialogs", "chat20_prompts.md"),
|
||||||
|
useMock: true,
|
||||||
|
promptVersion: "normalizer_v2_0_2",
|
||||||
|
sessionId: "",
|
||||||
|
casePrefix: "q"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
|
const token = argv[i];
|
||||||
|
if (token === "--questions-file") {
|
||||||
|
args.questionsFile = String(argv[i + 1] ?? "");
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--run-dir") {
|
||||||
|
args.runDir = String(argv[i + 1] ?? "");
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--raw-file") {
|
||||||
|
args.rawFileName = String(argv[i + 1] ?? args.rawFileName);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--chat-file") {
|
||||||
|
args.chatFileName = String(argv[i + 1] ?? args.chatFileName);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--chat-ru-file") {
|
||||||
|
args.chatRuFileName = String(argv[i + 1] ?? args.chatRuFileName);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--prompts-file") {
|
||||||
|
args.promptsFileName = String(argv[i + 1] ?? args.promptsFileName);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--use-mock") {
|
||||||
|
const value = String(argv[i + 1] ?? "true").toLowerCase();
|
||||||
|
args.useMock = value !== "0" && value !== "false" && value !== "no";
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--prompt-version") {
|
||||||
|
args.promptVersion = String(argv[i + 1] ?? args.promptVersion);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--session-id") {
|
||||||
|
args.sessionId = String(argv[i + 1] ?? "");
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--case-prefix") {
|
||||||
|
args.casePrefix = String(argv[i + 1] ?? args.casePrefix);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDir(dirPath) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJson(filePath) {
|
||||||
|
const raw = fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, "");
|
||||||
|
return JSON.parse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeUtf8Bom(filePath, content) {
|
||||||
|
ensureDir(path.dirname(filePath));
|
||||||
|
fs.writeFileSync(filePath, `\uFEFF${content}`, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function asText(value) {
|
||||||
|
return value == null ? "" : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCaseId(prefix, index) {
|
||||||
|
return `${prefix}${String(index + 1).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPromptsMarkdown(questions) {
|
||||||
|
const lines = [];
|
||||||
|
for (let i = 0; i < questions.length; i += 1) {
|
||||||
|
lines.push(`${i + 1}. ${questions[i]}`);
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
return `${lines.join("\n").trim()}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChatTxt(sessionId, exportedAt, rows) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push("# Assistant conversation export");
|
||||||
|
lines.push(`session_id: ${sessionId}`);
|
||||||
|
lines.push(`exported_at: ${exportedAt}`);
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
let messageCounter = 1;
|
||||||
|
for (const row of rows) {
|
||||||
|
lines.push(`## ${messageCounter}. user`);
|
||||||
|
lines.push("message_id: pending");
|
||||||
|
lines.push("created_at: pending");
|
||||||
|
lines.push("reply_type: n/a");
|
||||||
|
lines.push("");
|
||||||
|
lines.push(asText(row.user_message));
|
||||||
|
lines.push("");
|
||||||
|
messageCounter += 1;
|
||||||
|
|
||||||
|
lines.push(`## ${messageCounter}. assistant`);
|
||||||
|
lines.push(`message_id: ${asText(row.message_id) || "n/a"}`);
|
||||||
|
lines.push(`created_at: ${asText(row.created_at) || "n/a"}`);
|
||||||
|
lines.push(`reply_type: ${asText(row.reply_type) || "n/a"}`);
|
||||||
|
if (row.trace_id) {
|
||||||
|
lines.push(`trace_id: ${asText(row.trace_id)}`);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
lines.push(asText(row.assistant_reply));
|
||||||
|
lines.push("");
|
||||||
|
messageCounter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${lines.join("\n").trim()}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
if (!args.questionsFile) {
|
||||||
|
throw new Error("Missing required argument --questions-file");
|
||||||
|
}
|
||||||
|
if (!args.runDir) {
|
||||||
|
throw new Error("Missing required argument --run-dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
const questionsPath = path.resolve(args.questionsFile);
|
||||||
|
const runDir = path.resolve(args.runDir);
|
||||||
|
const backendRoot = path.resolve(__dirname, "..");
|
||||||
|
|
||||||
|
const questions = readJson(questionsPath);
|
||||||
|
if (!Array.isArray(questions) || questions.length === 0) {
|
||||||
|
throw new Error("Questions JSON must be a non-empty array of strings.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { createApp } = require(path.join(backendRoot, "dist", "server.js"));
|
||||||
|
const app = createApp();
|
||||||
|
|
||||||
|
ensureDir(runDir);
|
||||||
|
ensureDir(path.join(runDir, "prompt_dialogs"));
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
let sessionId = args.sessionId || `wave13-chat20-${Date.now()}`;
|
||||||
|
|
||||||
|
for (let i = 0; i < questions.length; i += 1) {
|
||||||
|
const userMessage = asText(questions[i]);
|
||||||
|
const response = await request(app).post("/api/assistant/message").send({
|
||||||
|
useMock: args.useMock,
|
||||||
|
promptVersion: args.promptVersion,
|
||||||
|
session_id: sessionId,
|
||||||
|
user_message: userMessage
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = response.body || {};
|
||||||
|
sessionId = asText(body.session_id) || sessionId;
|
||||||
|
const item = body.conversation_item || {};
|
||||||
|
rows.push({
|
||||||
|
case_id: makeCaseId(args.casePrefix, i),
|
||||||
|
user_message: userMessage,
|
||||||
|
assistant_reply: asText(body.assistant_reply),
|
||||||
|
reply_type: asText(body.reply_type),
|
||||||
|
message_id: asText(item.message_id),
|
||||||
|
created_at: asText(item.created_at),
|
||||||
|
trace_id: asText(item.trace_id || body.debug?.trace_id),
|
||||||
|
http_status: response.status,
|
||||||
|
debug: body.debug || {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportedAt = new Date().toISOString();
|
||||||
|
const rawPayload = {
|
||||||
|
session_id: sessionId,
|
||||||
|
exported_at: exportedAt,
|
||||||
|
cases_total: rows.length,
|
||||||
|
rows
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawPath = path.join(runDir, args.rawFileName);
|
||||||
|
const chatPath = path.join(runDir, args.chatFileName);
|
||||||
|
const chatRuPath = path.join(runDir, args.chatRuFileName);
|
||||||
|
const promptsPath = path.join(runDir, args.promptsFileName);
|
||||||
|
|
||||||
|
writeUtf8Bom(rawPath, `${JSON.stringify(rawPayload, null, 2)}\n`);
|
||||||
|
const chatBody = buildChatTxt(sessionId, exportedAt, rows);
|
||||||
|
writeUtf8Bom(chatPath, chatBody);
|
||||||
|
if (args.chatRuFileName) {
|
||||||
|
writeUtf8Bom(chatRuPath, chatBody);
|
||||||
|
}
|
||||||
|
writeUtf8Bom(promptsPath, buildPromptsMarkdown(questions));
|
||||||
|
|
||||||
|
process.stdout.write(
|
||||||
|
[
|
||||||
|
`run_dir=${runDir}`,
|
||||||
|
`session_id=${sessionId}`,
|
||||||
|
`cases_total=${rows.length}`,
|
||||||
|
`raw=${rawPath}`,
|
||||||
|
`chat=${chatPath}`,
|
||||||
|
`chat_ru=${chatRuPath}`,
|
||||||
|
`prompts=${promptsPath}`
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
process.stderr.write(`${error?.stack || error}\n`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
import type { RouteHintSummary } from "../types/normalizer";
|
import type { RouteHintSummary } from "../types/normalizer";
|
||||||
import type { AnswerStructureV11, EvidenceConfidence, EvidenceItem, EvidenceLimitationReasonCode } from "../types/stage1Contracts";
|
import type { AnswerStructureV11, EvidenceConfidence, EvidenceItem, EvidenceLimitationReasonCode } from "../types/stage1Contracts";
|
||||||
import type { ProblemUnit, ProblemUnitSummary, ProblemUnitType } from "../types/stage2ProblemUnits";
|
import type { ProblemUnit, ProblemUnitSummary, ProblemUnitType } from "../types/stage2ProblemUnits";
|
||||||
|
import type { QuestionTypeClass } from "./questionTypeResolver";
|
||||||
|
import type { CompanyAnchorSet } from "./companyAnchorResolver";
|
||||||
|
|
||||||
type ProblemAnswerMode = "stage1_policy_v11" | "stage2_problem_centric_v1" | "stage3_lifecycle_aware_v1";
|
type ProblemAnswerMode = "stage1_policy_v11" | "stage2_problem_centric_v1" | "stage3_lifecycle_aware_v1";
|
||||||
|
|
||||||
|
|
@ -20,6 +22,8 @@ interface ComposeAnswerInput {
|
||||||
coverageReport: RequirementCoverageReport;
|
coverageReport: RequirementCoverageReport;
|
||||||
groundingCheck: AnswerGroundingCheck;
|
groundingCheck: AnswerGroundingCheck;
|
||||||
focusDomainHint?: string | null;
|
focusDomainHint?: string | null;
|
||||||
|
questionTypeHint?: QuestionTypeClass | null;
|
||||||
|
companyAnchors?: CompanyAnchorSet | null;
|
||||||
enableAnswerPolicyV11?: boolean;
|
enableAnswerPolicyV11?: boolean;
|
||||||
enableProblemCentricAnswerV1?: boolean;
|
enableProblemCentricAnswerV1?: boolean;
|
||||||
enableLifecycleAnswerV1?: boolean;
|
enableLifecycleAnswerV1?: boolean;
|
||||||
|
|
@ -47,6 +51,123 @@ function uniqueStrings(values: string[], limit = 6): string[] {
|
||||||
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
|
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CompanyAnchorUsage {
|
||||||
|
present: string[];
|
||||||
|
used: string[];
|
||||||
|
unused: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnswerRenderContext {
|
||||||
|
questionType: QuestionTypeClass;
|
||||||
|
focusDomain: P0NarrativeDomain;
|
||||||
|
anchors: CompanyAnchorUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withUniquePush(target: string[], value: string): void {
|
||||||
|
const normalized = String(value ?? "").trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!target.includes(normalized)) {
|
||||||
|
target.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAnchorForMatch(value: string): string {
|
||||||
|
return String(value ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\p{L}\p{N}.:/-]+/gu, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectCompanyAnchorTokens(anchors: CompanyAnchorSet | null | undefined): string[] {
|
||||||
|
if (!anchors) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const tokens: string[] = [];
|
||||||
|
for (const item of anchors.contract_numbers ?? []) withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.document_numbers ?? []) withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.dates ?? []) withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.amounts ?? []) withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.accounts ?? []) withUniquePush(tokens, `\u0441\u0447\u0435\u0442 ${item}`);
|
||||||
|
for (const item of anchors.accounts ?? []) withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.periods ?? []) withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.document_types ?? []) withUniquePush(tokens, item);
|
||||||
|
for (const item of anchors.all ?? []) withUniquePush(tokens, item);
|
||||||
|
return uniqueStrings(tokens, 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRetrievalCorpus(results: UnifiedRetrievalResult[]): string {
|
||||||
|
const chunks: string[] = [];
|
||||||
|
for (const result of results) {
|
||||||
|
chunks.push(JSON.stringify(result.summary ?? {}));
|
||||||
|
for (const item of result.items.slice(0, 10)) {
|
||||||
|
chunks.push(JSON.stringify(item));
|
||||||
|
}
|
||||||
|
for (const evidence of result.evidence.slice(0, 16)) {
|
||||||
|
chunks.push(JSON.stringify(evidence));
|
||||||
|
}
|
||||||
|
chunks.push(...result.why_included.slice(0, 16));
|
||||||
|
chunks.push(...result.selection_reason.slice(0, 16));
|
||||||
|
chunks.push(...result.business_interpretation.slice(0, 16));
|
||||||
|
}
|
||||||
|
return chunks.join(" ").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAnchorMatchedInCorpus(anchor: string, corpus: string): boolean {
|
||||||
|
const normalized = normalizeAnchorForMatch(anchor);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (normalized.length < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (corpus.includes(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const withoutPrefix = normalized
|
||||||
|
.replace(/^(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|document|account|period|doc_type)\s*[:№#]?\s*/iu, "")
|
||||||
|
.trim();
|
||||||
|
if (withoutPrefix.length >= 3 && corpus.includes(withoutPrefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/^\d+(?:[.,]\d{2})?$/.test(withoutPrefix)) {
|
||||||
|
const normalizedAmount = withoutPrefix.replace(",", ".");
|
||||||
|
return corpus.includes(withoutPrefix) || corpus.includes(normalizedAmount);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateCompanyAnchorUsage(
|
||||||
|
anchors: CompanyAnchorSet | null | undefined,
|
||||||
|
retrievalResults: UnifiedRetrievalResult[]
|
||||||
|
): CompanyAnchorUsage {
|
||||||
|
const present = collectCompanyAnchorTokens(anchors);
|
||||||
|
if (present.length === 0) {
|
||||||
|
return {
|
||||||
|
present: [],
|
||||||
|
used: [],
|
||||||
|
unused: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const corpus = normalizeAnchorForMatch(collectRetrievalCorpus(retrievalResults));
|
||||||
|
const used: string[] = [];
|
||||||
|
const unused: string[] = [];
|
||||||
|
for (const anchor of present) {
|
||||||
|
if (isAnchorMatchedInCorpus(anchor, corpus)) {
|
||||||
|
withUniquePush(used, anchor);
|
||||||
|
} else {
|
||||||
|
withUniquePush(unused, anchor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
present: uniqueStrings(present, 24),
|
||||||
|
used: uniqueStrings(used, 12),
|
||||||
|
unused: uniqueStrings(unused, 12)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
||||||
const LONG_HEX_PATTERN = /\b[0-9a-f]{24,}\b/gi;
|
const LONG_HEX_PATTERN = /\b[0-9a-f]{24,}\b/gi;
|
||||||
const RAW_REF_BLOB_PATTERN = /\bevidence_source_ref_v1\|[^\s,;]+/gi;
|
const RAW_REF_BLOB_PATTERN = /\bevidence_source_ref_v1\|[^\s,;]+/gi;
|
||||||
|
|
@ -1129,6 +1250,12 @@ function isProblemUnitAlignedWithNarrativeDomain(unit: ProblemUnit, domain: P0Na
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domain === "vat_document_register_book") {
|
if (domain === "vat_document_register_book") {
|
||||||
|
const foreignVatDomain = ["period_close", "deferred_expense", "fixed_asset", "bank_settlement", "customer_settlement"].includes(
|
||||||
|
String(unit.lifecycle_domain ?? "")
|
||||||
|
);
|
||||||
|
if (foreignVatDomain && !hasControlledCrossDomainHandoff(unit)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (unit.lifecycle_domain === "vat_flow") {
|
if (unit.lifecycle_domain === "vat_flow") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1139,6 +1266,12 @@ function isProblemUnitAlignedWithNarrativeDomain(unit: ProblemUnit, domain: P0Na
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domain === "month_close_costs_20_44") {
|
if (domain === "month_close_costs_20_44") {
|
||||||
|
const foreignMonthCloseDomain = ["vat_flow", "bank_settlement", "customer_settlement", "fixed_asset"].includes(
|
||||||
|
String(unit.lifecycle_domain ?? "")
|
||||||
|
);
|
||||||
|
if (foreignMonthCloseDomain && !hasControlledCrossDomainHandoff(unit)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
unit.lifecycle_domain === "period_close" ||
|
unit.lifecycle_domain === "period_close" ||
|
||||||
unit.lifecycle_domain === "deferred_expense" ||
|
unit.lifecycle_domain === "deferred_expense" ||
|
||||||
|
|
@ -1775,12 +1908,178 @@ function mapDefectTokenToNarrative(value: string): string | null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractAccountNumbers(values: string[]): string[] {
|
const KNOWN_ACCOUNT_PREFIXES = new Set<string>([
|
||||||
const numbers = values.flatMap((value) => {
|
"01",
|
||||||
const matches = String(value ?? "").match(/\b\d{2}(?:\.\d{1,2})?\b/g);
|
"02",
|
||||||
return matches ?? [];
|
"07",
|
||||||
|
"08",
|
||||||
|
"10",
|
||||||
|
"13",
|
||||||
|
"19",
|
||||||
|
"20",
|
||||||
|
"21",
|
||||||
|
"23",
|
||||||
|
"25",
|
||||||
|
"26",
|
||||||
|
"28",
|
||||||
|
"29",
|
||||||
|
"41",
|
||||||
|
"43",
|
||||||
|
"44",
|
||||||
|
"45",
|
||||||
|
"50",
|
||||||
|
"51",
|
||||||
|
"52",
|
||||||
|
"55",
|
||||||
|
"57",
|
||||||
|
"58",
|
||||||
|
"60",
|
||||||
|
"62",
|
||||||
|
"66",
|
||||||
|
"67",
|
||||||
|
"68",
|
||||||
|
"69",
|
||||||
|
"70",
|
||||||
|
"71",
|
||||||
|
"73",
|
||||||
|
"76",
|
||||||
|
"90",
|
||||||
|
"91",
|
||||||
|
"94",
|
||||||
|
"96",
|
||||||
|
"97"
|
||||||
|
]);
|
||||||
|
|
||||||
|
function collectDateLikeSpansForNarrative(text: string): Array<{ start: number; end: number }> {
|
||||||
|
const spans: Array<{ start: number; end: number }> = [];
|
||||||
|
const patterns = [
|
||||||
|
/\b20\d{2}[./-](?:0[1-9]|1[0-2])(?:[./-](?:0[1-9]|[12]\d|3[01]))?\b/g,
|
||||||
|
/\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b/g,
|
||||||
|
/\b(?:0?[1-9]|[12]\d|3[01])\s+(?:января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)\b/giu
|
||||||
|
];
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
let match: RegExpExecArray | null = null;
|
||||||
|
while ((match = pattern.exec(text)) !== null) {
|
||||||
|
spans.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + match[0].length
|
||||||
});
|
});
|
||||||
return uniqueStrings(numbers, 12);
|
}
|
||||||
|
}
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAmountLikeSpansForNarrative(text: string): Array<{ start: number; end: number }> {
|
||||||
|
const spans: Array<{ start: number; end: number }> = [];
|
||||||
|
const pattern = /\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g;
|
||||||
|
let match: RegExpExecArray | null = null;
|
||||||
|
while ((match = pattern.exec(text)) !== null) {
|
||||||
|
spans.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + match[0].length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
|
||||||
|
function intersectsNarrativeSpan(
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
spans: Array<{ start: number; end: number }>
|
||||||
|
): boolean {
|
||||||
|
return spans.some((span) => start < span.end && end > span.start);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAccountContextMarker(text: string, start: number, end: number): boolean {
|
||||||
|
const left = text.slice(Math.max(0, start - 24), start);
|
||||||
|
const right = text.slice(end, Math.min(text.length, end + 24));
|
||||||
|
return /(?:счет|сч\.?|account|schet|по\s+60|по\s+62|по\s+19|по\s+68|по\s+20|по\s+25|по\s+26|по\s+44|расчет|ндс|закрыти|рбп|амортиз|settlement|vat|close)/iu.test(
|
||||||
|
`${left} ${right}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toKnownAccountToken(value: string): string | null {
|
||||||
|
const token = String(value ?? "").trim();
|
||||||
|
const prefix = token.match(/^(\d{2})/)?.[1];
|
||||||
|
if (!prefix || !KNOWN_ACCOUNT_PREFIXES.has(prefix)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAccountNumbers(values: string[]): string[] {
|
||||||
|
const tokens: string[] = [];
|
||||||
|
for (const value of values) {
|
||||||
|
const raw = String(value ?? "");
|
||||||
|
const matches = raw.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
|
||||||
|
for (const match of matches) {
|
||||||
|
const account = toKnownAccountToken(match);
|
||||||
|
if (account) {
|
||||||
|
tokens.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueStrings(tokens, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAccountNumbersFromNarrativeText(value: string): string[] {
|
||||||
|
const text = String(value ?? "").toLowerCase();
|
||||||
|
if (!text.trim()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: string[] = [];
|
||||||
|
const dateSpans = collectDateLikeSpansForNarrative(text);
|
||||||
|
const amountSpans = collectAmountLikeSpansForNarrative(text);
|
||||||
|
const blockedSpans = [...dateSpans, ...amountSpans];
|
||||||
|
|
||||||
|
const contextualPattern =
|
||||||
|
/(?:\b(?:счет(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)\b)\s*(?:№|#|:)?\s*([0-9./,\sиand]{2,96})/giu;
|
||||||
|
let contextualMatch: RegExpExecArray | null = null;
|
||||||
|
while ((contextualMatch = contextualPattern.exec(text)) !== null) {
|
||||||
|
const chunk = String(contextualMatch[1] ?? "");
|
||||||
|
const chunkTokens = chunk.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
|
||||||
|
for (const token of chunkTokens) {
|
||||||
|
const account = toKnownAccountToken(token);
|
||||||
|
if (account) {
|
||||||
|
result.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountPairPattern = /\b(\d{2}(?:\.\d{1,2})?)\s*\/\s*(\d{2}(?:\.\d{1,2})?)\b/g;
|
||||||
|
let pairMatch: RegExpExecArray | null = null;
|
||||||
|
while ((pairMatch = accountPairPattern.exec(text)) !== null) {
|
||||||
|
const left = toKnownAccountToken(String(pairMatch[1] ?? ""));
|
||||||
|
const right = toKnownAccountToken(String(pairMatch[2] ?? ""));
|
||||||
|
if (left) {
|
||||||
|
result.push(left);
|
||||||
|
}
|
||||||
|
if (right) {
|
||||||
|
result.push(right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const explicitPattern = /\b\d{2}(?:\.\d{1,2})?\b/g;
|
||||||
|
let explicitMatch: RegExpExecArray | null = null;
|
||||||
|
while ((explicitMatch = explicitPattern.exec(text)) !== null) {
|
||||||
|
const token = String(explicitMatch[0] ?? "");
|
||||||
|
const account = toKnownAccountToken(token);
|
||||||
|
if (!account) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const start = explicitMatch.index;
|
||||||
|
const end = start + token.length;
|
||||||
|
if (intersectsNarrativeSpan(start, end, blockedSpans)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!hasAccountContextMarker(text, start, end)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueStrings(result, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferP0NarrativeDomain(units: ProblemUnit[]): P0NarrativeDomain {
|
function inferP0NarrativeDomain(units: ProblemUnit[]): P0NarrativeDomain {
|
||||||
|
|
@ -1914,8 +2213,8 @@ function collectSemanticProfileScopes(results: UnifiedRetrievalResult[]): { acco
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SettlementEvidenceGrounding {
|
interface P0DomainEvidenceGrounding {
|
||||||
has_settlement_primary: boolean;
|
has_primary: boolean;
|
||||||
has_foreign_primary: boolean;
|
has_foreign_primary: boolean;
|
||||||
foreign_primary_domains: string[];
|
foreign_primary_domains: string[];
|
||||||
blocked: boolean;
|
blocked: boolean;
|
||||||
|
|
@ -1935,10 +2234,28 @@ function isSettlementDomainToken(value: string): boolean {
|
||||||
return /(?:bank_settlement|customer_settlement|settlements?|supplier_payments|suppliers?|customers?)/i.test(String(value ?? ""));
|
return /(?:bank_settlement|customer_settlement|settlements?|supplier_payments|suppliers?|customers?)/i.test(String(value ?? ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVatDomainToken(value: string): boolean {
|
||||||
|
return /(?:vat_flow|vat|nds|taxes?|purchase_book|sales_book|invoice|book_entry|register)/i.test(String(value ?? ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMonthCloseDomainToken(value: string): boolean {
|
||||||
|
return /(?:period_close|month_close|close_operation|cost_close|cost_allocation|deferred_expense)/i.test(String(value ?? ""));
|
||||||
|
}
|
||||||
|
|
||||||
function isForeignToSettlementDomainToken(value: string): boolean {
|
function isForeignToSettlementDomainToken(value: string): boolean {
|
||||||
return /(?:vat_flow|vat|deferred_expense|period_close|fixed_asset|fixed_assets|taxes?)/i.test(String(value ?? ""));
|
return /(?:vat_flow|vat|deferred_expense|period_close|fixed_asset|fixed_assets|taxes?)/i.test(String(value ?? ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isForeignToVatDomainToken(value: string): boolean {
|
||||||
|
return /(?:bank_settlement|customer_settlement|settlements?|period_close|deferred_expense|fixed_asset|fixed_assets|month_close)/i.test(
|
||||||
|
String(value ?? "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isForeignToMonthCloseDomainToken(value: string): boolean {
|
||||||
|
return /(?:bank_settlement|customer_settlement|settlements?|vat_flow|vat|fixed_asset|fixed_assets)/i.test(String(value ?? ""));
|
||||||
|
}
|
||||||
|
|
||||||
function collectResultAccounts(result: UnifiedRetrievalResult): string[] {
|
function collectResultAccounts(result: UnifiedRetrievalResult): string[] {
|
||||||
const accounts: string[] = [];
|
const accounts: string[] = [];
|
||||||
const semanticProfile = summaryValue(result, "semantic_profile");
|
const semanticProfile = summaryValue(result, "semantic_profile");
|
||||||
|
|
@ -1985,46 +2302,111 @@ function isSubstantiveResult(result: UnifiedRetrievalResult): boolean {
|
||||||
return result.items.length > 0 || result.evidence.length > 0;
|
return result.items.length > 0 || result.evidence.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function evaluateSettlementEvidenceGrounding(results: UnifiedRetrievalResult[]): SettlementEvidenceGrounding {
|
function evaluateP0DomainEvidenceGrounding(
|
||||||
const substantive = results.filter((item) => isSubstantiveResult(item));
|
results: UnifiedRetrievalResult[],
|
||||||
if (substantive.length === 0) {
|
focusDomain: P0NarrativeDomain
|
||||||
|
): P0DomainEvidenceGrounding {
|
||||||
|
if (!focusDomain) {
|
||||||
return {
|
return {
|
||||||
has_settlement_primary: false,
|
has_primary: false,
|
||||||
has_foreign_primary: false,
|
has_foreign_primary: false,
|
||||||
foreign_primary_domains: [],
|
foreign_primary_domains: [],
|
||||||
blocked: false
|
blocked: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const classify = (result: UnifiedRetrievalResult): { settlement: boolean; foreignDomains: string[] } => {
|
const substantive = results.filter((item) => isSubstantiveResult(item));
|
||||||
|
if (substantive.length === 0) {
|
||||||
|
return {
|
||||||
|
has_primary: false,
|
||||||
|
has_foreign_primary: false,
|
||||||
|
foreign_primary_domains: [],
|
||||||
|
blocked: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const classify = (result: UnifiedRetrievalResult): { inDomain: boolean; foreignDomains: string[] } => {
|
||||||
const accounts = collectResultAccounts(result);
|
const accounts = collectResultAccounts(result);
|
||||||
const domains = collectResultDomains(result);
|
const domains = collectResultDomains(result);
|
||||||
const relations = collectResultRelations(result);
|
const relations = collectResultRelations(result);
|
||||||
const settlement =
|
let inDomain = false;
|
||||||
|
let foreignDomains: string[] = [];
|
||||||
|
|
||||||
|
if (focusDomain === "settlements_60_62") {
|
||||||
|
inDomain =
|
||||||
accounts.some((item) => isSettlementAccountToken(item) || /^(?:51|76)(?:\.|$)/.test(item)) ||
|
accounts.some((item) => isSettlementAccountToken(item) || /^(?:51|76)(?:\.|$)/.test(item)) ||
|
||||||
domains.some((item) => isSettlementDomainToken(item)) ||
|
domains.some((item) => isSettlementDomainToken(item)) ||
|
||||||
relations.some((item) => /payment_to_settlement|statement_to_document|contract_to_documents/.test(item));
|
relations.some((item) => /payment_to_settlement|statement_to_document|contract_to_documents|linked_to_settlement|settlement_closed/.test(item));
|
||||||
const foreignDomains = domains.filter((item) => isForeignToSettlementDomainToken(item));
|
foreignDomains = domains.filter((item) => isForeignToSettlementDomainToken(item));
|
||||||
|
} else if (focusDomain === "vat_document_register_book") {
|
||||||
|
inDomain =
|
||||||
|
accounts.some((item) => isVatAccountToken(item)) ||
|
||||||
|
domains.some((item) => isVatDomainToken(item)) ||
|
||||||
|
relations.some((item) =>
|
||||||
|
/invoice_to_vat|source_doc_present|invoice_linked|book_entry_generated|deduction_posted|register_to_book|vat_/i.test(item)
|
||||||
|
);
|
||||||
|
foreignDomains = domains.filter((item) => isForeignToVatDomainToken(item));
|
||||||
|
} else if (focusDomain === "month_close_costs_20_44") {
|
||||||
|
inDomain =
|
||||||
|
accounts.some((item) => isCloseCostsAccountToken(item)) ||
|
||||||
|
domains.some((item) => isMonthCloseDomainToken(item)) ||
|
||||||
|
relations.some((item) =>
|
||||||
|
/costs_accumulated|allocation_rules_resolved|close_operation_runs|residuals_zero|close_operation|period_close|allocation|writeoff/i.test(
|
||||||
|
item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
foreignDomains = domains.filter((item) => isForeignToMonthCloseDomainToken(item));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settlement,
|
inDomain,
|
||||||
foreignDomains: uniqueStrings(foreignDomains, 8)
|
foreignDomains: uniqueStrings(foreignDomains, 8)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const top = substantive[0];
|
const top = substantive[0];
|
||||||
const topClass = classify(top);
|
const topClass = classify(top);
|
||||||
const hasAnySettlement = substantive.some((item) => classify(item).settlement);
|
const hasAnyPrimary = substantive.some((item) => classify(item).inDomain);
|
||||||
const hasForeignPrimary = topClass.foreignDomains.length > 0 && !topClass.settlement;
|
const hasForeignPrimary = topClass.foreignDomains.length > 0 && !topClass.inDomain;
|
||||||
const blocked = hasForeignPrimary && !hasAnySettlement && !hasControlledCrossDomainHandoffInResult(top);
|
const blocked = hasForeignPrimary && !hasAnyPrimary && !hasControlledCrossDomainHandoffInResult(top);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
has_settlement_primary: hasAnySettlement,
|
has_primary: hasAnyPrimary,
|
||||||
has_foreign_primary: hasForeignPrimary,
|
has_foreign_primary: hasForeignPrimary,
|
||||||
foreign_primary_domains: topClass.foreignDomains,
|
foreign_primary_domains: topClass.foreignDomains,
|
||||||
blocked
|
blocked
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasStrongNarrativeDomainSignalInText(userMessage: string, domain: P0NarrativeDomain): boolean {
|
||||||
|
if (!domain) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const text = String(userMessage ?? "").toLowerCase();
|
||||||
|
const accountTokens = extractAccountNumbersFromNarrativeText(text);
|
||||||
|
if (domain === "settlements_60_62") {
|
||||||
|
return (
|
||||||
|
accountTokens.some((item) => isSettlementAccountToken(item)) ||
|
||||||
|
/(60\.0[12]|62\.0[12]|долг|аванс|зач[её]т|взаимозач|расч[её]т)/i.test(text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (domain === "vat_document_register_book") {
|
||||||
|
return (
|
||||||
|
accountTokens.some((item) => isVatAccountToken(item)) ||
|
||||||
|
/(ндс|vat|счет[-\s]?фактур|сч[её]т[-\s]?фактур|книг[аи]|регистр)/i.test(text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (domain === "month_close_costs_20_44") {
|
||||||
|
return (
|
||||||
|
accountTokens.some((item) => isCloseCostsAccountToken(item)) ||
|
||||||
|
/(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(
|
||||||
|
text
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function inferP0FocusNarrativeDomain(
|
function inferP0FocusNarrativeDomain(
|
||||||
userMessage: string,
|
userMessage: string,
|
||||||
results: UnifiedRetrievalResult[],
|
results: UnifiedRetrievalResult[],
|
||||||
|
|
@ -2032,20 +2414,30 @@ function inferP0FocusNarrativeDomain(
|
||||||
focusDomainHint?: string | null
|
focusDomainHint?: string | null
|
||||||
): P0NarrativeDomain {
|
): P0NarrativeDomain {
|
||||||
const fromHint = p0NarrativeDomainFromHint(focusDomainHint);
|
const fromHint = p0NarrativeDomainFromHint(focusDomainHint);
|
||||||
|
const fromMessage = inferNarrativeDomainFromText(userMessage);
|
||||||
|
const strongFromMessage = Boolean(fromMessage && hasStrongNarrativeDomainSignalInText(userMessage, fromMessage));
|
||||||
|
const fromDomainGuard = inferP0NarrativeDomainFromDomainGuards(results);
|
||||||
|
if (fromHint && fromMessage && fromHint !== fromMessage) {
|
||||||
|
return strongFromMessage ? fromMessage : fromHint;
|
||||||
|
}
|
||||||
if (fromHint) {
|
if (fromHint) {
|
||||||
return fromHint;
|
return fromHint;
|
||||||
}
|
}
|
||||||
const fromDomainGuard = inferP0NarrativeDomainFromDomainGuards(results);
|
if (fromDomainGuard && fromMessage && fromDomainGuard !== fromMessage) {
|
||||||
|
return strongFromMessage ? fromMessage : fromDomainGuard;
|
||||||
|
}
|
||||||
if (fromDomainGuard) {
|
if (fromDomainGuard) {
|
||||||
return fromDomainGuard;
|
return fromDomainGuard;
|
||||||
}
|
}
|
||||||
const fromMessage = inferNarrativeDomainFromText(userMessage);
|
if (strongFromMessage) {
|
||||||
|
return fromMessage;
|
||||||
|
}
|
||||||
if (fromMessage) {
|
if (fromMessage) {
|
||||||
return fromMessage;
|
return fromMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const semanticScopes = collectSemanticProfileScopes(results);
|
const semanticScopes = collectSemanticProfileScopes(results);
|
||||||
const messageAccounts = extractAccountNumbers([userMessage]);
|
const messageAccounts = extractAccountNumbersFromNarrativeText(userMessage);
|
||||||
const hasExplicitP0AccountSignal = [...messageAccounts, ...semanticScopes.accounts].some(
|
const hasExplicitP0AccountSignal = [...messageAccounts, ...semanticScopes.accounts].some(
|
||||||
(item) => isSettlementAccountToken(item) || isVatAccountToken(item) || isCloseCostsAccountToken(item)
|
(item) => isSettlementAccountToken(item) || isVatAccountToken(item) || isCloseCostsAccountToken(item)
|
||||||
);
|
);
|
||||||
|
|
@ -2224,14 +2616,22 @@ function buildDirectAnswer(input: {
|
||||||
mode: PolicyMode;
|
mode: PolicyMode;
|
||||||
retrievalResults: UnifiedRetrievalResult[];
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
policySignals: PolicySignals;
|
policySignals: PolicySignals;
|
||||||
|
focusDomain: P0NarrativeDomain;
|
||||||
}): string {
|
}): string {
|
||||||
const topFact = humanizeFactForDirectAnswer(firstMeaningfulFact(input.retrievalResults));
|
const topFact = humanizeFactForDirectAnswer(firstMeaningfulFact(input.retrievalResults));
|
||||||
|
const domainAnchor = domainNarrativeAnchor(input.focusDomain);
|
||||||
|
const topFactDomain = topFact ? inferNarrativeDomainFromText(topFact) : null;
|
||||||
|
const topFactAligned = Boolean(topFact) && (!input.focusDomain || topFactDomain === input.focusDomain);
|
||||||
|
const preferredFact = topFactAligned ? topFact : null;
|
||||||
if (input.mode === "focused_grounded") {
|
if (input.mode === "focused_grounded") {
|
||||||
return topFact ?? "Проблема подтверждена на текущей опоре и готова к точечной проверке.";
|
return preferredFact ?? domainAnchor ?? "Проблема подтверждена на текущей опоре и готова к точечной проверке.";
|
||||||
}
|
}
|
||||||
if (input.mode === "broad_partial") {
|
if (input.mode === "broad_partial") {
|
||||||
if (topFact) {
|
if (preferredFact) {
|
||||||
return `${topFact.replace(/[.!?]+$/u, "")}; подтверждение пока частичное.`;
|
return `${preferredFact.replace(/[.!?]+$/u, "")}; подтверждение пока частичное.`;
|
||||||
|
}
|
||||||
|
if (domainAnchor) {
|
||||||
|
return `${domainAnchor.replace(/[.!?]+$/u, "")}; подтверждение пока частичное.`;
|
||||||
}
|
}
|
||||||
return "Есть признаки проблемы, но опора частичная и вывод ограничен.";
|
return "Есть признаки проблемы, но опора частичная и вывод ограничен.";
|
||||||
}
|
}
|
||||||
|
|
@ -2338,11 +2738,23 @@ function buildProblemCentricAnswerStructure(input: {
|
||||||
6
|
6
|
||||||
);
|
);
|
||||||
const evidenceIds = uniqueStrings(input.evidenceItems.map((item) => item.evidence_id), 10);
|
const evidenceIds = uniqueStrings(input.evidenceItems.map((item) => item.evidence_id), 10);
|
||||||
|
const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, input.evidenceItems);
|
||||||
|
const hasCriticalEvidenceLimitation =
|
||||||
|
input.limitationReasonCodes.includes("weak_source_mapping") ||
|
||||||
|
input.limitationReasonCodes.includes("insufficient_detail");
|
||||||
|
const confidenceLimited =
|
||||||
|
input.mode !== "focused_grounded" ||
|
||||||
|
weakUnits ||
|
||||||
|
input.domainLockMiss ||
|
||||||
|
input.limitationReasonCodes.includes("missing_mechanism") ||
|
||||||
|
input.limitationReasonCodes.includes("heuristic_inference") ||
|
||||||
|
hasCriticalEvidenceLimitation ||
|
||||||
|
aggregateEvidenceConfidence === "low";
|
||||||
|
|
||||||
const mechanismStatus: AnswerStructureV11["mechanism_block"]["status"] =
|
const mechanismStatus: AnswerStructureV11["mechanism_block"]["status"] =
|
||||||
unitMechanismNotes.length === 0
|
unitMechanismNotes.length === 0
|
||||||
? "unresolved"
|
? "unresolved"
|
||||||
: weakUnits || input.limitationReasonCodes.includes("missing_mechanism")
|
: confidenceLimited
|
||||||
? "limited"
|
? "limited"
|
||||||
: "grounded";
|
: "grounded";
|
||||||
|
|
||||||
|
|
@ -2453,21 +2865,50 @@ function limitationReasonToUserText(code: EvidenceLimitationReasonCode): string
|
||||||
|
|
||||||
function inferNarrativeDomainFromText(value: string): P0NarrativeDomain {
|
function inferNarrativeDomainFromText(value: string): P0NarrativeDomain {
|
||||||
const text = String(value ?? "").toLowerCase();
|
const text = String(value ?? "").toLowerCase();
|
||||||
const accountTokens = extractAccountNumbers([text]);
|
const accountTokens = extractAccountNumbersFromNarrativeText(text);
|
||||||
const hasSettlementLexicalSignal = /(оплат|долг|аванс|взаимозач|зачет|зачёт|поставщ|покупат|не\s+сход)/i.test(text);
|
|
||||||
|
|
||||||
if (accountTokens.some((token) => isSettlementAccountToken(token)) || hasSettlementLexicalSignal) {
|
let settlementScore = 0;
|
||||||
return "settlements_60_62";
|
let vatScore = 0;
|
||||||
|
let monthCloseScore = 0;
|
||||||
|
|
||||||
|
if (accountTokens.some((token) => isSettlementAccountToken(token))) {
|
||||||
|
settlementScore += 3;
|
||||||
}
|
}
|
||||||
if (accountTokens.some((token) => isVatAccountToken(token)) || /(ндс|счет[-\s]?фактур|регистр|книг)/i.test(text)) {
|
if (accountTokens.some((token) => isVatAccountToken(token))) {
|
||||||
return "vat_document_register_book";
|
vatScore += 3;
|
||||||
|
}
|
||||||
|
if (accountTokens.some((token) => isCloseCostsAccountToken(token))) {
|
||||||
|
monthCloseScore += 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/(долг|аванс|взаимозач|зачет|зачёт|62\.01|62\.02|60\.01|60\.02|не\s+сход)/i.test(text)) {
|
||||||
|
settlementScore += 2;
|
||||||
|
}
|
||||||
|
if (/(ндс|vat|счет[-\s]?фактур|сч[её]т[-\s]?фактур|книг[аи]|регистр)/i.test(text)) {
|
||||||
|
vatScore += 3;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
accountTokens.some((token) => isCloseCostsAccountToken(token)) ||
|
/(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(
|
||||||
/(закрыти[ея]\s+месяц|затрат|распределени|списан)/i.test(text)
|
text
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
|
monthCloseScore += 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxScore = Math.max(settlementScore, vatScore, monthCloseScore);
|
||||||
|
if (maxScore <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Tie-break prioritizes explicit VAT and month-close lexical markers over broad settlement wording.
|
||||||
|
if (vatScore === maxScore) {
|
||||||
|
return "vat_document_register_book";
|
||||||
|
}
|
||||||
|
if (monthCloseScore === maxScore) {
|
||||||
return "month_close_costs_20_44";
|
return "month_close_costs_20_44";
|
||||||
}
|
}
|
||||||
|
if (settlementScore === maxScore) {
|
||||||
|
return "settlements_60_62";
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2578,6 +3019,11 @@ function buildEvidenceSectionLines(structure: AnswerStructureV11): string[] {
|
||||||
const claimLinks = Array.isArray(structure.evidence_block.claim_evidence_links)
|
const claimLinks = Array.isArray(structure.evidence_block.claim_evidence_links)
|
||||||
? structure.evidence_block.claim_evidence_links.length
|
? structure.evidence_block.claim_evidence_links.length
|
||||||
: 0;
|
: 0;
|
||||||
|
const reliabilityLimited =
|
||||||
|
structure.mechanism_block.status !== "grounded" ||
|
||||||
|
structure.uncertainty_block.limitations.length > 0 ||
|
||||||
|
structure.uncertainty_block.open_uncertainties.length > 0 ||
|
||||||
|
structure.evidence_block.coverage_note === "coverage_partial_or_limited";
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
const coverageSplitLines = buildCoverageSplitLines(structure);
|
const coverageSplitLines = buildCoverageSplitLines(structure);
|
||||||
|
|
||||||
|
|
@ -2593,7 +3039,7 @@ function buildEvidenceSectionLines(structure: AnswerStructureV11): string[] {
|
||||||
if (structure.evidence_block.coverage_note === "coverage_partial_or_limited") {
|
if (structure.evidence_block.coverage_note === "coverage_partial_or_limited") {
|
||||||
lines.push("Опора частичная: часть требований покрыта не полностью.");
|
lines.push("Опора частичная: часть требований покрыта не полностью.");
|
||||||
} else if (evidenceCount > 0) {
|
} else if (evidenceCount > 0) {
|
||||||
lines.push("Опора достаточна для первичного вывода.");
|
lines.push(reliabilityLimited ? "Опора есть, но достаточна только для предварительного вывода." : "Опора достаточна для первичного вывода.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lines.length === 0) {
|
if (lines.length === 0) {
|
||||||
|
|
@ -2678,6 +3124,8 @@ function humanizeLimitationToken(value: string): string | null {
|
||||||
if (normalized === "missing_anchor:account") return "Счет или группа счетов не указаны.";
|
if (normalized === "missing_anchor:account") return "Счет или группа счетов не указаны.";
|
||||||
if (normalized === "missing_anchor:document_or_object") return "Не указан документ или объект для трассировки.";
|
if (normalized === "missing_anchor:document_or_object") return "Не указан документ или объект для трассировки.";
|
||||||
if (normalized === "missing_anchor:counterparty") return "Не указан контрагент или договор.";
|
if (normalized === "missing_anchor:counterparty") return "Не указан контрагент или договор.";
|
||||||
|
if (normalized === "primary_domain_evidence_not_confirmed")
|
||||||
|
return "Целевой механизм активного домена подтвержден частично; вывод ограничен.";
|
||||||
if (normalized === "settlement_primary_evidence_not_confirmed")
|
if (normalized === "settlement_primary_evidence_not_confirmed")
|
||||||
return "Опора по расчетному контуру не подтверждена: в приоритете были сигналы из смежных доменов.";
|
return "Опора по расчетному контуру не подтверждена: в приоритете были сигналы из смежных доменов.";
|
||||||
if (normalized.includes("snapshot")) return "Вывод сделан по snapshot и может не включать часть цепочки.";
|
if (normalized.includes("snapshot")) return "Вывод сделан по snapshot и может не включать часть цепочки.";
|
||||||
|
|
@ -2733,22 +3181,188 @@ function buildLimitationsSectionLines(structure: AnswerStructureV11): string[] {
|
||||||
return ["Существенных ограничений в текущем срезе не выявлено."];
|
return ["Существенных ограничений в текущем срезе не выявлено."];
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPolicyReply(structure: AnswerStructureV11): string {
|
function domainNameForQuestionType(domain: P0NarrativeDomain): string {
|
||||||
|
if (domain === "settlements_60_62") return "\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0443\u0440\u0430";
|
||||||
|
if (domain === "vat_document_register_book") return "\u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u041d\u0414\u0421";
|
||||||
|
if (domain === "month_close_costs_20_44")
|
||||||
|
return "\u043a\u043e\u043d\u0442\u0443\u0440\u0430 \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f \u043c\u0435\u0441\u044f\u0446\u0430";
|
||||||
|
return "\u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0443\u0447\u0430\u0441\u0442\u043a\u0430";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuestionTypeShortLine(context: AnswerRenderContext): string | null {
|
||||||
|
const domainName = domainNameForQuestionType(context.focusDomain);
|
||||||
|
if (context.questionType === "where_break_is") {
|
||||||
|
return `\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c \u0440\u0430\u0437\u0440\u044b\u0432 \u0432\u043d\u0443\u0442\u0440\u0438 ${domainName}.`;
|
||||||
|
}
|
||||||
|
if (context.questionType === "prove_or_guess") {
|
||||||
|
return "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u0440\u0430\u0437\u0432\u0435\u0441\u0442\u0438 \u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043e \u0438 \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u0443.";
|
||||||
|
}
|
||||||
|
if (context.questionType === "what_is_it_grounded_on") {
|
||||||
|
return "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u043e \u0434\u0430\u043d\u043d\u044b\u043c.";
|
||||||
|
}
|
||||||
|
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
|
||||||
|
return "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u0438 \u043d\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u0446\u0435\u043f\u043e\u0447\u043a\u0438.";
|
||||||
|
}
|
||||||
|
if (context.questionType === "what_to_check_first") {
|
||||||
|
return "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0430: \u0434\u0430\u0442\u044c \u043f\u0435\u0440\u0432\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuestionTypeBrokenLine(context: AnswerRenderContext): string | null {
|
||||||
|
if (context.questionType !== "where_break_is") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (context.focusDomain === "settlements_60_62") {
|
||||||
|
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430: \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0430 \u043e\u043f\u043b\u0430\u0442\u044b \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0443 \u0440\u0430\u0441\u0447\u0435\u0442\u043e\u0432 \u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0443 \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f.";
|
||||||
|
}
|
||||||
|
if (context.focusDomain === "vat_document_register_book") {
|
||||||
|
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430: \u0441\u0432\u044f\u0437\u043a\u0430 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430, \u0441\u0447\u0435\u0442\u0430-\u0444\u0430\u043a\u0442\u0443\u0440\u044b \u0438 \u0437\u0430\u043f\u0438\u0441\u0438 \u043a\u043d\u0438\u0433\u0438.";
|
||||||
|
}
|
||||||
|
if (context.focusDomain === "month_close_costs_20_44") {
|
||||||
|
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430: \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u043e\u0442 \u043d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f \u0437\u0430\u0442\u0440\u0430\u0442 \u043a \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044e/\u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044e.";
|
||||||
|
}
|
||||||
|
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430 \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u0447\u0430\u0441\u0442\u0438\u0447\u043d\u043e; \u043d\u0443\u0436\u043d\u0430 \u0442\u043e\u0447\u0435\u0447\u043d\u0430\u044f \u0441\u0432\u0435\u0440\u043a\u0430.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuestionTypeWhyLine(context: AnswerRenderContext): string | null {
|
||||||
|
if (context.questionType === "prove_or_guess") {
|
||||||
|
return "\u0417\u0434\u0435\u0441\u044c \u0447\u0435\u0441\u0442\u043d\u043e \u0440\u0430\u0437\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u0447\u0442\u043e \u0443\u0436\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e \u0438 \u0447\u0442\u043e \u043f\u043e\u043a\u0430 \u043e\u0441\u0442\u0430\u0435\u0442\u0441\u044f \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u043e\u0439.";
|
||||||
|
}
|
||||||
|
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
|
||||||
|
return "\u0426\u0435\u043f\u043e\u0447\u043a\u0438 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u044b \u043d\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u0438 \u043d\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u043f\u043e \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u043e\u043f\u043e\u0440\u0435.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuestionTypeEvidenceLine(context: AnswerRenderContext): string | null {
|
||||||
|
if (context.questionType === "what_is_it_grounded_on") {
|
||||||
|
return "\u0412 \u044d\u0442\u043e\u043c \u043e\u0442\u0432\u0435\u0442\u0435 \u0432 \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u0438\u043c\u0435\u043d\u043d\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u044f \u0432\u044b\u0432\u043e\u0434\u0430.";
|
||||||
|
}
|
||||||
|
if (context.questionType === "prove_or_guess") {
|
||||||
|
return "\u0421\u0438\u043b\u0430 \u0432\u044b\u0432\u043e\u0434\u0430 \u043e\u0446\u0435\u043d\u0435\u043d\u0430 \u043f\u043e \u043f\u0440\u044f\u043c\u043e\u0439 \u043e\u043f\u043e\u0440\u0435, \u0430 \u043d\u0435 \u043f\u043e \u0434\u043e\u0433\u0430\u0434\u043a\u0430\u043c.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAnchorList(anchors: string[], prefix: string): string | null {
|
||||||
|
if (anchors.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${prefix}: ${anchors.join(", ")}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuestionTypeCheckLine(context: AnswerRenderContext): string | null {
|
||||||
|
if (context.questionType === "what_to_check_first") {
|
||||||
|
return "\u041d\u0430\u0447\u043d\u0438\u0442\u0435 \u0441 \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u043f\u0443\u043d\u043a\u0442\u0430 \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u0431\u0435\u0437 \u043f\u0435\u0440\u0435\u0441\u043a\u043e\u043a\u0430.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuestionTypeLimitationLine(context: AnswerRenderContext): string | null {
|
||||||
|
if (context.questionType === "prove_or_guess") {
|
||||||
|
return "\u0414\u043b\u044f \u0444\u043e\u0440\u043c\u0430\u0442\u0430 \u00ab\u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043e \u0438\u043b\u0438 \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u0430\u00bb \u0432\u0441\u0435 \u043d\u0435\u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0447\u0430\u0441\u0442\u0438 \u043e\u0442\u0434\u0435\u043b\u0435\u043d\u044b \u0432 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f.";
|
||||||
|
}
|
||||||
|
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
|
||||||
|
return "\u0414\u0435\u043b\u0435\u043d\u0438\u0435 \u043d\u0430 \u00abcomplete/incomplete\u00bb \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043f\u043e\u043b\u043d\u043e\u0442\u044b \u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c \u0441\u0440\u0435\u0437\u0435.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyQuestionTypeAndAnchorPolicy(input: {
|
||||||
|
shortLine: string;
|
||||||
|
brokenLines: string[];
|
||||||
|
whyLines: string[];
|
||||||
|
evidenceLines: string[];
|
||||||
|
checkLines: string[];
|
||||||
|
limitationLines: string[];
|
||||||
|
context: AnswerRenderContext;
|
||||||
|
}): {
|
||||||
|
shortLine: string;
|
||||||
|
brokenLines: string[];
|
||||||
|
whyLines: string[];
|
||||||
|
evidenceLines: string[];
|
||||||
|
checkLines: string[];
|
||||||
|
limitationLines: string[];
|
||||||
|
} {
|
||||||
|
const nextShort = buildQuestionTypeShortLine(input.context) ?? input.shortLine;
|
||||||
|
const nextBroken = dedupeNarrativeLines(
|
||||||
|
[buildQuestionTypeBrokenLine(input.context), ...input.brokenLines].filter((item): item is string => Boolean(item)),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
const nextWhy = dedupeNarrativeLines(
|
||||||
|
[buildQuestionTypeWhyLine(input.context), ...input.whyLines].filter((item): item is string => Boolean(item)),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
const anchorUsedLine = formatAnchorList(
|
||||||
|
input.context.anchors.used,
|
||||||
|
"\u0412 \u043e\u043f\u043e\u0440\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u044b \u044f\u043a\u043e\u0440\u044f \u0432\u043e\u043f\u0440\u043e\u0441\u0430"
|
||||||
|
);
|
||||||
|
const anchorUnusedLine = formatAnchorList(
|
||||||
|
input.context.anchors.unused,
|
||||||
|
"\u042f\u043a\u043e\u0440\u044f \u0438\u0437 \u0432\u043e\u043f\u0440\u043e\u0441\u0430 \u0431\u0435\u0437 \u043f\u0440\u044f\u043c\u043e\u0433\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f"
|
||||||
|
);
|
||||||
|
const nextEvidence = dedupeNarrativeLines(
|
||||||
|
[buildQuestionTypeEvidenceLine(input.context), ...input.evidenceLines, anchorUsedLine].filter(
|
||||||
|
(item): item is string => Boolean(item)
|
||||||
|
),
|
||||||
|
7
|
||||||
|
);
|
||||||
|
const nextChecks = dedupeNarrativeLines(
|
||||||
|
[buildQuestionTypeCheckLine(input.context), ...input.checkLines].filter((item): item is string => Boolean(item)),
|
||||||
|
5
|
||||||
|
);
|
||||||
|
const nextLimitations = dedupeNarrativeLines(
|
||||||
|
[buildQuestionTypeLimitationLine(input.context), anchorUnusedLine, ...input.limitationLines].filter(
|
||||||
|
(item): item is string => Boolean(item)
|
||||||
|
),
|
||||||
|
6
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
shortLine: ensureSentence(nextShort),
|
||||||
|
brokenLines: nextBroken,
|
||||||
|
whyLines: nextWhy,
|
||||||
|
evidenceLines: nextEvidence,
|
||||||
|
checkLines: nextChecks,
|
||||||
|
limitationLines: nextLimitations
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPolicyReply(structure: AnswerStructureV11, context?: AnswerRenderContext): string {
|
||||||
const shortLine = ensureSentence(buildShortSectionLine(structure));
|
const shortLine = ensureSentence(buildShortSectionLine(structure));
|
||||||
const brokenLines = buildBrokenSectionLines(structure);
|
const brokenLines = buildBrokenSectionLines(structure);
|
||||||
const whyLines = buildWhySectionLines(structure);
|
const whyLines = buildWhySectionLines(structure);
|
||||||
const evidenceLines = buildEvidenceSectionLines(structure);
|
const evidenceLines = buildEvidenceSectionLines(structure);
|
||||||
const checkLines = buildChecksSectionLines(structure);
|
const checkLines = buildChecksSectionLines(structure);
|
||||||
const limitationLines = buildLimitationsSectionLines(structure);
|
const limitationLines = buildLimitationsSectionLines(structure);
|
||||||
|
const enriched = context
|
||||||
|
? applyQuestionTypeAndAnchorPolicy({
|
||||||
|
shortLine,
|
||||||
|
brokenLines,
|
||||||
|
whyLines,
|
||||||
|
evidenceLines,
|
||||||
|
checkLines,
|
||||||
|
limitationLines,
|
||||||
|
context
|
||||||
|
})
|
||||||
|
: {
|
||||||
|
shortLine,
|
||||||
|
brokenLines,
|
||||||
|
whyLines,
|
||||||
|
evidenceLines,
|
||||||
|
checkLines,
|
||||||
|
limitationLines
|
||||||
|
};
|
||||||
|
|
||||||
return sanitizeUserFacingReply(
|
return sanitizeUserFacingReply(
|
||||||
[
|
[
|
||||||
`Коротко: ${shortLine}`,
|
`Коротко: ${enriched.shortLine}`,
|
||||||
`Что сломано:\n${formatList(brokenLines)}`,
|
`Что сломано:\n${formatList(enriched.brokenLines)}`,
|
||||||
`Почему это похоже на проблему:\n${formatList(whyLines)}`,
|
`Почему это похоже на проблему:\n${formatList(enriched.whyLines)}`,
|
||||||
`На чем это основано:\n${formatList(evidenceLines)}`,
|
`На чем это основано:\n${formatList(enriched.evidenceLines)}`,
|
||||||
`Что проверить первым:\n${formatList(checkLines)}`,
|
`Что проверить первым:\n${formatList(enriched.checkLines)}`,
|
||||||
`Ограничения:\n${formatList(limitationLines)}`
|
`Ограничения:\n${formatList(enriched.limitationLines)}`
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n")
|
.join("\n\n")
|
||||||
|
|
@ -2757,6 +3371,8 @@ function renderPolicyReply(structure: AnswerStructureV11): string {
|
||||||
|
|
||||||
function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutput {
|
function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutput {
|
||||||
const fallbackType = fallbackFromSummary(input.routeSummary);
|
const fallbackType = fallbackFromSummary(input.routeSummary);
|
||||||
|
const questionType: QuestionTypeClass = input.questionTypeHint ?? "unknown";
|
||||||
|
const anchorUsage = evaluateCompanyAnchorUsage(input.companyAnchors, input.retrievalResults);
|
||||||
const okResults = input.retrievalResults.filter((item) => item.status === "ok");
|
const okResults = input.retrievalResults.filter((item) => item.status === "ok");
|
||||||
const partialResults = input.retrievalResults.filter((item) => item.status === "partial");
|
const partialResults = input.retrievalResults.filter((item) => item.status === "partial");
|
||||||
const emptyResults = input.retrievalResults.filter((item) => item.status === "empty");
|
const emptyResults = input.retrievalResults.filter((item) => item.status === "empty");
|
||||||
|
|
@ -2786,15 +3402,8 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||||
problemHeavyUnits,
|
problemHeavyUnits,
|
||||||
input.focusDomainHint
|
input.focusDomainHint
|
||||||
);
|
);
|
||||||
const settlementGrounding = focusNarrativeDomain === "settlements_60_62"
|
const focusDomainGrounding = evaluateP0DomainEvidenceGrounding(input.retrievalResults, focusNarrativeDomain);
|
||||||
? evaluateSettlementEvidenceGrounding(input.retrievalResults)
|
const focusDomainGroundingBlocked = Boolean(focusNarrativeDomain && focusDomainGrounding.blocked);
|
||||||
: {
|
|
||||||
has_settlement_primary: false,
|
|
||||||
has_foreign_primary: false,
|
|
||||||
foreign_primary_domains: [],
|
|
||||||
blocked: false
|
|
||||||
};
|
|
||||||
const settlementGroundingBlocked = focusNarrativeDomain === "settlements_60_62" && settlementGrounding.blocked;
|
|
||||||
const rankedProblemUnits = rankProblemUnitsForAnswer(problemHeavyUnits, lifecycleAnswerEnabled, focusNarrativeDomain);
|
const rankedProblemUnits = rankProblemUnitsForAnswer(problemHeavyUnits, lifecycleAnswerEnabled, focusNarrativeDomain);
|
||||||
const domainAlignedProblemUnits =
|
const domainAlignedProblemUnits =
|
||||||
focusNarrativeDomain === null
|
focusNarrativeDomain === null
|
||||||
|
|
@ -2805,7 +3414,7 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||||
rankedProblemUnits.length > 0 &&
|
rankedProblemUnits.length > 0 &&
|
||||||
domainAlignedProblemUnits.length === 0
|
domainAlignedProblemUnits.length === 0
|
||||||
);
|
);
|
||||||
const domainLockMiss = domainLockMissBase || settlementGroundingBlocked;
|
const domainLockMiss = domainLockMissBase || focusDomainGroundingBlocked;
|
||||||
const selectedProblemUnits = (
|
const selectedProblemUnits = (
|
||||||
focusNarrativeDomain === null ? rankedProblemUnits : domainAlignedProblemUnits
|
focusNarrativeDomain === null ? rankedProblemUnits : domainAlignedProblemUnits
|
||||||
).slice(0, 4);
|
).slice(0, 4);
|
||||||
|
|
@ -2853,7 +3462,7 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||||
policySignals
|
policySignals
|
||||||
});
|
});
|
||||||
const guardedDecision: PolicyDecision =
|
const guardedDecision: PolicyDecision =
|
||||||
settlementGroundingBlocked &&
|
focusDomainGroundingBlocked &&
|
||||||
decision.mode !== "out_of_scope" &&
|
decision.mode !== "out_of_scope" &&
|
||||||
decision.mode !== "route_mismatch" &&
|
decision.mode !== "route_mismatch" &&
|
||||||
decision.mode !== "backend_error"
|
decision.mode !== "backend_error"
|
||||||
|
|
@ -2870,7 +3479,9 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||||
policySignals.minimum_evidence_failed ||
|
policySignals.minimum_evidence_failed ||
|
||||||
limitationReasonCodes.includes("missing_mechanism") ||
|
limitationReasonCodes.includes("missing_mechanism") ||
|
||||||
limitationReasonCodes.includes("weak_source_mapping") ||
|
limitationReasonCodes.includes("weak_source_mapping") ||
|
||||||
|
limitationReasonCodes.includes("insufficient_detail") ||
|
||||||
aggregateEvidenceConfidence === "low" ||
|
aggregateEvidenceConfidence === "low" ||
|
||||||
|
domainLockMiss ||
|
||||||
lowConfidenceConcentration;
|
lowConfidenceConcentration;
|
||||||
const hardBlockedMode =
|
const hardBlockedMode =
|
||||||
guardedDecision.mode === "out_of_scope" ||
|
guardedDecision.mode === "out_of_scope" ||
|
||||||
|
|
@ -2907,7 +3518,11 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||||
const lifecycleModeActive = lifecycleAnswerEnabled && selectedProblemUnits.length > 0 && hasLifecycleResolution(selectedProblemUnits);
|
const lifecycleModeActive = lifecycleAnswerEnabled && selectedProblemUnits.length > 0 && hasLifecycleResolution(selectedProblemUnits);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assistant_reply: renderPolicyReply(problemCentricStructure),
|
assistant_reply: renderPolicyReply(problemCentricStructure, {
|
||||||
|
questionType,
|
||||||
|
focusDomain: focusNarrativeDomain,
|
||||||
|
anchors: anchorUsage
|
||||||
|
}),
|
||||||
fallback_type: guardedDecision.fallback_type,
|
fallback_type: guardedDecision.fallback_type,
|
||||||
reply_type: guardedDecision.reply_type,
|
reply_type: guardedDecision.reply_type,
|
||||||
answer_structure_v11: problemCentricStructure,
|
answer_structure_v11: problemCentricStructure,
|
||||||
|
|
@ -2937,11 +3552,12 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||||
...limitationReasonCodes.map((code) => limitationReasonToText(code)),
|
...limitationReasonCodes.map((code) => limitationReasonToText(code)),
|
||||||
...extractLimitations(input.retrievalResults),
|
...extractLimitations(input.retrievalResults),
|
||||||
...input.groundingCheck.reasons,
|
...input.groundingCheck.reasons,
|
||||||
...(settlementGroundingBlocked
|
...(focusDomainGroundingBlocked
|
||||||
|
? ["Целевой механизм активного домена подтвержден частично; часть первичной опоры пришла из смежного контура."]
|
||||||
|
: []),
|
||||||
|
...(anchorUsage.unused.length > 0
|
||||||
? [
|
? [
|
||||||
`Primary settlement evidence is not confirmed; foreign domains dominate: ${
|
`Часть якорей запроса пока не подтверждена в опоре: ${anchorUsage.unused.slice(0, 5).join(", ")}.`
|
||||||
settlementGrounding.foreign_primary_domains.join(", ") || "unknown"
|
|
||||||
}.`
|
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(policySignals.minimum_evidence_failed ? ["Minimum evidence gate failed for current scope."] : []),
|
...(policySignals.minimum_evidence_failed ? ["Minimum evidence gate failed for current scope."] : []),
|
||||||
|
|
@ -2958,15 +3574,24 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||||
...(guardedDecision.mode === "clarification_required" && missingAnchors.account ? ["missing_anchor:account"] : []),
|
...(guardedDecision.mode === "clarification_required" && missingAnchors.account ? ["missing_anchor:account"] : []),
|
||||||
...(guardedDecision.mode === "clarification_required" && missingAnchors.documentOrObject ? ["missing_anchor:document_or_object"] : []),
|
...(guardedDecision.mode === "clarification_required" && missingAnchors.documentOrObject ? ["missing_anchor:document_or_object"] : []),
|
||||||
...(guardedDecision.mode === "clarification_required" && missingAnchors.counterparty ? ["missing_anchor:counterparty"] : []),
|
...(guardedDecision.mode === "clarification_required" && missingAnchors.counterparty ? ["missing_anchor:counterparty"] : []),
|
||||||
...(settlementGroundingBlocked ? ["settlement_primary_evidence_not_confirmed"] : [])
|
...(focusDomainGroundingBlocked ? ["primary_domain_evidence_not_confirmed"] : [])
|
||||||
],
|
],
|
||||||
8
|
8
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const confidenceLimited =
|
||||||
|
guardedDecision.mode !== "focused_grounded" ||
|
||||||
|
limitationReasonCodes.includes("missing_mechanism") ||
|
||||||
|
limitationReasonCodes.includes("heuristic_inference") ||
|
||||||
|
limitationReasonCodes.includes("weak_source_mapping") ||
|
||||||
|
limitationReasonCodes.includes("insufficient_detail") ||
|
||||||
|
aggregateEvidenceConfidence === "low" ||
|
||||||
|
focusDomainGroundingBlocked;
|
||||||
|
|
||||||
const mechanismStatus: AnswerStructureV11["mechanism_block"]["status"] =
|
const mechanismStatus: AnswerStructureV11["mechanism_block"]["status"] =
|
||||||
mechanismNotes.length === 0
|
mechanismNotes.length === 0
|
||||||
? "unresolved"
|
? "unresolved"
|
||||||
: limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
|
: confidenceLimited
|
||||||
? "limited"
|
? "limited"
|
||||||
: "grounded";
|
: "grounded";
|
||||||
|
|
||||||
|
|
@ -2976,7 +3601,8 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||||
direct_answer: buildDirectAnswer({
|
direct_answer: buildDirectAnswer({
|
||||||
mode: guardedDecision.mode,
|
mode: guardedDecision.mode,
|
||||||
retrievalResults: input.retrievalResults,
|
retrievalResults: input.retrievalResults,
|
||||||
policySignals
|
policySignals,
|
||||||
|
focusDomain: focusNarrativeDomain
|
||||||
}),
|
}),
|
||||||
mechanism_block: {
|
mechanism_block: {
|
||||||
status: mechanismStatus,
|
status: mechanismStatus,
|
||||||
|
|
@ -3011,7 +3637,11 @@ function composeAssistantAnswerV11(input: ComposeAnswerInput): ComposeAnswerOutp
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assistant_reply: renderPolicyReply(answerStructure),
|
assistant_reply: renderPolicyReply(answerStructure, {
|
||||||
|
questionType,
|
||||||
|
focusDomain: focusNarrativeDomain,
|
||||||
|
anchors: anchorUsage
|
||||||
|
}),
|
||||||
fallback_type: guardedDecision.fallback_type,
|
fallback_type: guardedDecision.fallback_type,
|
||||||
reply_type: guardedDecision.reply_type,
|
reply_type: guardedDecision.reply_type,
|
||||||
answer_structure_v11: answerStructure,
|
answer_structure_v11: answerStructure,
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,11 @@ const ENTITY_SPECIFIC_MARKERS =
|
||||||
/(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|supplier|buyer|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|invoice|posting|register|guid|id[:=\s])/iu;
|
/(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|supplier|buyer|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|invoice|posting|register|guid|id[:=\s])/iu;
|
||||||
const EXACT_OBJECT_MARKERS =
|
const EXACT_OBJECT_MARKERS =
|
||||||
/(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\s*(?:#|\u2116)|\bref\b|\bid\b|trx-\d+|inv-\d+)/iu;
|
/(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\s*(?:#|\u2116)|\bref\b|\bid\b|trx-\d+|inv-\d+)/iu;
|
||||||
|
const CONTRACT_MARKERS =
|
||||||
|
/(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440(?:\u0430|\u0443|\u043e\u043c|\u0435)?\s*(?:№|#|n)\s*[a-z\u0430-\u044f0-9./_-]+)/iu;
|
||||||
|
const DOCUMENT_NUMBER_MARKERS =
|
||||||
|
/(?:(?:\u0441\u0447(?:\u0435|\u0451)\u0442(?:-\u0444\u0430\u043a\u0442\u0443\u0440(?:\u0430|\u044b))?|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446(?:\u0438\u044f|\u0438\u0438)|\u0430\u043a\u0442)\s*(?:№|#|n)\s*[a-z\u0430-\u044f0-9./_-]+)/iu;
|
||||||
|
const AMOUNT_MARKERS = /\b(?:\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?|\d+[.,]\d{2})\b/u;
|
||||||
|
|
||||||
const ROUTE_MIN_EVIDENCE_GATE: Record<string, RouteAwareEvidenceGate> = {
|
const ROUTE_MIN_EVIDENCE_GATE: Record<string, RouteAwareEvidenceGate> = {
|
||||||
hybrid_store_plus_live: {
|
hybrid_store_plus_live: {
|
||||||
|
|
@ -186,6 +191,9 @@ function detectBroadQuery(fragmentText: string, route: string): BroadQueryAssess
|
||||||
const hasEntityAnchor = ENTITY_SPECIFIC_MARKERS.test(lower);
|
const hasEntityAnchor = ENTITY_SPECIFIC_MARKERS.test(lower);
|
||||||
const hasExactObjectAnchor = EXACT_OBJECT_MARKERS.test(lower);
|
const hasExactObjectAnchor = EXACT_OBJECT_MARKERS.test(lower);
|
||||||
const hasGuidAnchor = extractGuids(lower).length > 0;
|
const hasGuidAnchor = extractGuids(lower).length > 0;
|
||||||
|
const hasContractAnchor = CONTRACT_MARKERS.test(lower);
|
||||||
|
const hasDocumentNumberAnchor = DOCUMENT_NUMBER_MARKERS.test(lower);
|
||||||
|
const hasAmountAnchor = AMOUNT_MARKERS.test(lower);
|
||||||
|
|
||||||
let anchorScore = 0;
|
let anchorScore = 0;
|
||||||
if (hasGuidAnchor) anchorScore += 3;
|
if (hasGuidAnchor) anchorScore += 3;
|
||||||
|
|
@ -193,9 +201,16 @@ function detectBroadQuery(fragmentText: string, route: string): BroadQueryAssess
|
||||||
if (hasPeriodAnchor) anchorScore += 1;
|
if (hasPeriodAnchor) anchorScore += 1;
|
||||||
if (hasEntityAnchor) anchorScore += 1;
|
if (hasEntityAnchor) anchorScore += 1;
|
||||||
if (hasExactObjectAnchor) anchorScore += 1;
|
if (hasExactObjectAnchor) anchorScore += 1;
|
||||||
|
if (hasContractAnchor) anchorScore += 2;
|
||||||
|
if (hasDocumentNumberAnchor) anchorScore += 2;
|
||||||
|
if (hasAmountAnchor) anchorScore += 1;
|
||||||
|
|
||||||
const weakAnchors = anchorScore <= 1;
|
const weakAnchors = anchorScore <= 1;
|
||||||
const strongFocus = hasGuidAnchor || (hasAccountAnchor && hasPeriodAnchor) || anchorScore >= 4;
|
const strongFocus =
|
||||||
|
hasGuidAnchor ||
|
||||||
|
(hasAccountAnchor && hasPeriodAnchor) ||
|
||||||
|
(hasContractAnchor && hasDocumentNumberAnchor) ||
|
||||||
|
anchorScore >= 4;
|
||||||
const routeSensitiveBroad = route === "batch_refresh_then_store" || route === "hybrid_store_plus_live";
|
const routeSensitiveBroad = route === "batch_refresh_then_store" || route === "hybrid_store_plus_live";
|
||||||
|
|
||||||
let broadnessLevel: BroadnessLevel = "low";
|
let broadnessLevel: BroadnessLevel = "low";
|
||||||
|
|
@ -376,9 +391,7 @@ const P0_DOMAIN_CARDS: P0DomainCard[] = [
|
||||||
/\u0441\u0447[её]т.?фактур/i,
|
/\u0441\u0447[её]т.?фактур/i,
|
||||||
/\u043a\u043d\u0438\u0433[аи]\s+\u043f\u043e\u043a\u0443\u043f/i,
|
/\u043a\u043d\u0438\u0433[аи]\s+\u043f\u043e\u043a\u0443\u043f/i,
|
||||||
/\u043a\u043d\u0438\u0433[аи]\s+\u043f\u0440\u043e\u0434\u0430\u0436/i,
|
/\u043a\u043d\u0438\u0433[аи]\s+\u043f\u0440\u043e\u0434\u0430\u0436/i,
|
||||||
/\u0432\u044b\u0447\u0435\u0442/i,
|
/\u0432\u044b\u0447\u0435\u0442/i
|
||||||
/\b19\b/,
|
|
||||||
/\b68\b/
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -394,19 +407,20 @@ const P0_DOMAIN_CARDS: P0DomainCard[] = [
|
||||||
expected_edges: ["document_to_posting", "deferred_expense_to_writeoff", "contract_to_documents"],
|
expected_edges: ["document_to_posting", "deferred_expense_to_writeoff", "contract_to_documents"],
|
||||||
forbidden_cross_domain_leakage: ["vat", "taxes", "bank", "settlements", "suppliers", "customers", "fixed_assets"],
|
forbidden_cross_domain_leakage: ["vat", "taxes", "bank", "settlements", "suppliers", "customers", "fixed_assets"],
|
||||||
symptom_markers: [
|
symptom_markers: [
|
||||||
/\b20\b/,
|
|
||||||
/\b21\b/,
|
|
||||||
/\b23\b/,
|
|
||||||
/\b25\b/,
|
|
||||||
/\b26\b/,
|
|
||||||
/\b28\b/,
|
|
||||||
/\b29\b/,
|
|
||||||
/\b44\b/,
|
|
||||||
/period\s*close/i,
|
/period\s*close/i,
|
||||||
/\u0437\u0430\u043a\u0440\u044b\u0442/i,
|
/month\s*close/i,
|
||||||
|
/close\s+period/i,
|
||||||
|
/закрыт[а-яё]*\s+период/i,
|
||||||
|
/close\s+operation/i,
|
||||||
|
/allocation/i,
|
||||||
|
/закр/i,
|
||||||
|
/перио/i,
|
||||||
|
/\u0437\u0430\u043a\u0440\u044b\u0442(?:\u0438|\u0438\u0435|\u044b|)\s*(?:\u043c\u0435\u0441\u044f\u0446|\u0441\u0447\u0435\u0442)/i,
|
||||||
|
/\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442/i,
|
||||||
/\u0437\u0430\u0442\u0440\u0430\u0442/i,
|
/\u0437\u0430\u0442\u0440\u0430\u0442/i,
|
||||||
/\u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b/i,
|
/\u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b/i,
|
||||||
/\u043e\u0441\u0442\u0430\u0442\u043a/i
|
/\u0440\u0431\u043f/i,
|
||||||
|
/\u0430\u043c\u043e\u0440\u0442\u0438\u0437/i
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
@ -1241,6 +1255,28 @@ function extractAccountScopeFromText(text: string): string[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const closePairPattern = /\b(?:20|21|23|25|26|28|29|44)\s*[-/]\s*(?:20|21|23|25|26|28|29|44)\b/g;
|
||||||
|
let closePairMatch: RegExpExecArray | null = null;
|
||||||
|
while ((closePairMatch = closePairPattern.exec(lower)) !== null) {
|
||||||
|
const pair = closePairMatch[0];
|
||||||
|
const pairAccounts = pair.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
|
||||||
|
for (const account of pairAccounts) {
|
||||||
|
pushAccount(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffixAnchorPattern = /\b(?:51|60|62|68|76|97)(?:\.\d{1,2})?(?:-(?:му|й|го|м|х))?\b/giu;
|
||||||
|
let suffixAnchorMatch: RegExpExecArray | null = null;
|
||||||
|
while ((suffixAnchorMatch = suffixAnchorPattern.exec(lower)) !== null) {
|
||||||
|
const token = suffixAnchorMatch[0];
|
||||||
|
const start = suffixAnchorMatch.index;
|
||||||
|
const end = start + token.length;
|
||||||
|
if (intersectsSpan(start, end, dateSpans)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pushAccount(token);
|
||||||
|
}
|
||||||
|
|
||||||
const explicitPattern = /\b\d{2}(?:\.\d{1,2})?\b/g;
|
const explicitPattern = /\b\d{2}(?:\.\d{1,2})?\b/g;
|
||||||
let explicitMatch: RegExpExecArray | null = null;
|
let explicitMatch: RegExpExecArray | null = null;
|
||||||
const settlementLexicalAnchor = /(оплат|расчет|расч[её]т|аванс|долг|постав|покуп|settlement|payment|supplier|customer)/i.test(
|
const settlementLexicalAnchor = /(оплат|расчет|расч[её]т|аванс|долг|постав|покуп|settlement|payment|supplier|customer)/i.test(
|
||||||
|
|
@ -1405,31 +1441,55 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
|
||||||
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
||||||
pushMany(relationPatterns, ["payment_to_settlement", "statement_to_document", "document_to_posting"]);
|
pushMany(relationPatterns, ["payment_to_settlement", "statement_to_document", "document_to_posting"]);
|
||||||
}
|
}
|
||||||
if (/постав|постав|supplier|vendor|60\b/i.test(lower)) {
|
const hasSettlementAccountScope = accountScope.some((item) => item === "51" || item === "60" || item === "62" || item === "76");
|
||||||
|
const hasVatAccountScope = accountScope.some((item) => item === "19" || item === "68");
|
||||||
|
const hasFixedAssetAccountScope = accountScope.some((item) => item === "01" || item === "02" || item === "08");
|
||||||
|
const hasDeferredExpenseAccountScope = accountScope.some((item) => item === "97");
|
||||||
|
const hasMonthCloseCostsAccountScope = accountScope.some((item) => CLOSE_COST_ACCOUNTS.includes(item));
|
||||||
|
const hasExplicitMonthCloseLexicalMarker =
|
||||||
|
/(?:закрыти[ея]\s+месяц|закрыт[а-яё]*\s+период|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+period|close\s+operation)/i.test(
|
||||||
|
lower
|
||||||
|
) ||
|
||||||
|
(/закр/i.test(lower) && /перио/i.test(lower));
|
||||||
|
|
||||||
|
if (/постав|постав|supplier|vendor/i.test(lower) || hasSettlementAccountScope) {
|
||||||
pushMany(domainScope, ["suppliers", "settlements"]);
|
pushMany(domainScope, ["suppliers", "settlements"]);
|
||||||
pushMany(documentTypes, ["supplier_receipt", "settlement_document"]);
|
pushMany(documentTypes, ["supplier_receipt", "settlement_document"]);
|
||||||
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
||||||
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
|
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
|
||||||
}
|
}
|
||||||
if (/покупат|покупат|customer|buyer|62\b/i.test(lower)) {
|
if (/покупат|покупат|customer|buyer/i.test(lower) || hasSettlementAccountScope) {
|
||||||
pushMany(domainScope, ["customers", "settlements"]);
|
pushMany(domainScope, ["customers", "settlements"]);
|
||||||
pushMany(documentTypes, ["sales_document", "settlement_document"]);
|
pushMany(documentTypes, ["sales_document", "settlement_document"]);
|
||||||
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
|
||||||
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
|
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
|
||||||
}
|
}
|
||||||
if (/РЅРґСЃ|ндс|vat|РєРЅРёРіР° РїРѕРєСѓРїРѕРє|РєРЅРёРіР° продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т.?фактур|19\b|68\b/i.test(lower)) {
|
if (
|
||||||
|
/РЅРґСЃ|ндс|vat|РєРЅРёРіР° РїРѕРєСѓРїРѕРє|РєРЅРёРіР° продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т.?фактур/i.test(
|
||||||
|
lower
|
||||||
|
) ||
|
||||||
|
hasVatAccountScope
|
||||||
|
) {
|
||||||
pushMany(domainScope, ["vat", "taxes"]);
|
pushMany(domainScope, ["vat", "taxes"]);
|
||||||
pushMany(documentTypes, ["invoice", "vat_document"]);
|
pushMany(documentTypes, ["invoice", "vat_document"]);
|
||||||
pushMany(entityTypes, ["document", "tax_entry", "posting"]);
|
pushMany(entityTypes, ["document", "tax_entry", "posting"]);
|
||||||
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
|
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
|
||||||
}
|
}
|
||||||
if (/РѕСЃ|РѕСЃРЅРѕРІРЅ(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз|01\b|02\b|08\b/i.test(lower)) {
|
if (
|
||||||
|
/РѕСЃ|РѕСЃРЅРѕРІРЅ(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(
|
||||||
|
lower
|
||||||
|
) ||
|
||||||
|
hasFixedAssetAccountScope
|
||||||
|
) {
|
||||||
pushMany(domainScope, ["fixed_assets"]);
|
pushMany(domainScope, ["fixed_assets"]);
|
||||||
pushMany(documentTypes, ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"]);
|
pushMany(documentTypes, ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"]);
|
||||||
pushMany(entityTypes, ["fixed_asset", "document", "posting"]);
|
pushMany(entityTypes, ["fixed_asset", "document", "posting"]);
|
||||||
pushMany(relationPatterns, ["asset_card_to_depreciation", "document_to_posting"]);
|
pushMany(relationPatterns, ["asset_card_to_depreciation", "document_to_posting"]);
|
||||||
}
|
}
|
||||||
if (/СЂР±Рї|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff|97\b/i.test(lower)) {
|
if (
|
||||||
|
/СЂР±Рї|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) ||
|
||||||
|
hasDeferredExpenseAccountScope
|
||||||
|
) {
|
||||||
pushMany(domainScope, ["deferred_expense", "period_close"]);
|
pushMany(domainScope, ["deferred_expense", "period_close"]);
|
||||||
pushMany(documentTypes, ["deferred_expense_document", "period_close_document"]);
|
pushMany(documentTypes, ["deferred_expense_document", "period_close_document"]);
|
||||||
pushMany(entityTypes, ["document", "posting"]);
|
pushMany(entityTypes, ["document", "posting"]);
|
||||||
|
|
@ -1452,7 +1512,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
|
||||||
pushMany(anomalyPatterns, ["repeated_anomaly"]);
|
pushMany(anomalyPatterns, ["repeated_anomaly"]);
|
||||||
pushMany(rankingBasis, ["repeatability"]);
|
pushMany(rankingBasis, ["repeatability"]);
|
||||||
}
|
}
|
||||||
if (/закрыт|закрытие|период|закрыт|закрытие|период|month close|period close|closure/i.test(lower)) {
|
if (hasExplicitMonthCloseLexicalMarker || hasMonthCloseCostsAccountScope || hasDeferredExpenseAccountScope) {
|
||||||
pushMany(domainScope, ["period_close"]);
|
pushMany(domainScope, ["period_close"]);
|
||||||
pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]);
|
pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]);
|
||||||
pushMany(documentTypes, ["period_close_document"]);
|
pushMany(documentTypes, ["period_close_document"]);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import * as assistantDataLayer_1 from "./assistantDataLayer";
|
||||||
import * as assistantSessionLogger_1 from "./assistantSessionLogger";
|
import * as assistantSessionLogger_1 from "./assistantSessionLogger";
|
||||||
import * as investigationState_1 from "./investigationState";
|
import * as investigationState_1 from "./investigationState";
|
||||||
import * as retrievalResultNormalizer_1 from "./retrievalResultNormalizer";
|
import * as retrievalResultNormalizer_1 from "./retrievalResultNormalizer";
|
||||||
|
import * as questionTypeResolver_1 from "./questionTypeResolver";
|
||||||
|
import * as companyAnchorResolver_1 from "./companyAnchorResolver";
|
||||||
function retrievalSummaryForRoute(route) {
|
function retrievalSummaryForRoute(route) {
|
||||||
if (route === "store_canonical")
|
if (route === "store_canonical")
|
||||||
return "Canonical accounting data path selected.";
|
return "Canonical accounting data path selected.";
|
||||||
|
|
@ -832,6 +834,26 @@ function extractFollowupAccountAnchorsLoose(text) {
|
||||||
}
|
}
|
||||||
return Array.from(new Set(anchors));
|
return Array.from(new Set(anchors));
|
||||||
}
|
}
|
||||||
|
function inferP0DomainFromMessage(text) {
|
||||||
|
const lower = String(text ?? "").toLowerCase();
|
||||||
|
const accountTokens = extractAccountTokens(lower);
|
||||||
|
const hasVatAccount = accountTokens.some((token) => /^(?:19|68)(?:\.|$)/.test(token));
|
||||||
|
const hasSettlementAccount = accountTokens.some((token) => /^(?:51|60|62|76)(?:\.|$)/.test(token));
|
||||||
|
const hasMonthCloseAccount = accountTokens.some((token) => /^(?:97|2\d|3\d|4[0-4])(?:\.|$)/.test(token));
|
||||||
|
const vatLexical = /(?:ндс|vat|счет[\s-]?фактур|сч[её]т[\s-]?фактур|книг[аи]\s+(?:покуп|продаж)|налогов)/i.test(lower);
|
||||||
|
const settlementLexical = /(?:долг|аванс|зач[её]т|взаимозач|расч[её]т|оплат|платеж|платёж|постав|покупател)/i.test(lower);
|
||||||
|
const monthCloseLexical = /(?:закрыти[ея]\s+месяц|закрытие счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых результат)/i.test(lower);
|
||||||
|
if (hasVatAccount || vatLexical) {
|
||||||
|
return "vat_document_register_book";
|
||||||
|
}
|
||||||
|
if (monthCloseLexical || hasMonthCloseAccount) {
|
||||||
|
return "month_close_costs_20_44";
|
||||||
|
}
|
||||||
|
if (hasSettlementAccount || settlementLexical) {
|
||||||
|
return "settlements_60_62";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function hasStrongFollowupAnchors(userMessage, state) {
|
function hasStrongFollowupAnchors(userMessage, state) {
|
||||||
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
|
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
|
||||||
if (explicitPeriod && state.focus.period && explicitPeriod !== state.focus.period) {
|
if (explicitPeriod && state.focus.period && explicitPeriod !== state.focus.period) {
|
||||||
|
|
@ -840,6 +862,14 @@ function hasStrongFollowupAnchors(userMessage, state) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const inferredDomain = inferP0DomainFromMessage(userMessage);
|
||||||
|
const activeDomain = compactWhitespace(state.followup_context?.active_domain ?? state.focus.domain ?? "");
|
||||||
|
if (inferredDomain && activeDomain && inferredDomain !== activeDomain) {
|
||||||
|
const domainLooksLikeFollowupRefinement = hasFollowupMarker(userMessage) && hasReferentialPointer(userMessage);
|
||||||
|
if (!domainLooksLikeFollowupRefinement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
const explicitAccounts = extractAccountTokens(userMessage);
|
const explicitAccounts = extractAccountTokens(userMessage);
|
||||||
const followupAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
|
const followupAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
|
||||||
if (followupAccounts.length > 0) {
|
if (followupAccounts.length > 0) {
|
||||||
|
|
@ -1155,6 +1185,8 @@ export class AssistantService {
|
||||||
const focusDomainHint = followupBinding.usage?.applied
|
const focusDomainHint = followupBinding.usage?.applied
|
||||||
? session.investigation_state?.followup_context?.active_domain ?? session.investigation_state?.focus.domain ?? null
|
? session.investigation_state?.followup_context?.active_domain ?? session.investigation_state?.focus.domain ?? null
|
||||||
: null;
|
: null;
|
||||||
|
const questionTypeClass = (0, questionTypeResolver_1.resolveQuestionType)(userMessage);
|
||||||
|
const companyAnchors = (0, companyAnchorResolver_1.resolveCompanyAnchors)(userMessage);
|
||||||
const composition = (0, answerComposer_1.composeAssistantAnswer)({
|
const composition = (0, answerComposer_1.composeAssistantAnswer)({
|
||||||
userMessage,
|
userMessage,
|
||||||
routeSummary: normalized.route_hint_summary,
|
routeSummary: normalized.route_hint_summary,
|
||||||
|
|
@ -1163,6 +1195,8 @@ export class AssistantService {
|
||||||
coverageReport: coverageEvaluation.coverage,
|
coverageReport: coverageEvaluation.coverage,
|
||||||
groundingCheck,
|
groundingCheck,
|
||||||
focusDomainHint,
|
focusDomainHint,
|
||||||
|
questionTypeHint: questionTypeClass,
|
||||||
|
companyAnchors,
|
||||||
enableAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11,
|
enableAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11,
|
||||||
enableProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1,
|
enableProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1,
|
||||||
enableLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1
|
enableLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1
|
||||||
|
|
@ -1213,6 +1247,8 @@ export class AssistantService {
|
||||||
retrieval_results: retrievalResults,
|
retrieval_results: retrievalResults,
|
||||||
answer_grounding_check: groundingCheck,
|
answer_grounding_check: groundingCheck,
|
||||||
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
||||||
|
question_type_class: questionTypeClass,
|
||||||
|
company_anchors: companyAnchors,
|
||||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||||
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
||||||
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
||||||
|
|
@ -1276,6 +1312,8 @@ export class AssistantService {
|
||||||
route_subject_match: groundingCheck.route_subject_match,
|
route_subject_match: groundingCheck.route_subject_match,
|
||||||
clarification_target: coverageEvaluation.coverage.clarification_needed_for,
|
clarification_target: coverageEvaluation.coverage.clarification_needed_for,
|
||||||
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
|
||||||
|
question_type_class: questionTypeClass,
|
||||||
|
company_anchors: companyAnchors,
|
||||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||||
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
problem_centric_answer_applied: composition.problem_centric_answer_applied ?? false,
|
||||||
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
problem_units_used_count: composition.problem_units_used_count ?? 0,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
export interface CompanyAnchorSet {
|
||||||
|
contract_numbers: string[];
|
||||||
|
document_numbers: string[];
|
||||||
|
dates: string[];
|
||||||
|
amounts: string[];
|
||||||
|
accounts: string[];
|
||||||
|
periods: string[];
|
||||||
|
document_types: string[];
|
||||||
|
all: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONTRACT_PATTERN =
|
||||||
|
/(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440(?:\u0430|\u0443|ом|е)?\s*(?:№|#|n)?\s*([a-zа-я0-9./_-]+))/giu;
|
||||||
|
const DOCUMENT_NUMBER_PATTERN =
|
||||||
|
/(?:(?:\u0441\u0447(?:\u0435|\u0451)\u0442(?:-\u0444\u0430\u043a\u0442\u0443\u0440(?:а|ы))?|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446(?:ия|ии)|\u0430\u043a\u0442)\s*(?:№|#|n)\s*([a-zа-я0-9./_-]+))/giu;
|
||||||
|
const DATE_PATTERN =
|
||||||
|
/\b(?:\d{1,2}[./]\d{1,2}[./]\d{2,4}|\d{1,2}\s+(?:\u044f\u043d\u0432\u0430\u0440\u044f|\u0444\u0435\u0432\u0440\u0430\u043b\u044f|\u043c\u0430\u0440\u0442\u0430|\u0430\u043f\u0440\u0435\u043b\u044f|\u043c\u0430\u044f|\u0438\u044e\u043d\u044f|\u0438\u044e\u043b\u044f|\u0430\u0432\u0433\u0443\u0441\u0442\u0430|\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f|\u043e\u043a\u0442\u044f\u0431\u0440\u044f|\u043d\u043e\u044f\u0431\u0440\u044f|\u0434\u0435\u043a\u0430\u0431\u0440\u044f))\b/giu;
|
||||||
|
const AMOUNT_PATTERN =
|
||||||
|
/\b(?:\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?|\d+[.,]\d{2})\b/gu;
|
||||||
|
const CONTEXTUAL_ACCOUNT_PATTERN =
|
||||||
|
/(?:\b(?:\u0441\u0447(?:\u0435|\u0451)\u0442(?:а|у|ом|ов)?|account|schet)\b\s*(?:№|#|:)?\s*)(\d{2}(?:\.\d{2})?)/giu;
|
||||||
|
const ACCOUNT_PAIR_PATTERN = /\b(\d{2}\.\d{2})\s*\/\s*(\d{2}\.\d{2})\b/gu;
|
||||||
|
const PERIOD_PATTERN =
|
||||||
|
/\b(?:20\d{2}(?:[-./](?:0?[1-9]|1[0-2]))?|(?:\u0438\u044e\u043b\u044c|\u0438\u044e\u043d\u044c|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c|\u043e\u043a\u0442\u044f\u0431\u0440\u044c|\u043d\u043e\u044f\u0431\u0440\u044c|\u0434\u0435\u043a\u0430\u0431\u0440\u044c|\u044f\u043d\u0432\u0430\u0440\u044c|\u0444\u0435\u0432\u0440\u0430\u043b\u044c|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b\u044c|\u043c\u0430\u0439)\s+20\d{2})\b/giu;
|
||||||
|
|
||||||
|
const DOCUMENT_TYPE_PATTERNS: Array<{ name: string; pattern: RegExp }> = [
|
||||||
|
{ name: "invoice", pattern: /\b(?:\u0441\u0447(?:\u0435|\u0451)\u0442-\u0444\u0430\u043a\u0442\u0443\u0440|invoice)\b/iu },
|
||||||
|
{ name: "realization", pattern: /\b(?:\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446|realization)\b/iu },
|
||||||
|
{ name: "payment", pattern: /\b(?:\u043e\u043f\u043b\u0430\u0442|payment|\u043f\u043b\u0430\u0442\u0435\u0436)\b/iu },
|
||||||
|
{ name: "receipt", pattern: /\b(?:\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|receipt)\b/iu },
|
||||||
|
{ name: "close", pattern: /\b(?:\u0437\u0430\u043a\u0440\u044b\u0442\u0438|\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442)\b/iu },
|
||||||
|
{ name: "rbp_writeoff", pattern: /\b(?:\u0440\u0431\u043f|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435)\b/iu },
|
||||||
|
{ name: "amortization", pattern: /\b(?:\u0430\u043c\u043e\u0440\u0442\u0438\u0437|amortization)\b/iu }
|
||||||
|
];
|
||||||
|
|
||||||
|
const KNOWN_ACCOUNT_PREFIXES = new Set<string>([
|
||||||
|
"01",
|
||||||
|
"02",
|
||||||
|
"07",
|
||||||
|
"08",
|
||||||
|
"10",
|
||||||
|
"13",
|
||||||
|
"19",
|
||||||
|
"20",
|
||||||
|
"21",
|
||||||
|
"23",
|
||||||
|
"25",
|
||||||
|
"26",
|
||||||
|
"41",
|
||||||
|
"43",
|
||||||
|
"44",
|
||||||
|
"45",
|
||||||
|
"50",
|
||||||
|
"51",
|
||||||
|
"52",
|
||||||
|
"55",
|
||||||
|
"57",
|
||||||
|
"58",
|
||||||
|
"60",
|
||||||
|
"62",
|
||||||
|
"66",
|
||||||
|
"67",
|
||||||
|
"68",
|
||||||
|
"69",
|
||||||
|
"70",
|
||||||
|
"71",
|
||||||
|
"73",
|
||||||
|
"76",
|
||||||
|
"90",
|
||||||
|
"91",
|
||||||
|
"94",
|
||||||
|
"96",
|
||||||
|
"97"
|
||||||
|
]);
|
||||||
|
|
||||||
|
function uniqueStrings(values: string[], limit = 48): string[] {
|
||||||
|
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAnchorToken(value: string): string {
|
||||||
|
return String(value ?? "")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectMatches(text: string, pattern: RegExp, useCaptures = true): string[] {
|
||||||
|
const values: string[] = [];
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
for (const match of text.matchAll(pattern)) {
|
||||||
|
if (!match) continue;
|
||||||
|
if (useCaptures && match.length > 1) {
|
||||||
|
for (let i = 1; i < match.length; i += 1) {
|
||||||
|
const token = normalizeAnchorToken(match[i] ?? "");
|
||||||
|
if (token) values.push(token);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const token = normalizeAnchorToken(match[0] ?? "");
|
||||||
|
if (token) values.push(token);
|
||||||
|
}
|
||||||
|
return uniqueStrings(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isKnownAccount(value: string): boolean {
|
||||||
|
const token = String(value ?? "").trim();
|
||||||
|
const match = token.match(/^(\d{2})/);
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return KNOWN_ACCOUNT_PREFIXES.has(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAccountAnchors(text: string): string[] {
|
||||||
|
const tokens = new Set<string>();
|
||||||
|
for (const token of collectMatches(text, CONTEXTUAL_ACCOUNT_PATTERN, true)) {
|
||||||
|
if (isKnownAccount(token)) {
|
||||||
|
tokens.add(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ACCOUNT_PAIR_PATTERN.lastIndex = 0;
|
||||||
|
for (const match of text.matchAll(ACCOUNT_PAIR_PATTERN)) {
|
||||||
|
const left = normalizeAnchorToken(match[1] ?? "");
|
||||||
|
const right = normalizeAnchorToken(match[2] ?? "");
|
||||||
|
if (left && isKnownAccount(left)) {
|
||||||
|
tokens.add(left);
|
||||||
|
}
|
||||||
|
if (right && isKnownAccount(right)) {
|
||||||
|
tokens.add(right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(tokens).slice(0, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectDocumentTypeAnchors(text: string): string[] {
|
||||||
|
return uniqueStrings(
|
||||||
|
DOCUMENT_TYPE_PATTERNS.filter((entry) => entry.pattern.test(text)).map((entry) => entry.name),
|
||||||
|
12
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenAnchors(input: Omit<CompanyAnchorSet, "all">): string[] {
|
||||||
|
return uniqueStrings(
|
||||||
|
[
|
||||||
|
...input.contract_numbers,
|
||||||
|
...input.document_numbers,
|
||||||
|
...input.dates,
|
||||||
|
...input.amounts,
|
||||||
|
...input.accounts.map((item) => `account:${item}`),
|
||||||
|
...input.periods.map((item) => `period:${item}`),
|
||||||
|
...input.document_types.map((item) => `doc_type:${item}`)
|
||||||
|
],
|
||||||
|
64
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveCompanyAnchors(input: string): CompanyAnchorSet {
|
||||||
|
const text = String(input ?? "");
|
||||||
|
|
||||||
|
const contractNumbers = collectMatches(text, CONTRACT_PATTERN, true).map((item) => `\u0434\u043e\u0433\u043e\u0432\u043e\u0440 № ${item}`);
|
||||||
|
const documentNumbers = collectMatches(text, DOCUMENT_NUMBER_PATTERN, true).map((item) => `\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 № ${item}`);
|
||||||
|
const dates = collectMatches(text, DATE_PATTERN, false);
|
||||||
|
const amounts = collectMatches(text, AMOUNT_PATTERN, false);
|
||||||
|
const accounts = collectAccountAnchors(text);
|
||||||
|
const periods = collectMatches(text, PERIOD_PATTERN, false);
|
||||||
|
const documentTypes = collectDocumentTypeAnchors(text);
|
||||||
|
|
||||||
|
const resultBase: Omit<CompanyAnchorSet, "all"> = {
|
||||||
|
contract_numbers: uniqueStrings(contractNumbers, 12),
|
||||||
|
document_numbers: uniqueStrings(documentNumbers, 16),
|
||||||
|
dates: uniqueStrings(dates, 16),
|
||||||
|
amounts: uniqueStrings(amounts, 16),
|
||||||
|
accounts: uniqueStrings(accounts, 24),
|
||||||
|
periods: uniqueStrings(periods, 12),
|
||||||
|
document_types: documentTypes
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...resultBase,
|
||||||
|
all: flattenAnchors(resultBase)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -627,8 +627,14 @@ function inferLifecycleDomain(input: LifecycleResolverInput): LifecycleDomain {
|
||||||
.join(" ")
|
.join(" ")
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
|
||||||
|
const hasExplicitVatHint = includesAny(unitTokens, [/domain_hint:vat_flow/]);
|
||||||
|
const hasExplicitDeferredHint = includesAny(unitTokens, [/domain_hint:deferred_expense/]);
|
||||||
|
const hasExplicitFixedAssetHint = includesAny(unitTokens, [/domain_hint:fixed_asset/]);
|
||||||
|
const hasExplicitPeriodCloseHint = includesAny(unitTokens, [/domain_hint:period_close/]);
|
||||||
|
const hasCustomerSettlementHint = includesAny(unitTokens, [/domain_hint:customer_settlement/]);
|
||||||
|
const hasBankSettlementHint = includesAny(unitTokens, [/domain_hint:bank_settlement/]);
|
||||||
|
|
||||||
const hasVatMarkers = includesAny(unitTokens, [
|
const hasVatMarkers = includesAny(unitTokens, [
|
||||||
/domain_hint:vat_flow/,
|
|
||||||
/\binvoice_to_vat\b/,
|
/\binvoice_to_vat\b/,
|
||||||
/\bvat_chain_conflict\b/,
|
/\bvat_chain_conflict\b/,
|
||||||
/(^|[^a-z0-9])nds([^a-z0-9]|$)/,
|
/(^|[^a-z0-9])nds([^a-z0-9]|$)/,
|
||||||
|
|
@ -637,7 +643,6 @@ function inferLifecycleDomain(input: LifecycleResolverInput): LifecycleDomain {
|
||||||
/\baccount[_:\s-]?(19|68)\b/
|
/\baccount[_:\s-]?(19|68)\b/
|
||||||
]);
|
]);
|
||||||
const hasDeferredMarkers = includesAny(unitTokens, [
|
const hasDeferredMarkers = includesAny(unitTokens, [
|
||||||
/domain_hint:deferred_expense/,
|
|
||||||
/\bdeferred(?:_expense)?\b/,
|
/\bdeferred(?:_expense)?\b/,
|
||||||
/\bdeferred_expense_to_writeoff\b/,
|
/\bdeferred_expense_to_writeoff\b/,
|
||||||
/\bwriteoff\b/,
|
/\bwriteoff\b/,
|
||||||
|
|
@ -646,7 +651,6 @@ function inferLifecycleDomain(input: LifecycleResolverInput): LifecycleDomain {
|
||||||
/\baccount[_:\s-]?97\b/
|
/\baccount[_:\s-]?97\b/
|
||||||
]);
|
]);
|
||||||
const hasFixedAssetMarkers = includesAny(unitTokens, [
|
const hasFixedAssetMarkers = includesAny(unitTokens, [
|
||||||
/domain_hint:fixed_asset/,
|
|
||||||
/\bfixed[_\s-]?asset(?:s)?\b/,
|
/\bfixed[_\s-]?asset(?:s)?\b/,
|
||||||
/\basset_card_to_depreciation\b/,
|
/\basset_card_to_depreciation\b/,
|
||||||
/\bdepreciation(?:_active)?\b/,
|
/\bdepreciation(?:_active)?\b/,
|
||||||
|
|
@ -655,7 +659,6 @@ function inferLifecycleDomain(input: LifecycleResolverInput): LifecycleDomain {
|
||||||
/\baccount[_:\s-]?(01|02|08)\b/
|
/\baccount[_:\s-]?(01|02|08)\b/
|
||||||
]);
|
]);
|
||||||
const hasPeriodCloseMarkers = includesAny(unitTokens, [
|
const hasPeriodCloseMarkers = includesAny(unitTokens, [
|
||||||
/domain_hint:period_close/,
|
|
||||||
/\bperiod[_\s-]?close\b/,
|
/\bperiod[_\s-]?close\b/,
|
||||||
/\bperiod_close_risk\b/,
|
/\bperiod_close_risk\b/,
|
||||||
/\bclose[_\s-]?risk\b/,
|
/\bclose[_\s-]?risk\b/,
|
||||||
|
|
@ -665,6 +668,25 @@ function inferLifecycleDomain(input: LifecycleResolverInput): LifecycleDomain {
|
||||||
/\bperiod_risk\b/
|
/\bperiod_risk\b/
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (hasExplicitDeferredHint) {
|
||||||
|
return "deferred_expense";
|
||||||
|
}
|
||||||
|
if (hasExplicitFixedAssetHint) {
|
||||||
|
return "fixed_asset";
|
||||||
|
}
|
||||||
|
if (hasExplicitVatHint) {
|
||||||
|
return "vat_flow";
|
||||||
|
}
|
||||||
|
if (hasExplicitPeriodCloseHint) {
|
||||||
|
return "period_close";
|
||||||
|
}
|
||||||
|
if (hasCustomerSettlementHint) {
|
||||||
|
return "customer_settlement";
|
||||||
|
}
|
||||||
|
if (hasBankSettlementHint) {
|
||||||
|
return "bank_settlement";
|
||||||
|
}
|
||||||
|
|
||||||
if (hasDeferredMarkers) {
|
if (hasDeferredMarkers) {
|
||||||
return "deferred_expense";
|
return "deferred_expense";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,14 +106,67 @@ function stringArrayFromPayload(item: EvidenceItem, key: string): string[] {
|
||||||
return stringArrayFromUnknown(item.payload[key]);
|
return stringArrayFromUnknown(item.payload[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function domainHintsFromSummary(summary: Record<string, unknown>): string[] {
|
||||||
|
const hints: string[] = [];
|
||||||
|
const purityGuard = toObject(summary.domain_purity_guard);
|
||||||
|
const domainCardId = String(purityGuard?.domain_card_id ?? "").trim();
|
||||||
|
if (domainCardId === "settlements_60_62") {
|
||||||
|
return ["bank_settlement", "customer_settlement"];
|
||||||
|
}
|
||||||
|
if (domainCardId === "vat_document_register_book") {
|
||||||
|
return ["vat_flow"];
|
||||||
|
}
|
||||||
|
if (domainCardId === "month_close_costs_20_44") {
|
||||||
|
return ["period_close"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const semanticProfile = toObject(summary.semantic_profile);
|
||||||
|
const domainScope = stringArrayFromUnknown(semanticProfile?.domain_scope);
|
||||||
|
for (const domain of domainScope) {
|
||||||
|
const normalized = domain.toLowerCase();
|
||||||
|
if (
|
||||||
|
normalized === "bank" ||
|
||||||
|
normalized === "settlements" ||
|
||||||
|
normalized === "suppliers" ||
|
||||||
|
normalized === "supplier_payments" ||
|
||||||
|
normalized === "other_settlements"
|
||||||
|
) {
|
||||||
|
hints.push("bank_settlement");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (normalized === "customers") {
|
||||||
|
hints.push("customer_settlement");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (normalized === "vat" || normalized === "taxes") {
|
||||||
|
hints.push("vat_flow");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (normalized === "period_close") {
|
||||||
|
hints.push("period_close");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (normalized === "deferred_expense") {
|
||||||
|
hints.push("deferred_expense");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (normalized === "fixed_assets") {
|
||||||
|
hints.push("fixed_asset");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueStrings(hints);
|
||||||
|
}
|
||||||
|
|
||||||
function extractSemanticProfile(summary: Record<string, unknown>): {
|
function extractSemanticProfile(summary: Record<string, unknown>): {
|
||||||
relation_patterns: string[];
|
relation_patterns: string[];
|
||||||
anomaly_patterns: string[];
|
anomaly_patterns: string[];
|
||||||
} {
|
} {
|
||||||
const semanticProfile = toObject(summary.semantic_profile);
|
const semanticProfile = toObject(summary.semantic_profile);
|
||||||
|
const domainHints = domainHintsFromSummary(summary).map((item) => `domain_hint:${item}`);
|
||||||
return {
|
return {
|
||||||
relation_patterns: stringArrayFromUnknown(semanticProfile?.relation_patterns),
|
relation_patterns: uniqueStrings([...stringArrayFromUnknown(semanticProfile?.relation_patterns), ...domainHints]),
|
||||||
anomaly_patterns: stringArrayFromUnknown(semanticProfile?.anomaly_patterns)
|
anomaly_patterns: uniqueStrings([...stringArrayFromUnknown(semanticProfile?.anomaly_patterns), ...domainHints])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
export type QuestionTypeClass =
|
||||||
|
| "why_breaks"
|
||||||
|
| "where_break_is"
|
||||||
|
| "prove_or_guess"
|
||||||
|
| "what_is_it_grounded_on"
|
||||||
|
| "which_chains_are_complete_vs_incomplete"
|
||||||
|
| "what_to_check_first"
|
||||||
|
| "unknown";
|
||||||
|
|
||||||
|
const QUESTION_TYPE_RULES: Array<{ type: QuestionTypeClass; pattern: RegExp }> = [
|
||||||
|
{
|
||||||
|
type: "what_to_check_first",
|
||||||
|
pattern:
|
||||||
|
/(?:\bwhat\s+to\s+check\s+first\b|\bfirst\s+check\b|\bcheck\s+first\b|\u0441\s+\u0447\u0435\u0433\u043e\s+\u043d\u0430\u0447\u0430\u0442\u044c\s+\u043f\u0440\u043e\u0432\u0435\u0440\u043a|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c\s+\u043f\u0435\u0440\u0432)/iu
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "what_is_it_grounded_on",
|
||||||
|
pattern:
|
||||||
|
/(?:\bwhat\s+is\s+it\s+grounded\s+on\b|\bgrounded\s+on\b|\bbased\s+on\b|\bwhat\s+evidence\b|\u043d\u0430\s+\u0447(?:\u0435|\u0451)\u043c\s+\u044d\u0442\u043e\s+\u043e\u0441\u043d\u043e\u0432\u0430\u043d|\u0447\u0435\u043c\s+\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434)/iu
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "prove_or_guess",
|
||||||
|
pattern:
|
||||||
|
/(?:\bprove\b|\bguess\b|\bprove\s+or\s+guess\b|\bis\s+it\s+proven\b|\u044d\u0442\u043e\s+\u0434\u043e\u043a\u0430\u0437\u0430\u043d|\u0438\u043b\u0438\s+\u0442\u043e\u043b\u044c\u043a\u043e\s+\u0433\u0438\u043f\u043e\u0442\u0435\u0437|\u0434\u043e\u043a\u0430\u0437\u0430\u043d|\u0434\u043e\u0433\u0430\u0434|\u0435\u0441\u0442\u044c\s+\u043b\u0438|\u043c\u043e\u0436\u0435\u0442\s+\u043b\u0438|\u044d\u0442\u043e\s+\u0443\u0436\u0435.*\u0438\u043b\u0438)/iu
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "which_chains_are_complete_vs_incomplete",
|
||||||
|
pattern:
|
||||||
|
/(?:\bcomplete(?:d)?\b.*\bincomplete\b|\bwhich\s+chains?\b|\bcomplete\s+vs\s+incomplete\b|\u043a\u0430\u043a\u0438\u0435\s+\u0446\u0435\u043f\u043e\u0447\u043a[аи]\s+.*\u0437\u0430\u0432\u0435\u0440\u0448|\u0447\u0442\u043e\s+\u0437\u0430\u043a\u0440\u044b\u0442\u043e.*\u0447\u0442\u043e\s+\u043d\u0435\u0442)/iu
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "where_break_is",
|
||||||
|
pattern:
|
||||||
|
/(?:\bwhere\s+is\s+the\s+break\b|\bwhere\s+exactly\b|\blocate\b|\u0433\u0434\u0435\s+\u0438\u043c\u0435\u043d\u043d\u043e|\u0433\u0434\u0435\s+\u0440\u0430\u0437\u0440\u044b\u0432|\u0432\s+\u043a\u0430\u043a\u043e\u043c\s+\u043c\u0435\u0441\u0442\u0435)/iu
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "why_breaks",
|
||||||
|
pattern:
|
||||||
|
/(?:\bwhy\b|\bwhy\s+does\s+it\s+break\b|\u043f\u043e\u0447\u0435\u043c\u0443|\u0432\s+\u0447(?:\u0435|\u0451)\u043c\s+\u043f\u0440\u0438\u0447\u0438\u043d\u0430|\u0438\u0437-\u0437\u0430\s+\u0447\u0435\u0433\u043e)/iu
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function resolveQuestionType(input: string): QuestionTypeClass {
|
||||||
|
const text = String(input ?? "").trim();
|
||||||
|
if (!text) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rule of QUESTION_TYPE_RULES) {
|
||||||
|
if (rule.pattern.test(text)) {
|
||||||
|
return rule.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[??]/u.test(text)) {
|
||||||
|
return "why_breaks";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,397 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { composeAssistantAnswer } from "../src/services/answerComposer";
|
||||||
|
import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../src/types/assistant";
|
||||||
|
import type { ProblemUnit } from "../src/types/stage2ProblemUnits";
|
||||||
|
|
||||||
|
function buildRouteSummary() {
|
||||||
|
return {
|
||||||
|
mode: "deterministic_v2" as const,
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high" as const,
|
||||||
|
planner: {
|
||||||
|
total_fragments: 1,
|
||||||
|
in_scope_fragments: 1,
|
||||||
|
out_of_scope_fragments: 0,
|
||||||
|
discarded_fragments: 0,
|
||||||
|
contains_multiple_tasks: false
|
||||||
|
},
|
||||||
|
decisions: [],
|
||||||
|
fallback: {
|
||||||
|
type: "none" as const,
|
||||||
|
message: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCoverage(input?: Partial<RequirementCoverageReport>): RequirementCoverageReport {
|
||||||
|
return {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: [],
|
||||||
|
...input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGrounding(input?: Partial<AnswerGroundingCheck>): AnswerGroundingCheck {
|
||||||
|
return {
|
||||||
|
status: "grounded",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: ["wave12-test"],
|
||||||
|
selection_reason_summary: ["wave12-test"],
|
||||||
|
...input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProblemUnit(input: {
|
||||||
|
id: string;
|
||||||
|
type: ProblemUnit["problem_unit_type"];
|
||||||
|
account: string;
|
||||||
|
defect: string;
|
||||||
|
lifecycleDomain?: ProblemUnit["lifecycle_domain"];
|
||||||
|
}): ProblemUnit {
|
||||||
|
return {
|
||||||
|
schema_version: "problem_unit_v0_1",
|
||||||
|
problem_unit_id: input.id,
|
||||||
|
problem_unit_type: input.type,
|
||||||
|
title: "Wave12 problem unit",
|
||||||
|
mechanism_summary: `Mechanism candidate: ${input.defect}.`,
|
||||||
|
business_defect_class: input.defect,
|
||||||
|
severity: {
|
||||||
|
score: 0.72,
|
||||||
|
grade: "high"
|
||||||
|
},
|
||||||
|
confidence: {
|
||||||
|
score: 0.58,
|
||||||
|
grade: "medium"
|
||||||
|
},
|
||||||
|
affected_entities: ["Document:DOC-1"],
|
||||||
|
affected_documents: ["Document:DOC-1"],
|
||||||
|
affected_postings: ["Posting:POST-1"],
|
||||||
|
affected_accounts: [input.account],
|
||||||
|
affected_counterparties: ["Counterparty:CP-1"],
|
||||||
|
affected_contracts: ["Contract:CTR-1"],
|
||||||
|
failed_expected_edge: input.defect,
|
||||||
|
period_impact: {
|
||||||
|
is_period_sensitive: true,
|
||||||
|
impact_class: "close_risk"
|
||||||
|
},
|
||||||
|
evidence_pack: ["cand-1"],
|
||||||
|
entity_backlinks: [{ entity: "Document", id: "DOC-1" }],
|
||||||
|
snapshot_limitations: [],
|
||||||
|
...(input.lifecycleDomain
|
||||||
|
? {
|
||||||
|
lifecycle_domain: input.lifecycleDomain
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRetrieval(input: {
|
||||||
|
requirementId: string;
|
||||||
|
status: UnifiedRetrievalResult["status"];
|
||||||
|
units?: ProblemUnit[];
|
||||||
|
accountScope?: string[];
|
||||||
|
domainScope?: string[];
|
||||||
|
relationPatterns?: string[];
|
||||||
|
limitations?: string[];
|
||||||
|
confidence?: UnifiedRetrievalResult["confidence"];
|
||||||
|
withEvidence?: boolean;
|
||||||
|
}): UnifiedRetrievalResult {
|
||||||
|
const units = input.units ?? [];
|
||||||
|
const withEvidence = input.withEvidence ?? input.status !== "empty";
|
||||||
|
return {
|
||||||
|
fragment_id: `F-${input.requirementId}`,
|
||||||
|
requirement_ids: [input.requirementId],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
status: input.status,
|
||||||
|
result_type: "chain",
|
||||||
|
items:
|
||||||
|
input.status === "empty"
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
source_entity: "Document",
|
||||||
|
source_id: "DOC-1",
|
||||||
|
account_context: input.accountScope ?? ["60"],
|
||||||
|
graph_domain_scope: input.domainScope ?? ["bank_settlement"],
|
||||||
|
relation_pattern_hits: input.relationPatterns ?? ["payment_to_settlement"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
summary: {
|
||||||
|
broad_query_detected: false,
|
||||||
|
broad_result_flag: false,
|
||||||
|
minimum_evidence_failed: false,
|
||||||
|
degraded_to: null,
|
||||||
|
narrowing_strength: "strong",
|
||||||
|
semantic_profile: {
|
||||||
|
account_scope: input.accountScope ?? ["60", "62"],
|
||||||
|
domain_scope: input.domainScope ?? ["bank_settlement", "customer_settlement"],
|
||||||
|
relation_patterns: input.relationPatterns ?? ["payment_to_settlement"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
evidence:
|
||||||
|
input.status === "empty" || !withEvidence
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
evidence_id: `ev-${input.requirementId}`,
|
||||||
|
claim_ref: `requirement:${input.requirementId}`,
|
||||||
|
source_type: "retrieval_item",
|
||||||
|
source_ref: {
|
||||||
|
schema_version: "evidence_source_ref_v1",
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "DOC-1",
|
||||||
|
period: "2020-07",
|
||||||
|
canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-07"
|
||||||
|
},
|
||||||
|
pointer: {
|
||||||
|
fragment_id: `F-${input.requirementId}`,
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
source: {
|
||||||
|
namespace: "snapshot_2020",
|
||||||
|
entity: "document",
|
||||||
|
id: "DOC-1",
|
||||||
|
period: "2020-07"
|
||||||
|
},
|
||||||
|
locator: {
|
||||||
|
field_path: "risk_score",
|
||||||
|
item_index: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
evidence_kind: "mechanism_link",
|
||||||
|
mechanism_note: (input.relationPatterns ?? ["payment_to_settlement"])[0],
|
||||||
|
confidence: input.status === "ok" ? "medium" : "low",
|
||||||
|
limitation: null,
|
||||||
|
payload: {
|
||||||
|
risk_score: 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
candidate_evidence: [],
|
||||||
|
problem_units: units,
|
||||||
|
problem_unit_summary:
|
||||||
|
units.length > 0
|
||||||
|
? {
|
||||||
|
schema_version: "problem_unit_summary_v0_1",
|
||||||
|
units_total: units.length,
|
||||||
|
duplicate_collapses: 0,
|
||||||
|
unit_types: units.map((unit) => unit.problem_unit_type),
|
||||||
|
type_distribution: {
|
||||||
|
[units[0]?.problem_unit_type ?? "broken_chain_segment"]: units.length
|
||||||
|
},
|
||||||
|
severity_distribution: {
|
||||||
|
low: 0,
|
||||||
|
medium: 0,
|
||||||
|
high: units.length
|
||||||
|
},
|
||||||
|
confidence_distribution: {
|
||||||
|
low: 0,
|
||||||
|
medium: units.length,
|
||||||
|
high: 0
|
||||||
|
},
|
||||||
|
primary_unit_type: units[0]?.problem_unit_type ?? null
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
why_included: ["wave12-test"],
|
||||||
|
selection_reason: ["wave12-test"],
|
||||||
|
risk_factors: ["wave12"],
|
||||||
|
business_interpretation: ["wave12"],
|
||||||
|
confidence: input.confidence ?? (input.status === "ok" ? "medium" : "low"),
|
||||||
|
limitations: input.limitations ?? [],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function composeCase(input: {
|
||||||
|
userMessage: string;
|
||||||
|
focusDomainHint: string | null;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
coverage?: Partial<RequirementCoverageReport>;
|
||||||
|
grounding?: Partial<AnswerGroundingCheck>;
|
||||||
|
}) {
|
||||||
|
return composeAssistantAnswer({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
routeSummary: buildRouteSummary(),
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F-R1",
|
||||||
|
requirement_text: "Wave 12 requirement",
|
||||||
|
subject_tokens: [],
|
||||||
|
status: "covered",
|
||||||
|
route: "hybrid_store_plus_live"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
coverageReport: buildCoverage(input.coverage),
|
||||||
|
groundingCheck: buildGrounding(input.grounding),
|
||||||
|
focusDomainHint: input.focusDomainHint,
|
||||||
|
enableAnswerPolicyV11: true,
|
||||||
|
enableProblemCentricAnswerV1: true,
|
||||||
|
enableLifecycleAnswerV1: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("wave12 vat/month-close consistency + confidence reconciliation", () => {
|
||||||
|
it("vat_query_with_strong_signal_must_override_stale_settlement_focus_hint", () => {
|
||||||
|
const vatUnit = buildProblemUnit({
|
||||||
|
id: "pu-vat-1",
|
||||||
|
type: "cross_branch_inconsistency_cluster",
|
||||||
|
account: "68",
|
||||||
|
defect: "invoice_to_vat",
|
||||||
|
lifecycleDomain: "vat_flow"
|
||||||
|
});
|
||||||
|
const output = composeCase({
|
||||||
|
userMessage: "VAT chain July: invoice exists, but purchase book is empty for accounts 19/68. Why?",
|
||||||
|
focusDomainHint: "settlements_60_62",
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
requirementId: "R1",
|
||||||
|
status: "ok",
|
||||||
|
units: [vatUnit],
|
||||||
|
accountScope: ["19", "68"],
|
||||||
|
domainScope: ["vat_flow"],
|
||||||
|
relationPatterns: ["invoice_to_vat"]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.assistant_reply).toMatch(/НДС|vat|книг|счет[-\s]?фактур/i);
|
||||||
|
expect(output.assistant_reply).not.toMatch(/60\/62|закрыти[ея]\s+расч[её]т/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("month_close_query_with_strong_signal_must_override_stale_vat_focus_hint", () => {
|
||||||
|
const closeUnit = buildProblemUnit({
|
||||||
|
id: "pu-close-1",
|
||||||
|
type: "period_risk_cluster",
|
||||||
|
account: "25",
|
||||||
|
defect: "close_operation_runs_missing",
|
||||||
|
lifecycleDomain: "period_close"
|
||||||
|
});
|
||||||
|
const output = composeCase({
|
||||||
|
userMessage: "Month close July: costs on accounts 20/44 were allocated partially. Where is the break?",
|
||||||
|
focusDomainHint: "vat_document_register_book",
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
requirementId: "R1",
|
||||||
|
status: "ok",
|
||||||
|
units: [closeUnit],
|
||||||
|
accountScope: ["25", "26"],
|
||||||
|
domainScope: ["period_close"],
|
||||||
|
relationPatterns: ["close_operation_runs"]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.assistant_reply).toMatch(/закрыти|месяц|затрат|распредел|month close|20\/44/i);
|
||||||
|
expect(output.assistant_reply).not.toMatch(/НДС|счет[-\s]?фактур|книг/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("vat_domain_with_foreign_primary_evidence_must_degrade_to_clarification", () => {
|
||||||
|
const output = composeCase({
|
||||||
|
userMessage: "Проверь НДС-цепочку по июлю: документ -> регистр -> книга.",
|
||||||
|
focusDomainHint: "vat_document_register_book",
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
requirementId: "R1",
|
||||||
|
status: "ok",
|
||||||
|
units: [],
|
||||||
|
accountScope: ["20"],
|
||||||
|
domainScope: ["period_close", "fixed_asset"],
|
||||||
|
relationPatterns: ["allocation_rules_resolved"]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.reply_type).toBe("clarification_required");
|
||||||
|
expect(output.assistant_reply).toContain("Ограничения:");
|
||||||
|
expect(output.assistant_reply).not.toContain("Опора достаточна для первичного вывода.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("month_close_domain_with_vat_primary_evidence_must_degrade_to_clarification", () => {
|
||||||
|
const output = composeCase({
|
||||||
|
userMessage: "Проверь закрытие месяца и контур затрат 20-44 за июль.",
|
||||||
|
focusDomainHint: "month_close_costs_20_44",
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
requirementId: "R1",
|
||||||
|
status: "ok",
|
||||||
|
units: [],
|
||||||
|
accountScope: ["19", "68"],
|
||||||
|
domainScope: ["vat_flow"],
|
||||||
|
relationPatterns: ["invoice_to_vat"]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.reply_type).toBe("clarification_required");
|
||||||
|
expect(output.assistant_reply).toContain("Ограничения:");
|
||||||
|
expect(output.assistant_reply).not.toContain("Опора достаточна для первичного вывода.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("confidence_limitation_must_not_contradict_each_other", () => {
|
||||||
|
const vatUnit = buildProblemUnit({
|
||||||
|
id: "pu-vat-2",
|
||||||
|
type: "lifecycle_anomaly_node",
|
||||||
|
account: "68",
|
||||||
|
defect: "invoice_to_vat",
|
||||||
|
lifecycleDomain: "vat_flow"
|
||||||
|
});
|
||||||
|
const output = composeCase({
|
||||||
|
userMessage: "Почему по НДС есть сигнал, но механизм выглядит неполным?",
|
||||||
|
focusDomainHint: "vat_document_register_book",
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
requirementId: "R1",
|
||||||
|
status: "ok",
|
||||||
|
units: [vatUnit],
|
||||||
|
accountScope: ["19", "68"],
|
||||||
|
domainScope: ["vat_flow"],
|
||||||
|
relationPatterns: ["invoice_to_vat"],
|
||||||
|
limitations: ["Source mapping is weak for part of the evidence."]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
grounding: {
|
||||||
|
status: "partial",
|
||||||
|
reasons: ["Mechanism is unresolved for part of the evidence."]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.answer_structure_v11?.mechanism_block?.status).toBe("limited");
|
||||||
|
expect(output.assistant_reply).toContain("Ограничения:");
|
||||||
|
expect(output.assistant_reply).not.toContain("Опора достаточна для первичного вывода.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("settlement_regression_must_remain_pass", () => {
|
||||||
|
const settlementUnit = buildProblemUnit({
|
||||||
|
id: "pu-settlement-1",
|
||||||
|
type: "broken_chain_segment",
|
||||||
|
account: "62",
|
||||||
|
defect: "failed_edge:payment_to_settlement",
|
||||||
|
lifecycleDomain: "customer_settlement"
|
||||||
|
});
|
||||||
|
const output = composeCase({
|
||||||
|
userMessage: "Оплата есть, но 62.01/62.02 не сходятся. Почему долг остался?",
|
||||||
|
focusDomainHint: "settlements_60_62",
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
requirementId: "R1",
|
||||||
|
status: "ok",
|
||||||
|
units: [settlementUnit],
|
||||||
|
accountScope: ["62.01", "62.02"],
|
||||||
|
domainScope: ["customer_settlement"],
|
||||||
|
relationPatterns: ["payment_to_settlement"]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.assistant_reply).toMatch(/62\.01|62\.02|расч[её]т|зач[её]т/i);
|
||||||
|
expect(output.assistant_reply).not.toMatch(/НДС|книг|закрыти[ея]\s+месяц/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,405 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { composeAssistantAnswer } from "../src/services/answerComposer";
|
||||||
|
import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../src/types/assistant";
|
||||||
|
import type { ProblemUnit } from "../src/types/stage2ProblemUnits";
|
||||||
|
import type { QuestionTypeClass } from "../src/services/questionTypeResolver";
|
||||||
|
|
||||||
|
function buildRouteSummary() {
|
||||||
|
return {
|
||||||
|
mode: "deterministic_v2" as const,
|
||||||
|
message_in_scope: true,
|
||||||
|
scope_confidence: "high" as const,
|
||||||
|
planner: {
|
||||||
|
total_fragments: 1,
|
||||||
|
in_scope_fragments: 1,
|
||||||
|
out_of_scope_fragments: 0,
|
||||||
|
discarded_fragments: 0,
|
||||||
|
contains_multiple_tasks: false
|
||||||
|
},
|
||||||
|
decisions: [],
|
||||||
|
fallback: {
|
||||||
|
type: "none" as const,
|
||||||
|
message: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCoverage(input?: Partial<RequirementCoverageReport>): RequirementCoverageReport {
|
||||||
|
return {
|
||||||
|
requirements_total: 1,
|
||||||
|
requirements_covered: 1,
|
||||||
|
requirements_uncovered: [],
|
||||||
|
requirements_partially_covered: [],
|
||||||
|
clarification_needed_for: [],
|
||||||
|
out_of_scope_requirements: [],
|
||||||
|
...input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGrounding(input?: Partial<AnswerGroundingCheck>): AnswerGroundingCheck {
|
||||||
|
return {
|
||||||
|
status: "grounded",
|
||||||
|
route_subject_match: true,
|
||||||
|
missing_requirements: [],
|
||||||
|
reasons: [],
|
||||||
|
why_included_summary: ["wave13-test"],
|
||||||
|
selection_reason_summary: ["wave13-test"],
|
||||||
|
...input
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProblemUnit(input: {
|
||||||
|
id: string;
|
||||||
|
type: ProblemUnit["problem_unit_type"];
|
||||||
|
account: string;
|
||||||
|
defect: string;
|
||||||
|
lifecycleDomain: ProblemUnit["lifecycle_domain"];
|
||||||
|
}): ProblemUnit {
|
||||||
|
return {
|
||||||
|
schema_version: "problem_unit_v0_1",
|
||||||
|
problem_unit_id: input.id,
|
||||||
|
problem_unit_type: input.type,
|
||||||
|
title: "Wave13 problem unit",
|
||||||
|
mechanism_summary: `Mechanism candidate: ${input.defect}.`,
|
||||||
|
business_defect_class: input.defect,
|
||||||
|
severity: {
|
||||||
|
score: 0.72,
|
||||||
|
grade: "high"
|
||||||
|
},
|
||||||
|
confidence: {
|
||||||
|
score: 0.63,
|
||||||
|
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],
|
||||||
|
affected_counterparties: ["Counterparty:CP-1"],
|
||||||
|
affected_contracts: ["Contract:CTR-1"],
|
||||||
|
failed_expected_edge: input.defect,
|
||||||
|
period_impact: {
|
||||||
|
is_period_sensitive: true,
|
||||||
|
impact_class: "close_risk"
|
||||||
|
},
|
||||||
|
evidence_pack: ["cand-1"],
|
||||||
|
entity_backlinks: [{ entity: "Document", id: "DOC-1" }],
|
||||||
|
snapshot_limitations: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRetrieval(input: {
|
||||||
|
requirementId: string;
|
||||||
|
status: UnifiedRetrievalResult["status"];
|
||||||
|
units?: ProblemUnit[];
|
||||||
|
accountScope?: string[];
|
||||||
|
domainScope?: string[];
|
||||||
|
relationPatterns?: string[];
|
||||||
|
withEvidence?: boolean;
|
||||||
|
notes?: string[];
|
||||||
|
}): UnifiedRetrievalResult {
|
||||||
|
const units = input.units ?? [];
|
||||||
|
const withEvidence = input.withEvidence ?? input.status !== "empty";
|
||||||
|
const accountScope = input.accountScope ?? ["60", "62"];
|
||||||
|
const domainScope = input.domainScope ?? ["bank_settlement"];
|
||||||
|
const relationPatterns = input.relationPatterns ?? ["payment_to_settlement"];
|
||||||
|
return {
|
||||||
|
fragment_id: `F-${input.requirementId}`,
|
||||||
|
requirement_ids: [input.requirementId],
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
status: input.status,
|
||||||
|
result_type: "chain",
|
||||||
|
items:
|
||||||
|
input.status === "empty"
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
source_entity: "Document",
|
||||||
|
source_id: "DOC-1",
|
||||||
|
display_name: "Счет № 4 от 07.07.20",
|
||||||
|
account_context: accountScope,
|
||||||
|
graph_domain_scope: domainScope,
|
||||||
|
relation_pattern_hits: relationPatterns,
|
||||||
|
period: "2020-07",
|
||||||
|
amount: "276 873,60"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
summary: {
|
||||||
|
broad_query_detected: false,
|
||||||
|
broad_result_flag: false,
|
||||||
|
minimum_evidence_failed: false,
|
||||||
|
degraded_to: null,
|
||||||
|
narrowing_strength: "strong",
|
||||||
|
semantic_profile: {
|
||||||
|
account_scope: accountScope,
|
||||||
|
domain_scope: domainScope,
|
||||||
|
relation_patterns: relationPatterns,
|
||||||
|
period_scope: {
|
||||||
|
from: "2020-07-01",
|
||||||
|
to: "2020-07-31",
|
||||||
|
granularity: "month"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
evidence:
|
||||||
|
input.status === "empty" || !withEvidence
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
evidence_id: `ev-${input.requirementId}`,
|
||||||
|
claim_ref: `requirement:${input.requirementId}`,
|
||||||
|
source_type: "retrieval_item",
|
||||||
|
source_ref: {
|
||||||
|
schema_version: "evidence_source_ref_v1",
|
||||||
|
namespace: "snapshot_2020_07",
|
||||||
|
entity: "document",
|
||||||
|
id: "DOC-1",
|
||||||
|
period: "2020-07",
|
||||||
|
canonical_ref: "evidence_source_ref_v1|snapshot_2020_07|document|doc-1|2020-07"
|
||||||
|
},
|
||||||
|
pointer: {
|
||||||
|
fragment_id: `F-${input.requirementId}`,
|
||||||
|
route: "hybrid_store_plus_live",
|
||||||
|
source: {
|
||||||
|
namespace: "snapshot_2020_07",
|
||||||
|
entity: "document",
|
||||||
|
id: "DOC-1",
|
||||||
|
period: "2020-07"
|
||||||
|
},
|
||||||
|
locator: {
|
||||||
|
field_path: "risk_score",
|
||||||
|
item_index: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
evidence_kind: "mechanism_link",
|
||||||
|
mechanism_note: relationPatterns[0],
|
||||||
|
confidence: input.status === "ok" ? "medium" : "low",
|
||||||
|
limitation: null,
|
||||||
|
payload: {
|
||||||
|
notes: input.notes ?? [],
|
||||||
|
contract: "договор № 01/19-ПТ",
|
||||||
|
amount: "276 873,60",
|
||||||
|
date: "07.07.20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
candidate_evidence: [],
|
||||||
|
problem_units: units,
|
||||||
|
problem_unit_summary:
|
||||||
|
units.length > 0
|
||||||
|
? {
|
||||||
|
schema_version: "problem_unit_summary_v0_1",
|
||||||
|
units_total: units.length,
|
||||||
|
duplicate_collapses: 0,
|
||||||
|
unit_types: units.map((unit) => unit.problem_unit_type),
|
||||||
|
type_distribution: {
|
||||||
|
[units[0]?.problem_unit_type ?? "broken_chain_segment"]: units.length
|
||||||
|
},
|
||||||
|
severity_distribution: {
|
||||||
|
low: 0,
|
||||||
|
medium: 0,
|
||||||
|
high: units.length
|
||||||
|
},
|
||||||
|
confidence_distribution: {
|
||||||
|
low: 0,
|
||||||
|
medium: units.length,
|
||||||
|
high: 0
|
||||||
|
},
|
||||||
|
primary_unit_type: units[0]?.problem_unit_type ?? null
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
why_included: ["wave13-test"],
|
||||||
|
selection_reason: ["wave13-test"],
|
||||||
|
risk_factors: ["wave13"],
|
||||||
|
business_interpretation: ["wave13"],
|
||||||
|
confidence: input.status === "ok" ? "medium" : "low",
|
||||||
|
limitations: [],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function composeCase(input: {
|
||||||
|
userMessage: string;
|
||||||
|
questionType: QuestionTypeClass;
|
||||||
|
focusDomainHint: string | null;
|
||||||
|
retrievalResults: UnifiedRetrievalResult[];
|
||||||
|
coverage?: Partial<RequirementCoverageReport>;
|
||||||
|
grounding?: Partial<AnswerGroundingCheck>;
|
||||||
|
}) {
|
||||||
|
return composeAssistantAnswer({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
routeSummary: buildRouteSummary(),
|
||||||
|
retrievalResults: input.retrievalResults,
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
requirement_id: "R1",
|
||||||
|
source_fragment_id: "F-R1",
|
||||||
|
requirement_text: "Wave13 requirement",
|
||||||
|
subject_tokens: [],
|
||||||
|
status: "covered",
|
||||||
|
route: "hybrid_store_plus_live"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
coverageReport: buildCoverage(input.coverage),
|
||||||
|
groundingCheck: buildGrounding(input.grounding),
|
||||||
|
focusDomainHint: input.focusDomainHint,
|
||||||
|
questionTypeHint: input.questionType,
|
||||||
|
companyAnchors: {
|
||||||
|
contract_numbers: ["договор № 01/19-ПТ"],
|
||||||
|
document_numbers: ["документ № 4"],
|
||||||
|
dates: ["07.07.20", "13.07.20"],
|
||||||
|
amounts: ["276 873,60"],
|
||||||
|
accounts: ["62.02"],
|
||||||
|
periods: ["июль 2020"],
|
||||||
|
document_types: ["payment", "invoice"],
|
||||||
|
all: [
|
||||||
|
"договор № 01/19-ПТ",
|
||||||
|
"документ № 4",
|
||||||
|
"07.07.20",
|
||||||
|
"13.07.20",
|
||||||
|
"276 873,60",
|
||||||
|
"account:62.02",
|
||||||
|
"period:июль 2020"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
enableAnswerPolicyV11: true,
|
||||||
|
enableProblemCentricAnswerV1: true,
|
||||||
|
enableLifecycleAnswerV1: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("wave13 domain fit + question-type fit + company-anchor grounding", () => {
|
||||||
|
it("settlement_query_must_keep_settlement_domain_even_if_retrieval_contains_vat_noise", () => {
|
||||||
|
const settlementUnit = buildProblemUnit({
|
||||||
|
id: "pu-settlement-1",
|
||||||
|
type: "broken_chain_segment",
|
||||||
|
account: "62.02",
|
||||||
|
defect: "failed_edge:payment_to_settlement",
|
||||||
|
lifecycleDomain: "customer_settlement"
|
||||||
|
});
|
||||||
|
const output = composeCase({
|
||||||
|
userMessage:
|
||||||
|
"Почему по договору № 01/19-ПТ от 09.01.2019 оплата 276 873,60 есть, а 62.01/62.02 все равно не сходятся?",
|
||||||
|
questionType: "why_breaks",
|
||||||
|
focusDomainHint: "vat_document_register_book",
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
requirementId: "R1",
|
||||||
|
status: "ok",
|
||||||
|
units: [settlementUnit],
|
||||||
|
accountScope: ["62.01", "62.02", "19"],
|
||||||
|
domainScope: ["customer_settlement", "vat_flow"],
|
||||||
|
relationPatterns: ["payment_to_settlement", "invoice_to_vat"]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.assistant_reply).toMatch(/расчет|62\.01|62\.02|зачет|зачёт/i);
|
||||||
|
expect(output.assistant_reply).not.toMatch(/переход от документа к регистру и книге|цепочке ндс/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("question_type_where_break_is_must_produce_localization_style_line", () => {
|
||||||
|
const settlementUnit = buildProblemUnit({
|
||||||
|
id: "pu-settlement-2",
|
||||||
|
type: "broken_chain_segment",
|
||||||
|
account: "62.02",
|
||||||
|
defect: "failed_edge:payment_to_settlement",
|
||||||
|
lifecycleDomain: "customer_settlement"
|
||||||
|
});
|
||||||
|
const output = composeCase({
|
||||||
|
userMessage:
|
||||||
|
"Где именно разрыв по договору № 01/19-ПТ: в договоре, объекте расчетов или в связке документов?",
|
||||||
|
questionType: "where_break_is",
|
||||||
|
focusDomainHint: "settlements_60_62",
|
||||||
|
retrievalResults: [buildRetrieval({ requirementId: "R1", status: "ok", units: [settlementUnit] })]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.assistant_reply).toMatch(/локализ|узел разрыва|где именно/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("question_type_prove_or_guess_must_explicitly_separate_proven_vs_hypothesis", () => {
|
||||||
|
const vatUnit = buildProblemUnit({
|
||||||
|
id: "pu-vat-1",
|
||||||
|
type: "cross_branch_inconsistency_cluster",
|
||||||
|
account: "68",
|
||||||
|
defect: "invoice_to_vat",
|
||||||
|
lifecycleDomain: "vat_flow"
|
||||||
|
});
|
||||||
|
const output = composeCase({
|
||||||
|
userMessage:
|
||||||
|
"По НДС это доказано по данным или это только гипотеза? На чем основано утверждение?",
|
||||||
|
questionType: "prove_or_guess",
|
||||||
|
focusDomainHint: "vat_document_register_book",
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
requirementId: "R1",
|
||||||
|
status: "ok",
|
||||||
|
units: [vatUnit],
|
||||||
|
accountScope: ["19", "68"],
|
||||||
|
domainScope: ["vat_flow"],
|
||||||
|
relationPatterns: ["invoice_to_vat"]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
grounding: {
|
||||||
|
status: "partial",
|
||||||
|
reasons: ["Mechanism is unresolved for part of the evidence."]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.assistant_reply).toMatch(/доказан|гипотез|ограничени/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("anchor_usage_lines_must_be_present_when_company_anchors_are_used", () => {
|
||||||
|
const output = composeCase({
|
||||||
|
userMessage:
|
||||||
|
"Оплата по счету № 4 от 07.07.20 на 276 873,60 пришла 13 июля. Что проверить первым по 62.02?",
|
||||||
|
questionType: "what_to_check_first",
|
||||||
|
focusDomainHint: "settlements_60_62",
|
||||||
|
retrievalResults: [buildRetrieval({ requirementId: "R1", status: "ok" })]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.assistant_reply).toMatch(/якоря вопроса/i);
|
||||||
|
expect(output.assistant_reply).toMatch(/договор|07\.07\.20|276 873,60|62\.02/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("anchor_usage_must_be_honest_when_part_of_anchors_not_confirmed", () => {
|
||||||
|
const output = composeCase({
|
||||||
|
userMessage:
|
||||||
|
"Почему по договору № 01/19-ПТ не сходится 62.02 в июле 2020, если была оплата 276 873,60?",
|
||||||
|
questionType: "what_is_it_grounded_on",
|
||||||
|
focusDomainHint: "settlements_60_62",
|
||||||
|
retrievalResults: [
|
||||||
|
buildRetrieval({
|
||||||
|
requirementId: "R1",
|
||||||
|
status: "ok",
|
||||||
|
accountScope: ["60"],
|
||||||
|
domainScope: ["bank_settlement"],
|
||||||
|
relationPatterns: ["payment_recorded"],
|
||||||
|
withEvidence: true
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.assistant_reply).toMatch(/без прямого подтверждения|ограничени/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("answers_for_different_question_types_must_not_collapse_to_same_generic_pattern", () => {
|
||||||
|
const baseRetrieval = [buildRetrieval({ requirementId: "R1", status: "ok" })];
|
||||||
|
const whereOutput = composeCase({
|
||||||
|
userMessage: "Где именно разрыв по 62.01/62.02?",
|
||||||
|
questionType: "where_break_is",
|
||||||
|
focusDomainHint: "settlements_60_62",
|
||||||
|
retrievalResults: baseRetrieval
|
||||||
|
});
|
||||||
|
const checkFirstOutput = composeCase({
|
||||||
|
userMessage: "Что проверить первым по 62.01/62.02?",
|
||||||
|
questionType: "what_to_check_first",
|
||||||
|
focusDomainHint: "settlements_60_62",
|
||||||
|
retrievalResults: baseRetrieval
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(whereOutput.assistant_reply).not.toEqual(checkFirstOutput.assistant_reply);
|
||||||
|
expect(whereOutput.assistant_reply).toMatch(/локализ|разрыв/i);
|
||||||
|
expect(checkFirstOutput.assistant_reply).toMatch(/первый маршрут проверки|начните с первого пункта/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-7Sx8faOQpm",
|
||||||
|
"timestamp": "2026-03-28T09:54:19.332Z",
|
||||||
|
"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": "1wEfvoR_2DJrlV",
|
||||||
|
"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": "wp-jtQq3mp7uMk",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-DI-aCpLWqK",
|
||||||
|
"timestamp": "2026-03-28T09:54:17.753Z",
|
||||||
|
"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": "LQSTgr_jZDLKSE",
|
||||||
|
"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": "C_DgPdm03zoNRm",
|
||||||
|
"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": "ou478il0iFNsIQ",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-KEtQ8SYPKI",
|
||||||
|
"timestamp": "2026-03-28T09:23:28.807Z",
|
||||||
|
"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": "wKL8CXbPIJDi5V",
|
||||||
|
"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": "VGu1HWqb9Ka5QF",
|
||||||
|
"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": "rvXo7PioBUelzY",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-KSX4Dsn7Xg",
|
||||||
|
"timestamp": "2026-03-28T09:54:19.191Z",
|
||||||
|
"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": "airV6dR4a5sk0p",
|
||||||
|
"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": "FpNPo_qn_TakTC",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-O1pl1uP_Kc",
|
||||||
|
"timestamp": "2026-03-28T09:09:42.733Z",
|
||||||
|
"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": "DYfGFpealu2tnx",
|
||||||
|
"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": "ZweuomcToD_MJn",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-WTP_zUxzSk",
|
||||||
|
"timestamp": "2026-03-28T09:23:30.429Z",
|
||||||
|
"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": "pgJG_e5WDfRPGr",
|
||||||
|
"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": "0iHAq9Sf3LQoGv",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-XunNzxsTLF",
|
||||||
|
"timestamp": "2026-03-28T09:43:10.148Z",
|
||||||
|
"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": "iwEKj8yXXL_94j",
|
||||||
|
"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": "Ww5O8UQ8xVYdqX",
|
||||||
|
"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": "U2ODfQtj3y9ieS",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-XyP2pAdsJB",
|
||||||
|
"timestamp": "2026-03-28T09:58:48.945Z",
|
||||||
|
"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": "KH-f-MZ4cm--WR",
|
||||||
|
"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": "yiAsfJaSW5Qm3o",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-a3v4udOWPA",
|
||||||
|
"timestamp": "2026-03-28T09:43:11.528Z",
|
||||||
|
"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": "YGuB_vMi7qg803",
|
||||||
|
"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": "snXRfmWFgVHm3i",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-do0FoiH-Xe",
|
||||||
|
"timestamp": "2026-03-28T09:43:11.767Z",
|
||||||
|
"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": "dOLnE76GCDaC7H",
|
||||||
|
"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": "cTDkKra0xTEPWK",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-gt_MR9X37J",
|
||||||
|
"timestamp": "2026-03-28T09:09:42.504Z",
|
||||||
|
"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": "u0PbQMwXMrEkkc",
|
||||||
|
"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": "HzY3_UubbqgLW1",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-nBbn-LjIiA",
|
||||||
|
"timestamp": "2026-03-28T09:09:41.153Z",
|
||||||
|
"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": "-H_wQprzOS7_Id",
|
||||||
|
"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": "fQOvPenqT3TF8w",
|
||||||
|
"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": "Z3MSiUIB6-qCqJ",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-qsqmRe1REB",
|
||||||
|
"timestamp": "2026-03-28T09:23:30.226Z",
|
||||||
|
"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": "t27mUt40DZkNxA",
|
||||||
|
"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": "SRUJifR4MwjGqq",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-vXCfmE5dFG",
|
||||||
|
"timestamp": "2026-03-28T09:58:49.158Z",
|
||||||
|
"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": "ZU34_DiRow0vZ5",
|
||||||
|
"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": "Nojaq_kesX2XGI",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
{
|
||||||
|
"run_id": "eval-xytqNgINxK",
|
||||||
|
"timestamp": "2026-03-28T09:58:47.522Z",
|
||||||
|
"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": "JUjGkgksG-QX_y",
|
||||||
|
"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": "9Os3edsGypmVvr",
|
||||||
|
"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": "FgsR4vO6BqpNvV",
|
||||||
|
"request_count_for_case": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
Loading…
Reference in New Issue