ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.46: склейка всего address-потока в отдельный assistantAddressAttemptRuntimeAdapter, для того чтобы из handleMessage убрать три локальных closure целиком. Все три моста (lane, response, living chat) прокидываются и не ломают контракт.

This commit is contained in:
dctouch 2026-04-11 00:22:00 +03:00
parent 5790e25b68
commit 5f4e898c7c
11 changed files with 1269 additions and 143 deletions

View File

@ -1437,7 +1437,38 @@ Validation:
- `assistantDeepTurnPackagingRuntimeAdapter.test.ts` - `assistantDeepTurnPackagingRuntimeAdapter.test.ts`
- `assistantWave10SettlementCorrectiveRegression.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 + 2.43 + 2.44 + 2.45 completed)** Implemented in current pass (Phase 2.46):
1. Activated full address-turn attempt runtime boundary inside `assistantService`:
- rewired address handling call-site to `runAssistantAddressAttemptRuntime(...)`;
- removed local inline closures for lane-attempt, lane-response, and living-chat handoff wiring from `handleMessage`.
2. Finalized and type-hardened address attempt adapter contract:
- `assistantAddressAttemptRuntimeAdapter.ts`
- aligned `logEvent` and `messageIdFactory` contract typing to runtime expectations.
3. Added focused unit tests:
- `assistantAddressAttemptRuntimeAdapter.test.ts`
Validation:
1. `npm run build` passed.
2. Targeted living/address/deep followup pack passed:
- `assistantAddressAttemptRuntimeAdapter.test.ts`
- `assistantDeepTurnAttemptRuntimeAdapter.test.ts`
- `assistantDeepTurnResponseAttemptRuntimeAdapter.test.ts`
- `assistantDeepTurnAnalysisAttemptRuntimeAdapter.test.ts`
- `assistantDeepTurnAnalysisRuntimeAdapter.test.ts`
- `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 + 2.43 + 2.44 + 2.45 + 2.46 completed)**
## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards) ## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards)

View File

@ -0,0 +1,117 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantAddressAttemptRuntime = runAssistantAddressAttemptRuntime;
const assistantAddressRuntimeAdapter_1 = require("./assistantAddressRuntimeAdapter");
const assistantAddressLaneAttemptRuntimeAdapter_1 = require("./assistantAddressLaneAttemptRuntimeAdapter");
const assistantAddressLaneResponseAttemptRuntimeAdapter_1 = require("./assistantAddressLaneResponseAttemptRuntimeAdapter");
const assistantLivingChatAttemptRuntimeAdapter_1 = require("./assistantLivingChatAttemptRuntimeAdapter");
async function runAssistantAddressAttemptRuntime(input) {
const runAddressRuntimeSafe = input.runAddressRuntime ?? assistantAddressRuntimeAdapter_1.runAssistantAddressRuntime;
const runAddressLaneAttemptRuntimeSafe = input.runAddressLaneAttemptRuntime ?? assistantAddressLaneAttemptRuntimeAdapter_1.runAssistantAddressLaneAttemptRuntime;
const runAddressLaneResponseAttemptRuntimeSafe = input.runAddressLaneResponseAttemptRuntime ?? assistantAddressLaneResponseAttemptRuntimeAdapter_1.runAssistantAddressLaneResponseAttemptRuntime;
const runLivingChatAttemptRuntimeSafe = input.runLivingChatAttemptRuntime ?? assistantLivingChatAttemptRuntimeAdapter_1.runAssistantLivingChatAttemptRuntime;
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => runAddressLaneResponseAttemptRuntimeSafe({
sessionId: input.sessionId,
userMessage: input.userMessage,
effectiveAddressUserMessage,
addressLane,
carryoverMeta,
llmPreDecomposeMeta,
knownOrganizations: input.sessionScope.knownOrganizations,
activeOrganization: input.sessionScope.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
});
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => runLivingChatAttemptRuntimeSafe({
sessionId: input.sessionId,
userMessage: input.userMessage,
sessionItems: input.sessionItems,
modeDecision,
sessionScope: {
knownOrganizations: input.sessionScope.knownOrganizations,
selectedOrganization: input.sessionScope.selectedOrganization,
activeOrganization: input.sessionScope.activeOrganization
},
addressRuntimeMeta,
traceIdFactory: () => `chat-${input.messageIdFactory().replace(/^msg-/, "")}`,
toNonEmptyString: input.toNonEmptyString,
mergeKnownOrganizations: input.mergeKnownOrganizations,
hasAssistantDataScopeMetaQuestionSignal: input.hasAssistantDataScopeMetaQuestionSignal,
shouldHandleAsAssistantCapabilityMetaQuery: input.shouldHandleAsAssistantCapabilityMetaQuery,
hasDestructiveDataActionSignal: input.hasDestructiveDataActionSignal,
hasDangerOrCoercionSignal: input.hasDangerOrCoercionSignal,
hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal,
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: input.resolveDataScopeProbe,
applyScriptGuard: input.applyScriptGuard,
applyGroundingGuard: input.applyGroundingGuard,
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
buildAssistantCapabilityContractReply: input.buildAssistantCapabilityContractReply,
appendItem: input.appendItem,
getSession: input.getSession,
persistSession: input.persistSession,
cloneConversation: input.cloneConversation,
logEvent: input.logEvent,
messageIdFactory: input.messageIdFactory,
nowIso: input.nowIso,
payload: input.payload,
chatClient: input.chatClient,
loadAssistantCanonExcerpt: input.loadAssistantCanonExcerpt,
sanitizeOutgoingAssistantText: input.sanitizeOutgoingAssistantText,
defaultModel: input.defaultModel,
defaultBaseUrl: input.defaultBaseUrl,
defaultApiKey: input.defaultApiKey
});
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => runAddressLaneAttemptRuntimeSafe({
messageUsed,
carryMeta,
analysisDateHint,
activeOrganization: input.sessionScope.activeOrganization,
mergeFollowupContextWithOrganizationScope: input.mergeFollowupContextWithOrganizationScope,
runAddressQueryTryHandle: input.runAddressQueryTryHandle
});
return runAddressRuntimeSafe({
featureAssistantAddressQueryV1: input.featureAssistantAddressQueryV1,
sessionId: input.sessionId,
userMessage: input.userMessage,
sessionItems: input.sessionItems,
llmProvider: input.payload.llmProvider,
useMock: Boolean(input.payload.useMock),
featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1,
runAddressLlmPreDecompose: input.runAddressLlmPreDecompose,
buildAddressLlmPredecomposeContractV1: input.buildAddressLlmPredecomposeContractV1,
sanitizeAddressMessageForFallback: input.sanitizeAddressMessageForFallback,
toNonEmptyString: input.toNonEmptyString,
resolveAddressFollowupCarryoverContext: input.resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision,
buildAddressDialogContinuationContractV2: input.buildAddressDialogContinuationContractV2,
runtimeAnalysisContextAsOfDate: input.runtimeAnalysisContextAsOfDate,
payloadContextPeriodHint: input.payload?.context?.period_hint,
compactWhitespace: input.compactWhitespace,
runAddressLaneAttempt,
isRetryableAddressLimitedResult: input.isRetryableAddressLimitedResult,
finalizeAddressLaneResponse,
tryHandleLivingChat,
logEvent: input.logEvent,
nowIso: input.nowIso,
runAddressOrchestrationRuntime: input.runAddressOrchestrationRuntime,
runAddressToolGateRuntime: input.runAddressToolGateRuntime,
runAddressLaneRuntime: input.runAddressLaneRuntime
});
}

