ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.30: вынос ветки tool_gate_skip + chat fallback из assistantService в отдельный runtime-адаптер, чтобы в сервисе остался только orchestration flow.

This commit is contained in:
2026-04-10 20:52:04 +03:00
parent 3531f7ddfe
commit 353cbc1763
16 changed files with 1706 additions and 405 deletions
@@ -0,0 +1,80 @@
import { describe, expect, it, vi } from "vitest";
import { runAssistantAddressToolGateRuntime } from "../src/services/assistantAddressToolGateRuntimeAdapter";
describe("assistant address tool-gate runtime adapter", () => {
it("does nothing when runAddressLane is true", async () => {
const logEvent = vi.fn();
const tryHandleLivingChat = vi.fn(async () => "chat-response");
const result = await runAssistantAddressToolGateRuntime({
sessionId: "asst-1",
userMessage: "вопрос",
addressInputMessage: "вопрос",
orchestrationDecision: { runAddressLane: true },
livingModeDecision: { mode: "chat", reason: "x" },
addressRuntimeMeta: {},
logEvent,
tryHandleLivingChat,
nowIso: () => "2026-04-10T00:00:00.000Z"
});
expect(result.handled).toBe(false);
expect(result.response).toBeNull();
expect(logEvent).not.toHaveBeenCalled();
expect(tryHandleLivingChat).not.toHaveBeenCalled();
});
it("logs skip and returns chat response when chat fallback handles", async () => {
const logEvent = vi.fn();
const tryHandleLivingChat = vi.fn(async () => ({ ok: true }));
const result = await runAssistantAddressToolGateRuntime({
sessionId: "asst-2",
userMessage: "вопрос",
addressInputMessage: "канон",
orchestrationDecision: { runAddressLane: false },
livingModeDecision: { mode: "chat", reason: "predecompose_unsupported_mode" },
addressRuntimeMeta: {
attempted: true,
applied: false,
reason: "normalize_failed",
predecomposeContract: {
intent: "unknown",
aggregation_profile: "unknown",
period: { scope: "unspecified" }
}
},
logEvent,
tryHandleLivingChat,
nowIso: () => "2026-04-10T00:00:00.000Z"
});
expect(result.handled).toBe(true);
expect(result.response).toEqual({ ok: true });
expect(logEvent).toHaveBeenCalledTimes(1);
expect(tryHandleLivingChat).toHaveBeenCalledTimes(1);
});
it("logs skip and returns unhandled when mode is not chat", async () => {
const logEvent = vi.fn();
const tryHandleLivingChat = vi.fn(async () => ({ ok: true }));
const result = await runAssistantAddressToolGateRuntime({
sessionId: "asst-3",
userMessage: "вопрос",
addressInputMessage: "канон",
orchestrationDecision: { runAddressLane: false },
livingModeDecision: { mode: "deep_analysis", reason: "strong_data_signal_detected" },
addressRuntimeMeta: {},
logEvent,
tryHandleLivingChat,
nowIso: () => "2026-04-10T00:00:00.000Z"
});
expect(result.handled).toBe(false);
expect(result.response).toBeNull();
expect(logEvent).toHaveBeenCalledTimes(1);
expect(tryHandleLivingChat).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,164 @@
import { describe, expect, it, vi } from "vitest";
import { runAssistantDeepTurnAnalysisRuntime } from "../src/services/assistantDeepTurnAnalysisRuntimeAdapter";
describe("assistant deep turn analysis runtime adapter", () => {
it("orchestrates deep pipeline steps in deterministic order", async () => {
const callOrder: string[] = [];
const runContextRuntime = vi.fn(() => {
callOrder.push("context");
return {
companyAnchors: { accounts: ["60.01"] },
focusDomainForGuards: "settlements_60_62",
temporalGuard: { primary_period_window: { from: "2020-07-01", to: "2020-07-31" } },
domainPolarityGuardInitial: { polarity: "supplier_payable" },
claimAnchorAudit: { claim_type: "prove_settlement_closure_state" },
businessScopeResolution: { business_scope_resolved: ["company_specific_accounting"] },
resolvedRouteSummary: { mode: "deterministic_v2", decisions: [] as any[] } as any,
liveTemporalHint: {
as_of_date: "2020-07-31",
period_from: null,
period_to: null,
source: "analysis_context"
}
};
});
const runExecutionPlanRuntime = vi.fn((input) => {
callOrder.push("plan");
expect(input.claimAnchorAudit.claim_type).toBe("prove_settlement_closure_state");
return {
requirementExtraction: {
requirements: [{ id: "R1" }],
byFragment: new Map<string, string[]>([["F1", ["R1"]]])
},
executionPlan: [{ fragment_id: "F1" }],
rbpRoutePlanEnforcement: { executionPlan: [{ fragment_id: "F1" }], audit: { rbp: true } },
faRoutePlanEnforcement: { executionPlan: [{ fragment_id: "F1" }], audit: { fa: true } }
} as any;
});
const runRetrievalRuntime = vi.fn(async (input) => {
callOrder.push("retrieval");
expect(input.liveTemporalHint?.as_of_date).toBe("2020-07-31");
return {
retrievalCalls: [{ fragment_id: "F1", route: "store_canonical" }],
retrievalResultsRaw: [{ fragment_id: "F1", route: "store_canonical", raw_result: {} }],
retrievalResults: [{ fragment_id: "F1", requirement_ids: ["R1"] }]
} as any;
});
const runGuardRuntime = vi.fn((input) => {
callOrder.push("guard");
expect(input.retrievalResults[0].fragment_id).toBe("F1");
return {
retrievalResults: [{ fragment_id: "F1-guarded", requirement_ids: ["R1"] }],
polarityGuardResult: { audit: { polarity: "supplier_payable" } },
targetedEvidenceResult: {
audit: {
targeted_evidence_hit_rate: 0.5
}
},
evidenceGateResult: {
audit: {
admissible_evidence_count: 3
}
}
} as any;
});
const runGroundingRuntime = vi.fn((input) => {
callOrder.push("grounding");
expect(input.claimType).toBe("prove_settlement_closure_state");
expect(input.targetedEvidenceHitRate).toBe(0.5);
expect(input.businessScopeResolved).toEqual(["company_specific_accounting"]);
return {
rbpLiveRouteAudit: { routed: 1 },
faLiveRouteAudit: { routed: 1 },
coverageEvaluation: {
requirements: [{ id: "R1" }],
coverage: { requirements_total: 1, requirements_covered: 1 }
},
groundedAnswerEligibilityGuard: { eligible: true },
groundingCheck: { status: "grounded", reasons: [] }
} as any;
});
const runCompositionRuntime = vi.fn((input) => {
callOrder.push("composition");
expect(input.retrievalResults[0].fragment_id).toBe("F1-guarded");
return {
questionTypeClass: "causal_trace",
composition: { reply_type: "factual", answer: "ok" }
} as any;
});
const runtime = await runAssistantDeepTurnAnalysisRuntime({
userMessage: "where closure failed",
runContextRuntime,
runExecutionPlanRuntime,
runRetrievalRuntime,
runGuardRuntime,
runGroundingRuntime,
runCompositionRuntime
});
expect(callOrder).toEqual(["context", "plan", "retrieval", "guard", "grounding", "composition"]);
expect(runtime.retrievalResults[0].fragment_id).toBe("F1-guarded");
expect(runtime.questionTypeClass).toBe("causal_trace");
expect(runtime.composition.reply_type).toBe("factual");
});
it("passes null business scope when not resolved", async () => {
const runGroundingRuntime = vi.fn(() => ({
rbpLiveRouteAudit: {},
faLiveRouteAudit: {},
coverageEvaluation: { requirements: [], coverage: {} },
groundedAnswerEligibilityGuard: {},
groundingCheck: { status: "no_grounded_answer", reasons: [] }
}));
await runAssistantDeepTurnAnalysisRuntime({
userMessage: "question",
runContextRuntime: () =>
({
companyAnchors: {},
focusDomainForGuards: null,
temporalGuard: {},
domainPolarityGuardInitial: {},
claimAnchorAudit: { claim_type: "unknown" },
businessScopeResolution: {},
resolvedRouteSummary: null,
liveTemporalHint: null
}) as any,
runExecutionPlanRuntime: () =>
({
requirementExtraction: { requirements: [], byFragment: new Map() },
executionPlan: [],
rbpRoutePlanEnforcement: { executionPlan: [], audit: {} },
faRoutePlanEnforcement: { executionPlan: [], audit: {} }
}) as any,
runRetrievalRuntime: async () =>
({
retrievalCalls: [],
retrievalResultsRaw: [],
retrievalResults: []
}) as any,
runGuardRuntime: () =>
({
retrievalResults: [],
polarityGuardResult: { audit: {} },
targetedEvidenceResult: { audit: { targeted_evidence_hit_rate: null } },
evidenceGateResult: { audit: {} }
}) as any,
runGroundingRuntime: runGroundingRuntime as any,
runCompositionRuntime: () =>
({
questionTypeClass: "single_fact_lookup",
composition: { reply_type: "partial_coverage" }
}) as any
});
expect(runGroundingRuntime).toHaveBeenCalledWith(
expect.objectContaining({
routeSummary: null,
businessScopeResolved: null
})
);
});
});
@@ -0,0 +1,145 @@
import { describe, expect, it, vi } from "vitest";
import { buildAssistantDeepTurnNormalizationRuntime } from "../src/services/assistantDeepTurnNormalizationRuntimeAdapter";
describe("assistant deep turn normalization runtime adapter", () => {
it("uses followup state binding when feature flags are enabled and state exists", async () => {
const followupBinding = {
normalizedQuestion: "normalized question",
mergedContext: {
period_hint: "2020-07",
business_context: "ctx-from-followup"
},
usage: {
applied: true
}
};
const buildFollowupStateBinding = vi.fn(() => followupBinding);
const normalize = vi.fn(async (request) => ({
trace_id: "trace-1",
ok: true,
normalized: { schema_version: "normalized_query_v2_0_2" } as any,
route_hint_summary: null,
raw_model_output: {},
validation: { passed: true, errors: [] },
usage: { input_tokens: 10, output_tokens: 20, total_tokens: 30 },
latency_ms: 7,
prompt_version: String(request.promptVersion ?? ""),
schema_version: "normalized_query_v2_0_2",
request_count_for_case: 1
}));
const runtime = await buildAssistantDeepTurnNormalizationRuntime({
userMessage: "raw question",
payload: {
llmProvider: "openai",
apiKey: "k",
model: "m",
baseUrl: "https://api.example.com",
temperature: 0.2,
maxOutputTokens: 333,
promptVersion: "normalizer_v2_0_2",
systemPrompt: "sys",
developerPrompt: "dev",
domainPrompt: "dom",
fewShotExamples: "few",
context: {
period_hint: "2020-06"
},
useMock: true
},
featureInvestigationStateV1: true,
featureStateFollowupBindingV1: true,
sessionInvestigationState: {
active_domain: "settlements_60_62"
},
buildFollowupStateBinding,
normalize
});
expect(buildFollowupStateBinding).toHaveBeenCalledTimes(1);
expect(normalize).toHaveBeenCalledTimes(1);
expect(normalize).toHaveBeenCalledWith({
llmProvider: "openai",
apiKey: "k",
model: "m",
baseUrl: "https://api.example.com",
temperature: 0.2,
maxOutputTokens: 333,
promptVersion: "normalizer_v2_0_2",
systemPrompt: "sys",
developerPrompt: "dev",
domainPrompt: "dom",
fewShotExamples: "few",
userQuestion: "normalized question",
context: {
period_hint: "2020-07",
business_context: "ctx-from-followup"
},
useMock: true
});
expect(runtime.followupBinding).toBe(followupBinding);
expect(runtime.normalizePayload.userQuestion).toBe("normalized question");
});
it("falls back to raw user message when followup binding is disabled", async () => {
const buildFollowupStateBinding = vi.fn();
const normalize = vi.fn(async () => ({
trace_id: "trace-2",
ok: true,
normalized: { schema_version: "normalized_query_v2_0_2" } as any,
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: "address_query_runtime_v1",
schema_version: "normalized_query_v2_0_2",
request_count_for_case: 1
}));
const runtime = await buildAssistantDeepTurnNormalizationRuntime({
userMessage: "raw fallback",
payload: {
llmProvider: "openai",
context: {
business_context: "payload-context"
},
useMock: undefined
},
featureInvestigationStateV1: false,
featureStateFollowupBindingV1: true,
sessionInvestigationState: {
some: "state"
},
buildFollowupStateBinding,
normalize
});
expect(buildFollowupStateBinding).not.toHaveBeenCalled();
expect(normalize).toHaveBeenCalledWith({
llmProvider: "openai",
apiKey: undefined,
model: undefined,
baseUrl: undefined,
temperature: undefined,
maxOutputTokens: undefined,
promptVersion: "address_query_runtime_v1",
systemPrompt: undefined,
developerPrompt: undefined,
domainPrompt: undefined,
fewShotExamples: undefined,
userQuestion: "raw fallback",
context: {
business_context: "payload-context"
},
useMock: false
});
expect(runtime.followupBinding).toEqual({
normalizedQuestion: "raw fallback",
mergedContext: {
business_context: "payload-context"
},
usage: null
});
});
});
@@ -0,0 +1,163 @@
import { describe, expect, it, vi } from "vitest";
import { runAssistantDeepTurnResponseRuntime } from "../src/services/assistantDeepTurnResponseRuntimeAdapter";
function buildBaseInput(overrides: Record<string, unknown> = {}) {
return {
featureInvestigationStateV1: true,
featureContractsV11: true,
featureAnswerPolicyV11: true,
sessionId: "asst-1",
questionId: "msg-q1",
userMessage: "question",
normalized: {
trace_id: "trace-1",
prompt_version: "normalizer_v2_0_2",
schema_version: "normalized_query_v2_0_2",
normalized: { schema_version: "normalized_query_v2_0_2" }
},
normalizedQuestion: "normalized-question",
routeSummary: { mode: "deterministic_v2", decisions: [] as any[] },
executionPlan: [],
requirementExtractionRequirements: [],
coverageEvaluationRequirements: [],
coverageReport: {},
groundingCheck: { status: "no_grounded_answer", reasons: [] },
retrievalCalls: [],
retrievalResultsRaw: [],
retrievalResults: [],
questionTypeClass: "single_fact_lookup",
companyAnchors: {},
runtimeAnalysisContext: {},
businessScopeResolution: {},
temporalGuard: {},
polarityAudit: {},
claimAnchorAudit: {},
targetedEvidenceAudit: {},
evidenceAdmissibilityGateAudit: {},
rbpLiveRouteAudit: {},
faLiveRouteAudit: {},
groundedAnswerEligibilityGuard: {},
followupStateUsage: null,
followupApplied: false,
composition: {
reply_type: "factual",
assistant_reply: "assistant answer"
},
previousInvestigationState: null,
addressRuntimeMetaForDeep: null,
extractDroppedIntentSegments: () => [],
buildDebugRoutes: () => [],
extractExecutionState: () => [],
sanitizeReply: (value: string) => value,
persistInvestigationState: () => {},
messageIdFactory: () => "msg-a1",
appendItem: () => {},
getSession: () => ({ session_id: "asst-1", items: [] }),
persistSession: () => {},
cloneConversation: () => [],
logEvent: () => {},
...overrides
} as any;
}
describe("assistant deep turn response runtime adapter", () => {
it("wires packaging output into deep finalization", () => {
const runPackagingRuntime = vi.fn(() => ({
messageId: "msg-a1",
investigationStateSnapshot: null,
droppedIntentSegments: [],
analysisContextForContract: null,
routesForDebug: [],
resolvedExecutionState: [],
safeAssistantReplyBase: "base",
safeAssistantReply: "safe-reply",
debug: { trace_id: "trace-1" },
assistantItem: {
message_id: "msg-a1",
session_id: "asst-1",
role: "assistant",
text: "safe-reply",
reply_type: "factual",
created_at: "2026-04-10T00:00:00.000Z",
trace_id: "trace-1",
debug: null
},
deepAnalysisLogDetails: { info: "ok" }
}));
const responsePayload = {
ok: true,
session_id: "asst-1",
conversation: [],
debug: { trace_id: "trace-1" }
};
const runFinalizeDeepTurn = vi.fn(() => ({
response: responsePayload
}));
const runtime = runAssistantDeepTurnResponseRuntime(
buildBaseInput({
runPackagingRuntime,
runFinalizeDeepTurn
})
);
expect(runPackagingRuntime).toHaveBeenCalledTimes(1);
expect(runFinalizeDeepTurn).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "asst-1",
assistantReply: "safe-reply",
replyType: "factual"
})
);
expect(runtime.response).toEqual(responsePayload);
expect(runtime.debug).toEqual({ trace_id: "trace-1" });
});
it("passes feature flags and followup flags into packaging stage", () => {
const runPackagingRuntime = vi.fn(() => ({
messageId: "msg-a1",
investigationStateSnapshot: null,
droppedIntentSegments: [],
analysisContextForContract: null,
routesForDebug: [],
resolvedExecutionState: [],
safeAssistantReplyBase: "base",
safeAssistantReply: "safe-reply",
debug: {},
assistantItem: {
message_id: "msg-a1",
session_id: "asst-1",
role: "assistant",
text: "safe-reply",
reply_type: "factual",
created_at: "2026-04-10T00:00:00.000Z",
trace_id: "trace-1",
debug: null
},
deepAnalysisLogDetails: {}
}));
runAssistantDeepTurnResponseRuntime(
buildBaseInput({
featureInvestigationStateV1: false,
followupApplied: true,
runPackagingRuntime,
runFinalizeDeepTurn: () => ({
response: {
ok: true,
session_id: "asst-1",
conversation: [],
debug: null
}
})
})
);
expect(runPackagingRuntime).toHaveBeenCalledWith(
expect.objectContaining({
featureInvestigationStateV1: false,
followupApplied: true
})
);
});
});