Укрепить debt-boundary ответы MCP discovery

This commit is contained in:
dctouch 2026-06-15 23:46:56 +03:00
parent ee98b04706
commit fa1731a5b4
14 changed files with 418 additions and 24 deletions

View File

@ -4317,11 +4317,10 @@ class AddressQueryService {
rowsMatched: expandedFilteredRows.length rowsMatched: expandedFilteredRows.length
}); });
const expandedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, expandedFilteredRows, composeRuntimeOptions(expandedLimitFilters)); const expandedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, expandedFilteredRows, composeRuntimeOptions(expandedLimitFilters));
const expandedPrefix = `Период сохранен. Глубина live-выборки автоматически расширена до ${expandedPlan.limit} строк.`;
const expandedLimitations = [...filters.warnings, "query_limit_auto_expanded_for_anchor_recovery"]; const expandedLimitations = [...filters.warnings, "query_limit_auto_expanded_for_anchor_recovery"];
const expandedReasons = [...baseReasons, "query_limit_auto_expanded_for_anchor_recovery"]; const expandedReasons = [...baseReasons, "query_limit_auto_expanded_for_anchor_recovery"];
return buildFactualExecutionResult({ return buildFactualExecutionResult({
replyText: `${expandedPrefix}\n${expandedFactual.text}`, replyText: expandedFactual.text,
responseType: expandedFactual.responseType, responseType: expandedFactual.responseType,
responseSemantics: expandedFactual.semantics, responseSemantics: expandedFactual.semantics,
selectedRecipe: expandedSelection.selected_recipe.recipe_id, selectedRecipe: expandedSelection.selected_recipe.recipe_id,

View File

@ -582,7 +582,8 @@ function runAssistantAddressLaneResponseRuntime(input) {
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate, mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied, mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
comparison_scope_response_augmentation_v1: comparisonScopeProofReply?.audit ?? null, comparison_scope_response_augmentation_v1: comparisonScopeProofReply?.audit ?? null,
...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied) ...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied),
...(0, assistantMcpDiscoveryResponsePolicy_1.buildAppliedMcpDiscoveryTruthPatch)(mcpDiscoveryResponsePolicy, debugWithResponseGuard)
}; };
const finalization = finalizeAddressTurnSafe({ const finalization = finalizeAddressTurnSafe({
sessionId: input.sessionId, sessionId: input.sessionId,

View File

@ -142,7 +142,8 @@ function runAssistantDeepTurnResponseRuntime(input) {
...packagingRuntime.debug, ...packagingRuntime.debug,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy, mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate, mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
...(0, assistantMcpDiscoveryResponsePolicy_1.buildAppliedMcpDiscoveryTruthPatch)(mcpDiscoveryResponsePolicy, packagingRuntime.debug)
}; };
const assistantItem = mcpDiscoveryResponsePolicy.applied const assistantItem = mcpDiscoveryResponsePolicy.applied
? { ? {

View File

@ -362,6 +362,7 @@ async function runAssistantLivingChatRuntime(input) {
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy, mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate, mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied, mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
...(0, assistantMcpDiscoveryResponsePolicy_1.buildAppliedMcpDiscoveryTruthPatch)(mcpDiscoveryResponsePolicy, addressRuntimeMeta),
living_chat_script_guard_applied: livingChatScriptGuardApplied, living_chat_script_guard_applied: livingChatScriptGuardApplied,
living_chat_script_guard_reason: livingChatScriptGuardReason, living_chat_script_guard_reason: livingChatScriptGuardReason,
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied, living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,

View File

@ -73,6 +73,18 @@ function requestsBusinessOverviewNextChecks(turnMeaning, graph) {
const text = normalizedTurnAndGraphText(turnMeaning, graph); const text = normalizedTurnAndGraphText(turnMeaning, graph);
return /(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(text); return /(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(text);
} }
function requestsShortDebtBoundaryFollowup(turnMeaning, graph) {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
if (!/\u043f\u0440\u043e\u0441\u0440\u043e\u0447/iu.test(text)) {
return false;
}
return /(?:\u043a\u043e\u0440\u043e\u0442\u043a|\u043f\u043e\u0447\u0435\u043c\u0443|\u0434\u043e\u043a\u0430\u0437\p{L}*)/iu.test(text);
}
function requestsCompactCashflowAnswer(turnMeaning, graph) { function requestsCompactCashflowAnswer(turnMeaning, graph) {
const text = normalizeQuestionText([ const text = normalizeQuestionText([
turnMeaning?.raw_message, turnMeaning?.raw_message,
@ -902,6 +914,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary"; const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary"; const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary"; const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
const shortDebtBoundaryFollowup = debtDueDateBoundary && requestsShortDebtBoundaryFollowup(turnMeaning, graph);
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph); const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph); const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph); const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
@ -1035,19 +1048,20 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const rowsWithPaymentTerms = typeof dueDateAging.rows_with_payment_terms === "number" && Number.isFinite(dueDateAging.rows_with_payment_terms) const rowsWithPaymentTerms = typeof dueDateAging.rows_with_payment_terms === "number" && Number.isFinite(dueDateAging.rows_with_payment_terms)
? dueDateAging.rows_with_payment_terms ? dueDateAging.rows_with_payment_terms
: null; : null;
const rowsWithAmount = typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount)
? dueDateAging.rows_with_amount
: null;
const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : ""; const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : "";
if (status === "confirmed_overdue") { if (status === "confirmed_overdue") {
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`); lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`);
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора."); lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора.");
} }
else if (status === "no_payment_terms_configured") { else if (status === "no_payment_terms_configured") {
if (shortDebtBoundaryFollowup) {
lines.push(`Коротко: да, доказать просрочку ${dueDateScopePrefix}на ${asOfDate} нельзя, потому что в подтвержденных данных не установлен срок оплаты.`);
lines.push(`Открытый долг${grossAmount ? ` на ${grossAmount}` : ""} виден, но без due date это задолженность без доказанной договорной просрочки.`);
return joinBusinessReplyLines(lines);
}
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`); lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`);
lines.push(rowsWithAmount !== null lines.push("Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.");
? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.` lines.push("Что проверить дальше: сроки оплаты и даты документов-оснований в договорах/расчетах; без них просрочку доказать нельзя.");
: "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.");
} }
else if (status === "insufficient_due_date_basis") { else if (status === "insufficient_due_date_basis") {
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`); lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`);

View File

@ -1,9 +1,111 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION = void 0; exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION = void 0;
exports.buildAppliedMcpDiscoveryTruthPatch = buildAppliedMcpDiscoveryTruthPatch;
exports.applyAssistantMcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy; exports.applyAssistantMcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy;
const assistantMcpDiscoveryResponseCandidate_1 = require("./assistantMcpDiscoveryResponseCandidate"); const assistantMcpDiscoveryResponseCandidate_1 = require("./assistantMcpDiscoveryResponseCandidate");
const assistantRuntimeContracts_1 = require("../types/assistantRuntimeContracts");
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION = "assistant_mcp_discovery_response_policy_v1"; exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION = "assistant_mcp_discovery_response_policy_v1";
function uniqueReasonCodes(values) {
const result = [];
values.forEach((value) => {
const text = toNonEmptyString(value);
if (!text) {
return;
}
const normalized = normalizeReasonCode(text);
if (normalized && !result.includes(normalized)) {
result.push(normalized);
}
});
return result;
}
function patchCoverageGateState(value, gate) {
const source = toRecordObject(value);
if (!source) {
return value;
}
const currentGate = toRecordObject(source.coverage_gate_state);
return {
...source,
coverage_gate_state: {
...(currentGate ?? {}),
coverage_status: gate.coverage_status,
evidence_grade: gate.evidence_grade,
grounding_status: gate.grounding_status,
truth_mode: gate.truth_mode,
carryover_eligibility: gate.carryover_eligibility,
reason_codes: gate.reason_codes
}
};
}
function buildAppliedMcpDiscoveryTruthPatch(policy, currentDebug) {
if (!policy.applied || !ALLOWED_CANDIDATE_STATUSES.has(policy.candidate.candidate_status)) {
return {};
}
const reasonCodes = uniqueReasonCodes([
...policy.reason_codes,
"mcp_discovery_response_policy_candidate_applied",
"mcp_discovery_response_candidate_guarded_truth_patch"
]);
const truthGate = {
schema_version: assistantRuntimeContracts_1.ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
coverage_status: "partial",
evidence_grade: "medium",
grounding_status: "partial",
truth_mode: "limited",
carryover_eligibility: "root_only",
reason_codes: reasonCodes,
source_truth_gate_status: "partial_supported",
blocked_or_limited_explanation: "mcp_discovery_guarded_bounded_answer"
};
const answerShape = {
schema_version: assistantRuntimeContracts_1.ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
answer_shape: "limited_with_reason",
reply_type: "partial_coverage",
capability_contract_id: null,
transition_contract_id: null,
may_state_confirmed_facts: true,
must_include_limitation: true,
may_power_followup: true,
required_sections: ["direct_answer", "evidence_window", "limitations"],
downgrade_only: true
};
const truthPolicy = {
schema_version: assistantRuntimeContracts_1.ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
truth_gate: truthGate,
answer_shape: answerShape
};
const debug = currentDebug ?? {};
const patch = {
assistant_truth_answer_policy_v1: truthPolicy,
coverage_gate_contract: truthGate,
answer_shape_contract: answerShape,
truth_gate_contract_status: "partial_supported",
truth_mode: "limited",
carryover_eligibility: "root_only",
answer_shape: "limited_with_reason"
};
const stateTransition = patchCoverageGateState(debug.assistant_state_transition_v1, truthGate);
if (stateTransition !== debug.assistant_state_transition_v1) {
patch.assistant_state_transition_v1 = stateTransition;
}
const stateTransitionContract = patchCoverageGateState(debug.state_transition_contract, truthGate);
if (stateTransitionContract !== debug.state_transition_contract) {
patch.state_transition_contract = stateTransitionContract;
}
const runtimeContract = toRecordObject(debug.assistant_runtime_contract_v1);
if (runtimeContract) {
patch.assistant_runtime_contract_v1 = {
...runtimeContract,
truth_gate_contract_status: "partial_supported"
};
}
return patch;
}
const ALLOWED_CANDIDATE_STATUSES = new Set([ const ALLOWED_CANDIDATE_STATUSES = new Set([
"ready_for_guarded_use", "ready_for_guarded_use",
"checked_sources_only_candidate", "checked_sources_only_candidate",

View File

@ -5278,11 +5278,10 @@ export class AddressQueryService {
expandedFilteredRows, expandedFilteredRows,
composeRuntimeOptions(expandedLimitFilters) composeRuntimeOptions(expandedLimitFilters)
); );
const expandedPrefix = `Период сохранен. Глубина live-выборки автоматически расширена до ${expandedPlan.limit} строк.`;
const expandedLimitations = [...filters.warnings, "query_limit_auto_expanded_for_anchor_recovery"]; const expandedLimitations = [...filters.warnings, "query_limit_auto_expanded_for_anchor_recovery"];
const expandedReasons = [...baseReasons, "query_limit_auto_expanded_for_anchor_recovery"]; const expandedReasons = [...baseReasons, "query_limit_auto_expanded_for_anchor_recovery"];
return buildFactualExecutionResult({ return buildFactualExecutionResult({
replyText: `${expandedPrefix}\n${expandedFactual.text}`, replyText: expandedFactual.text,
responseType: expandedFactual.responseType, responseType: expandedFactual.responseType,
responseSemantics: expandedFactual.semantics, responseSemantics: expandedFactual.semantics,
selectedRecipe: expandedSelection.selected_recipe.recipe_id, selectedRecipe: expandedSelection.selected_recipe.recipe_id,

View File

@ -10,7 +10,10 @@ import {
import { applyAssistantCapabilityBindingResponseGuard } from "./assistantCapabilityBindingResponseGuard"; import { applyAssistantCapabilityBindingResponseGuard } from "./assistantCapabilityBindingResponseGuard";
import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter"; import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter";
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment"; import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy"; import {
applyAssistantMcpDiscoveryResponsePolicy,
buildAppliedMcpDiscoveryTruthPatch
} from "./assistantMcpDiscoveryResponsePolicy";
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver"; import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter"; import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter"; import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter";
@ -712,7 +715,8 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate, mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied, mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
comparison_scope_response_augmentation_v1: comparisonScopeProofReply?.audit ?? null, comparison_scope_response_augmentation_v1: comparisonScopeProofReply?.audit ?? null,
...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied) ...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied),
...buildAppliedMcpDiscoveryTruthPatch(mcpDiscoveryResponsePolicy, debugWithResponseGuard)
}; };
const finalization = finalizeAddressTurnSafe({ const finalization = finalizeAddressTurnSafe({
sessionId: input.sessionId, sessionId: input.sessionId,

View File

@ -20,7 +20,10 @@ import {
finalizeAssistantDeepTurn, finalizeAssistantDeepTurn,
type FinalizeAssistantDeepTurnInput type FinalizeAssistantDeepTurnInput
} from "./assistantDeepTurnFinalizeRuntimeAdapter"; } from "./assistantDeepTurnFinalizeRuntimeAdapter";
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy"; import {
applyAssistantMcpDiscoveryResponsePolicy,
buildAppliedMcpDiscoveryTruthPatch
} from "./assistantMcpDiscoveryResponsePolicy";
import type { ClaimBoundAnchorAudit, TargetedEvidenceAcquisitionAudit } from "./assistantClaimBoundEvidence"; import type { ClaimBoundAnchorAudit, TargetedEvidenceAcquisitionAudit } from "./assistantClaimBoundEvidence";
import type { CompanyAnchorSet } from "./companyAnchorResolver"; import type { CompanyAnchorSet } from "./companyAnchorResolver";
import type { import type {
@ -268,7 +271,11 @@ export function runAssistantDeepTurnResponseRuntime(
...packagingRuntime.debug, ...packagingRuntime.debug,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy, mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate, mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
...buildAppliedMcpDiscoveryTruthPatch(
mcpDiscoveryResponsePolicy,
packagingRuntime.debug as unknown as Record<string, unknown>
)
} as AssistantDebugPayload; } as AssistantDebugPayload;
const assistantItem = mcpDiscoveryResponsePolicy.applied const assistantItem = mcpDiscoveryResponsePolicy.applied
? { ? {

View File

@ -8,7 +8,10 @@ import {
} from "./assistantMemoryRecapPolicy"; } from "./assistantMemoryRecapPolicy";
import { resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy"; import { resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy";
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment"; import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy"; import {
applyAssistantMcpDiscoveryResponsePolicy,
buildAppliedMcpDiscoveryTruthPatch
} from "./assistantMcpDiscoveryResponsePolicy";
export interface AssistantLivingChatSessionScopeInput { export interface AssistantLivingChatSessionScopeInput {
knownOrganizations?: unknown[]; knownOrganizations?: unknown[];
@ -473,6 +476,7 @@ export async function runAssistantLivingChatRuntime(
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy, mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate, mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied, mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
...buildAppliedMcpDiscoveryTruthPatch(mcpDiscoveryResponsePolicy, addressRuntimeMeta),
living_chat_script_guard_applied: livingChatScriptGuardApplied, living_chat_script_guard_applied: livingChatScriptGuardApplied,
living_chat_script_guard_reason: livingChatScriptGuardReason, living_chat_script_guard_reason: livingChatScriptGuardReason,
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied, living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,

View File

@ -117,6 +117,22 @@ function requestsBusinessOverviewNextChecks(
); );
} }
function requestsShortDebtBoundaryFollowup(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
): boolean {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
if (!/\u043f\u0440\u043e\u0441\u0440\u043e\u0447/iu.test(text)) {
return false;
}
return /(?:\u043a\u043e\u0440\u043e\u0442\u043a|\u043f\u043e\u0447\u0435\u043c\u0443|\u0434\u043e\u043a\u0430\u0437\p{L}*)/iu.test(text);
}
function requestsCompactCashflowAnswer( function requestsCompactCashflowAnswer(
turnMeaning: Record<string, unknown> | null, turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null graph: Record<string, unknown> | null
@ -1086,6 +1102,7 @@ function buildCompactBusinessOverviewReply(
actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary"; actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
const inventoryReserveBoundary = const inventoryReserveBoundary =
actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary"; actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
const shortDebtBoundaryFollowup = debtDueDateBoundary && requestsShortDebtBoundaryFollowup(turnMeaning, graph);
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph); const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph); const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph); const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
@ -1259,10 +1276,6 @@ function buildCompactBusinessOverviewReply(
typeof dueDateAging.rows_with_payment_terms === "number" && Number.isFinite(dueDateAging.rows_with_payment_terms) typeof dueDateAging.rows_with_payment_terms === "number" && Number.isFinite(dueDateAging.rows_with_payment_terms)
? dueDateAging.rows_with_payment_terms ? dueDateAging.rows_with_payment_terms
: null; : null;
const rowsWithAmount =
typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount)
? dueDateAging.rows_with_amount
: null;
const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : ""; const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : "";
if (status === "confirmed_overdue") { if (status === "confirmed_overdue") {
lines.push( lines.push(
@ -1270,13 +1283,23 @@ function buildCompactBusinessOverviewReply(
); );
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора."); lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора.");
} else if (status === "no_payment_terms_configured") { } else if (status === "no_payment_terms_configured") {
if (shortDebtBoundaryFollowup) {
lines.push(
`Коротко: да, доказать просрочку ${dueDateScopePrefix}на ${asOfDate} нельзя, потому что в подтвержденных данных не установлен срок оплаты.`
);
lines.push(
`Открытый долг${grossAmount ? ` на ${grossAmount}` : ""} виден, но без due date это задолженность без доказанной договорной просрочки.`
);
return joinBusinessReplyLines(lines);
}
lines.push( lines.push(
`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.` `Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`
); );
lines.push( lines.push(
rowsWithAmount !== null "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой."
? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.` );
: "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой." lines.push(
"Что проверить дальше: сроки оплаты и даты документов-оснований в договорах/расчетах; без них просрочку доказать нельзя."
); );
} else if (status === "insufficient_due_date_basis") { } else if (status === "insufficient_due_date_basis") {
lines.push( lines.push(

View File

@ -4,6 +4,11 @@ import {
type AssistantMcpDiscoveryResponseCandidateStatus type AssistantMcpDiscoveryResponseCandidateStatus
} from "./assistantMcpDiscoveryResponseCandidate"; } from "./assistantMcpDiscoveryResponseCandidate";
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint"; import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
import {
ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
type AssistantCoverageGateState,
type AssistantTruthAnswerPolicyRuntimeContract
} from "../types/assistantRuntimeContracts";
export const ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION = export const ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION =
"assistant_mcp_discovery_response_policy_v1" as const; "assistant_mcp_discovery_response_policy_v1" as const;
@ -31,6 +36,112 @@ export interface AssistantMcpDiscoveryResponsePolicyResult {
reason_codes: string[]; reason_codes: string[];
} }
function uniqueReasonCodes(values: unknown[]): string[] {
const result: string[] = [];
values.forEach((value) => {
const text = toNonEmptyString(value);
if (!text) {
return;
}
const normalized = normalizeReasonCode(text);
if (normalized && !result.includes(normalized)) {
result.push(normalized);
}
});
return result;
}
function patchCoverageGateState(value: unknown, gate: AssistantCoverageGateState): unknown {
const source = toRecordObject(value);
if (!source) {
return value;
}
const currentGate = toRecordObject(source.coverage_gate_state);
return {
...source,
coverage_gate_state: {
...(currentGate ?? {}),
coverage_status: gate.coverage_status,
evidence_grade: gate.evidence_grade,
grounding_status: gate.grounding_status,
truth_mode: gate.truth_mode,
carryover_eligibility: gate.carryover_eligibility,
reason_codes: gate.reason_codes
}
};
}
export function buildAppliedMcpDiscoveryTruthPatch(
policy: AssistantMcpDiscoveryResponsePolicyResult,
currentDebug?: Record<string, unknown> | null
): Record<string, unknown> {
if (!policy.applied || !ALLOWED_CANDIDATE_STATUSES.has(policy.candidate.candidate_status)) {
return {};
}
const reasonCodes = uniqueReasonCodes([
...policy.reason_codes,
"mcp_discovery_response_policy_candidate_applied",
"mcp_discovery_response_candidate_guarded_truth_patch"
]);
const truthGate: AssistantTruthAnswerPolicyRuntimeContract["truth_gate"] = {
schema_version: ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
coverage_status: "partial",
evidence_grade: "medium",
grounding_status: "partial",
truth_mode: "limited",
carryover_eligibility: "root_only",
reason_codes: reasonCodes,
source_truth_gate_status: "partial_supported",
blocked_or_limited_explanation: "mcp_discovery_guarded_bounded_answer"
};
const answerShape: AssistantTruthAnswerPolicyRuntimeContract["answer_shape"] = {
schema_version: ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
answer_shape: "limited_with_reason",
reply_type: "partial_coverage",
capability_contract_id: null,
transition_contract_id: null,
may_state_confirmed_facts: true,
must_include_limitation: true,
may_power_followup: true,
required_sections: ["direct_answer", "evidence_window", "limitations"],
downgrade_only: true
};
const truthPolicy: AssistantTruthAnswerPolicyRuntimeContract = {
schema_version: ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
truth_gate: truthGate,
answer_shape: answerShape
};
const debug = currentDebug ?? {};
const patch: Record<string, unknown> = {
assistant_truth_answer_policy_v1: truthPolicy,
coverage_gate_contract: truthGate,
answer_shape_contract: answerShape,
truth_gate_contract_status: "partial_supported",
truth_mode: "limited",
carryover_eligibility: "root_only",
answer_shape: "limited_with_reason"
};
const stateTransition = patchCoverageGateState(debug.assistant_state_transition_v1, truthGate);
if (stateTransition !== debug.assistant_state_transition_v1) {
patch.assistant_state_transition_v1 = stateTransition;
}
const stateTransitionContract = patchCoverageGateState(debug.state_transition_contract, truthGate);
if (stateTransitionContract !== debug.state_transition_contract) {
patch.state_transition_contract = stateTransitionContract;
}
const runtimeContract = toRecordObject(debug.assistant_runtime_contract_v1);
if (runtimeContract) {
patch.assistant_runtime_contract_v1 = {
...runtimeContract,
truth_gate_contract_status: "partial_supported"
};
}
return patch;
}
const ALLOWED_CANDIDATE_STATUSES = new Set<AssistantMcpDiscoveryResponseCandidateStatus>([ const ALLOWED_CANDIDATE_STATUSES = new Set<AssistantMcpDiscoveryResponseCandidateStatus>([
"ready_for_guarded_use", "ready_for_guarded_use",
"checked_sources_only_candidate", "checked_sources_only_candidate",

View File

@ -235,6 +235,24 @@ describe("assistant deep turn response runtime adapter", () => {
expect(runtime.response.assistant_reply).toContain("Discovery answer"); expect(runtime.response.assistant_reply).toContain("Discovery answer");
expect(runtime.debug?.mcp_discovery_response_applied).toBe(true); expect(runtime.debug?.mcp_discovery_response_applied).toBe(true);
expect(runtime.debug?.truth_gate_contract_status).toBe("partial_supported");
expect(runtime.debug?.truth_mode).toBe("limited");
expect(runtime.debug?.coverage_gate_contract).toEqual(
expect.objectContaining({
coverage_status: "partial",
evidence_grade: "medium",
grounding_status: "partial",
truth_mode: "limited",
source_truth_gate_status: "partial_supported"
})
);
expect(runtime.debug?.answer_shape_contract).toEqual(
expect.objectContaining({
answer_shape: "limited_with_reason",
reply_type: "partial_coverage",
may_state_confirmed_facts: true
})
);
expect(runFinalizeDeepTurn).toHaveBeenCalledWith( expect(runFinalizeDeepTurn).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
assistantReply: expect.stringContaining("Discovery answer"), assistantReply: expect.stringContaining("Discovery answer"),

View File

@ -523,6 +523,116 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("руб.."); expect(candidate.reply_text).not.toContain("руб..");
}); });
it("keeps debt due-date boundary answers useful and hides row counters", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message: "По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
effective_message: "Проверить просроченную дебиторку ООО Альтернатива Плюс на конец 2020.",
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020-12-31"
},
data_need_graph: {
business_fact_family: "business_overview",
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
debt_due_date_aging: {
evidence_status: "no_payment_terms_configured",
as_of_date: "2020-12-31",
gross_open_amount_human_ru: "35 472 380,36 руб.",
rows_with_amount: 46
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Debt boundary.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("35 472 380,36 руб.");
expect(candidate.reply_text).toContain("срок оплаты не установлен");
expect(candidate.reply_text).toContain("нельзя честно назвать эти остатки просрочкой");
expect(candidate.reply_text).toContain("Что проверить дальше");
expect(candidate.reply_text).not.toContain("Проверено строк");
expect(candidate.reply_text).not.toContain("строк с суммой");
});
it("answers short debt boundary follow-ups directly instead of replaying the full debt packet", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message: "То есть просрочку доказать нельзя, коротко почему?",
effective_message: "Коротко объяснить, почему просрочку нельзя доказать.",
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020-12-31"
},
data_need_graph: {
business_fact_family: "business_overview",
question: "То есть просрочку доказать нельзя, коротко почему?",
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
debt_due_date_aging: {
evidence_status: "no_payment_terms_configured",
as_of_date: "2020-12-31",
gross_open_amount_human_ru: "35 472 380,36 руб.",
rows_with_amount: 46
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Debt boundary.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text?.split("\n")[0]).toContain("доказать просрочку");
expect(candidate.reply_text?.split("\n")[0]).toContain("нельзя");
expect(candidate.reply_text).toContain("due date");
expect(candidate.reply_text).not.toContain("Проверено строк");
expect(candidate.reply_text).not.toContain("строк с суммой");
});
it("uses a compact direct answer for business-overview top year questions", () => { it("uses a compact direct answer for business-overview top year questions", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate( const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({ entryPoint({