Этап 4 / Пакет по РБП восстановление живого контура сбора данных и доказательной базы
This commit is contained in:
@@ -329,7 +329,15 @@ function requiredChecksByClaim(claimType: ClaimType): string[] {
|
||||
if (claimType === "prove_month_close_state") {
|
||||
return ["close_operation_found", "distribution_step_found", "residual_tail_found"];
|
||||
}
|
||||
return ["rbp_writeoff_lifecycle_confirmed", "residual_tail_found", "close_contradiction_or_normal_residual"];
|
||||
return [
|
||||
"rbp_writeoff_document_found",
|
||||
"rbp_object_identified",
|
||||
"rbp_movement_found",
|
||||
"rbp_period_end_residual_found",
|
||||
"rbp_writeoff_lifecycle_confirmed",
|
||||
"residual_tail_found",
|
||||
"close_contradiction_or_normal_residual"
|
||||
];
|
||||
}
|
||||
|
||||
function detectChecksForCorpus(corpus: string, claimType: ClaimType, anchors: Record<string, string[]>): string[] {
|
||||
@@ -351,6 +359,10 @@ function detectChecksForCorpus(corpus: string, claimType: ClaimType, anchors: Re
|
||||
const hasRbp = /(?:\brbp\b|рбп|account\s*97|счет\s*97|deferred)/i.test(corpus);
|
||||
const hasResidual = /(?:tail|остат|незакры|overdue|period_boundary|terminal_state_gap)/i.test(corpus);
|
||||
const hasContradiction = /(?:contradiction|invalid_transition|normal residual|нормальн)/i.test(corpus);
|
||||
const hasRbpWriteoffDoc = /(?:списани[ея]\s+рбп|rbp_writeoff|deferred_expense_document|writeoff document)/i.test(corpus);
|
||||
const hasRbpObject = /(?:rbp[_\s-]?object|объект\s+рбп|analytics|subkonto|расходыбудущихпериодов)/i.test(corpus);
|
||||
const hasMovement = /(?:movement|движен|хозрасчетный|document_to_posting|posting|проводк)/i.test(corpus);
|
||||
const hasPeriodEndResidual = /(?:period_boundary|end_period|2020-07-31|остат)/i.test(corpus);
|
||||
|
||||
if (claimType === "prove_settlement_closure_state") {
|
||||
if (hasPayment) checks.add("payment_document_found");
|
||||
@@ -377,6 +389,10 @@ function detectChecksForCorpus(corpus: string, claimType: ClaimType, anchors: Re
|
||||
if (hasDistribution) checks.add("distribution_step_found");
|
||||
if (hasResidual) checks.add("residual_tail_found");
|
||||
} else {
|
||||
if (hasRbpWriteoffDoc || (hasRbp && hasDistribution)) checks.add("rbp_writeoff_document_found");
|
||||
if (hasRbpObject || hasRbp) checks.add("rbp_object_identified");
|
||||
if (hasMovement) checks.add("rbp_movement_found");
|
||||
if (hasPeriodEndResidual || hasResidual) checks.add("rbp_period_end_residual_found");
|
||||
if (hasRbp && hasDistribution) checks.add("rbp_writeoff_lifecycle_confirmed");
|
||||
if (hasResidual) checks.add("residual_tail_found");
|
||||
if (hasContradiction || hasClose) checks.add("close_contradiction_or_normal_residual");
|
||||
|
||||
@@ -65,6 +65,31 @@ interface LiveMcpOverlay {
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
interface LiveMcpCallPlan {
|
||||
claim_type: string | null;
|
||||
query_subject: string;
|
||||
required_live_calls: string[];
|
||||
calls: Array<{
|
||||
call_id: string;
|
||||
purpose: string;
|
||||
query: string;
|
||||
required_for_claim: boolean;
|
||||
account_scope_override?: string[];
|
||||
}>;
|
||||
route_gap_reason: string | null;
|
||||
}
|
||||
|
||||
interface LiveMcpCallExecution {
|
||||
call_id: string;
|
||||
purpose: string;
|
||||
required_for_claim: boolean;
|
||||
status: "ok" | "empty" | "error";
|
||||
fetched_rows: number;
|
||||
matched_rows: number;
|
||||
returned_rows: number;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
type BroadnessLevel = "low" | "medium" | "high";
|
||||
|
||||
interface BroadQueryAssessment {
|
||||
@@ -150,6 +175,28 @@ const MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE = `
|
||||
Движения.Период УБЫВ
|
||||
`;
|
||||
|
||||
const MCP_LIVE_MOVEMENTS_BY_PERIOD_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Движения.Период КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Движения.Регистратор) КАК Регистратор,
|
||||
ПРЕДСТАВЛЕНИЕ(Движения.СчетДт) КАК СчетДт,
|
||||
ПРЕДСТАВЛЕНИЕ(Движения.СчетКт) КАК СчетКт,
|
||||
Движения.Сумма КАК Сумма
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный КАК Движения
|
||||
ГДЕ
|
||||
Движения.Период МЕЖДУ __FROM_DATETIME__ И __TO_DATETIME__
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Движения.Период УБЫВ
|
||||
`;
|
||||
|
||||
const RBP_REQUIRED_LIVE_CALLS = [
|
||||
"find_rbp_writeoff_documents_in_period",
|
||||
"find_rbp_object_movements_account_97",
|
||||
"find_month_close_entries_linked_to_rbp",
|
||||
"compute_end_period_residual_by_rbp_object"
|
||||
];
|
||||
|
||||
function pushUniqueLine(target: string[], line: string): void {
|
||||
if (!target.includes(line)) {
|
||||
target.push(line);
|
||||
@@ -181,6 +228,141 @@ function parseFiniteNumber(value: unknown): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatIsoDateUtc(date: Date): string {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getUTCDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function monthEndFromIso(isoDate: string): string | null {
|
||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month)) {
|
||||
return null;
|
||||
}
|
||||
const end = new Date(Date.UTC(year, month, 0));
|
||||
return formatIsoDateUtc(end);
|
||||
}
|
||||
|
||||
function shiftIsoDate(isoDate: string, deltaDays: number): string | null {
|
||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return null;
|
||||
}
|
||||
date.setUTCDate(date.getUTCDate() + deltaDays);
|
||||
return formatIsoDateUtc(date);
|
||||
}
|
||||
|
||||
function toDateTimeExpr(isoDate: string, endOfDay: boolean): string | null {
|
||||
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
const day = Number(match[3]);
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||
return null;
|
||||
}
|
||||
const hour = endOfDay ? 23 : 0;
|
||||
const minute = endOfDay ? 59 : 0;
|
||||
const second = endOfDay ? 59 : 0;
|
||||
return `ДАТАВРЕМЯ(${year}, ${month}, ${day}, ${hour}, ${minute}, ${second})`;
|
||||
}
|
||||
|
||||
function buildLiveRangeQuery(fromIso: string, toIso: string, limit: number): string {
|
||||
const fromExpr = toDateTimeExpr(fromIso, false);
|
||||
const toExpr = toDateTimeExpr(toIso, true);
|
||||
if (!fromExpr || !toExpr) {
|
||||
return MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(limit));
|
||||
}
|
||||
return MCP_LIVE_MOVEMENTS_BY_PERIOD_QUERY_TEMPLATE.replace("__LIMIT__", String(limit))
|
||||
.replace("__FROM_DATETIME__", fromExpr)
|
||||
.replace("__TO_DATETIME__", toExpr);
|
||||
}
|
||||
|
||||
function hasRbpSignal(text: string): boolean {
|
||||
return /(?:\brbp\b|рбп|расходы\s+будущих\s+периодов|deferred|writeoff|списани[ея]\s+рбп|account\s*97|счет\s*97)/i.test(
|
||||
String(text ?? "").toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
function buildLiveMcpCallPlan(route: string, fragmentText: string): LiveMcpCallPlan {
|
||||
const semanticProfile = buildSemanticRetrievalProfile(fragmentText);
|
||||
const rbpClaim =
|
||||
hasRbpSignal(fragmentText) ||
|
||||
semanticProfile.query_subject === "deferred_expense_lifecycle_anomaly" ||
|
||||
semanticProfile.domain_scope.includes("deferred_expense");
|
||||
if (!rbpClaim) {
|
||||
return {
|
||||
claim_type: null,
|
||||
query_subject: semanticProfile.query_subject,
|
||||
required_live_calls: [],
|
||||
calls: [
|
||||
{
|
||||
call_id: "generic_accounting_register_probe",
|
||||
purpose: "live_overlay_probe",
|
||||
query: MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(ASSISTANT_MCP_LIVE_LIMIT)),
|
||||
required_for_claim: false
|
||||
}
|
||||
],
|
||||
route_gap_reason: null
|
||||
};
|
||||
}
|
||||
|
||||
const periodScope = inferPeriodScope(fragmentText);
|
||||
const primaryFrom = periodScope.from ?? "2020-07-01";
|
||||
const primaryTo = periodScope.to ?? monthEndFromIso(primaryFrom) ?? "2020-07-31";
|
||||
const carryFrom = shiftIsoDate(primaryFrom, -31) ?? primaryFrom;
|
||||
const carryTo = shiftIsoDate(primaryTo, 31) ?? primaryTo;
|
||||
|
||||
return {
|
||||
claim_type: "prove_rbp_tail_state",
|
||||
query_subject: "deferred_expense_lifecycle_anomaly",
|
||||
required_live_calls: [...RBP_REQUIRED_LIVE_CALLS],
|
||||
calls: [
|
||||
{
|
||||
call_id: "find_rbp_writeoff_documents_in_period",
|
||||
purpose: "seed_writeoff_documents",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, ASSISTANT_MCP_LIVE_LIMIT),
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
},
|
||||
{
|
||||
call_id: "find_rbp_object_movements_account_97",
|
||||
purpose: "collect_rbp_object_movements",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, ASSISTANT_MCP_LIVE_LIMIT),
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97"]
|
||||
},
|
||||
{
|
||||
call_id: "find_month_close_entries_linked_to_rbp",
|
||||
purpose: "link_month_close_to_rbp",
|
||||
query: buildLiveRangeQuery(primaryFrom, primaryTo, ASSISTANT_MCP_LIVE_LIMIT),
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
},
|
||||
{
|
||||
call_id: "compute_end_period_residual_by_rbp_object",
|
||||
purpose: "collect_residual_tail_signals",
|
||||
query: buildLiveRangeQuery(carryFrom, carryTo, ASSISTANT_MCP_LIVE_LIMIT),
|
||||
required_for_claim: true,
|
||||
account_scope_override: ["97", "20", "25", "26", "44"]
|
||||
}
|
||||
],
|
||||
route_gap_reason: null
|
||||
};
|
||||
}
|
||||
|
||||
function detectBroadQuery(fragmentText: string, route: string): BroadQueryAssessment {
|
||||
const text = String(fragmentText ?? "").trim();
|
||||
const lower = text.toLowerCase();
|
||||
@@ -2587,79 +2769,170 @@ export class AssistantDataLayer {
|
||||
}
|
||||
|
||||
private async fetchLiveMcpOverlay(route: string, fragmentText: string): Promise<LiveMcpOverlay> {
|
||||
const accountScope = extractAccountScopeFromText(fragmentText);
|
||||
const endpoint = this.buildMcpUrl("/api/execute_query");
|
||||
const query = MCP_LIVE_MOVEMENTS_QUERY_TEMPLATE.replace("__LIMIT__", String(ASSISTANT_MCP_LIVE_LIMIT));
|
||||
const livePlan = buildLiveMcpCallPlan(route, fragmentText);
|
||||
const explicitAccountScope = extractAccountScopeFromText(fragmentText);
|
||||
const accountScope =
|
||||
explicitAccountScope.length > 0
|
||||
? explicitAccountScope
|
||||
: livePlan.claim_type === "prove_rbp_tail_state"
|
||||
? ["97", "20", "25", "26", "44"]
|
||||
: [];
|
||||
const callExecutions: LiveMcpCallExecution[] = [];
|
||||
const collectedRows: Array<Record<string, unknown>> = [];
|
||||
const errors: string[] = [];
|
||||
let fetchedRowsTotal = 0;
|
||||
let matchedRowsTotal = 0;
|
||||
|
||||
const payload = await this.fetchJsonWithTimeout(endpoint, {
|
||||
query,
|
||||
limit: ASSISTANT_MCP_LIVE_LIMIT
|
||||
});
|
||||
for (const call of livePlan.calls) {
|
||||
const callAccountScope =
|
||||
Array.isArray(call.account_scope_override) && call.account_scope_override.length > 0
|
||||
? call.account_scope_override
|
||||
: accountScope;
|
||||
try {
|
||||
const payload = await this.fetchJsonWithTimeout(endpoint, {
|
||||
query: call.query,
|
||||
limit: ASSISTANT_MCP_LIVE_LIMIT
|
||||
});
|
||||
const parsed = this.parseExecuteQueryPayload(payload);
|
||||
if (parsed.error) {
|
||||
errors.push(parsed.error);
|
||||
callExecutions.push({
|
||||
call_id: call.call_id,
|
||||
purpose: call.purpose,
|
||||
required_for_claim: call.required_for_claim,
|
||||
status: "error",
|
||||
fetched_rows: 0,
|
||||
matched_rows: 0,
|
||||
returned_rows: 0,
|
||||
error: parsed.error
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed = this.parseExecuteQueryPayload(payload);
|
||||
if (parsed.error) {
|
||||
return {
|
||||
status: "error",
|
||||
items: [],
|
||||
evidence: [],
|
||||
summary: {
|
||||
enabled: true,
|
||||
const matchedRows = this.filterLiveRowsByAccountScope(parsed.rows, callAccountScope);
|
||||
const rowsForAnswer = callAccountScope.length > 0 ? matchedRows : parsed.rows;
|
||||
fetchedRowsTotal += parsed.rows.length;
|
||||
matchedRowsTotal += matchedRows.length;
|
||||
for (const row of rowsForAnswer) {
|
||||
collectedRows.push({
|
||||
...row,
|
||||
__live_call_id: call.call_id,
|
||||
__live_call_purpose: call.purpose,
|
||||
__claim_type: livePlan.claim_type,
|
||||
__query_subject: livePlan.query_subject,
|
||||
__account_scope_applied: callAccountScope
|
||||
});
|
||||
}
|
||||
callExecutions.push({
|
||||
call_id: call.call_id,
|
||||
purpose: call.purpose,
|
||||
required_for_claim: call.required_for_claim,
|
||||
status: rowsForAnswer.length > 0 ? "ok" : "empty",
|
||||
fetched_rows: parsed.rows.length,
|
||||
matched_rows: matchedRows.length,
|
||||
returned_rows: rowsForAnswer.length,
|
||||
error: null
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
errors.push(errorMessage);
|
||||
callExecutions.push({
|
||||
call_id: call.call_id,
|
||||
purpose: call.purpose,
|
||||
required_for_claim: call.required_for_claim,
|
||||
status: "error",
|
||||
route,
|
||||
channel: ASSISTANT_MCP_CHANNEL,
|
||||
proxy: ASSISTANT_MCP_PROXY_URL,
|
||||
account_scope: accountScope,
|
||||
error: parsed.error
|
||||
},
|
||||
selection_reason: ["Live MCP probe завершился ошибкой, использован snapshot fallback."],
|
||||
limitations: ["Live MCP недоступен или вернул ошибку; результат ограничен локальным snapshot."],
|
||||
errors: [parsed.error]
|
||||
};
|
||||
fetched_rows: 0,
|
||||
matched_rows: 0,
|
||||
returned_rows: 0,
|
||||
error: errorMessage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const matchedRows = this.filterLiveRowsByAccountScope(parsed.rows, accountScope);
|
||||
const rowsForAnswer = matchedRows.length > 0 || accountScope.length === 0 ? matchedRows : parsed.rows;
|
||||
const items = this.toLiveOverlayItems(rowsForAnswer.slice(0, 12), route);
|
||||
const evidence = items.slice(0, 8).map((item) => ({
|
||||
const items = this.toLiveOverlayItems(collectedRows.slice(0, 16), route);
|
||||
const evidence = items.slice(0, 12).map((item) => ({
|
||||
source_entity: item.source_entity,
|
||||
source_id: item.source_id,
|
||||
source_namespace: "assistant_derived",
|
||||
period: item.period,
|
||||
account_debit: item.account_debit,
|
||||
account_credit: item.account_credit
|
||||
account_credit: item.account_credit,
|
||||
source_layer: item.source_layer,
|
||||
document_context: item.document_context,
|
||||
relation_pattern_hits: item.relation_pattern_hits,
|
||||
lifecycle_markers: item.lifecycle_markers,
|
||||
live_call_id: item.live_call_id,
|
||||
live_call_purpose: item.live_call_purpose,
|
||||
claim_type: item.claim_type
|
||||
}));
|
||||
|
||||
const executedRequiredCalls = callExecutions
|
||||
.filter((item) => item.required_for_claim && item.status !== "error")
|
||||
.map((item) => item.call_id);
|
||||
const missingLiveCalls = livePlan.required_live_calls.filter((callId) => !executedRequiredCalls.includes(callId));
|
||||
const liveRouteExecutionRate =
|
||||
livePlan.required_live_calls.length > 0
|
||||
? Number((executedRequiredCalls.length / livePlan.required_live_calls.length).toFixed(4))
|
||||
: 1;
|
||||
const routeGapReason =
|
||||
missingLiveCalls.length > 0
|
||||
? "required_live_calls_not_executed"
|
||||
: livePlan.claim_type && matchedRowsTotal <= 0
|
||||
? "claim_live_calls_executed_but_zero_matches"
|
||||
: livePlan.route_gap_reason;
|
||||
|
||||
const selectionReason = [
|
||||
`Live MCP probe: ${parsed.rows.length} rows fetched from 1C register.`,
|
||||
livePlan.claim_type
|
||||
? `Claim-bound live path selected for ${livePlan.claim_type}.`
|
||||
: `Live MCP probe: ${fetchedRowsTotal} rows fetched from 1C register.`,
|
||||
accountScope.length > 0
|
||||
? `Account scope filter (${accountScope.join(", ")}) matched ${matchedRows.length} rows.`
|
||||
? `Account scope filter (${accountScope.join(", ")}) matched ${matchedRowsTotal} rows.`
|
||||
: "Account scope filter was not applied."
|
||||
];
|
||||
|
||||
const limitations: string[] = [
|
||||
"Live probe использует ограниченный выборочный read-only запрос к 1С."
|
||||
];
|
||||
if (missingLiveCalls.length > 0) {
|
||||
limitations.push(`Required live calls were not executed: ${missingLiveCalls.join(", ")}.`);
|
||||
}
|
||||
if (items.length === 0) {
|
||||
limitations.push("Live probe не вернул строк, релевантных текущему запросу.");
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
limitations.push("Часть live вызовов завершилась ошибкой; включен ограниченный fallback.");
|
||||
}
|
||||
|
||||
const status: LiveMcpOverlay["status"] =
|
||||
items.length > 0 ? "ok" : callExecutions.every((item) => item.status === "error") ? "error" : "empty";
|
||||
|
||||
return {
|
||||
status: items.length > 0 ? "ok" : "empty",
|
||||
status,
|
||||
items,
|
||||
evidence,
|
||||
summary: {
|
||||
enabled: true,
|
||||
status: items.length > 0 ? "ok" : "empty",
|
||||
status,
|
||||
route,
|
||||
channel: ASSISTANT_MCP_CHANNEL,
|
||||
proxy: ASSISTANT_MCP_PROXY_URL,
|
||||
source_profile: livePlan.claim_type ? "claim_bound_rbp_live_path" : "generic_live_probe",
|
||||
claim_type: livePlan.claim_type,
|
||||
query_subject: livePlan.query_subject,
|
||||
account_scope: accountScope,
|
||||
fetched_rows: parsed.rows.length,
|
||||
matched_rows: matchedRows.length,
|
||||
returned_rows: items.length
|
||||
fetched_rows: fetchedRowsTotal,
|
||||
matched_rows: matchedRowsTotal,
|
||||
returned_rows: items.length,
|
||||
required_live_calls: livePlan.required_live_calls,
|
||||
executed_live_calls: callExecutions,
|
||||
missing_live_calls: missingLiveCalls,
|
||||
live_route_execution_rate: liveRouteExecutionRate,
|
||||
route_gap_reason: routeGapReason
|
||||
},
|
||||
selection_reason: selectionReason,
|
||||
limitations,
|
||||
errors: []
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2831,6 +3104,37 @@ export class AssistantDataLayer {
|
||||
const baseId = `${route}-mcp-${index + 1}`;
|
||||
const sourceId = periodRaw ? `${baseId}-${periodRaw}` : baseId;
|
||||
const displayName = registrator || `Live movement row #${index + 1}`;
|
||||
const accountContext = uniqueStrings([debit, credit].filter((item) => item.length > 0));
|
||||
const callId = valueAsString(row.__live_call_id ?? "").trim();
|
||||
const callPurpose = valueAsString(row.__live_call_purpose ?? "").trim();
|
||||
const claimType = valueAsString(row.__claim_type ?? "").trim() || null;
|
||||
const querySubject = valueAsString(row.__query_subject ?? "").trim() || null;
|
||||
const registratorLower = registrator.toLowerCase();
|
||||
const hasRbpByDocument = /(?:рбп|deferred|списани[ея]\s+рбп)/i.test(registratorLower);
|
||||
const hasAccount97 = accountContext.some((item) => /^97(?:\.|$)/.test(item));
|
||||
const hasCloseDoc =
|
||||
/(?:закрыти[ея]\s+месяц|period\s*close|month\s*close|close\s+operation)/i.test(registratorLower) ||
|
||||
callId.includes("month_close");
|
||||
const relationPatternHits = uniqueStrings([
|
||||
"document_to_posting",
|
||||
hasRbpByDocument || hasAccount97 ? "deferred_expense_to_writeoff" : "",
|
||||
hasCloseDoc ? "close_operation" : "",
|
||||
callId.includes("residual") ? "residuals_zero_or_explained" : ""
|
||||
]);
|
||||
const documentContext = uniqueStrings([
|
||||
hasRbpByDocument || hasAccount97 ? "deferred_expense_document" : "",
|
||||
hasCloseDoc ? "period_close_document" : "",
|
||||
"posting"
|
||||
]);
|
||||
const graphDomainScope = uniqueStrings([
|
||||
hasRbpByDocument || hasAccount97 ? "deferred_expense" : "",
|
||||
hasCloseDoc ? "period_close" : ""
|
||||
]);
|
||||
const lifecycleMarkers = uniqueStrings([
|
||||
callId.includes("residual") ? "period_boundary" : "",
|
||||
callId.includes("residual") ? "tail_state_observed" : "",
|
||||
hasCloseDoc ? "close_operation" : ""
|
||||
]);
|
||||
return {
|
||||
source_entity: "MCPLiveMovement",
|
||||
source_id: sourceId,
|
||||
@@ -2838,6 +3142,16 @@ export class AssistantDataLayer {
|
||||
period: periodRaw || null,
|
||||
account_debit: debit || null,
|
||||
account_credit: credit || null,
|
||||
account_context: accountContext,
|
||||
document_context: documentContext,
|
||||
relation_pattern_hits: relationPatternHits,
|
||||
graph_domain_scope: graphDomainScope,
|
||||
lifecycle_markers: lifecycleMarkers,
|
||||
source_namespace: "assistant_derived",
|
||||
live_call_id: callId || null,
|
||||
live_call_purpose: callPurpose || null,
|
||||
claim_type: claimType,
|
||||
query_subject: querySubject,
|
||||
amount,
|
||||
source_layer: "mcp_live_probe",
|
||||
route
|
||||
|
||||
@@ -582,6 +582,159 @@ function toExecutionPlan(routeSummary, normalized, userMessage, requirementByFra
|
||||
};
|
||||
});
|
||||
}
|
||||
function enrichRbpFragmentForLive(fragmentText, temporalGuard) {
|
||||
const base = compactWhitespace(String(fragmentText ?? ""));
|
||||
const hints = ["Списание РБП", "объект РБП", "остаток на конец периода", "счет 97"];
|
||||
const effective = temporalGuard && typeof temporalGuard === "object" ? temporalGuard.effective_primary_period : null;
|
||||
if (effective && effective.from && effective.to) {
|
||||
hints.push(`период ${effective.from}..${effective.to}`);
|
||||
}
|
||||
const hintText = hints.filter(Boolean).join(", ");
|
||||
if (!base) {
|
||||
return hintText;
|
||||
}
|
||||
if (/списани[ея]\s+рбп|счет\s*97|account\s*97|остат/i.test(base)) {
|
||||
return base;
|
||||
}
|
||||
return `${base}; ${hintText}`;
|
||||
}
|
||||
function enforceRbpLiveRoutePlan(input) {
|
||||
if (input.claimType !== "prove_rbp_tail_state") {
|
||||
return {
|
||||
executionPlan: input.executionPlan,
|
||||
audit: null
|
||||
};
|
||||
}
|
||||
const requiredLiveCalls = [
|
||||
"find_rbp_writeoff_documents_in_period",
|
||||
"find_rbp_object_movements_account_97",
|
||||
"find_month_close_entries_linked_to_rbp",
|
||||
"compute_end_period_residual_by_rbp_object"
|
||||
];
|
||||
let routeAdjusted = 0;
|
||||
let rescuedNoRoute = 0;
|
||||
const replacedRoutes = [];
|
||||
const adjustedPlan = input.executionPlan.map((item) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
return item;
|
||||
}
|
||||
if (item.should_execute !== true && item.no_route_reason === "insufficient_specificity") {
|
||||
rescuedNoRoute += 1;
|
||||
routeAdjusted += 1;
|
||||
return {
|
||||
...item,
|
||||
route: "live_mcp_drilldown",
|
||||
should_execute: true,
|
||||
no_route_reason: null,
|
||||
clarification_reason: null,
|
||||
fragment_text: enrichRbpFragmentForLive(item.fragment_text, input.temporalGuard)
|
||||
};
|
||||
}
|
||||
if (item.should_execute === true && item.route !== "hybrid_store_plus_live" && item.route !== "live_mcp_drilldown") {
|
||||
routeAdjusted += 1;
|
||||
if (item.route && item.route !== "no_route") {
|
||||
replacedRoutes.push(String(item.route));
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
route: "hybrid_store_plus_live",
|
||||
fragment_text: enrichRbpFragmentForLive(item.fragment_text, input.temporalGuard)
|
||||
};
|
||||
}
|
||||
if (item.should_execute === true) {
|
||||
return {
|
||||
...item,
|
||||
fragment_text: enrichRbpFragmentForLive(item.fragment_text, input.temporalGuard)
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return {
|
||||
executionPlan: adjustedPlan,
|
||||
audit: {
|
||||
claim_type: "prove_rbp_tail_state",
|
||||
required_live_calls: requiredLiveCalls,
|
||||
route_adjustments_applied: routeAdjusted,
|
||||
rescued_no_route_fragments: rescuedNoRoute,
|
||||
replaced_routes: Array.from(new Set(replacedRoutes)),
|
||||
route_gap_reason: routeAdjusted > 0 ? "rbp_claim_bound_live_route_override_applied" : null
|
||||
}
|
||||
};
|
||||
}
|
||||
function collectRbpLiveRouteAudit(input) {
|
||||
if (input.claimType !== "prove_rbp_tail_state") {
|
||||
return null;
|
||||
}
|
||||
const required = new Set(Array.isArray(input.planAudit?.required_live_calls) ? input.planAudit.required_live_calls : []);
|
||||
const executed = [];
|
||||
const missing = new Set();
|
||||
const routeGaps = [];
|
||||
let matchedRowsTotal = 0;
|
||||
let returnedRowsTotal = 0;
|
||||
let fetchedRowsTotal = 0;
|
||||
for (const result of input.retrievalResults) {
|
||||
if (!result || typeof result !== "object") {
|
||||
continue;
|
||||
}
|
||||
const summary = result.summary && typeof result.summary === "object" ? result.summary : null;
|
||||
const live = summary && typeof summary.live_mcp === "object" && summary.live_mcp ? summary.live_mcp : null;
|
||||
if (!live) {
|
||||
continue;
|
||||
}
|
||||
const requiredCalls = Array.isArray(live.required_live_calls) ? live.required_live_calls : [];
|
||||
for (const callId of requiredCalls) {
|
||||
required.add(String(callId ?? "").trim());
|
||||
}
|
||||
const executedCalls = Array.isArray(live.executed_live_calls) ? live.executed_live_calls : [];
|
||||
for (const call of executedCalls) {
|
||||
if (!call || typeof call !== "object") {
|
||||
continue;
|
||||
}
|
||||
executed.push(call);
|
||||
}
|
||||
const missingCalls = Array.isArray(live.missing_live_calls) ? live.missing_live_calls : [];
|
||||
for (const callId of missingCalls) {
|
||||
const token = String(callId ?? "").trim();
|
||||
if (token) {
|
||||
missing.add(token);
|
||||
}
|
||||
}
|
||||
const routeGapReason = String(live.route_gap_reason ?? "").trim();
|
||||
if (routeGapReason) {
|
||||
routeGaps.push(routeGapReason);
|
||||
}
|
||||
fetchedRowsTotal += Number(live.fetched_rows ?? 0) || 0;
|
||||
matchedRowsTotal += Number(live.matched_rows ?? 0) || 0;
|
||||
returnedRowsTotal += Number(live.returned_rows ?? 0) || 0;
|
||||
}
|
||||
const requiredList = Array.from(required).filter(Boolean);
|
||||
const executedList = executed;
|
||||
const missingFromExecuted = requiredList.filter((callId) => !executedList.some((item) => String(item.call_id ?? "") === callId));
|
||||
for (const callId of missingFromExecuted) {
|
||||
missing.add(callId);
|
||||
}
|
||||
const missingList = Array.from(missing);
|
||||
const routeGapReason = missingList.length > 0
|
||||
? "required_live_calls_not_executed"
|
||||
: matchedRowsTotal <= 0
|
||||
? "claim_live_calls_executed_but_zero_matches"
|
||||
: routeGaps[0] ?? null;
|
||||
const executionRate = requiredList.length > 0
|
||||
? Number(((requiredList.length - missingList.length) / requiredList.length).toFixed(4))
|
||||
: 1;
|
||||
return {
|
||||
claim_type: "prove_rbp_tail_state",
|
||||
required_live_calls: requiredList,
|
||||
executed_live_calls: executedList,
|
||||
missing_live_calls: missingList,
|
||||
route_gap_reason: routeGapReason,
|
||||
live_route_execution_rate: executionRate,
|
||||
fetched_rows_total: fetchedRowsTotal,
|
||||
matched_rows_total: matchedRowsTotal,
|
||||
returned_rows_total: returnedRowsTotal,
|
||||
plan_override: input.planAudit ?? null
|
||||
};
|
||||
}
|
||||
function toDebugRoutes(routeSummary) {
|
||||
if (!routeSummary) {
|
||||
return [];
|
||||
@@ -1433,6 +1586,12 @@ export class AssistantService {
|
||||
const resolvedRouteSummary = businessScopeResolution.route_summary_resolved;
|
||||
const requirementExtraction = extractRequirements(resolvedRouteSummary, normalized.normalized, userMessage);
|
||||
let executionPlan = toExecutionPlan(resolvedRouteSummary, normalized.normalized, userMessage, requirementExtraction.byFragment);
|
||||
const rbpRoutePlanEnforcement = enforceRbpLiveRoutePlan({
|
||||
executionPlan,
|
||||
claimType: claimAnchorAudit.claim_type,
|
||||
temporalGuard
|
||||
});
|
||||
executionPlan = rbpRoutePlanEnforcement.executionPlan;
|
||||
executionPlan = (0, assistantRuntimeGuards_1.applyTemporalHintToExecutionPlan)(executionPlan, temporalGuard);
|
||||
executionPlan = (0, assistantRuntimeGuards_1.applyPolarityHintToExecutionPlan)(executionPlan, domainPolarityGuardInitial);
|
||||
const retrievalCalls = [];
|
||||
@@ -1515,6 +1674,11 @@ export class AssistantService {
|
||||
userMessage
|
||||
});
|
||||
retrievalResults = evidenceGateResult.retrievalResults;
|
||||
const rbpLiveRouteAudit = collectRbpLiveRouteAudit({
|
||||
claimType: claimAnchorAudit.claim_type,
|
||||
retrievalResults,
|
||||
planAudit: rbpRoutePlanEnforcement.audit
|
||||
});
|
||||
const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
|
||||
const groundingCheckBase = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
|
||||
const groundedAnswerEligibilityGuard = (0, assistantRuntimeGuards_1.evaluateGroundedAnswerEligibility)({
|
||||
@@ -1627,6 +1791,7 @@ export class AssistantService {
|
||||
claim_anchor_audit: claimAnchorAudit,
|
||||
targeted_evidence_acquisition: targetedEvidenceResult.audit,
|
||||
evidence_admissibility_gate: evidenceGateResult.audit,
|
||||
...(rbpLiveRouteAudit ? { rbp_live_route_audit: rbpLiveRouteAudit } : {}),
|
||||
eligibility_time_basis: groundedAnswerEligibilityGuard.eligibility_time_basis,
|
||||
grounded_answer_eligibility_guard: groundedAnswerEligibilityGuard,
|
||||
...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
|
||||
|
||||
@@ -207,6 +207,19 @@ export interface GroundedAnswerEligibilityGuardDebug {
|
||||
reason_codes: string[];
|
||||
}
|
||||
|
||||
export interface RbpLiveRouteAuditDebug {
|
||||
claim_type: "prove_rbp_tail_state";
|
||||
required_live_calls: string[];
|
||||
executed_live_calls: Array<Record<string, unknown>>;
|
||||
missing_live_calls: string[];
|
||||
route_gap_reason: string | null;
|
||||
live_route_execution_rate: number;
|
||||
fetched_rows_total: number;
|
||||
matched_rows_total: number;
|
||||
returned_rows_total: number;
|
||||
plan_override: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface AssistantMessageRequestPayload {
|
||||
session_id?: string;
|
||||
user_message?: string;
|
||||
@@ -302,6 +315,7 @@ export interface AssistantDebugPayload {
|
||||
claim_anchor_audit?: ClaimBoundAnchorAuditDebug;
|
||||
targeted_evidence_acquisition?: TargetedEvidenceAcquisitionDebug;
|
||||
evidence_admissibility_gate?: EvidenceAdmissibilityGateDebug;
|
||||
rbp_live_route_audit?: RbpLiveRouteAuditDebug;
|
||||
eligibility_time_basis?: GroundedAnswerEligibilityGuardDebug["eligibility_time_basis"];
|
||||
grounded_answer_eligibility_guard?: GroundedAnswerEligibilityGuardDebug;
|
||||
followup_state_usage?: FollowupStateUsageDebug;
|
||||
|
||||
Reference in New Issue
Block a user