View File

@ -62,12 +62,9 @@ const openaiResponsesClient_1 = __importStar(require("./openaiResponsesClient"))
const addressMcpClient_1 = __importStar(require("./addressMcpClient")); const addressMcpClient_1 = __importStar(require("./addressMcpClient"));
const capabilitiesRegistry_1 = __importStar(require("./capabilitiesRegistry")); const capabilitiesRegistry_1 = __importStar(require("./capabilitiesRegistry"));
const assistantCanon_1 = __importStar(require("./assistantCanon")); const assistantCanon_1 = __importStar(require("./assistantCanon"));
const assistantAddressLaneResponseAttemptRuntimeAdapter_1 = __importStar(require("./assistantAddressLaneResponseAttemptRuntimeAdapter")); const assistantAddressAttemptRuntimeAdapter_1 = __importStar(require("./assistantAddressAttemptRuntimeAdapter"));
const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding")); const assistantCoverageGrounding_1 = __importStar(require("./assistantCoverageGrounding"));
const assistantDeepTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAttemptRuntimeAdapter")); const assistantDeepTurnAttemptRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnAttemptRuntimeAdapter"));
const assistantAddressRuntimeAdapter_1 = __importStar(require("./assistantAddressRuntimeAdapter"));
const assistantAddressLaneAttemptRuntimeAdapter_1 = __importStar(require("./assistantAddressLaneAttemptRuntimeAdapter"));
const assistantLivingChatAttemptRuntimeAdapter_1 = __importStar(require("./assistantLivingChatAttemptRuntimeAdapter"));
const assistantUserTurnBootstrapRuntimeAdapter_1 = __importStar(require("./assistantUserTurnBootstrapRuntimeAdapter")); const assistantUserTurnBootstrapRuntimeAdapter_1 = __importStar(require("./assistantUserTurnBootstrapRuntimeAdapter"));
const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning")); const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning"));
const iconv_lite_1 = __importDefault(require("iconv-lite")); const iconv_lite_1 = __importDefault(require("iconv-lite"));
@ -4385,40 +4382,30 @@ class AssistantService {
nowIso: () => new Date().toISOString() nowIso: () => new Date().toISOString()
}); });
const sessionOrganizationScope = resolveSessionOrganizationScopeContext(userMessage, session.items); const sessionOrganizationScope = resolveSessionOrganizationScopeContext(userMessage, session.items);
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => (0, assistantAddressLaneResponseAttemptRuntimeAdapter_1.runAssistantAddressLaneResponseAttemptRuntime)({ const addressRuntime = await (0, assistantAddressAttemptRuntimeAdapter_1.runAssistantAddressAttemptRuntime)({
sessionId, featureAssistantAddressQueryV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1,
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, sessionId,
userMessage, userMessage,
sessionItems: session.items, sessionItems: session.items,
modeDecision, payload,
sessionScope: { sessionScope: {
knownOrganizations: sessionOrganizationScope.knownOrganizations, knownOrganizations: sessionOrganizationScope.knownOrganizations,
selectedOrganization: sessionOrganizationScope.selectedOrganization, selectedOrganization: sessionOrganizationScope.selectedOrganization,
activeOrganization: sessionOrganizationScope.activeOrganization activeOrganization: sessionOrganizationScope.activeOrganization
}, },
addressRuntimeMeta, featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1,
traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`, runAddressLlmPreDecompose: async () => runAddressLlmPreDecompose(this.normalizerService, payload, userMessage),
buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1,
sanitizeAddressMessageForFallback,
toNonEmptyString, toNonEmptyString,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision,
buildAddressDialogContinuationContractV2,
runtimeAnalysisContextAsOfDate: runtimeAnalysisContext.as_of_date,
compactWhitespace,
mergeFollowupContextWithOrganizationScope,
runAddressQueryTryHandle: (laneMessageUsed, options) => this.addressQueryService.tryHandle(laneMessageUsed, options),
isRetryableAddressLimitedResult,
mergeKnownOrganizations, mergeKnownOrganizations,
hasAssistantDataScopeMetaQuestionSignal, hasAssistantDataScopeMetaQuestionSignal,
shouldHandleAsAssistantCapabilityMetaQuery, shouldHandleAsAssistantCapabilityMetaQuery,
@ -4438,56 +4425,23 @@ class AssistantService {
buildAssistantDataScopeSelectionReply, buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply, buildAssistantOperationalBoundaryReply,
buildAssistantCapabilityContractReply, buildAssistantCapabilityContractReply,
chatClient: this.chatClient,
loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt,
sanitizeOutgoingAssistantText,
defaultModel: config_1.DEFAULT_MODEL,
defaultBaseUrl: config_1.DEFAULT_OPENAI_BASE_URL,
defaultApiKey: process.env.OPENAI_API_KEY ?? "",
buildAddressDebugPayload,
buildAddressFollowupOffer,
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), getSession: (targetSessionId) => this.sessions.getSession(targetSessionId),
persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState),
cloneConversation: (items) => cloneItems(items), cloneConversation: (items) => cloneItems(items),
logEvent: (payload) => (0, log_1.logJson)(payload), logEvent: (payload) => (0, log_1.logJson)(payload),
messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`,
nowIso: () => new Date().toISOString(),
payload,
chatClient: this.chatClient,
loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt,
sanitizeOutgoingAssistantText,
defaultModel: config_1.DEFAULT_MODEL,
defaultBaseUrl: config_1.DEFAULT_OPENAI_BASE_URL,
defaultApiKey: process.env.OPENAI_API_KEY ?? ""
});
let addressRuntimeMetaForDeep = null;
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => (0, assistantAddressLaneAttemptRuntimeAdapter_1.runAssistantAddressLaneAttemptRuntime)({
messageUsed,
carryMeta: carryMeta ?? null,
analysisDateHint: analysisDateHint ?? null,
activeOrganization: sessionOrganizationScope.activeOrganization,
mergeFollowupContextWithOrganizationScope,
runAddressQueryTryHandle: (laneMessageUsed, options) => this.addressQueryService.tryHandle(laneMessageUsed, options)
});
const addressRuntime = await (0, assistantAddressRuntimeAdapter_1.runAssistantAddressRuntime)({
featureAssistantAddressQueryV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1,
sessionId,
userMessage,
sessionItems: session.items,
llmProvider: payload?.llmProvider,
useMock: Boolean(payload.useMock),
featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1,
runAddressLlmPreDecompose: async () => runAddressLlmPreDecompose(this.normalizerService, payload, userMessage),
buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1,
sanitizeAddressMessageForFallback,
toNonEmptyString,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision,
buildAddressDialogContinuationContractV2,
runtimeAnalysisContextAsOfDate: runtimeAnalysisContext.as_of_date,
payloadContextPeriodHint: payload?.context?.period_hint,
compactWhitespace,
runAddressLaneAttempt,
isRetryableAddressLimitedResult,
finalizeAddressLaneResponse,
tryHandleLivingChat: (modeDecision, runtimeMeta) => tryHandleLivingChat(modeDecision, runtimeMeta),
logEvent: (payload) => (0, log_1.logJson)(payload),
nowIso: () => new Date().toISOString() nowIso: () => new Date().toISOString()
}); });
addressRuntimeMetaForDeep = addressRuntime.addressRuntimeMetaForDeep; const addressRuntimeMetaForDeep = addressRuntime.addressRuntimeMetaForDeep;
if (addressRuntime.handled && addressRuntime.response) { if (addressRuntime.handled && addressRuntime.response) {
return addressRuntime.response; return addressRuntime.response;
} }

