345 lines
15 KiB
JavaScript
345 lines
15 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.extractRequirementsForRoute = extractRequirementsForRoute;
|
||
exports.evaluateCoverageForRequirements = evaluateCoverageForRequirements;
|
||
exports.checkGroundingForRequirements = checkGroundingForRequirements;
|
||
function summarizeUnique(values, limit = 6) {
|
||
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
|
||
}
|
||
const SUBJECT_TOKEN_RULES = {
|
||
nds: {
|
||
critical: true,
|
||
patterns: [
|
||
"vat",
|
||
"accumulationregister",
|
||
"ндс",
|
||
"книгипокупок",
|
||
"книгипродаж",
|
||
"налогнадобавленнуюстоимость"
|
||
]
|
||
},
|
||
os: {
|
||
critical: true,
|
||
patterns: ["fixed_asset", "fixedasset", "основн", "амортиз"]
|
||
},
|
||
saldo: {
|
||
critical: true,
|
||
patterns: ["balance", "saldo", "сальдо", "остат"]
|
||
},
|
||
counterparty: {
|
||
critical: false,
|
||
patterns: [
|
||
"counterparty",
|
||
"supplier",
|
||
"buyer",
|
||
"counterparty_id",
|
||
"journal_counterparty",
|
||
"document_has_counterparty",
|
||
"контрагент",
|
||
"поставщик",
|
||
"покупател"
|
||
],
|
||
routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical"]
|
||
},
|
||
document: {
|
||
critical: false,
|
||
patterns: [
|
||
"document",
|
||
"recorder",
|
||
"journal",
|
||
"document_refs_count",
|
||
"recorded_by_document",
|
||
"journal_refers_to_document",
|
||
"документ"
|
||
],
|
||
routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical", "live_mcp_drilldown"]
|
||
},
|
||
anomaly: {
|
||
critical: false,
|
||
patterns: [
|
||
"risk",
|
||
"risk_score",
|
||
"unknown_link_count",
|
||
"zero_guid",
|
||
"navigation_links",
|
||
"missing_counterparty_link",
|
||
"аномал",
|
||
"риск"
|
||
],
|
||
routes: ["store_feature_risk", "batch_refresh_then_store"]
|
||
},
|
||
chain: {
|
||
critical: false,
|
||
patterns: ["chain", "cross_entity_chain", "relation_types", "operations_count", "matched_counterparties", "цепоч"],
|
||
routes: ["hybrid_store_plus_live"]
|
||
}
|
||
};
|
||
function hasRegexMatch(corpus, pattern) {
|
||
try {
|
||
return pattern.test(corpus);
|
||
}
|
||
catch {
|
||
return false;
|
||
}
|
||
}
|
||
function evaluateSubjectTokenMatch(token, corpus, executedRoutes) {
|
||
if (token.startsWith("account_")) {
|
||
const account = token.slice("account_".length).trim();
|
||
if (!account) {
|
||
return { matched: false, critical: true };
|
||
}
|
||
const escaped = account.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
const accountPattern = new RegExp(`(^|[^0-9])${escaped}([^0-9]|$)`, "i");
|
||
return { matched: hasRegexMatch(corpus, accountPattern), critical: true };
|
||
}
|
||
const rule = SUBJECT_TOKEN_RULES[token];
|
||
if (rule) {
|
||
const byPattern = rule.patterns.some((pattern) => corpus.includes(pattern));
|
||
const byRoute = Array.isArray(rule.routes) ? rule.routes.some((route) => executedRoutes.has(route)) : false;
|
||
return { matched: byPattern || byRoute, critical: rule.critical };
|
||
}
|
||
return { matched: corpus.includes(token), critical: false };
|
||
}
|
||
function evidenceCountForRequirement(requirementId, result) {
|
||
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
|
||
if (evidence.length === 0) {
|
||
return 0;
|
||
}
|
||
const tagged = evidence.filter((item) => {
|
||
const claimRef = typeof item?.claim_ref === "string" ? item.claim_ref : "";
|
||
return claimRef.toLowerCase() === `requirement:${String(requirementId).toLowerCase()}`;
|
||
}).length;
|
||
if (tagged > 0) {
|
||
return tagged;
|
||
}
|
||
if (Array.isArray(result.requirement_ids) &&
|
||
result.requirement_ids.length === 1 &&
|
||
result.requirement_ids[0] === requirementId) {
|
||
return evidence.length;
|
||
}
|
||
return 0;
|
||
}
|
||
function hasSubstantiveCoverageForRequirement(requirementId, result) {
|
||
const evidenceCount = evidenceCountForRequirement(requirementId, result);
|
||
if (evidenceCount > 0) {
|
||
return true;
|
||
}
|
||
const problemUnitsCount = Array.isArray(result.problem_units) ? result.problem_units.length : 0;
|
||
const candidateEvidenceCount = Array.isArray(result.candidate_evidence) ? result.candidate_evidence.length : 0;
|
||
if (problemUnitsCount > 0 || candidateEvidenceCount > 0) {
|
||
if (Array.isArray(result.requirement_ids) &&
|
||
result.requirement_ids.length === 1 &&
|
||
result.requirement_ids[0] === requirementId) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
function extractRequirementsForRoute(input) {
|
||
const byFragment = new Map();
|
||
const requirements = [];
|
||
const pushRequirement = (item) => {
|
||
const subjectTokens = input.extractSubjectTokens(item.requirement_text);
|
||
requirements.push({
|
||
requirement_id: item.requirement_id,
|
||
source_fragment_id: item.source_fragment_id,
|
||
requirement_text: item.requirement_text,
|
||
subject_tokens: subjectTokens,
|
||
status: item.status,
|
||
route: item.route
|
||
});
|
||
if (item.source_fragment_id) {
|
||
const current = byFragment.get(item.source_fragment_id) ?? [];
|
||
current.push(item.requirement_id);
|
||
byFragment.set(item.source_fragment_id, current);
|
||
}
|
||
};
|
||
if (!input.routeSummary) {
|
||
pushRequirement({
|
||
requirement_id: "R1",
|
||
source_fragment_id: null,
|
||
requirement_text: input.userMessage,
|
||
status: "clarification_needed",
|
||
route: null
|
||
});
|
||
return { requirements, byFragment };
|
||
}
|
||
if (input.routeSummary.mode === "legacy_v1") {
|
||
pushRequirement({
|
||
requirement_id: "R1",
|
||
source_fragment_id: "F1",
|
||
requirement_text: input.userMessage,
|
||
status: "covered",
|
||
route: input.routeSummary.route_hint
|
||
});
|
||
return { requirements, byFragment };
|
||
}
|
||
input.routeSummary.decisions.forEach((decision, index) => {
|
||
const requirementId = `R${index + 1}`;
|
||
const text = input.fragmentTextById.get(decision.fragment_id) ?? input.userMessage;
|
||
let status = "covered";
|
||
if (decision.route === "no_route") {
|
||
if (decision.no_route_reason === "out_of_scope") {
|
||
status = "out_of_scope";
|
||
}
|
||
else if (decision.no_route_reason === "insufficient_specificity") {
|
||
status = "clarification_needed";
|
||
}
|
||
else {
|
||
status = "uncovered";
|
||
}
|
||
}
|
||
pushRequirement({
|
||
requirement_id: requirementId,
|
||
source_fragment_id: decision.fragment_id,
|
||
requirement_text: text,
|
||
status,
|
||
route: decision.route === "no_route" ? null : decision.route
|
||
});
|
||
});
|
||
return { requirements, byFragment };
|
||
}
|
||
function evaluateCoverageForRequirements(requirements, retrievalResults) {
|
||
const statusByRequirement = new Map();
|
||
for (const result of retrievalResults) {
|
||
for (const requirementId of result.requirement_ids) {
|
||
const list = statusByRequirement.get(requirementId) ?? [];
|
||
list.push({
|
||
status: result.status,
|
||
substantive: hasSubstantiveCoverageForRequirement(requirementId, result)
|
||
});
|
||
statusByRequirement.set(requirementId, list);
|
||
}
|
||
}
|
||
const resolvedRequirements = requirements.map((requirement) => {
|
||
if (requirement.status === "out_of_scope" || requirement.status === "clarification_needed") {
|
||
return requirement;
|
||
}
|
||
const states = statusByRequirement.get(requirement.requirement_id) ?? [];
|
||
if (states.length === 0) {
|
||
return { ...requirement, status: "uncovered" };
|
||
}
|
||
const hasAnySubstantive = states.some((item) => item.substantive);
|
||
if (!hasAnySubstantive) {
|
||
return { ...requirement, status: "uncovered" };
|
||
}
|
||
const hasOk = states.some((item) => item.status === "ok");
|
||
const hasPartial = states.some((item) => item.status === "partial");
|
||
const hasEmpty = states.some((item) => item.status === "empty");
|
||
const hasError = states.some((item) => item.status === "error");
|
||
const hasWeakOk = states.some((item) => item.status === "ok" && !item.substantive);
|
||
const hasSubstantiveOk = states.some((item) => item.status === "ok" && item.substantive);
|
||
const hasSubstantivePartial = states.some((item) => item.status === "partial" && item.substantive);
|
||
if (hasSubstantiveOk && !hasSubstantivePartial && !hasWeakOk && !hasEmpty && !hasError) {
|
||
return { ...requirement, status: "covered" };
|
||
}
|
||
if (hasSubstantiveOk || hasSubstantivePartial || hasOk || hasPartial) {
|
||
return { ...requirement, status: "partially_covered" };
|
||
}
|
||
return { ...requirement, status: "uncovered" };
|
||
});
|
||
const requirementsCovered = resolvedRequirements.filter((item) => item.status === "covered").length;
|
||
const requirementsUncovered = resolvedRequirements
|
||
.filter((item) => item.status === "uncovered")
|
||
.map((item) => item.requirement_id);
|
||
const requirementsPartiallyCovered = resolvedRequirements
|
||
.filter((item) => item.status === "partially_covered")
|
||
.map((item) => item.requirement_id);
|
||
const clarificationNeededFor = resolvedRequirements
|
||
.filter((item) => item.status === "clarification_needed")
|
||
.map((item) => item.requirement_id);
|
||
const outOfScopeRequirements = resolvedRequirements
|
||
.filter((item) => item.status === "out_of_scope")
|
||
.map((item) => item.requirement_id);
|
||
return {
|
||
requirements: resolvedRequirements,
|
||
coverage: {
|
||
requirements_total: resolvedRequirements.length,
|
||
requirements_covered: requirementsCovered,
|
||
requirements_uncovered: requirementsUncovered,
|
||
requirements_partially_covered: requirementsPartiallyCovered,
|
||
clarification_needed_for: clarificationNeededFor,
|
||
out_of_scope_requirements: outOfScopeRequirements
|
||
}
|
||
};
|
||
}
|
||
function checkGroundingForRequirements(input) {
|
||
const whyIncludedSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.why_included));
|
||
const selectionReasonSummary = summarizeUnique(input.retrievalResults.flatMap((item) => item.selection_reason));
|
||
const hasMaterialResults = input.retrievalResults.some((item) => item.status === "ok" || item.status === "partial");
|
||
const subjectTokens = input.extractSubjectTokens(input.userMessage);
|
||
const executedRoutes = new Set(input.retrievalResults
|
||
.filter((item) => item.status !== "error")
|
||
.map((item) => item.route)
|
||
.filter(Boolean));
|
||
const retrievalCorpus = JSON.stringify(input.retrievalResults.map((item) => ({
|
||
route: item.route,
|
||
result_type: item.result_type,
|
||
summary: item.summary,
|
||
items: item.items,
|
||
evidence: item.evidence,
|
||
why_included: item.why_included,
|
||
selection_reason: item.selection_reason,
|
||
risk_factors: item.risk_factors,
|
||
business_interpretation: item.business_interpretation
|
||
}))).toLowerCase();
|
||
const missingSubjectTokens = [];
|
||
const missingCriticalTokens = [];
|
||
for (const token of subjectTokens) {
|
||
const match = evaluateSubjectTokenMatch(token, retrievalCorpus, executedRoutes);
|
||
if (!match.matched) {
|
||
missingSubjectTokens.push(token);
|
||
if (match.critical) {
|
||
missingCriticalTokens.push(token);
|
||
}
|
||
}
|
||
}
|
||
const onlyAccountCriticalMissing = missingCriticalTokens.length > 0 && missingCriticalTokens.every((token) => token.startsWith("account_"));
|
||
const accountOnlyMismatchRecoverable = hasMaterialResults &&
|
||
input.coverage.requirements_covered > 0 &&
|
||
onlyAccountCriticalMissing &&
|
||
(whyIncludedSummary.length > 0 || selectionReasonSummary.length > 0);
|
||
const routeSubjectMatch = !hasMaterialResults || missingCriticalTokens.length === 0 || accountOnlyMismatchRecoverable;
|
||
let status = "grounded";
|
||
const reasons = [];
|
||
if (!routeSubjectMatch) {
|
||
status = "route_mismatch_blocked";
|
||
reasons.push(`Ключевые ориентиры вопроса не подтверждены в найденных данных: ${missingCriticalTokens.join(", ")}`);
|
||
}
|
||
else if (accountOnlyMismatchRecoverable) {
|
||
status = "partial";
|
||
reasons.push(`Часть счетных ориентиров не подтвердилась напрямую (${missingCriticalTokens.join(", ")}), но есть опора для ограниченного вывода.`);
|
||
}
|
||
else if (input.coverage.requirements_covered === 0) {
|
||
status = "no_grounded_answer";
|
||
reasons.push("Ни одно требование не получило подтвержденного покрытия.");
|
||
}
|
||
else if (input.coverage.requirements_uncovered.length > 0 ||
|
||
input.coverage.requirements_partially_covered.length > 0 ||
|
||
input.coverage.clarification_needed_for.length > 0 ||
|
||
input.coverage.out_of_scope_requirements.length > 0) {
|
||
status = "partial";
|
||
reasons.push("Вопрос покрыт частично: есть непокрытые или требующие уточнения требования.");
|
||
}
|
||
if (whyIncludedSummary.length === 0) {
|
||
reasons.push("В текущей выборке не хватает явных подтверждений, почему записи попали в ответ.");
|
||
}
|
||
if (missingSubjectTokens.length > 0 && missingCriticalTokens.length === 0) {
|
||
reasons.push(`Часть контекста вопроса не подтверждена напрямую в найденных данных: ${missingSubjectTokens.join(", ")}`);
|
||
}
|
||
const missingRequirements = [
|
||
...input.coverage.requirements_uncovered,
|
||
...input.coverage.requirements_partially_covered,
|
||
...input.coverage.clarification_needed_for,
|
||
...input.coverage.out_of_scope_requirements
|
||
];
|
||
return {
|
||
status,
|
||
route_subject_match: routeSubjectMatch,
|
||
missing_requirements: missingRequirements,
|
||
reasons,
|
||
why_included_summary: whyIncludedSummary,
|
||
selection_reason_summary: selectionReasonSummary
|
||
};
|
||
}
|