АРЧ АП11 - Архитектура после регресса: Архитектура: исправить deep-turn normalizer schema contract и вернуть hybrid follow-up authority в phase13 replay

This commit is contained in:
2026-04-18 19:23:31 +03:00
parent 09dd0e4513
commit 823cb56a63
6 changed files with 434 additions and 7 deletions
@@ -1,6 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAssistantDeepTurnNormalizationRuntime = buildAssistantDeepTurnNormalizationRuntime;
const routeHintAdapter_1 = require("./routeHintAdapter");
function resolveDeepTurnNormalizerPromptVersion(promptVersion) {
const normalized = String(promptVersion ?? "").trim().toLowerCase();
if (normalized === "normalizer_v2_0_2" ||
normalized === "normalizer_v2_0_1" ||
normalized === "normalizer_v2" ||
normalized === "normalizer_v1") {
return String(promptVersion);
}
return "normalizer_v2_0_2";
}
function canSynthesizeRouteSummary(normalized) {
if (!normalized || typeof normalized !== "object") {
return false;
}
const source = normalized;
return Array.isArray(source.fragments);
}
async function buildAssistantDeepTurnNormalizationRuntime(input) {
const investigationState = input.sessionInvestigationState;
const canUseFollowupBinding = input.featureInvestigationStateV1 &&
@@ -25,7 +43,8 @@ async function buildAssistantDeepTurnNormalizationRuntime(input) {
baseUrl: input.payload.baseUrl,
temperature: input.payload.temperature,
maxOutputTokens: input.payload.maxOutputTokens,
promptVersion: input.payload.promptVersion ?? "address_query_runtime_v1",
promptVersion: resolveDeepTurnNormalizerPromptVersion(input.payload.promptVersion),
schemaVersion: "v2_0_2",
systemPrompt: input.payload.systemPrompt,
developerPrompt: input.payload.developerPrompt,
domainPrompt: input.payload.domainPrompt,
@@ -35,9 +54,18 @@ async function buildAssistantDeepTurnNormalizationRuntime(input) {
useMock: Boolean(input.payload.useMock)
};
const normalized = await input.normalize(normalizePayload);
const normalizedPayload = normalized.normalized;
const synthesizedRouteSummary = normalized.route_hint_summary ??
(normalizedPayload && canSynthesizeRouteSummary(normalizedPayload) ? (0, routeHintAdapter_1.toRouteHintSummary)(normalizedPayload) : null);
const normalizedWithRouteSummary = synthesizedRouteSummary === normalized.route_hint_summary
? normalized
: {
...normalized,
route_hint_summary: synthesizedRouteSummary
};
return {
followupBinding,
normalizePayload,
normalized
normalized: normalizedWithRouteSummary
};
}
@@ -2,6 +2,7 @@ import type { AssistantMessageRequestPayload } from "../types/assistant";
import type { NormalizeRequestPayload, NormalizeResponsePayload } from "../types/normalizer";
import type { InvestigationStateWithProblemUnits } from "../types/stage2ProblemUnits";
import type { AssistantFollowupUsage } from "./assistantFollowupUsage";
import { toRouteHintSummary } from "./routeHintAdapter";
export interface AssistantDeepTurnFollowupBinding {
normalizedQuestion: string;
@@ -29,6 +30,27 @@ export interface BuildAssistantDeepTurnNormalizationRuntimeOutput {
normalized: NormalizeResponsePayload;
}
function resolveDeepTurnNormalizerPromptVersion(promptVersion: NormalizeRequestPayload["promptVersion"]): string {
const normalized = String(promptVersion ?? "").trim().toLowerCase();
if (
normalized === "normalizer_v2_0_2" ||
normalized === "normalizer_v2_0_1" ||
normalized === "normalizer_v2" ||
normalized === "normalizer_v1"
) {
return String(promptVersion);
}
return "normalizer_v2_0_2";
}
function canSynthesizeRouteSummary(normalized: NormalizeResponsePayload["normalized"]): boolean {
if (!normalized || typeof normalized !== "object") {
return false;
}
const source = normalized as unknown as Record<string, unknown>;
return Array.isArray(source.fragments);
}
export async function buildAssistantDeepTurnNormalizationRuntime(
input: BuildAssistantDeepTurnNormalizationRuntimeInput
): Promise<BuildAssistantDeepTurnNormalizationRuntimeOutput> {
@@ -58,7 +80,8 @@ export async function buildAssistantDeepTurnNormalizationRuntime(
baseUrl: input.payload.baseUrl,
temperature: input.payload.temperature,
maxOutputTokens: input.payload.maxOutputTokens,
promptVersion: input.payload.promptVersion ?? "address_query_runtime_v1",
promptVersion: resolveDeepTurnNormalizerPromptVersion(input.payload.promptVersion),
schemaVersion: "v2_0_2",
systemPrompt: input.payload.systemPrompt,
developerPrompt: input.payload.developerPrompt,
domainPrompt: input.payload.domainPrompt,
@@ -69,10 +92,21 @@ export async function buildAssistantDeepTurnNormalizationRuntime(
};
const normalized = await input.normalize(normalizePayload);
const normalizedPayload = normalized.normalized;
const synthesizedRouteSummary =
normalized.route_hint_summary ??
(normalizedPayload && canSynthesizeRouteSummary(normalizedPayload) ? toRouteHintSummary(normalizedPayload) : null);
const normalizedWithRouteSummary: NormalizeResponsePayload =
synthesizedRouteSummary === normalized.route_hint_summary
? normalized
: {
...normalized,
route_hint_summary: synthesizedRouteSummary
};
return {
followupBinding,
normalizePayload,
normalized
normalized: normalizedWithRouteSummary
};
}
@@ -66,6 +66,7 @@ describe("assistant deep turn normalization runtime adapter", () => {
temperature: 0.2,
maxOutputTokens: 333,
promptVersion: "normalizer_v2_0_2",
schemaVersion: "v2_0_2",
systemPrompt: "sys",
developerPrompt: "dev",
domainPrompt: "dom",
@@ -123,7 +124,8 @@ describe("assistant deep turn normalization runtime adapter", () => {
baseUrl: undefined,
temperature: undefined,
maxOutputTokens: undefined,
promptVersion: "address_query_runtime_v1",
promptVersion: "normalizer_v2_0_2",
schemaVersion: "v2_0_2",
systemPrompt: undefined,
developerPrompt: undefined,
domainPrompt: undefined,
@@ -142,4 +144,110 @@ describe("assistant deep turn normalization runtime adapter", () => {
usage: null
});
});
it("synthesizes route summary from normalized payload when normalize omits it", async () => {
const normalize = vi.fn(async () => ({
trace_id: "trace-3",
ok: true,
normalized: {
schema_version: "normalized_query_v2_0_2",
user_message_raw: "Разложи хвосты по поставщикам",
message_in_scope: true,
scope_confidence: "high",
contains_multiple_tasks: false,
discarded_fragments: [],
global_notes: {
needs_clarification: false,
clarification_reason: null
},
fragments: [
{
fragment_id: "F1",
raw_fragment_text: "Разложи хвосты по поставщикам: где разрыв между оплатой и документами выглядит системным.",
normalized_fragment_text:
"Разложи хвосты по поставщикам: где разрыв между оплатой и документами выглядит системным.",
domain_relevance: "in_scope",
business_scope: "company_specific_accounting",
entity_hints: ["контрагент", "документ"],
account_hints: ["60"],
document_hints: ["документ"],
register_hints: [],
time_scope: {
type: "missing",
value: null,
confidence: "low"
},
flags: {
has_multi_entity_scope: true,
asks_for_chain_explanation: true,
asks_for_ranking_or_top: false,
asks_for_period_summary: false,
asks_for_rule_check: false,
asks_for_anomaly_scan: true,
asks_for_exact_object_trace: false,
asks_for_evidence: true,
mentions_period_close_context: false
},
semantic_hints: {
scope_target_kind: "none",
scope_target_text: null,
date_scope_kind: "missing",
self_scope_detected: false,
selected_object_scope_detected: false
},
candidate_labels: ["cross_entity", "anomaly_probe"],
confidence: "medium",
execution_readiness: "executable_with_soft_assumptions",
clarification_reason: null,
soft_assumption_used: ["problem_scan_mode_enabled"],
route_status: "routed",
no_route_reason: null
}
]
},
route_hint_summary: null,
raw_model_output: {},
validation: { passed: true, errors: [] },
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
latency_ms: 1,
prompt_version: "normalizer_v2_0_2",
schema_version: "normalized_query_v2_0_2",
request_count_for_case: 1
}));
const runtime = await buildAssistantDeepTurnNormalizationRuntime({
userMessage: "Разложи хвосты по поставщикам",
payload: {
llmProvider: "openai",
promptVersion: "address_query_runtime_v1",
useMock: true
},
featureInvestigationStateV1: false,
featureStateFollowupBindingV1: false,
sessionInvestigationState: null,
buildFollowupStateBinding: vi.fn(),
normalize
});
expect(runtime.normalized.route_hint_summary).toEqual(
expect.objectContaining({
mode: "deterministic_v2",
fallback: expect.objectContaining({
type: "none"
}),
decisions: [
expect.objectContaining({
fragment_id: "F1",
route: "hybrid_store_plus_live"
})
]
})
);
expect(normalize).toHaveBeenCalledWith(
expect.objectContaining({
promptVersion: "normalizer_v2_0_2",
schemaVersion: "v2_0_2"
})
);
});
});
@@ -0,0 +1,120 @@
{
"suite_id": "assistant_saved_session_runtime_job-_tMDIcKCq-",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
}
]
}
]
}