diff --git a/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js index 16da78d..cee61e0 100644 --- a/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantAddressOrchestrationRuntimeAdapter.js @@ -1,6 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildAssistantAddressOrchestrationRuntime = buildAssistantAddressOrchestrationRuntime; +const assistantRoutePolicyRuntimeAdapter_1 = require("./assistantRoutePolicyRuntimeAdapter"); function hasSelectedObjectInventorySignal(text) { return /(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|selected\s+object)/iu.test(String(text ?? "")); } @@ -78,26 +79,25 @@ async function buildAssistantAddressOrchestrationRuntime(input) { carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose, input.sessionAddressNavigationState); } const followupContext = carryover?.followupContext ?? null; - const orchestrationDecision = input.resolveAssistantOrchestrationDecision({ + const routePolicyRuntime = (0, assistantRoutePolicyRuntimeAdapter_1.runAssistantRoutePolicyRuntime)({ rawUserMessage: input.userMessage, effectiveAddressUserMessage: addressInputMessage, followupContext, llmPreDecomposeMeta: addressPreDecompose, sessionItems: input.sessionItems, sessionOrganizationScope: input.sessionOrganizationScope ?? null, - useMock: input.useMock + useMock: input.useMock, + resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision }); + const orchestrationDecision = routePolicyRuntime.orchestrationDecision; const dialogContinuationContract = input.buildAddressDialogContinuationContractV2(input.userMessage, addressInputMessage, carryover, addressPreDecompose); const addressRuntimeMeta = { ...addressPreDecompose, toolGateDecision: orchestrationDecision.toolGateDecision ?? null, toolGateReason: orchestrationDecision.toolGateReason ?? null, dialogContinuationContract, - orchestrationContract: orchestrationDecision.orchestrationContract ?? null - }; - const livingModeDecision = { - mode: orchestrationDecision.livingMode, - reason: orchestrationDecision.livingReason + orchestrationContract: orchestrationDecision.orchestrationContract ?? null, + routePolicyContract: routePolicyRuntime.routePolicyContract }; return { addressPreDecompose, @@ -105,6 +105,6 @@ async function buildAssistantAddressOrchestrationRuntime(input) { carryover, orchestrationDecision, addressRuntimeMeta, - livingModeDecision + livingModeDecision: routePolicyRuntime.livingModeDecision }; } diff --git a/llm_normalizer/backend/dist/services/assistantRoutePolicyRuntimeAdapter.js b/llm_normalizer/backend/dist/services/assistantRoutePolicyRuntimeAdapter.js new file mode 100644 index 0000000..81f9745 --- /dev/null +++ b/llm_normalizer/backend/dist/services/assistantRoutePolicyRuntimeAdapter.js @@ -0,0 +1,38 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ASSISTANT_ROUTE_POLICY_RUNTIME_SCHEMA_VERSION = void 0; +exports.runAssistantRoutePolicyRuntime = runAssistantRoutePolicyRuntime; +exports.ASSISTANT_ROUTE_POLICY_RUNTIME_SCHEMA_VERSION = "assistant_route_policy_runtime_v1"; +function hasObject(value) { + return Boolean(value && typeof value === "object"); +} +function runAssistantRoutePolicyRuntime(input) { + const orchestrationDecision = input.resolveAssistantOrchestrationDecision({ + rawUserMessage: input.rawUserMessage, + effectiveAddressUserMessage: input.effectiveAddressUserMessage, + followupContext: input.followupContext, + llmPreDecomposeMeta: input.llmPreDecomposeMeta, + sessionItems: input.sessionItems, + sessionOrganizationScope: input.sessionOrganizationScope, + useMock: input.useMock + }); + const livingModeDecision = { + mode: orchestrationDecision.livingMode, + reason: orchestrationDecision.livingReason + }; + return { + orchestrationDecision, + livingModeDecision, + routePolicyContract: { + schema_version: exports.ASSISTANT_ROUTE_POLICY_RUNTIME_SCHEMA_VERSION, + policy_owner: "assistantRoutePolicyRuntimeAdapter", + decision_source: "resolveAssistantOrchestrationDecision", + living_mode: livingModeDecision.mode, + living_reason: livingModeDecision.reason, + tool_gate_decision: orchestrationDecision.toolGateDecision ?? null, + tool_gate_reason: orchestrationDecision.toolGateReason ?? null, + has_followup_context: hasObject(input.followupContext), + has_orchestration_contract: hasObject(orchestrationDecision.orchestrationContract) + } + }; +} diff --git a/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts index ba21bf4..fa21c67 100644 --- a/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts @@ -1,3 +1,5 @@ +import { runAssistantRoutePolicyRuntime } from "./assistantRoutePolicyRuntimeAdapter"; + export interface BuildAssistantAddressOrchestrationRuntimeInput { userMessage: string; sessionItems: unknown[]; @@ -195,15 +197,17 @@ export async function buildAssistantAddressOrchestrationRuntime( } const followupContext = carryover?.followupContext ?? null; - const orchestrationDecision = input.resolveAssistantOrchestrationDecision({ + const routePolicyRuntime = runAssistantRoutePolicyRuntime({ rawUserMessage: input.userMessage, effectiveAddressUserMessage: addressInputMessage, followupContext, llmPreDecomposeMeta: addressPreDecompose, sessionItems: input.sessionItems, sessionOrganizationScope: input.sessionOrganizationScope ?? null, - useMock: input.useMock + useMock: input.useMock, + resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision }); + const orchestrationDecision = routePolicyRuntime.orchestrationDecision; const dialogContinuationContract = input.buildAddressDialogContinuationContractV2( input.userMessage, addressInputMessage, @@ -215,11 +219,8 @@ export async function buildAssistantAddressOrchestrationRuntime( toolGateDecision: orchestrationDecision.toolGateDecision ?? null, toolGateReason: orchestrationDecision.toolGateReason ?? null, dialogContinuationContract, - orchestrationContract: orchestrationDecision.orchestrationContract ?? null - }; - const livingModeDecision = { - mode: orchestrationDecision.livingMode, - reason: orchestrationDecision.livingReason + orchestrationContract: orchestrationDecision.orchestrationContract ?? null, + routePolicyContract: routePolicyRuntime.routePolicyContract }; return { @@ -228,6 +229,6 @@ export async function buildAssistantAddressOrchestrationRuntime( carryover, orchestrationDecision, addressRuntimeMeta, - livingModeDecision + livingModeDecision: routePolicyRuntime.livingModeDecision }; } diff --git a/llm_normalizer/backend/src/services/assistantRoutePolicyRuntimeAdapter.ts b/llm_normalizer/backend/src/services/assistantRoutePolicyRuntimeAdapter.ts new file mode 100644 index 0000000..bae261d --- /dev/null +++ b/llm_normalizer/backend/src/services/assistantRoutePolicyRuntimeAdapter.ts @@ -0,0 +1,79 @@ +export const ASSISTANT_ROUTE_POLICY_RUNTIME_SCHEMA_VERSION = "assistant_route_policy_runtime_v1" as const; + +export interface RunAssistantRoutePolicyRuntimeInput { + rawUserMessage: string; + effectiveAddressUserMessage: string; + followupContext: unknown; + llmPreDecomposeMeta: Record; + sessionItems?: unknown[]; + sessionOrganizationScope?: unknown; + useMock: boolean; + resolveAssistantOrchestrationDecision: (input: { + rawUserMessage: string; + effectiveAddressUserMessage: string; + followupContext: unknown; + llmPreDecomposeMeta: Record; + sessionItems?: unknown[]; + sessionOrganizationScope?: unknown; + useMock: boolean; + }) => Record; +} + +export interface AssistantRoutePolicyRuntimeContract { + schema_version: typeof ASSISTANT_ROUTE_POLICY_RUNTIME_SCHEMA_VERSION; + policy_owner: "assistantRoutePolicyRuntimeAdapter"; + decision_source: "resolveAssistantOrchestrationDecision"; + living_mode: unknown; + living_reason: unknown; + tool_gate_decision: unknown; + tool_gate_reason: unknown; + has_followup_context: boolean; + has_orchestration_contract: boolean; +} + +export interface RunAssistantRoutePolicyRuntimeOutput { + orchestrationDecision: Record; + livingModeDecision: { + mode: unknown; + reason: unknown; + }; + routePolicyContract: AssistantRoutePolicyRuntimeContract; +} + +function hasObject(value: unknown): boolean { + return Boolean(value && typeof value === "object"); +} + +export function runAssistantRoutePolicyRuntime( + input: RunAssistantRoutePolicyRuntimeInput +): RunAssistantRoutePolicyRuntimeOutput { + const orchestrationDecision = input.resolveAssistantOrchestrationDecision({ + rawUserMessage: input.rawUserMessage, + effectiveAddressUserMessage: input.effectiveAddressUserMessage, + followupContext: input.followupContext, + llmPreDecomposeMeta: input.llmPreDecomposeMeta, + sessionItems: input.sessionItems, + sessionOrganizationScope: input.sessionOrganizationScope, + useMock: input.useMock + }); + const livingModeDecision = { + mode: orchestrationDecision.livingMode, + reason: orchestrationDecision.livingReason + }; + + return { + orchestrationDecision, + livingModeDecision, + routePolicyContract: { + schema_version: ASSISTANT_ROUTE_POLICY_RUNTIME_SCHEMA_VERSION, + policy_owner: "assistantRoutePolicyRuntimeAdapter", + decision_source: "resolveAssistantOrchestrationDecision", + living_mode: livingModeDecision.mode, + living_reason: livingModeDecision.reason, + tool_gate_decision: orchestrationDecision.toolGateDecision ?? null, + tool_gate_reason: orchestrationDecision.toolGateReason ?? null, + has_followup_context: hasObject(input.followupContext), + has_orchestration_contract: hasObject(orchestrationDecision.orchestrationContract) + } + }; +} diff --git a/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts index 356603a..555d63d 100644 --- a/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantAddressOrchestrationRuntimeAdapter.test.ts @@ -63,6 +63,16 @@ describe("assistant address orchestration runtime adapter", () => { expect(output.orchestrationDecision.runAddressLane).toBe(true); expect(output.livingModeDecision.mode).toBe("deep_analysis"); expect(output.addressRuntimeMeta.toolGateDecision).toBe("run_address_lane"); + expect(output.addressRuntimeMeta.routePolicyContract).toEqual( + expect.objectContaining({ + schema_version: "assistant_route_policy_runtime_v1", + policy_owner: "assistantRoutePolicyRuntimeAdapter", + living_mode: "deep_analysis", + tool_gate_decision: "run_address_lane", + has_followup_context: true, + has_orchestration_contract: true + }) + ); expect(output.addressRuntimeMeta.dialogContinuationContract).toEqual({ schema_version: "address_dialog_continuation_contract_v2" }); diff --git a/llm_normalizer/backend/tests/assistantRoutePolicyRuntimeAdapter.test.ts b/llm_normalizer/backend/tests/assistantRoutePolicyRuntimeAdapter.test.ts new file mode 100644 index 0000000..1e09012 --- /dev/null +++ b/llm_normalizer/backend/tests/assistantRoutePolicyRuntimeAdapter.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it, vi } from "vitest"; +import { runAssistantRoutePolicyRuntime } from "../src/services/assistantRoutePolicyRuntimeAdapter"; + +describe("assistant route policy runtime adapter", () => { + it("delegates to the current orchestration decision function and emits a route policy contract", () => { + const resolveAssistantOrchestrationDecision = vi.fn(() => ({ + runAddressLane: true, + livingMode: "address_data", + livingReason: "address_lane_triggered", + toolGateDecision: "run_address_lane", + toolGateReason: "followup_context_detected", + orchestrationContract: { + schema_version: "assistant_orchestration_contract_v1" + } + })); + const followupContext = { previous_intent: "inventory_on_hand_as_of_date" }; + + const output = runAssistantRoutePolicyRuntime({ + rawUserMessage: "кто это поставил нам", + effectiveAddressUserMessage: "кто это поставил нам", + followupContext, + llmPreDecomposeMeta: { + attempted: true + }, + sessionItems: [{ role: "assistant" }], + sessionOrganizationScope: { + selectedOrganization: "ООО Ромашка" + }, + useMock: false, + resolveAssistantOrchestrationDecision + }); + + expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith({ + rawUserMessage: "кто это поставил нам", + effectiveAddressUserMessage: "кто это поставил нам", + followupContext, + llmPreDecomposeMeta: { + attempted: true + }, + sessionItems: [{ role: "assistant" }], + sessionOrganizationScope: { + selectedOrganization: "ООО Ромашка" + }, + useMock: false + }); + expect(output.orchestrationDecision.runAddressLane).toBe(true); + expect(output.livingModeDecision).toEqual({ + mode: "address_data", + reason: "address_lane_triggered" + }); + expect(output.routePolicyContract).toEqual({ + schema_version: "assistant_route_policy_runtime_v1", + policy_owner: "assistantRoutePolicyRuntimeAdapter", + decision_source: "resolveAssistantOrchestrationDecision", + living_mode: "address_data", + living_reason: "address_lane_triggered", + tool_gate_decision: "run_address_lane", + tool_gate_reason: "followup_context_detected", + has_followup_context: true, + has_orchestration_contract: true + }); + }); +});