View File

@ -0,0 +1,228 @@
import {
runAssistantAddressRuntime,
type RunAssistantAddressRuntimeInput,
type RunAssistantAddressRuntimeOutput
} from "./assistantAddressRuntimeAdapter";
import {
runAssistantAddressLaneAttemptRuntime,
type RunAssistantAddressLaneAttemptRuntimeInput
} from "./assistantAddressLaneAttemptRuntimeAdapter";
import {
runAssistantAddressLaneResponseAttemptRuntime,
type RunAssistantAddressLaneResponseAttemptRuntimeInput
} from "./assistantAddressLaneResponseAttemptRuntimeAdapter";
import {
runAssistantLivingChatAttemptRuntime,
type RunAssistantLivingChatAttemptRuntimeInput
} from "./assistantLivingChatAttemptRuntimeAdapter";
interface AddressAttemptPayload {
llmProvider?: unknown;
useMock?: unknown;
context?: {
period_hint?: unknown;
} | null;
apiKey?: unknown;
model?: unknown;
baseUrl?: unknown;
temperature?: number;
maxOutputTokens?: number;
}
interface AddressSessionScope {
knownOrganizations: string[];
selectedOrganization: string | null;
activeOrganization: string | null;
}
export interface RunAssistantAddressAttemptRuntimeInput<ResponseType = unknown>
extends Omit<
RunAssistantAddressRuntimeInput<ResponseType>,
"llmProvider" | "useMock" | "payloadContextPeriodHint" | "runAddressLaneAttempt" | "finalizeAddressLaneResponse" | "tryHandleLivingChat"
> {
payload: AddressAttemptPayload;
sessionScope: AddressSessionScope;
mergeFollowupContextWithOrganizationScope: RunAssistantAddressLaneAttemptRuntimeInput["mergeFollowupContextWithOrganizationScope"];
runAddressQueryTryHandle: RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"];
mergeKnownOrganizations: RunAssistantAddressLaneResponseAttemptRuntimeInput<ResponseType>["mergeKnownOrganizations"];
hasAssistantDataScopeMetaQuestionSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasAssistantDataScopeMetaQuestionSignal"];
shouldHandleAsAssistantCapabilityMetaQuery: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["shouldHandleAsAssistantCapabilityMetaQuery"];
hasDestructiveDataActionSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasDestructiveDataActionSignal"];
hasDangerOrCoercionSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasDangerOrCoercionSignal"];
hasOperationalAdminActionRequestSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasOperationalAdminActionRequestSignal"];
hasOrganizationFactLookupSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasOrganizationFactLookupSignal"];
hasOrganizationFactFollowupSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasOrganizationFactFollowupSignal"];
shouldEmitOrganizationSelectionReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["shouldEmitOrganizationSelectionReply"];
hasAssistantCapabilityQuestionSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasAssistantCapabilityQuestionSignal"];
resolveDataScopeProbe: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["resolveDataScopeProbe"];
applyScriptGuard: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["applyScriptGuard"];
applyGroundingGuard: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["applyGroundingGuard"];
buildAssistantSafetyRefusalReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantSafetyRefusalReply"];
buildAssistantDataScopeContractReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantDataScopeContractReply"];
buildAssistantOrganizationFactBoundaryReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantOrganizationFactBoundaryReply"];
buildAssistantDataScopeSelectionReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantDataScopeSelectionReply"];
buildAssistantOperationalBoundaryReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantOperationalBoundaryReply"];
buildAssistantCapabilityContractReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["buildAssistantCapabilityContractReply"];
chatClient: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["chatClient"];
loadAssistantCanonExcerpt: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["loadAssistantCanonExcerpt"];
sanitizeOutgoingAssistantText: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["sanitizeOutgoingAssistantText"];
defaultModel: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["defaultModel"];
defaultBaseUrl: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["defaultBaseUrl"];
defaultApiKey?: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["defaultApiKey"];
buildAddressDebugPayload: RunAssistantAddressLaneResponseAttemptRuntimeInput<ResponseType>["buildAddressDebugPayload"];
buildAddressFollowupOffer: RunAssistantAddressLaneResponseAttemptRuntimeInput<ResponseType>["buildAddressFollowupOffer"];
appendItem: RunAssistantAddressLaneResponseAttemptRuntimeInput<ResponseType>["appendItem"];
getSession: RunAssistantAddressLaneResponseAttemptRuntimeInput<ResponseType>["getSession"];
persistSession: RunAssistantAddressLaneResponseAttemptRuntimeInput<ResponseType>["persistSession"];
cloneConversation: RunAssistantAddressLaneResponseAttemptRuntimeInput<ResponseType>["cloneConversation"];
logEvent: RunAssistantAddressRuntimeInput<ResponseType>["logEvent"];
messageIdFactory: NonNullable<
RunAssistantAddressLaneResponseAttemptRuntimeInput<ResponseType>["messageIdFactory"]
>;
runAddressRuntime?: (
input: RunAssistantAddressRuntimeInput<ResponseType>
) => Promise<RunAssistantAddressRuntimeOutput<ResponseType>>;
runAddressLaneAttemptRuntime?: (
input: RunAssistantAddressLaneAttemptRuntimeInput
) => Promise<Awaited<ReturnType<RunAssistantAddressLaneAttemptRuntimeInput["runAddressQueryTryHandle"]>>>;
runAddressLaneResponseAttemptRuntime?: (
input: RunAssistantAddressLaneResponseAttemptRuntimeInput<ResponseType>
) => ResponseType;
runLivingChatAttemptRuntime?: (
input: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>
) => Promise<ResponseType | null>;
}
export async function runAssistantAddressAttemptRuntime<ResponseType = unknown>(
input: RunAssistantAddressAttemptRuntimeInput<ResponseType>
): Promise<RunAssistantAddressRuntimeOutput<ResponseType>> {
const runAddressRuntimeSafe = input.runAddressRuntime ?? runAssistantAddressRuntime;
const runAddressLaneAttemptRuntimeSafe = input.runAddressLaneAttemptRuntime ?? runAssistantAddressLaneAttemptRuntime;
const runAddressLaneResponseAttemptRuntimeSafe =
input.runAddressLaneResponseAttemptRuntime ?? runAssistantAddressLaneResponseAttemptRuntime;
const runLivingChatAttemptRuntimeSafe =
input.runLivingChatAttemptRuntime ?? runAssistantLivingChatAttemptRuntime;
const finalizeAddressLaneResponse: RunAssistantAddressRuntimeInput<ResponseType>["finalizeAddressLaneResponse"] = (
addressLane,
effectiveAddressUserMessage,
carryoverMeta = null,
llmPreDecomposeMeta = null
) =>
runAddressLaneResponseAttemptRuntimeSafe({
sessionId: input.sessionId,
userMessage: input.userMessage,
effectiveAddressUserMessage,
addressLane,
carryoverMeta,
llmPreDecomposeMeta,
knownOrganizations: input.sessionScope.knownOrganizations,
activeOrganization: input.sessionScope.activeOrganization,
sanitizeOutgoingAssistantText: input.sanitizeOutgoingAssistantText,
buildAddressDebugPayload: input.buildAddressDebugPayload,
buildAddressFollowupOffer: input.buildAddressFollowupOffer,
mergeKnownOrganizations: input.mergeKnownOrganizations as any,
toNonEmptyString: input.toNonEmptyString,
appendItem: input.appendItem,
getSession: input.getSession,
persistSession: input.persistSession,
cloneConversation: input.cloneConversation,
logEvent: input.logEvent,
messageIdFactory: input.messageIdFactory
} as RunAssistantAddressLaneResponseAttemptRuntimeInput<ResponseType>);
const tryHandleLivingChat: RunAssistantAddressRuntimeInput<ResponseType>["tryHandleLivingChat"] = async (
modeDecision,
addressRuntimeMeta = null
) =>
runLivingChatAttemptRuntimeSafe({
sessionId: input.sessionId,
userMessage: input.userMessage,
sessionItems: input.sessionItems,
modeDecision,
sessionScope: {
knownOrganizations: input.sessionScope.knownOrganizations,
selectedOrganization: input.sessionScope.selectedOrganization,
activeOrganization: input.sessionScope.activeOrganization
},
addressRuntimeMeta,
traceIdFactory: () => `chat-${input.messageIdFactory().replace(/^msg-/, "")}`,
toNonEmptyString: input.toNonEmptyString,
mergeKnownOrganizations: input.mergeKnownOrganizations as any,
hasAssistantDataScopeMetaQuestionSignal: input.hasAssistantDataScopeMetaQuestionSignal,
shouldHandleAsAssistantCapabilityMetaQuery: input.shouldHandleAsAssistantCapabilityMetaQuery,
hasDestructiveDataActionSignal: input.hasDestructiveDataActionSignal,
hasDangerOrCoercionSignal: input.hasDangerOrCoercionSignal,
hasOperationalAdminActionRequestSignal: input.hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal: input.hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal: input.hasOrganizationFactFollowupSignal,
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: input.resolveDataScopeProbe,
applyScriptGuard: input.applyScriptGuard,
applyGroundingGuard: input.applyGroundingGuard,
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
buildAssistantCapabilityContractReply: input.buildAssistantCapabilityContractReply,
appendItem: input.appendItem,
getSession: input.getSession,
persistSession: input.persistSession,
cloneConversation: input.cloneConversation,
logEvent: input.logEvent,
messageIdFactory: input.messageIdFactory,
nowIso: input.nowIso,
payload: input.payload,
chatClient: input.chatClient,
loadAssistantCanonExcerpt: input.loadAssistantCanonExcerpt,
sanitizeOutgoingAssistantText: input.sanitizeOutgoingAssistantText,
defaultModel: input.defaultModel,
defaultBaseUrl: input.defaultBaseUrl,
defaultApiKey: input.defaultApiKey
} as RunAssistantLivingChatAttemptRuntimeInput<ResponseType>);
const runAddressLaneAttempt: RunAssistantAddressRuntimeInput<ResponseType>["runAddressLaneAttempt"] = async (
messageUsed,
carryMeta,
analysisDateHint
) =>
runAddressLaneAttemptRuntimeSafe({
messageUsed,
carryMeta,
analysisDateHint,
activeOrganization: input.sessionScope.activeOrganization,
mergeFollowupContextWithOrganizationScope: input.mergeFollowupContextWithOrganizationScope,
runAddressQueryTryHandle: input.runAddressQueryTryHandle
});
return runAddressRuntimeSafe({
featureAssistantAddressQueryV1: input.featureAssistantAddressQueryV1,
sessionId: input.sessionId,
userMessage: input.userMessage,
sessionItems: input.sessionItems,
llmProvider: input.payload.llmProvider,
useMock: Boolean(input.payload.useMock),
featureAddressLlmPredecomposeV1: input.featureAddressLlmPredecomposeV1,
runAddressLlmPreDecompose: input.runAddressLlmPreDecompose,
buildAddressLlmPredecomposeContractV1: input.buildAddressLlmPredecomposeContractV1,
sanitizeAddressMessageForFallback: input.sanitizeAddressMessageForFallback,
toNonEmptyString: input.toNonEmptyString,
resolveAddressFollowupCarryoverContext: input.resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision: input.resolveAssistantOrchestrationDecision,
buildAddressDialogContinuationContractV2: input.buildAddressDialogContinuationContractV2,
runtimeAnalysisContextAsOfDate: input.runtimeAnalysisContextAsOfDate,
payloadContextPeriodHint: input.payload?.context?.period_hint,
compactWhitespace: input.compactWhitespace,
runAddressLaneAttempt,
isRetryableAddressLimitedResult: input.isRetryableAddressLimitedResult,
finalizeAddressLaneResponse,
tryHandleLivingChat,
logEvent: input.logEvent,
nowIso: input.nowIso,
runAddressOrchestrationRuntime: input.runAddressOrchestrationRuntime,
runAddressToolGateRuntime: input.runAddressToolGateRuntime,
runAddressLaneRuntime: input.runAddressLaneRuntime
});
}

