ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Архитектура маршрутов v2: baseline ожиданий intentrecipe/result_mode с runtime-аудитом
This commit is contained in:
parent
bd1fbbdb67
commit
278eb4abeb
|
|
@ -100,6 +100,13 @@ Route baseline contract:
|
||||||
|
|
||||||
This baseline freezes capability mapping for key intents and acts as anti-regression control when routing evolves.
|
This baseline freezes capability mapping for key intents and acts as anti-regression control when routing evolves.
|
||||||
|
|
||||||
|
Route expectation contract (level 2):
|
||||||
|
|
||||||
|
- `docs/TECH/address_route_expectations_v1.json`
|
||||||
|
- loader/evaluator: `llm_normalizer/backend/src/services/addressRouteExpectations.ts`
|
||||||
|
|
||||||
|
This second-level baseline freezes expected `intent -> selected_recipe/result_mode` semantics and provides runtime audit with optional hard guard.
|
||||||
|
|
||||||
## Why This Is a Foundation, Not a Patch
|
## Why This Is a Foundation, Not a Patch
|
||||||
|
|
||||||
This change does not only tune one scenario. It introduces stable contracts:
|
This change does not only tune one scenario. It introduces stable contracts:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"schema_version": "address_route_expectations_v1",
|
||||||
|
"updated_at": "2026-04-12T13:00:00.000Z",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"intent": "payables_confirmed_as_of_date",
|
||||||
|
"expected_selected_recipes": ["address_payables_confirmed_as_of_date_v1"],
|
||||||
|
"expected_requested_result_modes": ["confirmed_balance"],
|
||||||
|
"expected_result_modes": ["confirmed_balance"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "list_payables_counterparties",
|
||||||
|
"expected_selected_recipes": ["address_movements_payables_v1", "address_open_items_by_party_or_contract_v1"],
|
||||||
|
"expected_requested_result_modes": ["heuristic_candidates", "confirmed_balance"],
|
||||||
|
"expected_result_modes": ["heuristic_candidates", "confirmed_balance"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "list_receivables_counterparties",
|
||||||
|
"expected_selected_recipes": ["address_movements_receivables_v1", "address_open_items_by_party_or_contract_v1"],
|
||||||
|
"expected_requested_result_modes": ["heuristic_candidates", "confirmed_balance"],
|
||||||
|
"expected_result_modes": ["heuristic_candidates", "confirmed_balance"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "account_balance_snapshot",
|
||||||
|
"expected_selected_recipes": ["address_open_items_by_party_or_contract_v1"],
|
||||||
|
"expected_requested_result_modes": ["confirmed_balance"],
|
||||||
|
"expected_result_modes": ["confirmed_balance"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "documents_forming_balance",
|
||||||
|
"expected_selected_recipes": ["address_open_items_by_party_or_contract_v1"],
|
||||||
|
"expected_requested_result_modes": ["confirmed_balance"],
|
||||||
|
"expected_result_modes": ["confirmed_balance"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "list_contracts_by_counterparty",
|
||||||
|
"expected_selected_recipes": ["address_contracts_by_counterparty_v1"],
|
||||||
|
"expected_result_modes": ["heuristic_candidates", "confirmed_balance"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "list_documents_by_counterparty",
|
||||||
|
"expected_selected_recipes": ["address_documents_by_counterparty_v1"],
|
||||||
|
"expected_result_modes": ["heuristic_candidates", "confirmed_balance"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "list_documents_by_contract",
|
||||||
|
"expected_selected_recipes": ["address_documents_by_contract_v1"],
|
||||||
|
"expected_result_modes": ["heuristic_candidates", "confirmed_balance"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "bank_operations_by_counterparty",
|
||||||
|
"expected_selected_recipes": ["address_bank_operations_by_counterparty_v1"],
|
||||||
|
"expected_result_modes": ["heuristic_candidates", "confirmed_balance"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "bank_operations_by_contract",
|
||||||
|
"expected_selected_recipes": ["address_bank_operations_by_contract_v1"],
|
||||||
|
"expected_result_modes": ["heuristic_candidates", "confirmed_balance"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.VAT_PAYABLE_19_PREFIXES = exports.VAT_PAYABLE_68_PREFIXES = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = exports.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1 = exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1 = exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1 = exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 = exports.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1 = exports.FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1 = exports.FEATURE_ASSISTANT_ROUTE_ADDRESS_GENERIC_V1 = exports.FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 = exports.FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0;
|
exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.VAT_PAYABLE_19_PREFIXES = exports.VAT_PAYABLE_68_PREFIXES = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = exports.FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 = exports.FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1 = exports.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1 = exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1 = exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1 = exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 = exports.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1 = exports.FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1 = exports.FEATURE_ASSISTANT_ROUTE_ADDRESS_GENERIC_V1 = exports.FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 = exports.FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0;
|
||||||
exports.MANUAL_CASE_DECISION_SCHEMA_FILE = exports.ASSISTANT_CAPABILITIES_REGISTRY_FILE = exports.ASSISTANT_CANON_FILE = exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.AUTORUN_GENERATOR_HISTORY_FILE = exports.AUTORUN_GENERATOR_DIR = exports.AUTORUN_ANNOTATIONS_FILE = exports.AUTORUN_ANNOTATIONS_DIR = exports.ASSISTANT_ANNOTATIONS_FILE = exports.ASSISTANT_ANNOTATIONS_DIR = void 0;
|
exports.MANUAL_CASE_DECISION_SCHEMA_FILE = exports.ASSISTANT_CAPABILITIES_REGISTRY_FILE = exports.ASSISTANT_CANON_FILE = exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.AUTORUN_GENERATOR_HISTORY_FILE = exports.AUTORUN_GENERATOR_DIR = exports.AUTORUN_ANNOTATIONS_FILE = exports.AUTORUN_ANNOTATIONS_DIR = exports.ASSISTANT_ANNOTATIONS_FILE = exports.ASSISTANT_ANNOTATIONS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = void 0;
|
||||||
const path_1 = __importDefault(require("path"));
|
const path_1 = __importDefault(require("path"));
|
||||||
exports.BACKEND_ROOT = path_1.default.resolve(__dirname, "..");
|
exports.BACKEND_ROOT = path_1.default.resolve(__dirname, "..");
|
||||||
exports.MODULE_ROOT = path_1.default.resolve(exports.BACKEND_ROOT, "..");
|
exports.MODULE_ROOT = path_1.default.resolve(exports.BACKEND_ROOT, "..");
|
||||||
|
|
@ -69,6 +69,8 @@ exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 = toBooleanFlag(process.en
|
||||||
exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1, true);
|
exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1, true);
|
||||||
exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1, true);
|
exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1, true);
|
||||||
exports.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1, false);
|
exports.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1, false);
|
||||||
|
exports.FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1, true);
|
||||||
|
exports.FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1, false);
|
||||||
exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1, true);
|
exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = toBooleanFlag(process.env.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1, true);
|
||||||
exports.ASSISTANT_MCP_PROXY_URL = (process.env.ASSISTANT_MCP_PROXY_URL ?? "http://127.0.0.1:6003").replace(/\/+$/, "");
|
exports.ASSISTANT_MCP_PROXY_URL = (process.env.ASSISTANT_MCP_PROXY_URL ?? "http://127.0.0.1:6003").replace(/\/+$/, "");
|
||||||
exports.ASSISTANT_MCP_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default";
|
exports.ASSISTANT_MCP_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default";
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const decomposeStage_1 = require("./address_runtime/decomposeStage");
|
||||||
const resolveStage_1 = require("./address_runtime/resolveStage");
|
const resolveStage_1 = require("./address_runtime/resolveStage");
|
||||||
const composeStage_1 = require("./address_runtime/composeStage");
|
const composeStage_1 = require("./address_runtime/composeStage");
|
||||||
const addressCapabilityPolicy_1 = require("./addressCapabilityPolicy");
|
const addressCapabilityPolicy_1 = require("./addressCapabilityPolicy");
|
||||||
|
const addressRouteExpectations_1 = require("./addressRouteExpectations");
|
||||||
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"];
|
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"];
|
||||||
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1";
|
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1";
|
||||||
const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000;
|
const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000;
|
||||||
|
|
@ -781,6 +782,30 @@ function buildShadowRouteAudit(input) {
|
||||||
status: "planned"
|
status: "planned"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function buildRouteExpectationAudit(input) {
|
||||||
|
if (!config_1.FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1) {
|
||||||
|
return {
|
||||||
|
status: "not_found",
|
||||||
|
reason: "route_expectation_audit_disabled",
|
||||||
|
expectedSelectedRecipes: [],
|
||||||
|
expectedRequestedResultModes: [],
|
||||||
|
expectedResultModes: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const audit = (0, addressRouteExpectations_1.evaluateAddressRouteExpectation)({
|
||||||
|
intent: input.intent,
|
||||||
|
selectedRecipe: input.selectedRecipe,
|
||||||
|
requestedResultMode: input.requestedResultMode,
|
||||||
|
resultMode: input.resultMode
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
status: audit.status,
|
||||||
|
reason: audit.reason,
|
||||||
|
expectedSelectedRecipes: audit.expected_selected_recipes,
|
||||||
|
expectedRequestedResultModes: audit.expected_requested_result_modes,
|
||||||
|
expectedResultModes: audit.expected_result_modes
|
||||||
|
};
|
||||||
|
}
|
||||||
function enforceStrictAccountScopeForIntent(plan, intent) {
|
function enforceStrictAccountScopeForIntent(plan, intent) {
|
||||||
if (intent !== "list_receivables_counterparties" || plan.account_scope_mode === "strict") {
|
if (intent !== "list_receivables_counterparties" || plan.account_scope_mode === "strict") {
|
||||||
return plan;
|
return plan;
|
||||||
|
|
@ -1395,6 +1420,13 @@ function buildLimitedExecutionResult(input) {
|
||||||
!reasonsWithConfirmedFallback.includes("exact_payables_mode_limited_response")
|
!reasonsWithConfirmedFallback.includes("exact_payables_mode_limited_response")
|
||||||
? [...reasonsWithConfirmedFallback, "exact_payables_mode_limited_response"]
|
? [...reasonsWithConfirmedFallback, "exact_payables_mode_limited_response"]
|
||||||
: reasonsWithConfirmedFallback;
|
: reasonsWithConfirmedFallback;
|
||||||
|
const routeExpectationAudit = input.routeExpectationAudit ??
|
||||||
|
buildRouteExpectationAudit({
|
||||||
|
intent: input.intent.intent,
|
||||||
|
selectedRecipe: input.selectedRecipe,
|
||||||
|
requestedResultMode: requestedResultMode,
|
||||||
|
resultMode: resultSemantics.result_mode
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
handled: true,
|
handled: true,
|
||||||
reply_text: composeLimitedReply({
|
reply_text: composeLimitedReply({
|
||||||
|
|
@ -1453,6 +1485,11 @@ function buildLimitedExecutionResult(input) {
|
||||||
shadow_route_intent: input.shadowRouteAudit?.intent ?? null,
|
shadow_route_intent: input.shadowRouteAudit?.intent ?? null,
|
||||||
shadow_route_selected_recipe: input.shadowRouteAudit?.selectedRecipe ?? null,
|
shadow_route_selected_recipe: input.shadowRouteAudit?.selectedRecipe ?? null,
|
||||||
shadow_route_status: input.shadowRouteAudit?.status ?? "skipped",
|
shadow_route_status: input.shadowRouteAudit?.status ?? "skipped",
|
||||||
|
route_expectation_status: routeExpectationAudit.status,
|
||||||
|
route_expectation_reason: routeExpectationAudit.reason,
|
||||||
|
route_expectation_expected_selected_recipes: routeExpectationAudit.expectedSelectedRecipes,
|
||||||
|
route_expectation_expected_requested_result_modes: routeExpectationAudit.expectedRequestedResultModes,
|
||||||
|
route_expectation_expected_result_modes: routeExpectationAudit.expectedResultModes,
|
||||||
...resultSemantics,
|
...resultSemantics,
|
||||||
limitations: input.limitations,
|
limitations: input.limitations,
|
||||||
reasons
|
reasons
|
||||||
|
|
@ -2451,6 +2488,45 @@ class AddressQueryService {
|
||||||
responseType: factual.responseType,
|
responseType: factual.responseType,
|
||||||
rowsMatched: filteredRows.length
|
rowsMatched: filteredRows.length
|
||||||
}), factual.semantics);
|
}), factual.semantics);
|
||||||
|
const finalRouteExpectationAudit = buildRouteExpectationAudit({
|
||||||
|
intent: intent.intent,
|
||||||
|
selectedRecipe: effectiveRecipeId,
|
||||||
|
requestedResultMode,
|
||||||
|
resultMode: factualResultSemantics.result_mode
|
||||||
|
});
|
||||||
|
if (finalRouteExpectationAudit.status === "mismatch" && config_1.FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1) {
|
||||||
|
return buildLimitedExecutionResult({
|
||||||
|
mode,
|
||||||
|
shape,
|
||||||
|
intent,
|
||||||
|
filters: filters.extracted_filters,
|
||||||
|
missingRequiredFilters: [],
|
||||||
|
selectedRecipe: effectiveRecipeId,
|
||||||
|
accountScopeMode: plan.account_scope_mode,
|
||||||
|
accountScopeFallbackApplied,
|
||||||
|
accountScopeAudit,
|
||||||
|
anchor,
|
||||||
|
matchFailureStage,
|
||||||
|
matchFailureReason,
|
||||||
|
mcpCallStatus: stageStatus,
|
||||||
|
rowsFetched: mcp.fetched_rows,
|
||||||
|
rawRowsReceived: mcp.raw_rows.length,
|
||||||
|
rowsAfterAccountScope: normalizedRows.length,
|
||||||
|
rowsAfterRecipeFilter: filterByAnchors.length,
|
||||||
|
rowsMaterialized: normalizedRows.length,
|
||||||
|
rowsMatched: filteredRows.length,
|
||||||
|
rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
|
||||||
|
materializationDropReason: rowDiagnostics.materializationDropReason,
|
||||||
|
category: "recipe_visibility_gap",
|
||||||
|
reasonText: "маршрут не прошел baseline route expectation contract",
|
||||||
|
nextStep: "проверьте intent/recipe mapping или отключите FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 для безопасного rollout",
|
||||||
|
limitations: ["route_expectation_mismatch_guard_blocked"],
|
||||||
|
reasons: [...baseReasons, `route_expectation_mismatch:${finalRouteExpectationAudit.reason}`],
|
||||||
|
capabilityAudit,
|
||||||
|
shadowRouteAudit,
|
||||||
|
routeExpectationAudit: finalRouteExpectationAudit
|
||||||
|
});
|
||||||
|
}
|
||||||
if (intent.intent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
|
if (intent.intent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
|
||||||
return buildLimitedExecutionResult({
|
return buildLimitedExecutionResult({
|
||||||
mode,
|
mode,
|
||||||
|
|
@ -2480,9 +2556,13 @@ class AddressQueryService {
|
||||||
limitations: ["exact_payables_mode_unconfirmed_output_blocked"],
|
limitations: ["exact_payables_mode_unconfirmed_output_blocked"],
|
||||||
reasons: [...baseReasons, "exact_payables_mode_unconfirmed_output_blocked"],
|
reasons: [...baseReasons, "exact_payables_mode_unconfirmed_output_blocked"],
|
||||||
capabilityAudit,
|
capabilityAudit,
|
||||||
shadowRouteAudit
|
shadowRouteAudit,
|
||||||
|
routeExpectationAudit: finalRouteExpectationAudit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const reasonsWithRouteExpectation = finalRouteExpectationAudit.status === "mismatch"
|
||||||
|
? [...baseReasons, `route_expectation_mismatch:${finalRouteExpectationAudit.reason}`]
|
||||||
|
: baseReasons;
|
||||||
return {
|
return {
|
||||||
handled: true,
|
handled: true,
|
||||||
reply_text: factual.text,
|
reply_text: factual.text,
|
||||||
|
|
@ -2533,9 +2613,14 @@ class AddressQueryService {
|
||||||
shadow_route_intent: shadowRouteAudit.intent,
|
shadow_route_intent: shadowRouteAudit.intent,
|
||||||
shadow_route_selected_recipe: shadowRouteAudit.selectedRecipe,
|
shadow_route_selected_recipe: shadowRouteAudit.selectedRecipe,
|
||||||
shadow_route_status: shadowRouteAudit.status,
|
shadow_route_status: shadowRouteAudit.status,
|
||||||
|
route_expectation_status: finalRouteExpectationAudit.status,
|
||||||
|
route_expectation_reason: finalRouteExpectationAudit.reason,
|
||||||
|
route_expectation_expected_selected_recipes: finalRouteExpectationAudit.expectedSelectedRecipes,
|
||||||
|
route_expectation_expected_requested_result_modes: finalRouteExpectationAudit.expectedRequestedResultModes,
|
||||||
|
route_expectation_expected_result_modes: finalRouteExpectationAudit.expectedResultModes,
|
||||||
...factualResultSemantics,
|
...factualResultSemantics,
|
||||||
limitations: filters.warnings,
|
limitations: filters.warnings,
|
||||||
reasons: withConfirmedBalanceFallbackReason(baseReasons, requestedResultMode, factual.semantics, factualResultSemantics.result_mode)
|
reasons: withConfirmedBalanceFallbackReason(reasonsWithRouteExpectation, requestedResultMode, factual.semantics, factualResultSemantics.result_mode)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.loadAddressRouteExpectationsContract = loadAddressRouteExpectationsContract;
|
||||||
|
exports.evaluateAddressRouteExpectation = evaluateAddressRouteExpectation;
|
||||||
|
const fs_1 = __importDefault(require("fs"));
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
const EXPECTATIONS_FILE = path_1.default.resolve(__dirname, "..", "..", "..", "..", "docs", "TECH", "address_route_expectations_v1.json");
|
||||||
|
function toObject(value) {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
function toNonEmptyString(value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
|
}
|
||||||
|
function toStringArray(value) {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return value.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
|
||||||
|
}
|
||||||
|
function parseResultModes(value) {
|
||||||
|
const raw = toStringArray(value);
|
||||||
|
return raw.filter((mode) => mode === "heuristic_candidates" || mode === "confirmed_balance");
|
||||||
|
}
|
||||||
|
function parseEntry(value) {
|
||||||
|
const object = toObject(value);
|
||||||
|
if (!object) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const intent = toNonEmptyString(object.intent);
|
||||||
|
const expectedSelectedRecipes = toStringArray(object.expected_selected_recipes);
|
||||||
|
if (!intent || expectedSelectedRecipes.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const expectedRequestedResultModes = parseResultModes(object.expected_requested_result_modes);
|
||||||
|
const expectedResultModes = parseResultModes(object.expected_result_modes);
|
||||||
|
return {
|
||||||
|
intent,
|
||||||
|
expected_selected_recipes: expectedSelectedRecipes,
|
||||||
|
...(expectedRequestedResultModes.length > 0 ? { expected_requested_result_modes: expectedRequestedResultModes } : {}),
|
||||||
|
...(expectedResultModes.length > 0 ? { expected_result_modes: expectedResultModes } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function loadAddressRouteExpectationsContract() {
|
||||||
|
const raw = fs_1.default.readFileSync(EXPECTATIONS_FILE, "utf-8");
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const root = toObject(parsed);
|
||||||
|
if (!root) {
|
||||||
|
throw new Error("address_route_expectations_v1: invalid root payload");
|
||||||
|
}
|
||||||
|
const schemaVersion = toNonEmptyString(root.schema_version);
|
||||||
|
if (schemaVersion !== "address_route_expectations_v1") {
|
||||||
|
throw new Error(`address_route_expectations_v1: unexpected schema version '${schemaVersion ?? "null"}'`);
|
||||||
|
}
|
||||||
|
const updatedAt = toNonEmptyString(root.updated_at) ?? new Date().toISOString();
|
||||||
|
const entriesRaw = Array.isArray(root.entries) ? root.entries : [];
|
||||||
|
const entries = entriesRaw.map(parseEntry).filter((entry) => entry !== null);
|
||||||
|
if (entries.length === 0) {
|
||||||
|
throw new Error("address_route_expectations_v1: no valid entries");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
schema_version: "address_route_expectations_v1",
|
||||||
|
updated_at: updatedAt,
|
||||||
|
entries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function evaluateAddressRouteExpectation(input) {
|
||||||
|
const contract = loadAddressRouteExpectationsContract();
|
||||||
|
const entry = contract.entries.find((item) => item.intent === input.intent);
|
||||||
|
if (!entry) {
|
||||||
|
return {
|
||||||
|
status: "not_found",
|
||||||
|
reason: "route_expectation_not_defined_for_intent",
|
||||||
|
expected_selected_recipes: [],
|
||||||
|
expected_requested_result_modes: [],
|
||||||
|
expected_result_modes: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (input.selectedRecipe && !entry.expected_selected_recipes.includes(input.selectedRecipe)) {
|
||||||
|
return {
|
||||||
|
status: "mismatch",
|
||||||
|
reason: "selected_recipe_mismatch",
|
||||||
|
expected_selected_recipes: entry.expected_selected_recipes,
|
||||||
|
expected_requested_result_modes: entry.expected_requested_result_modes ?? [],
|
||||||
|
expected_result_modes: entry.expected_result_modes ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (input.requestedResultMode &&
|
||||||
|
Array.isArray(entry.expected_requested_result_modes) &&
|
||||||
|
entry.expected_requested_result_modes.length > 0 &&
|
||||||
|
!entry.expected_requested_result_modes.includes(input.requestedResultMode)) {
|
||||||
|
return {
|
||||||
|
status: "mismatch",
|
||||||
|
reason: "requested_result_mode_mismatch",
|
||||||
|
expected_selected_recipes: entry.expected_selected_recipes,
|
||||||
|
expected_requested_result_modes: entry.expected_requested_result_modes,
|
||||||
|
expected_result_modes: entry.expected_result_modes ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (input.resultMode &&
|
||||||
|
Array.isArray(entry.expected_result_modes) &&
|
||||||
|
entry.expected_result_modes.length > 0 &&
|
||||||
|
!entry.expected_result_modes.includes(input.resultMode)) {
|
||||||
|
return {
|
||||||
|
status: "mismatch",
|
||||||
|
reason: "result_mode_mismatch",
|
||||||
|
expected_selected_recipes: entry.expected_selected_recipes,
|
||||||
|
expected_requested_result_modes: entry.expected_requested_result_modes ?? [],
|
||||||
|
expected_result_modes: entry.expected_result_modes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: "matched",
|
||||||
|
reason: "route_expectation_matched",
|
||||||
|
expected_selected_recipes: entry.expected_selected_recipes,
|
||||||
|
expected_requested_result_modes: entry.expected_requested_result_modes ?? [],
|
||||||
|
expected_result_modes: entry.expected_result_modes ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1473,6 +1473,11 @@ function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
|
||||||
shadow_route_intent: addressDebug.shadow_route_intent ?? undefined,
|
shadow_route_intent: addressDebug.shadow_route_intent ?? undefined,
|
||||||
shadow_route_selected_recipe: addressDebug.shadow_route_selected_recipe ?? undefined,
|
shadow_route_selected_recipe: addressDebug.shadow_route_selected_recipe ?? undefined,
|
||||||
shadow_route_status: addressDebug.shadow_route_status ?? undefined,
|
shadow_route_status: addressDebug.shadow_route_status ?? undefined,
|
||||||
|
route_expectation_status: addressDebug.route_expectation_status ?? undefined,
|
||||||
|
route_expectation_reason: addressDebug.route_expectation_reason ?? undefined,
|
||||||
|
route_expectation_expected_selected_recipes: addressDebug.route_expectation_expected_selected_recipes ?? undefined,
|
||||||
|
route_expectation_expected_requested_result_modes: addressDebug.route_expectation_expected_requested_result_modes ?? undefined,
|
||||||
|
route_expectation_expected_result_modes: addressDebug.route_expectation_expected_result_modes ?? undefined,
|
||||||
execution_lane: "address_query",
|
execution_lane: "address_query",
|
||||||
llm_decomposition_applied: Boolean(llmMeta?.applied),
|
llm_decomposition_applied: Boolean(llmMeta?.applied),
|
||||||
llm_decomposition_attempted: Boolean(llmMeta?.attempted),
|
llm_decomposition_attempted: Boolean(llmMeta?.attempted),
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,14 @@ export const FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1 = toBooleanFlag(
|
||||||
process.env.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1,
|
process.env.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
export const FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1 = toBooleanFlag(
|
||||||
|
process.env.FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
export const FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 = toBooleanFlag(
|
||||||
|
process.env.FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1,
|
||||||
|
false
|
||||||
|
);
|
||||||
export const FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = toBooleanFlag(
|
export const FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = toBooleanFlag(
|
||||||
process.env.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1,
|
process.env.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1,
|
||||||
true
|
true
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import {
|
import {
|
||||||
FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1,
|
FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1,
|
||||||
|
FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1,
|
||||||
|
FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1,
|
||||||
FEATURE_ASSISTANT_ADDRESS_QUERY_V1,
|
FEATURE_ASSISTANT_ADDRESS_QUERY_V1,
|
||||||
FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1
|
FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1
|
||||||
} from "../config";
|
} from "../config";
|
||||||
|
|
@ -34,6 +36,7 @@ import {
|
||||||
resolveAddressCapabilityRouteDecision,
|
resolveAddressCapabilityRouteDecision,
|
||||||
resolveShadowRouteIntent
|
resolveShadowRouteIntent
|
||||||
} from "./addressCapabilityPolicy";
|
} from "./addressCapabilityPolicy";
|
||||||
|
import { evaluateAddressRouteExpectation, type AddressRouteExpectationAudit } from "./addressRouteExpectations";
|
||||||
|
|
||||||
interface NormalizedAddressRow {
|
interface NormalizedAddressRow {
|
||||||
period: string | null;
|
period: string | null;
|
||||||
|
|
@ -63,6 +66,14 @@ interface AddressShadowRouteAudit {
|
||||||
status: AddressShadowRouteStatus;
|
status: AddressShadowRouteStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AddressRouteExpectationAuditState {
|
||||||
|
status: AddressRouteExpectationAudit["status"];
|
||||||
|
reason: string;
|
||||||
|
expectedSelectedRecipes: string[];
|
||||||
|
expectedRequestedResultModes: AddressResultMode[];
|
||||||
|
expectedResultModes: AddressResultMode[];
|
||||||
|
}
|
||||||
|
|
||||||
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"] as const;
|
const ACCOUNT_SCOPE_FIELDS_CHECKED = ["account_dt", "account_kt", "registrator", "analytics"] as const;
|
||||||
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1" as const;
|
const ACCOUNT_SCOPE_MATCH_STRATEGY = "account_code_regex_plus_alias_map_v1" as const;
|
||||||
const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000;
|
const ADDRESS_ANCHOR_RECOVERY_LIMIT = 1000;
|
||||||
|
|
@ -966,6 +977,36 @@ function buildShadowRouteAudit(input: {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRouteExpectationAudit(input: {
|
||||||
|
intent: AddressIntent;
|
||||||
|
selectedRecipe: string | null;
|
||||||
|
requestedResultMode?: AddressResultMode;
|
||||||
|
resultMode?: AddressResultMode;
|
||||||
|
}): AddressRouteExpectationAuditState {
|
||||||
|
if (!FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1) {
|
||||||
|
return {
|
||||||
|
status: "not_found",
|
||||||
|
reason: "route_expectation_audit_disabled",
|
||||||
|
expectedSelectedRecipes: [],
|
||||||
|
expectedRequestedResultModes: [],
|
||||||
|
expectedResultModes: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const audit = evaluateAddressRouteExpectation({
|
||||||
|
intent: input.intent,
|
||||||
|
selectedRecipe: input.selectedRecipe,
|
||||||
|
requestedResultMode: input.requestedResultMode,
|
||||||
|
resultMode: input.resultMode
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
status: audit.status,
|
||||||
|
reason: audit.reason,
|
||||||
|
expectedSelectedRecipes: audit.expected_selected_recipes,
|
||||||
|
expectedRequestedResultModes: audit.expected_requested_result_modes,
|
||||||
|
expectedResultModes: audit.expected_result_modes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function enforceStrictAccountScopeForIntent(
|
function enforceStrictAccountScopeForIntent(
|
||||||
plan: AddressRecipeExecutionPlan,
|
plan: AddressRecipeExecutionPlan,
|
||||||
intent: AddressIntent
|
intent: AddressIntent
|
||||||
|
|
@ -1765,6 +1806,7 @@ function buildLimitedExecutionResult(input: {
|
||||||
category: AddressLimitedReasonCategory;
|
category: AddressLimitedReasonCategory;
|
||||||
capabilityAudit?: AddressCapabilityAudit;
|
capabilityAudit?: AddressCapabilityAudit;
|
||||||
shadowRouteAudit?: AddressShadowRouteAudit;
|
shadowRouteAudit?: AddressShadowRouteAudit;
|
||||||
|
routeExpectationAudit?: AddressRouteExpectationAuditState;
|
||||||
}): AddressExecutionResult {
|
}): AddressExecutionResult {
|
||||||
const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters);
|
const accountScopeAudit = input.accountScopeAudit ?? buildDefaultAccountScopeAudit(input.filters);
|
||||||
const resultSemantics = deriveAddressResultSemantics({
|
const resultSemantics = deriveAddressResultSemantics({
|
||||||
|
|
@ -1786,6 +1828,14 @@ function buildLimitedExecutionResult(input: {
|
||||||
!reasonsWithConfirmedFallback.includes("exact_payables_mode_limited_response")
|
!reasonsWithConfirmedFallback.includes("exact_payables_mode_limited_response")
|
||||||
? [...reasonsWithConfirmedFallback, "exact_payables_mode_limited_response"]
|
? [...reasonsWithConfirmedFallback, "exact_payables_mode_limited_response"]
|
||||||
: reasonsWithConfirmedFallback;
|
: reasonsWithConfirmedFallback;
|
||||||
|
const routeExpectationAudit =
|
||||||
|
input.routeExpectationAudit ??
|
||||||
|
buildRouteExpectationAudit({
|
||||||
|
intent: input.intent.intent,
|
||||||
|
selectedRecipe: input.selectedRecipe,
|
||||||
|
requestedResultMode: requestedResultMode,
|
||||||
|
resultMode: resultSemantics.result_mode
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
handled: true,
|
handled: true,
|
||||||
reply_text: composeLimitedReply({
|
reply_text: composeLimitedReply({
|
||||||
|
|
@ -1844,6 +1894,11 @@ function buildLimitedExecutionResult(input: {
|
||||||
shadow_route_intent: input.shadowRouteAudit?.intent ?? null,
|
shadow_route_intent: input.shadowRouteAudit?.intent ?? null,
|
||||||
shadow_route_selected_recipe: input.shadowRouteAudit?.selectedRecipe ?? null,
|
shadow_route_selected_recipe: input.shadowRouteAudit?.selectedRecipe ?? null,
|
||||||
shadow_route_status: input.shadowRouteAudit?.status ?? "skipped",
|
shadow_route_status: input.shadowRouteAudit?.status ?? "skipped",
|
||||||
|
route_expectation_status: routeExpectationAudit.status,
|
||||||
|
route_expectation_reason: routeExpectationAudit.reason,
|
||||||
|
route_expectation_expected_selected_recipes: routeExpectationAudit.expectedSelectedRecipes,
|
||||||
|
route_expectation_expected_requested_result_modes: routeExpectationAudit.expectedRequestedResultModes,
|
||||||
|
route_expectation_expected_result_modes: routeExpectationAudit.expectedResultModes,
|
||||||
...resultSemantics,
|
...resultSemantics,
|
||||||
limitations: input.limitations,
|
limitations: input.limitations,
|
||||||
reasons
|
reasons
|
||||||
|
|
@ -2991,6 +3046,45 @@ export class AddressQueryService {
|
||||||
}),
|
}),
|
||||||
factual.semantics
|
factual.semantics
|
||||||
);
|
);
|
||||||
|
const finalRouteExpectationAudit = buildRouteExpectationAudit({
|
||||||
|
intent: intent.intent,
|
||||||
|
selectedRecipe: effectiveRecipeId,
|
||||||
|
requestedResultMode,
|
||||||
|
resultMode: factualResultSemantics.result_mode
|
||||||
|
});
|
||||||
|
if (finalRouteExpectationAudit.status === "mismatch" && FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1) {
|
||||||
|
return buildLimitedExecutionResult({
|
||||||
|
mode,
|
||||||
|
shape,
|
||||||
|
intent,
|
||||||
|
filters: filters.extracted_filters,
|
||||||
|
missingRequiredFilters: [],
|
||||||
|
selectedRecipe: effectiveRecipeId,
|
||||||
|
accountScopeMode: plan.account_scope_mode,
|
||||||
|
accountScopeFallbackApplied,
|
||||||
|
accountScopeAudit,
|
||||||
|
anchor,
|
||||||
|
matchFailureStage,
|
||||||
|
matchFailureReason,
|
||||||
|
mcpCallStatus: stageStatus,
|
||||||
|
rowsFetched: mcp.fetched_rows,
|
||||||
|
rawRowsReceived: mcp.raw_rows.length,
|
||||||
|
rowsAfterAccountScope: normalizedRows.length,
|
||||||
|
rowsAfterRecipeFilter: filterByAnchors.length,
|
||||||
|
rowsMaterialized: normalizedRows.length,
|
||||||
|
rowsMatched: filteredRows.length,
|
||||||
|
rawRowKeysSample: rowDiagnostics.rawRowKeysSample,
|
||||||
|
materializationDropReason: rowDiagnostics.materializationDropReason,
|
||||||
|
category: "recipe_visibility_gap",
|
||||||
|
reasonText: "маршрут не прошел baseline route expectation contract",
|
||||||
|
nextStep: "проверьте intent/recipe mapping или отключите FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 для безопасного rollout",
|
||||||
|
limitations: ["route_expectation_mismatch_guard_blocked"],
|
||||||
|
reasons: [...baseReasons, `route_expectation_mismatch:${finalRouteExpectationAudit.reason}`],
|
||||||
|
capabilityAudit,
|
||||||
|
shadowRouteAudit,
|
||||||
|
routeExpectationAudit: finalRouteExpectationAudit
|
||||||
|
});
|
||||||
|
}
|
||||||
if (intent.intent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
|
if (intent.intent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
|
||||||
return buildLimitedExecutionResult({
|
return buildLimitedExecutionResult({
|
||||||
mode,
|
mode,
|
||||||
|
|
@ -3020,9 +3114,14 @@ export class AddressQueryService {
|
||||||
limitations: ["exact_payables_mode_unconfirmed_output_blocked"],
|
limitations: ["exact_payables_mode_unconfirmed_output_blocked"],
|
||||||
reasons: [...baseReasons, "exact_payables_mode_unconfirmed_output_blocked"],
|
reasons: [...baseReasons, "exact_payables_mode_unconfirmed_output_blocked"],
|
||||||
capabilityAudit,
|
capabilityAudit,
|
||||||
shadowRouteAudit
|
shadowRouteAudit,
|
||||||
|
routeExpectationAudit: finalRouteExpectationAudit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const reasonsWithRouteExpectation =
|
||||||
|
finalRouteExpectationAudit.status === "mismatch"
|
||||||
|
? [...baseReasons, `route_expectation_mismatch:${finalRouteExpectationAudit.reason}`]
|
||||||
|
: baseReasons;
|
||||||
return {
|
return {
|
||||||
handled: true,
|
handled: true,
|
||||||
reply_text: factual.text,
|
reply_text: factual.text,
|
||||||
|
|
@ -3073,10 +3172,15 @@ export class AddressQueryService {
|
||||||
shadow_route_intent: shadowRouteAudit.intent,
|
shadow_route_intent: shadowRouteAudit.intent,
|
||||||
shadow_route_selected_recipe: shadowRouteAudit.selectedRecipe,
|
shadow_route_selected_recipe: shadowRouteAudit.selectedRecipe,
|
||||||
shadow_route_status: shadowRouteAudit.status,
|
shadow_route_status: shadowRouteAudit.status,
|
||||||
|
route_expectation_status: finalRouteExpectationAudit.status,
|
||||||
|
route_expectation_reason: finalRouteExpectationAudit.reason,
|
||||||
|
route_expectation_expected_selected_recipes: finalRouteExpectationAudit.expectedSelectedRecipes,
|
||||||
|
route_expectation_expected_requested_result_modes: finalRouteExpectationAudit.expectedRequestedResultModes,
|
||||||
|
route_expectation_expected_result_modes: finalRouteExpectationAudit.expectedResultModes,
|
||||||
...factualResultSemantics,
|
...factualResultSemantics,
|
||||||
limitations: filters.warnings,
|
limitations: filters.warnings,
|
||||||
reasons: withConfirmedBalanceFallbackReason(
|
reasons: withConfirmedBalanceFallbackReason(
|
||||||
baseReasons,
|
reasonsWithRouteExpectation,
|
||||||
requestedResultMode,
|
requestedResultMode,
|
||||||
factual.semantics,
|
factual.semantics,
|
||||||
factualResultSemantics.result_mode
|
factualResultSemantics.result_mode
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import type { AddressIntent, AddressResultMode } from "../types/addressQuery";
|
||||||
|
|
||||||
|
export type AddressRouteExpectationStatus = "matched" | "mismatch" | "not_found";
|
||||||
|
|
||||||
|
export interface AddressRouteExpectationEntry {
|
||||||
|
intent: AddressIntent;
|
||||||
|
expected_selected_recipes: string[];
|
||||||
|
expected_requested_result_modes?: AddressResultMode[];
|
||||||
|
expected_result_modes?: AddressResultMode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressRouteExpectationsContract {
|
||||||
|
schema_version: "address_route_expectations_v1";
|
||||||
|
updated_at: string;
|
||||||
|
entries: AddressRouteExpectationEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressRouteExpectationAudit {
|
||||||
|
status: AddressRouteExpectationStatus;
|
||||||
|
reason: string;
|
||||||
|
expected_selected_recipes: string[];
|
||||||
|
expected_requested_result_modes: AddressResultMode[];
|
||||||
|
expected_result_modes: AddressResultMode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXPECTATIONS_FILE = path.resolve(__dirname, "..", "..", "..", "..", "docs", "TECH", "address_route_expectations_v1.json");
|
||||||
|
|
||||||
|
function toObject(value: unknown): Record<string, unknown> | null {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNonEmptyString(value: unknown): string | null {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toStringArray(value: unknown): string[] {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return value.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseResultModes(value: unknown): AddressResultMode[] {
|
||||||
|
const raw = toStringArray(value);
|
||||||
|
return raw.filter((mode): mode is AddressResultMode => mode === "heuristic_candidates" || mode === "confirmed_balance");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEntry(value: unknown): AddressRouteExpectationEntry | null {
|
||||||
|
const object = toObject(value);
|
||||||
|
if (!object) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const intent = toNonEmptyString(object.intent) as AddressIntent | null;
|
||||||
|
const expectedSelectedRecipes = toStringArray(object.expected_selected_recipes);
|
||||||
|
if (!intent || expectedSelectedRecipes.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const expectedRequestedResultModes = parseResultModes(object.expected_requested_result_modes);
|
||||||
|
const expectedResultModes = parseResultModes(object.expected_result_modes);
|
||||||
|
return {
|
||||||
|
intent,
|
||||||
|
expected_selected_recipes: expectedSelectedRecipes,
|
||||||
|
...(expectedRequestedResultModes.length > 0 ? { expected_requested_result_modes: expectedRequestedResultModes } : {}),
|
||||||
|
...(expectedResultModes.length > 0 ? { expected_result_modes: expectedResultModes } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadAddressRouteExpectationsContract(): AddressRouteExpectationsContract {
|
||||||
|
const raw = fs.readFileSync(EXPECTATIONS_FILE, "utf-8");
|
||||||
|
const parsed = JSON.parse(raw) as unknown;
|
||||||
|
const root = toObject(parsed);
|
||||||
|
if (!root) {
|
||||||
|
throw new Error("address_route_expectations_v1: invalid root payload");
|
||||||
|
}
|
||||||
|
const schemaVersion = toNonEmptyString(root.schema_version);
|
||||||
|
if (schemaVersion !== "address_route_expectations_v1") {
|
||||||
|
throw new Error(`address_route_expectations_v1: unexpected schema version '${schemaVersion ?? "null"}'`);
|
||||||
|
}
|
||||||
|
const updatedAt = toNonEmptyString(root.updated_at) ?? new Date().toISOString();
|
||||||
|
const entriesRaw = Array.isArray(root.entries) ? root.entries : [];
|
||||||
|
const entries = entriesRaw.map(parseEntry).filter((entry): entry is AddressRouteExpectationEntry => entry !== null);
|
||||||
|
if (entries.length === 0) {
|
||||||
|
throw new Error("address_route_expectations_v1: no valid entries");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
schema_version: "address_route_expectations_v1",
|
||||||
|
updated_at: updatedAt,
|
||||||
|
entries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evaluateAddressRouteExpectation(input: {
|
||||||
|
intent: AddressIntent;
|
||||||
|
selectedRecipe: string | null;
|
||||||
|
requestedResultMode?: AddressResultMode;
|
||||||
|
resultMode?: AddressResultMode;
|
||||||
|
}): AddressRouteExpectationAudit {
|
||||||
|
const contract = loadAddressRouteExpectationsContract();
|
||||||
|
const entry = contract.entries.find((item) => item.intent === input.intent);
|
||||||
|
if (!entry) {
|
||||||
|
return {
|
||||||
|
status: "not_found",
|
||||||
|
reason: "route_expectation_not_defined_for_intent",
|
||||||
|
expected_selected_recipes: [],
|
||||||
|
expected_requested_result_modes: [],
|
||||||
|
expected_result_modes: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (input.selectedRecipe && !entry.expected_selected_recipes.includes(input.selectedRecipe)) {
|
||||||
|
return {
|
||||||
|
status: "mismatch",
|
||||||
|
reason: "selected_recipe_mismatch",
|
||||||
|
expected_selected_recipes: entry.expected_selected_recipes,
|
||||||
|
expected_requested_result_modes: entry.expected_requested_result_modes ?? [],
|
||||||
|
expected_result_modes: entry.expected_result_modes ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
input.requestedResultMode &&
|
||||||
|
Array.isArray(entry.expected_requested_result_modes) &&
|
||||||
|
entry.expected_requested_result_modes.length > 0 &&
|
||||||
|
!entry.expected_requested_result_modes.includes(input.requestedResultMode)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
status: "mismatch",
|
||||||
|
reason: "requested_result_mode_mismatch",
|
||||||
|
expected_selected_recipes: entry.expected_selected_recipes,
|
||||||
|
expected_requested_result_modes: entry.expected_requested_result_modes,
|
||||||
|
expected_result_modes: entry.expected_result_modes ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
input.resultMode &&
|
||||||
|
Array.isArray(entry.expected_result_modes) &&
|
||||||
|
entry.expected_result_modes.length > 0 &&
|
||||||
|
!entry.expected_result_modes.includes(input.resultMode)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
status: "mismatch",
|
||||||
|
reason: "result_mode_mismatch",
|
||||||
|
expected_selected_recipes: entry.expected_selected_recipes,
|
||||||
|
expected_requested_result_modes: entry.expected_requested_result_modes ?? [],
|
||||||
|
expected_result_modes: entry.expected_result_modes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: "matched",
|
||||||
|
reason: "route_expectation_matched",
|
||||||
|
expected_selected_recipes: entry.expected_selected_recipes,
|
||||||
|
expected_requested_result_modes: entry.expected_requested_result_modes ?? [],
|
||||||
|
expected_result_modes: entry.expected_result_modes ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1427,6 +1427,12 @@ function buildAddressDebugPayload(addressDebug, llmPreDecomposeMeta = null) {
|
||||||
shadow_route_intent: addressDebug.shadow_route_intent ?? undefined,
|
shadow_route_intent: addressDebug.shadow_route_intent ?? undefined,
|
||||||
shadow_route_selected_recipe: addressDebug.shadow_route_selected_recipe ?? undefined,
|
shadow_route_selected_recipe: addressDebug.shadow_route_selected_recipe ?? undefined,
|
||||||
shadow_route_status: addressDebug.shadow_route_status ?? undefined,
|
shadow_route_status: addressDebug.shadow_route_status ?? undefined,
|
||||||
|
route_expectation_status: addressDebug.route_expectation_status ?? undefined,
|
||||||
|
route_expectation_reason: addressDebug.route_expectation_reason ?? undefined,
|
||||||
|
route_expectation_expected_selected_recipes: addressDebug.route_expectation_expected_selected_recipes ?? undefined,
|
||||||
|
route_expectation_expected_requested_result_modes:
|
||||||
|
addressDebug.route_expectation_expected_requested_result_modes ?? undefined,
|
||||||
|
route_expectation_expected_result_modes: addressDebug.route_expectation_expected_result_modes ?? undefined,
|
||||||
execution_lane: "address_query",
|
execution_lane: "address_query",
|
||||||
llm_decomposition_applied: Boolean(llmMeta?.applied),
|
llm_decomposition_applied: Boolean(llmMeta?.applied),
|
||||||
llm_decomposition_attempted: Boolean(llmMeta?.attempted),
|
llm_decomposition_attempted: Boolean(llmMeta?.attempted),
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export type AddressAsOfDateBasis = "period_end" | "explicit_as_of_date" | "perio
|
||||||
export type AddressCapabilityLayer = "compute" | "navigation" | "conversational";
|
export type AddressCapabilityLayer = "compute" | "navigation" | "conversational";
|
||||||
export type AddressCapabilityRouteMode = "exact" | "heuristic";
|
export type AddressCapabilityRouteMode = "exact" | "heuristic";
|
||||||
export type AddressShadowRouteStatus = "skipped" | "planned" | "unavailable";
|
export type AddressShadowRouteStatus = "skipped" | "planned" | "unavailable";
|
||||||
|
export type AddressRouteExpectationStatus = "matched" | "mismatch" | "not_found";
|
||||||
|
|
||||||
export type AddressQueryShape =
|
export type AddressQueryShape =
|
||||||
| "AGGREGATE_LOOKUP"
|
| "AGGREGATE_LOOKUP"
|
||||||
|
|
@ -209,6 +210,11 @@ export interface AddressExecutionDebug {
|
||||||
shadow_route_intent?: AddressIntent | null;
|
shadow_route_intent?: AddressIntent | null;
|
||||||
shadow_route_selected_recipe?: string | null;
|
shadow_route_selected_recipe?: string | null;
|
||||||
shadow_route_status?: AddressShadowRouteStatus | null;
|
shadow_route_status?: AddressShadowRouteStatus | null;
|
||||||
|
route_expectation_status?: AddressRouteExpectationStatus | null;
|
||||||
|
route_expectation_reason?: string | null;
|
||||||
|
route_expectation_expected_selected_recipes?: string[];
|
||||||
|
route_expectation_expected_requested_result_modes?: AddressResultMode[];
|
||||||
|
route_expectation_expected_result_modes?: AddressResultMode[];
|
||||||
limitations: string[];
|
limitations: string[];
|
||||||
reasons: string[];
|
reasons: string[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -441,6 +441,11 @@ export interface AssistantDebugPayload {
|
||||||
shadow_route_intent?: string | null;
|
shadow_route_intent?: string | null;
|
||||||
shadow_route_selected_recipe?: string | null;
|
shadow_route_selected_recipe?: string | null;
|
||||||
shadow_route_status?: "skipped" | "planned" | "unavailable" | null;
|
shadow_route_status?: "skipped" | "planned" | "unavailable" | null;
|
||||||
|
route_expectation_status?: "matched" | "mismatch" | "not_found" | null;
|
||||||
|
route_expectation_reason?: string | null;
|
||||||
|
route_expectation_expected_selected_recipes?: string[];
|
||||||
|
route_expectation_expected_requested_result_modes?: Array<"heuristic_candidates" | "confirmed_balance">;
|
||||||
|
route_expectation_expected_result_modes?: Array<"heuristic_candidates" | "confirmed_balance">;
|
||||||
execution_lane?: "address_query" | "deep_analysis";
|
execution_lane?: "address_query" | "deep_analysis";
|
||||||
llm_decomposition_applied?: boolean;
|
llm_decomposition_applied?: boolean;
|
||||||
llm_decomposition_attempted?: boolean;
|
llm_decomposition_attempted?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -2503,6 +2503,8 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
||||||
expect(result?.debug.result_mode).toBe("confirmed_balance");
|
expect(result?.debug.result_mode).toBe("confirmed_balance");
|
||||||
expect(result?.debug.as_of_date_basis).toBe("explicit_as_of_date");
|
expect(result?.debug.as_of_date_basis).toBe("explicit_as_of_date");
|
||||||
expect(result?.debug.selected_recipe).toBe("address_payables_confirmed_as_of_date_v1");
|
expect(result?.debug.selected_recipe).toBe("address_payables_confirmed_as_of_date_v1");
|
||||||
|
expect(result?.debug.route_expectation_status).toBe("matched");
|
||||||
|
expect(result?.debug.route_expectation_reason).toBe("route_expectation_matched");
|
||||||
expect(Array.isArray(result?.debug.reasons)).toBe(true);
|
expect(Array.isArray(result?.debug.reasons)).toBe(true);
|
||||||
expect(result?.debug.reasons).not.toContain("confirmed_balance_unavailable_fallback_to_heuristic_candidates");
|
expect(result?.debug.reasons).not.toContain("confirmed_balance_unavailable_fallback_to_heuristic_candidates");
|
||||||
expect(["FACTUAL_LIST", "FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
|
expect(["FACTUAL_LIST", "FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
evaluateAddressRouteExpectation,
|
||||||
|
loadAddressRouteExpectationsContract
|
||||||
|
} from "../src/services/addressRouteExpectations";
|
||||||
|
|
||||||
|
describe("address route expectations contract", () => {
|
||||||
|
it("loads expectations contract with entries", () => {
|
||||||
|
const contract = loadAddressRouteExpectationsContract();
|
||||||
|
expect(contract.schema_version).toBe("address_route_expectations_v1");
|
||||||
|
expect(Array.isArray(contract.entries)).toBe(true);
|
||||||
|
expect(contract.entries.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches expected recipe and result mode for exact payables route", () => {
|
||||||
|
const audit = evaluateAddressRouteExpectation({
|
||||||
|
intent: "payables_confirmed_as_of_date",
|
||||||
|
selectedRecipe: "address_payables_confirmed_as_of_date_v1",
|
||||||
|
requestedResultMode: "confirmed_balance",
|
||||||
|
resultMode: "confirmed_balance"
|
||||||
|
});
|
||||||
|
expect(audit.status).toBe("matched");
|
||||||
|
expect(audit.reason).toBe("route_expectation_matched");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects selected recipe mismatch", () => {
|
||||||
|
const audit = evaluateAddressRouteExpectation({
|
||||||
|
intent: "payables_confirmed_as_of_date",
|
||||||
|
selectedRecipe: "address_movements_payables_v1",
|
||||||
|
requestedResultMode: "confirmed_balance",
|
||||||
|
resultMode: "confirmed_balance"
|
||||||
|
});
|
||||||
|
expect(audit.status).toBe("mismatch");
|
||||||
|
expect(audit.reason).toBe("selected_recipe_mismatch");
|
||||||
|
expect(audit.expected_selected_recipes).toContain("address_payables_confirmed_as_of_date_v1");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue