ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.12.23: декомпозиция deep-turn пайплайна ассистента в runtime-адаптеры

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