View File

@ -16,12 +16,9 @@ import * as openaiResponsesClient_1 from "./openaiResponsesClient";
import * as addressMcpClient_1 from "./addressMcpClient"; import * as addressMcpClient_1 from "./addressMcpClient";
import * as capabilitiesRegistry_1 from "./capabilitiesRegistry"; import * as capabilitiesRegistry_1 from "./capabilitiesRegistry";
import * as assistantCanon_1 from "./assistantCanon"; import * as assistantCanon_1 from "./assistantCanon";
import * as assistantAddressLaneResponseAttemptRuntimeAdapter_1 from "./assistantAddressLaneResponseAttemptRuntimeAdapter"; import * as assistantAddressAttemptRuntimeAdapter_1 from "./assistantAddressAttemptRuntimeAdapter";
import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding"; import * as assistantCoverageGrounding_1 from "./assistantCoverageGrounding";
import * as assistantDeepTurnAttemptRuntimeAdapter_1 from "./assistantDeepTurnAttemptRuntimeAdapter"; import * as assistantDeepTurnAttemptRuntimeAdapter_1 from "./assistantDeepTurnAttemptRuntimeAdapter";
import * as assistantAddressRuntimeAdapter_1 from "./assistantAddressRuntimeAdapter";
import * as assistantAddressLaneAttemptRuntimeAdapter_1 from "./assistantAddressLaneAttemptRuntimeAdapter";
import * as assistantLivingChatAttemptRuntimeAdapter_1 from "./assistantLivingChatAttemptRuntimeAdapter";
import * as assistantUserTurnBootstrapRuntimeAdapter_1 from "./assistantUserTurnBootstrapRuntimeAdapter"; import * as assistantUserTurnBootstrapRuntimeAdapter_1 from "./assistantUserTurnBootstrapRuntimeAdapter";
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning"; import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
import iconv from "iconv-lite"; import iconv from "iconv-lite";
@ -4340,40 +4337,30 @@ export class AssistantService {
nowIso: () => new Date().toISOString() nowIso: () => new Date().toISOString()
}); });
const sessionOrganizationScope = resolveSessionOrganizationScopeContext(userMessage, session.items); const sessionOrganizationScope = resolveSessionOrganizationScopeContext(userMessage, session.items);
const finalizeAddressLaneResponse = (addressLane, effectiveAddressUserMessage, carryoverMeta = null, llmPreDecomposeMeta = null) => (0, assistantAddressLaneResponseAttemptRuntimeAdapter_1.runAssistantAddressLaneResponseAttemptRuntime)({ const addressRuntime = await (0, assistantAddressAttemptRuntimeAdapter_1.runAssistantAddressAttemptRuntime)({
sessionId, featureAssistantAddressQueryV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1,
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, sessionId,
userMessage, userMessage,
sessionItems: session.items, sessionItems: session.items,
modeDecision, payload,
sessionScope: { sessionScope: {
knownOrganizations: sessionOrganizationScope.knownOrganizations, knownOrganizations: sessionOrganizationScope.knownOrganizations,
selectedOrganization: sessionOrganizationScope.selectedOrganization, selectedOrganization: sessionOrganizationScope.selectedOrganization,
activeOrganization: sessionOrganizationScope.activeOrganization activeOrganization: sessionOrganizationScope.activeOrganization
}, },
addressRuntimeMeta, featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1,
traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`, runAddressLlmPreDecompose: async () => runAddressLlmPreDecompose(this.normalizerService, payload, userMessage),
buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1,
sanitizeAddressMessageForFallback,
toNonEmptyString, toNonEmptyString,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision,
buildAddressDialogContinuationContractV2,
runtimeAnalysisContextAsOfDate: runtimeAnalysisContext.as_of_date,
compactWhitespace,
mergeFollowupContextWithOrganizationScope,
runAddressQueryTryHandle: (laneMessageUsed, options) => this.addressQueryService.tryHandle(laneMessageUsed, options),
isRetryableAddressLimitedResult,
mergeKnownOrganizations, mergeKnownOrganizations,
hasAssistantDataScopeMetaQuestionSignal, hasAssistantDataScopeMetaQuestionSignal,
shouldHandleAsAssistantCapabilityMetaQuery, shouldHandleAsAssistantCapabilityMetaQuery,
@ -4393,56 +4380,23 @@ export class AssistantService {
buildAssistantDataScopeSelectionReply, buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply, buildAssistantOperationalBoundaryReply,
buildAssistantCapabilityContractReply, buildAssistantCapabilityContractReply,
chatClient: this.chatClient,
loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt,
sanitizeOutgoingAssistantText,
defaultModel: config_1.DEFAULT_MODEL,
defaultBaseUrl: config_1.DEFAULT_OPENAI_BASE_URL,
defaultApiKey: process.env.OPENAI_API_KEY ?? "",
buildAddressDebugPayload,
buildAddressFollowupOffer,
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item), appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
getSession: (targetSessionId) => this.sessions.getSession(targetSessionId), getSession: (targetSessionId) => this.sessions.getSession(targetSessionId),
persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState), persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState),
cloneConversation: (items) => cloneItems(items), cloneConversation: (items) => cloneItems(items),
logEvent: (payload) => (0, log_1.logJson)(payload), logEvent: (payload) => (0, log_1.logJson)(payload),
messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`, messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`,
nowIso: () => new Date().toISOString(),
payload,
chatClient: this.chatClient,
loadAssistantCanonExcerpt: assistantCanon_1.loadAssistantCanonExcerpt,
sanitizeOutgoingAssistantText,
defaultModel: config_1.DEFAULT_MODEL,
defaultBaseUrl: config_1.DEFAULT_OPENAI_BASE_URL,
defaultApiKey: process.env.OPENAI_API_KEY ?? ""
});
let addressRuntimeMetaForDeep = null;
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => (0, assistantAddressLaneAttemptRuntimeAdapter_1.runAssistantAddressLaneAttemptRuntime)({
messageUsed,
carryMeta: carryMeta ?? null,
analysisDateHint: analysisDateHint ?? null,
activeOrganization: sessionOrganizationScope.activeOrganization,
mergeFollowupContextWithOrganizationScope,
runAddressQueryTryHandle: (laneMessageUsed, options) => this.addressQueryService.tryHandle(laneMessageUsed, options)
});
const addressRuntime = await (0, assistantAddressRuntimeAdapter_1.runAssistantAddressRuntime)({
featureAssistantAddressQueryV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_V1,
sessionId,
userMessage,
sessionItems: session.items,
llmProvider: payload?.llmProvider,
useMock: Boolean(payload.useMock),
featureAddressLlmPredecomposeV1: config_1.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1,
runAddressLlmPreDecompose: async () => runAddressLlmPreDecompose(this.normalizerService, payload, userMessage),
buildAddressLlmPredecomposeContractV1: predecomposeContract_1.buildAddressLlmPredecomposeContractV1,
sanitizeAddressMessageForFallback,
toNonEmptyString,
resolveAddressFollowupCarryoverContext,
resolveAssistantOrchestrationDecision,
buildAddressDialogContinuationContractV2,
runtimeAnalysisContextAsOfDate: runtimeAnalysisContext.as_of_date,
payloadContextPeriodHint: payload?.context?.period_hint,
compactWhitespace,
runAddressLaneAttempt,
isRetryableAddressLimitedResult,
finalizeAddressLaneResponse,
tryHandleLivingChat: (modeDecision, runtimeMeta) => tryHandleLivingChat(modeDecision, runtimeMeta),
logEvent: (payload) => (0, log_1.logJson)(payload),
nowIso: () => new Date().toISOString() nowIso: () => new Date().toISOString()
}); });
addressRuntimeMetaForDeep = addressRuntime.addressRuntimeMetaForDeep; const addressRuntimeMetaForDeep = addressRuntime.addressRuntimeMetaForDeep;
if (addressRuntime.handled && addressRuntime.response) { if (addressRuntime.handled && addressRuntime.response) {
return addressRuntime.response; return addressRuntime.response;
} }

View File

@ -0,0 +1,196 @@
import { describe, expect, it, vi } from "vitest";
import { runAssistantAddressAttemptRuntime } from "../src/services/assistantAddressAttemptRuntimeAdapter";
function buildInput(overrides: Record<string, unknown> = {}) {
return {
featureAssistantAddressQueryV1: true,
sessionId: "asst-1",
userMessage: "where are overdue docs",
sessionItems: [],
payload: {
llmProvider: "openai",
useMock: 1,
context: {
period_hint: "2020-07-31"
},
apiKey: "key",
model: "gpt-5",
baseUrl: "http://localhost"
},
sessionScope: {
knownOrganizations: ["Org A"],
selectedOrganization: "Org A",
activeOrganization: "Org A"
},
featureAddressLlmPredecomposeV1: true,
runAddressLlmPreDecompose: async () => ({}),
buildAddressLlmPredecomposeContractV1: () => ({}),
sanitizeAddressMessageForFallback: (value: string) => value,
toNonEmptyString: (value: unknown) =>
typeof value === "string" && value.trim().length > 0 ? value.trim() : null,
resolveAddressFollowupCarryoverContext: () => null,
resolveAssistantOrchestrationDecision: () => ({ mode: "address_query", runAddressLane: true }),
buildAddressDialogContinuationContractV2: () => ({}),
runtimeAnalysisContextAsOfDate: "2020-07-31",
compactWhitespace: (value: string) => String(value ?? "").replace(/\s+/g, " ").trim(),
mergeFollowupContextWithOrganizationScope: (followupContext: Record<string, unknown> | null) =>
followupContext,
runAddressQueryTryHandle: async () => ({ response_type: "READY" }),
isRetryableAddressLimitedResult: () => false,
mergeKnownOrganizations: (knownOrganizations: string[], selectedOrganization: string | null) => ({
knownOrganizations,
selectedOrganization
}),
hasAssistantDataScopeMetaQuestionSignal: () => false,
shouldHandleAsAssistantCapabilityMetaQuery: () => false,
hasDestructiveDataActionSignal: () => false,
hasDangerOrCoercionSignal: () => false,
hasOperationalAdminActionRequestSignal: () => false,
hasOrganizationFactLookupSignal: () => false,
hasOrganizationFactFollowupSignal: () => false,
shouldEmitOrganizationSelectionReply: () => false,
hasAssistantCapabilityQuestionSignal: () => false,
resolveDataScopeProbe: () => null,
applyScriptGuard: (chatText: string) => chatText,
applyGroundingGuard: (guardInput: Record<string, unknown>) => guardInput,
buildAssistantSafetyRefusalReply: () => "safety",
buildAssistantDataScopeContractReply: () => "scope",
buildAssistantOrganizationFactBoundaryReply: () => "boundary",
buildAssistantDataScopeSelectionReply: () => "selection",
buildAssistantOperationalBoundaryReply: () => "operational",
buildAssistantCapabilityContractReply: () => "capability",
chatClient: {} as any,
loadAssistantCanonExcerpt: () => "",
sanitizeOutgoingAssistantText: (value: unknown, fallback = "") => {
const text = typeof value === "string" ? value.trim() : "";
return text || fallback;
},
defaultModel: "gpt-5",
defaultBaseUrl: "http://localhost",
defaultApiKey: "key",
buildAddressDebugPayload: () => ({}),
buildAddressFollowupOffer: () => null,
appendItem: () => {},
getSession: () => ({
session_id: "asst-1",
updated_at: "",
items: [],
investigation_state: null
}),
persistSession: () => {},
cloneConversation: (items: unknown[]) => items,
logEvent: () => {},
messageIdFactory: () => "msg-111",
nowIso: () => "2026-01-01T00:00:00.000Z",
...overrides
} as any;
}
describe("assistant address attempt runtime adapter", () => {
it("wires lane, response and living-chat attempt runtimes through one boundary", async () => {
const runAddressLaneAttemptRuntime = vi.fn(async () => ({
response_type: "READY"
}));
const runAddressLaneResponseAttemptRuntime = vi.fn(() => ({
kind: "address"
}));
const runLivingChatAttemptRuntime = vi.fn(async () => ({
kind: "chat"
}));
const runAddressRuntime = vi.fn(async (input: any) => {
const laneResult = await input.runAddressLaneAttempt(
"lane-message",
{ followupContext: { previous_intent: "docs_by_counterparty" } },
"2020-08-31"
);
expect(laneResult).toEqual({ response_type: "READY" });
const livingChatResult = await input.tryHandleLivingChat(
{ mode: "chat", reason: "living_chat_signal_detected" },
{ source: "address_runtime" }
);
expect(livingChatResult).toEqual({ kind: "chat" });
const response = input.finalizeAddressLaneResponse(
{ reply_text: "address reply", reply_type: "factual_with_explanation" },
"lane-message",
{ previousReplyType: "partial_coverage" },
{ mode: "supported", confidence: "high" }
);
expect(response).toEqual({ kind: "address" });
return {
handled: true,
response: { ok: true, lane: "address" },
addressRuntimeMetaForDeep: { source: "address_runtime" }
};
});
const runtime = await runAssistantAddressAttemptRuntime(
buildInput({
runAddressRuntime,
runAddressLaneAttemptRuntime,
runAddressLaneResponseAttemptRuntime,
runLivingChatAttemptRuntime
})
);
expect(runtime).toEqual({
handled: true,
response: { ok: true, lane: "address" },
addressRuntimeMetaForDeep: { source: "address_runtime" }
});
expect(runAddressRuntime).toHaveBeenCalledWith(
expect.objectContaining({
llmProvider: "openai",
useMock: true,
payloadContextPeriodHint: "2020-07-31"
})
);
expect(runAddressLaneAttemptRuntime).toHaveBeenCalledWith(
expect.objectContaining({
messageUsed: "lane-message",
analysisDateHint: "2020-08-31",
activeOrganization: "Org A"
})
);
expect(runLivingChatAttemptRuntime).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "asst-1",
sessionScope: expect.objectContaining({
selectedOrganization: "Org A"
})
})
);
expect(runAddressLaneResponseAttemptRuntime).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "asst-1",
effectiveAddressUserMessage: "lane-message",
knownOrganizations: ["Org A"]
})
);
});
it("passes empty payload fields to address runtime without breaking defaults", async () => {
const runAddressRuntime = vi.fn(async () => ({
handled: false,
response: null,
addressRuntimeMetaForDeep: null
}));
await runAssistantAddressAttemptRuntime(
buildInput({
payload: {},
runAddressRuntime
})
);
expect(runAddressRuntime).toHaveBeenCalledWith(
expect.objectContaining({
llmProvider: undefined,
useMock: false,
payloadContextPeriodHint: undefined
})
);
});
});

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,64 @@
[ [
{
"generation_id": "gen-mnte8abx-ax3v3tr",
"created_at": "2026-04-10T21:03:44.205Z",
"mode": "qwen_seed",
"count": 10,
"domain": null,
"questions": [
"Покажи контрагентов с максимальными долгами, которые уже больше месяца не платят, и проверь, нет ли у них непроверенных авансовых отгрузок.",
"Где по покупателям висят заказы на конец месяца, но денег за них нет - требует ручной сверки?",
"Посмотри контрагентов, где сальдо не совпадает с актом сверки, и уточни, кого нужно уже непременно запросить справку по этой разнице.",
"Какие авансы давно остались висящими без закрытия - их пора либо отменять, либо перекладывать на счета реальных поставок?",
"Где у нас документы есть, но нет денег за них, и это уже выглядит как серьезная задолженность контрагента?",
"Проверь контрагентов с максимальными долгами - нет ли среди них тех, кто просто не закрыл накладные или оставил их без оплаты?",
"Какие реализации зависли на конец периода и могут портить выручку, если не проверять заранее?",
"Покажи контрагентов с максимальными долгами и уточни, нет ли среди них тех, кто просто игнорирует наши накладные.",
"Какие поставщики уже больше месяца не закрывают свои счета - это требует ручной проверки?",
"Проверь зависшие авансы и уточни, можно ли их перепривязать на текущие отгрузки или пора списывать как нереальные?"
],
"generated_by": "manual_reviewer",
"saved_case_set_file": "assistant_autogen_qwen_seed_20260410210344_gen-mnte8abx-ax3v3tr.json",
"context": {
"llm_provider": "local",
"model": "Qwen2.5 14B Instruct 1M",
"assistant_prompt_version": "address_query_runtime_v1",
"decomposition_prompt_version": "normalizer_v2_0_2",
"prompt_fingerprint": "Ты semantic-normalizer для бухгалтерского ассистента NDC.\nТвоя роль: только нормализация запроса пользователя в строгий JSON-контракт.\n\nЖесткие правила:\n1) Не давай бухгалтерский ответ по сути вопроса.\n2) Возвращай только JSON без markdown и пояснений.\n3) JSON обязан соответствовать переданной schema normalized_query_v1.\n4) Если период не указан, не выдумывай его; отмечай ambiguity.\n5) Для цепочек документов/проводок/оплат поднимай causal и cross-entity признаки.\n6) Для точечного object trace (номер/строка/ref) поднимай needs_exact_object_trace=true.\n7) Используй терминологию NDC.\nYou are semantic-normalizer for accounting assistant NDC.\nReturn strict JSON only, no markdown, no comments.\n\nTarget schema: normalized_query_v2_0_2.\n\nCore behavior (v2.0.2):\n1. Decompose message into semantic fragments.\n2. Classify fragment domain relevance and business scope.\n3. Fill route-critical flags and ",
"autogen_personality_id": "general",
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов, долги нсд, счета, общий вывод по компании - контрагенты, заказчикам, скока денег кто принес и какие остатки по счетам, поиск документов, сальдо, банковские операции, незакрытые договора, документы по договорам, долги, Активность заказчиков по периодам, Поставщики и выплаты"
}
},
{
"generation_id": "gen-mnte6y9p-4v1kfbw",
"created_at": "2026-04-10T21:02:41.918Z",
"mode": "qwen_seed",
"count": 10,
"domain": null,
"questions": [
"Какие поставщики пока не закрыли взаиморасчёты на конец месяца и это выглядит как серьёзная проблема, а не просто задержка?",
"Где у нас висят покупатели 'грузили - денег нет - закрытия нет' и кто из них требует ручной проверки уже сейчас?",
"Покажи контрагентов с вероятным несоответствием сальдо, если мы запросим их акт сверки прямо сейчас.",
"Где у нас есть оплаты, но документы для закрытия взаиморасчётов всё ещё не пришли?",
"Какие контрагенты имеют документы, но нет нормального закрытия по оплатам?",
"Есть ли зависшие авансы, которые давно нужно перепроверить или закрыть?",
"Какие реализации на конец периода выглядят так, будто они зависли и могут испортить картину по выручке?",
"Где у нас отгрузки с проблемами не только в оплате, но и в самой связке документов?",
"Кто из поставщиков активно работает с нами последнее время и сколько денег принесли за последние 3 месяца?",
"Какие незакрытые договора есть на данный момент и что связано с ними по документам, долги и оплаты?"
],
"generated_by": "manual_reviewer",
"saved_case_set_file": "assistant_autogen_qwen_seed_20260410210241_gen-mnte6y9p-4v1kfbw.json",
"context": {
"llm_provider": "local",
"model": "Qwen2.5 14B Instruct 1M",
"assistant_prompt_version": "address_query_runtime_v1",
"decomposition_prompt_version": "normalizer_v2_0_2",
"prompt_fingerprint": "Ты semantic-normalizer для бухгалтерского ассистента NDC.\nТвоя роль: только нормализация запроса пользователя в строгий JSON-контракт.\n\nЖесткие правила:\n1) Не давай бухгалтерский ответ по сути вопроса.\n2) Возвращай только JSON без markdown и пояснений.\n3) JSON обязан соответствовать переданной schema normalized_query_v1.\n4) Если период не указан, не выдумывай его; отмечай ambiguity.\n5) Для цепочек документов/проводок/оплат поднимай causal и cross-entity признаки.\n6) Для точечного object trace (номер/строка/ref) поднимай needs_exact_object_trace=true.\n7) Используй терминологию NDC.\nYou are semantic-normalizer for accounting assistant NDC.\nReturn strict JSON only, no markdown, no comments.\n\nTarget schema: normalized_query_v2_0_2.\n\nCore behavior (v2.0.2):\n1. Decompose message into semantic fragments.\n2. Classify fragment domain relevance and business scope.\n3. Fill route-critical flags and ",
"autogen_personality_id": "general",
"autogen_personality_prompt": "Генерируй реалистичные живые вопросы бухгалтера по 1С. Добавляй разговорные формулировки и опечатки, но сохраняй бизнес-смысл. акцент на контрагентов, долги нсд, счета, общий вывод по компании - контрагенты, заказчикам, скока денег кто принес и какие остатки по счетам, поиск документов, сальдо, банковские операции, незакрытые договора, документы по договорам, долги, Активность заказчиков по периодам, Поставщики и выплаты"
}
},
{ {
"generation_id": "gen-mnsolawk-vugqyoc", "generation_id": "gen-mnsolawk-vugqyoc",
"created_at": "2026-04-10T09:06:01.461Z", "created_at": "2026-04-10T09:06:01.461Z",

View File

@ -0,0 +1,174 @@
{
"suite_id": "assistant_autogen_gen-mnte6y9p-4v1kfbw",
"suite_version": "0.1.0",
"schema_version": "assistant_autogen_suite_v0_1",
"generated_at": "2026-04-10T21:02:41.918Z",
"generation_id": "gen-mnte6y9p-4v1kfbw",
"mode": "qwen_seed",
"domain": null,
"scenario_count": 10,
"case_ids": [
"AUTO-001",
"AUTO-002",
"AUTO-003",
"AUTO-004",
"AUTO-005",
"AUTO-006",
"AUTO-007",
"AUTO-008",
"AUTO-009",
"AUTO-010"
],
"cases": [
{
"case_id": "AUTO-001",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие поставщики пока не закрыли взаиморасчёты на конец месяца и это выглядит как серьёзная проблема, а не просто задержка?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-002",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Где у нас висят покупатели 'грузили - денег нет - закрытия нет' и кто из них требует ручной проверки уже сейчас?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-003",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов с вероятным несоответствием сальдо, если мы запросим их акт сверки прямо сейчас."
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-004",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Где у нас есть оплаты, но документы для закрытия взаиморасчётов всё ещё не пришли?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-005",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие контрагенты имеют документы, но нет нормального закрытия по оплатам?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-006",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Есть ли зависшие авансы, которые давно нужно перепроверить или закрыть?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-007",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие реализации на конец периода выглядят так, будто они зависли и могут испортить картину по выручке?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-008",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Где у нас отгрузки с проблемами не только в оплате, но и в самой связке документов?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-009",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Кто из поставщиков активно работает с нами последнее время и сколько денег принесли за последние 3 месяца?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-010",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие незакрытые договора есть на данный момент и что связано с ними по документам, долги и оплаты?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
}
]
}

View File

@ -0,0 +1,174 @@
{
"suite_id": "assistant_autogen_gen-mnte8abx-ax3v3tr",
"suite_version": "0.1.0",
"schema_version": "assistant_autogen_suite_v0_1",
"generated_at": "2026-04-10T21:03:44.205Z",
"generation_id": "gen-mnte8abx-ax3v3tr",
"mode": "qwen_seed",
"domain": null,
"scenario_count": 10,
"case_ids": [
"AUTO-001",
"AUTO-002",
"AUTO-003",
"AUTO-004",
"AUTO-005",
"AUTO-006",
"AUTO-007",
"AUTO-008",
"AUTO-009",
"AUTO-010"
],
"cases": [
{
"case_id": "AUTO-001",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов с максимальными долгами, которые уже больше месяца не платят, и проверь, нет ли у них непроверенных авансовых отгрузок."
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-002",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Где по покупателям висят заказы на конец месяца, но денег за них нет - требует ручной сверки?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-003",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Посмотри контрагентов, где сальдо не совпадает с актом сверки, и уточни, кого нужно уже непременно запросить справку по этой разнице."
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-004",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие авансы давно остались висящими без закрытия - их пора либо отменять, либо перекладывать на счета реальных поставок?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-005",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Где у нас документы есть, но нет денег за них, и это уже выглядит как серьезная задолженность контрагента?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-006",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Проверь контрагентов с максимальными долгами - нет ли среди них тех, кто просто не закрыл накладные или оставил их без оплаты?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-007",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие реализации зависли на конец периода и могут портить выручку, если не проверять заранее?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-008",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов с максимальными долгами и уточни, нет ли среди них тех, кто просто игнорирует наши накладные."
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-009",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие поставщики уже больше месяца не закрывают свои счета - это требует ручной проверки?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
},
{
"case_id": "AUTO-010",
"scenario_tag": "qwen_seed_general",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Проверь зависшие авансы и уточни, можно ли их перепривязать на текущие отгрузки или пора списывать как нереальные?"
}
],
"expected_hints": {
"expected_reply_type": null,
"expected_degraded_to": null
}
}
]
}

View File

@ -0,0 +1,130 @@
{
"suite_id": "assistant_autogen_runtime_job-bOkyd627Q3",
"suite_version": "0.1.0",
"schema_version": "assistant_autogen_runtime_v0_1",
"scenario_count": 10,
"case_ids": [
"AUTO-001",
"AUTO-002",
"AUTO-003",
"AUTO-004",
"AUTO-005",
"AUTO-006",
"AUTO-007",
"AUTO-008",
"AUTO-009",
"AUTO-010"
],
"cases": [
{
"case_id": "AUTO-001",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов с максимальными долгами, которые уже больше месяца не платят, и проверь, нет ли у них непроверенных авансовых отгрузок."
}
]
},
{
"case_id": "AUTO-002",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Где по покупателям висят заказы на конец месяца, но денег за них нет - требует ручной сверки?"
}
]
},
{
"case_id": "AUTO-003",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Посмотри контрагентов, где сальдо не совпадает с актом сверки, и уточни, кого нужно уже непременно запросить справку по этой разнице."
}
]
},
{
"case_id": "AUTO-004",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие авансы давно остались висящими без закрытия - их пора либо отменять, либо перекладывать на счета реальных поставок?"
}
]
},
{
"case_id": "AUTO-005",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Где у нас документы есть, но нет денег за них, и это уже выглядит как серьезная задолженность контрагента?"
}
]
},
{
"case_id": "AUTO-006",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Проверь контрагентов с максимальными долгами - нет ли среди них тех, кто просто не закрыл накладные или оставил их без оплаты?"
}
]
},
{
"case_id": "AUTO-007",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие реализации зависли на конец периода и могут портить выручку, если не проверять заранее?"
}
]
},
{
"case_id": "AUTO-008",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи контрагентов с максимальными долгами и уточни, нет ли среди них тех, кто просто игнорирует наши накладные."
}
]
},
{
"case_id": "AUTO-009",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие поставщики уже больше месяца не закрывают свои счета - это требует ручной проверки?"
}
]
},
{
"case_id": "AUTO-010",
"scenario_tag": "autogen_runtime",
"question_type": "direct",
"broadness_level": "medium",
"turns": [
{
"user_message": "Проверь зависшие авансы и уточни, можно ли их перепривязать на текущие отгрузки или пора списывать как нереальные?"
}
]
}
]
}