ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.42: вынос finalizeAddressLaneResponse из assistantService в отдельный attempt-bridge (как для living chat), для уменьшения монолита без изменения поведения.

This commit is contained in:
dctouch 2026-04-10 23:27:37 +03:00
parent fbf2d6a19a
commit 875f3bfbcd
6 changed files with 211 additions and 31 deletions

View File

@ -1316,7 +1316,35 @@ Validation:
- `assistantDeepTurnPackagingRuntimeAdapter.test.ts`
- `assistantWave10SettlementCorrectiveRegression.test.ts`
Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 + 2.28 + 2.29 + 2.30 + 2.31 + 2.32 + 2.33 + 2.34 + 2.35 + 2.36 + 2.37 + 2.38 + 2.39 + 2.40 + 2.41 completed)**
Implemented in current pass (Phase 2.42):
1. Extracted address-lane response attempt bridge (`finalizeAddressLaneResponse`) from `assistantService` into dedicated runtime adapter:
- `assistantAddressLaneResponseAttemptRuntimeAdapter.ts`
- introduced:
- `runAssistantAddressLaneResponseAttemptRuntime(...)`
2. Centralized address-lane response handoff logic (behavior-preserving):
- delegated response runtime invocation (`runAssistantAddressLaneResponseRuntime(...)`);
- preserved followup-offer/debug payload and session finalization contract wiring.
3. Rewired `assistantService` to consume address-lane response attempt runtime adapter.
4. Added focused unit tests:
- `assistantAddressLaneResponseAttemptRuntimeAdapter.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted living/address/deep followup pack passed:
- `assistantAddressLaneResponseAttemptRuntimeAdapter.test.ts`
- `assistantLivingChatAttemptRuntimeAdapter.test.ts`
- `assistantAddressLaneAttemptRuntimeAdapter.test.ts`
- `assistantUserTurnBootstrapRuntimeAdapter.test.ts`
- `assistantLivingChatLlmRuntimeAdapter.test.ts`
- `assistantLivingChatHandlerRuntimeAdapter.test.ts`
- `assistantLivingChatRuntimeAdapter.test.ts`
- `assistantAddressRuntimeAdapter.test.ts`
- `assistantAddressLaneResponseRuntimeAdapter.test.ts`
- `assistantDeepTurnResponseRuntimeAdapter.test.ts`
- `assistantDeepTurnPackagingRuntimeAdapter.test.ts`
- `assistantWave10SettlementCorrectiveRegression.test.ts`
Status: **In progress (Phase 2.1 + 2.2 + 2.3 + 2.4 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 + 2.10 + 2.11 + 2.12 + 2.13 + 2.14 + 2.15 + 2.16 + 2.17 + 2.18 + 2.19 + 2.20 + 2.21 + 2.22 + 2.23 + 2.24 + 2.25 + 2.26 + 2.27 + 2.28 + 2.29 + 2.30 + 2.31 + 2.32 + 2.33 + 2.34 + 2.35 + 2.36 + 2.37 + 2.38 + 2.39 + 2.40 + 2.41 + 2.42 completed)**
## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards)

View File

@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantAddressLaneResponseAttemptRuntime = runAssistantAddressLaneResponseAttemptRuntime;
const assistantAddressLaneResponseRuntimeAdapter_1 = require("./assistantAddressLaneResponseRuntimeAdapter");
function runAssistantAddressLaneResponseAttemptRuntime(input) {
const runAddressLaneResponseRuntimeSafe = input.runAddressLaneResponseRuntime ?? assistantAddressLaneResponseRuntimeAdapter_1.runAssistantAddressLaneResponseRuntime;
const runtime = runAddressLaneResponseRuntimeSafe({
sessionId: input.sessionId,
userMessage: input.userMessage,
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
addressLane: input.addressLane,
carryoverMeta: input.carryoverMeta,
llmPreDecomposeMeta: input.llmPreDecomposeMeta,
knownOrganizations: input.knownOrganizations,
activeOrganization: input.activeOrganization,
sanitizeOutgoingAssistantText: input.sanitizeOutgoingAssistantText,
buildAddressDebugPayload: input.buildAddressDebugPayload,
buildAddressFollowupOffer: input.buildAddressFollowupOffer,
mergeKnownOrganizations: input.mergeKnownOrganizations,
toNonEmptyString: input.toNonEmptyString,
appendItem: input.appendItem,
getSession: input.getSession,
persistSession: input.persistSession,
cloneConversation: input.cloneConversation,
logEvent: input.logEvent,
messageIdFactory: input.messageIdFactory
});
return runtime.response;
}

View File

@ -65,7 +65,7 @@ const openaiResponsesClient_1 = __importStar(require("./openaiResponsesClient"))
const addressMcpClient_1 = __importStar(require("./addressMcpClient"));
const capabilitiesRegistry_1 = __importStar(require("./capabilitiesRegistry"));
const assistantCanon_1 = __importStar(require("./assistantCanon"));
const assistantAddressLaneResponseRuntimeAdapter_1 = __importStar(require("./assistantAddressLaneResponseRuntimeAdapter"));
const assistantAddressLaneResponseAttemptRuntimeAdapter_1 = __importStar(require("./assistantAddressLaneResponseAttemptRuntimeAdapter"));
const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding"));
const assistantDeepTurnAnalysisRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAnalysisRuntimeAdapter"));
const assistantDeepTurnCompositionRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnCompositionRuntimeAdapter"));
@ -4398,30 +4398,27 @@ class AssistantService {
nowIso: () => new Date().toISOString()
});
const sessionOrganizationScope = resolveSessionOrganizationScopeContext(userMessage, session.items);
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => {
const runtime = (0, assistantAddressLaneResponseRuntimeAdapter_1.runAssistantAddressLaneResponseRuntime)({
sessionId,
userMessage,
effectiveAddressUserMessage,
addressLane,
carryoverMeta,
llmPreDecomposeMeta,
knownOrganizations: sessionOrganizationScope.knownOrganizations,
activeOrganization: sessionOrganizationScope.activeOrganization,
sanitizeOutgoingAssistantText,
buildAddressDebugPayload,
buildAddressFollowupOffer,
mergeKnownOrganizations,
toNonEmptyString,
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
getSession: (targetSessionId) => this.sessions.getSession(targetSessionId),
persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState),
cloneConversation: (items) => cloneItems(items),
logEvent: (payload) => (0, log_1.logJson)(payload),
messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`
});
return runtime.response;
};
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => (0, assistantAddressLaneResponseAttemptRuntimeAdapter_1.runAssistantAddressLaneResponseAttemptRuntime)({
sessionId,
userMessage,
effectiveAddressUserMessage,
addressLane,
carryoverMeta,
llmPreDecomposeMeta,
knownOrganizations: sessionOrganizationScope.knownOrganizations,
activeOrganization: sessionOrganizationScope.activeOrganization,
sanitizeOutgoingAssistantText,
buildAddressDebugPayload,
buildAddressFollowupOffer,
mergeKnownOrganizations,
toNonEmptyString,
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
getSession: (targetSessionId) => this.sessions.getSession(targetSessionId),
persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState),
cloneConversation: (items) => cloneItems(items),
logEvent: (payload) => (0, log_1.logJson)(payload),
messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`
});
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => (0, assistantLivingChatAttemptRuntimeAdapter_1.runAssistantLivingChatAttemptRuntime)({
sessionId,
userMessage,

View File

@ -0,0 +1,45 @@
import type { AssistantMessageResponsePayload } from "../types/assistant";
import {
runAssistantAddressLaneResponseRuntime,
type RunAssistantAddressLaneResponseRuntimeInput,
type RunAssistantAddressLaneResponseRuntimeOutput
} from "./assistantAddressLaneResponseRuntimeAdapter";
export interface RunAssistantAddressLaneResponseAttemptRuntimeInput<
ResponseType = AssistantMessageResponsePayload
> extends RunAssistantAddressLaneResponseRuntimeInput<ResponseType> {
runAddressLaneResponseRuntime?: (
input: RunAssistantAddressLaneResponseRuntimeInput<ResponseType>
) => RunAssistantAddressLaneResponseRuntimeOutput<ResponseType>;
}
export function runAssistantAddressLaneResponseAttemptRuntime<
ResponseType = AssistantMessageResponsePayload
>(
input: RunAssistantAddressLaneResponseAttemptRuntimeInput<ResponseType>
): ResponseType {
const runAddressLaneResponseRuntimeSafe =
input.runAddressLaneResponseRuntime ?? runAssistantAddressLaneResponseRuntime;
const runtime = runAddressLaneResponseRuntimeSafe({
sessionId: input.sessionId,
userMessage: input.userMessage,
effectiveAddressUserMessage: input.effectiveAddressUserMessage,
addressLane: input.addressLane,
carryoverMeta: input.carryoverMeta,
llmPreDecomposeMeta: input.llmPreDecomposeMeta,
knownOrganizations: input.knownOrganizations,
activeOrganization: input.activeOrganization,
sanitizeOutgoingAssistantText: input.sanitizeOutgoingAssistantText,
buildAddressDebugPayload: input.buildAddressDebugPayload,
buildAddressFollowupOffer: input.buildAddressFollowupOffer,
mergeKnownOrganizations: input.mergeKnownOrganizations,
toNonEmptyString: input.toNonEmptyString,
appendItem: input.appendItem,
getSession: input.getSession,
persistSession: input.persistSession,
cloneConversation: input.cloneConversation,
logEvent: input.logEvent,
messageIdFactory: input.messageIdFactory
});
return runtime.response;
}

View File

@ -19,7 +19,7 @@ import * as openaiResponsesClient_1 from "./openaiResponsesClient";
import * as addressMcpClient_1 from "./addressMcpClient";
import * as capabilitiesRegistry_1 from "./capabilitiesRegistry";
import * as assistantCanon_1 from "./assistantCanon";
import * as assistantAddressLaneResponseRuntimeAdapter_1 from "./assistantAddressLaneResponseRuntimeAdapter";
import * as assistantAddressLaneResponseAttemptRuntimeAdapter_1 from "./assistantAddressLaneResponseAttemptRuntimeAdapter";
import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding";
import * as assistantDeepTurnAnalysisRuntimeAdapter_1 from "./assistantDeepTurnAnalysisRuntimeAdapter";
import * as assistantDeepTurnCompositionRuntimeAdapter_1 from "./assistantDeepTurnCompositionRuntimeAdapter";
@ -4353,8 +4353,7 @@ export class AssistantService {
nowIso: () => new Date().toISOString()
});
const sessionOrganizationScope = resolveSessionOrganizationScopeContext(userMessage, session.items);
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => {
const runtime = (0, assistantAddressLaneResponseRuntimeAdapter_1.runAssistantAddressLaneResponseRuntime)({
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => (0, assistantAddressLaneResponseAttemptRuntimeAdapter_1.runAssistantAddressLaneResponseAttemptRuntime)({
sessionId,
userMessage,
effectiveAddressUserMessage,
@ -4375,8 +4374,6 @@ export class AssistantService {
logEvent: (payload) => (0, log_1.logJson)(payload),
messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`
});
return runtime.response;
};
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => (0, assistantLivingChatAttemptRuntimeAdapter_1.runAssistantLivingChatAttemptRuntime)({
sessionId,
userMessage,

View File

@ -0,0 +1,84 @@
import { describe, expect, it, vi } from "vitest";
import { runAssistantAddressLaneResponseAttemptRuntime } from "../src/services/assistantAddressLaneResponseAttemptRuntimeAdapter";
function buildInput(overrides: Record<string, unknown> = {}) {
return {
sessionId: "asst-1",
userMessage: "где хвост по оплате",
effectiveAddressUserMessage: "где хвост по оплате",
addressLane: {
reply_text: "address reply",
reply_type: "factual_with_explanation",
debug: { extracted_filters: {} }
},
carryoverMeta: null,
llmPreDecomposeMeta: null,
knownOrganizations: [],
activeOrganization: null,
sanitizeOutgoingAssistantText: (value: unknown, fallback = "") => {
const text = String(value ?? "").trim();
return text || fallback;
},
buildAddressDebugPayload: () => ({}),
buildAddressFollowupOffer: () => null,
mergeKnownOrganizations: (value: string[]) => value,
toNonEmptyString: (value: unknown) => (typeof value === "string" && value.trim() ? value.trim() : null),
appendItem: () => {},
getSession: () => ({
session_id: "asst-1",
updated_at: "",
items: [],
investigation_state: null
}),
persistSession: () => {},
cloneConversation: (items: unknown[]) => items,
logEvent: () => {},
messageIdFactory: () => "msg-1",
...overrides
} as any;
}
describe("assistant address lane response attempt runtime adapter", () => {
it("returns delegated runtime response", () => {
const runAddressLaneResponseRuntime = vi.fn(() => ({
response: { ok: true, lane: "address" },
debug: { marker: "v1" }
}));
const response = runAssistantAddressLaneResponseAttemptRuntime(
buildInput({ runAddressLaneResponseRuntime })
);
expect(response).toEqual({ ok: true, lane: "address" });
expect(runAddressLaneResponseRuntime).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "asst-1",
userMessage: "где хвост по оплате"
})
);
});
it("forwards carryover and llm predecompose metadata", () => {
const carryoverMeta = { previousReplyType: "partial_coverage" };
const llmPreDecomposeMeta = { mode: "supported", confidence: "high" };
const runAddressLaneResponseRuntime = vi.fn(() => ({
response: { ok: true },
debug: {}
}));
runAssistantAddressLaneResponseAttemptRuntime(
buildInput({
carryoverMeta,
llmPreDecomposeMeta,
runAddressLaneResponseRuntime
})
);
expect(runAddressLaneResponseRuntime).toHaveBeenCalledWith(
expect.objectContaining({
carryoverMeta,
llmPreDecomposeMeta
})
);
});
});