ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.36 вынос tryHandleLivingChat в отдельный runtime-handler чтобы убрать еще один большой локальный блок. новый living-chat handler-адаптер с try/catch + finalize + warn fallback
This commit is contained in:
parent
9c22460e8b
commit
be116dcbde
|
|
@ -1156,7 +1156,37 @@ Validation:
|
||||||
- `assistantLivingRouter.test.ts`
|
- `assistantLivingRouter.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 completed)**
|
Implemented in current pass (Phase 2.36):
|
||||||
|
1. Extracted living-chat handler branch (`tryHandleLivingChat`) from `assistantService` into dedicated runtime adapter:
|
||||||
|
- `assistantLivingChatHandlerRuntimeAdapter.ts`
|
||||||
|
- introduced:
|
||||||
|
- `tryHandleAssistantLivingChatRuntime(...)`
|
||||||
|
2. Centralized living-chat handler sequence (behavior-preserving):
|
||||||
|
- living-chat runtime invocation (deterministic/LLM guard chain);
|
||||||
|
- chat finalization invocation for `assistant_message_chat` response path;
|
||||||
|
- warn-log fallback path for runtime failures.
|
||||||
|
3. Rewired `assistantService` `tryHandleLivingChat(...)` closure to consume handler runtime adapter output.
|
||||||
|
4. Added focused unit tests:
|
||||||
|
- `assistantLivingChatHandlerRuntimeAdapter.test.ts`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
1. `npm run build` passed.
|
||||||
|
2. Targeted living/address/deep followup pack passed:
|
||||||
|
- `assistantLivingChatHandlerRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAddressLaneResponseRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAddressRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnResponseRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnAnalysisRuntimeAdapter.test.ts`
|
||||||
|
- `assistantDeepTurnNormalizationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAddressToolGateRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAddressOrchestrationRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAddressLaneRuntimeAdapter.test.ts`
|
||||||
|
- `assistantAddressFollowupContext.test.ts`
|
||||||
|
- `assistantLivingChatMode.test.ts`
|
||||||
|
- `assistantLivingRouter.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 completed)**
|
||||||
|
|
||||||
## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards)
|
## Stage 3 (P2): Hybrid Semantic Layer (LLM + Deterministic Guards)
|
||||||
|
|
||||||
|
|
|
||||||
73
llm_normalizer/backend/dist/services/assistantLivingChatHandlerRuntimeAdapter.js
vendored
Normal file
73
llm_normalizer/backend/dist/services/assistantLivingChatHandlerRuntimeAdapter.js
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.tryHandleAssistantLivingChatRuntime = tryHandleAssistantLivingChatRuntime;
|
||||||
|
const assistantLivingChatRuntimeAdapter_1 = require("./assistantLivingChatRuntimeAdapter");
|
||||||
|
const assistantLivingChatTurnFinalizeRuntimeAdapter_1 = require("./assistantLivingChatTurnFinalizeRuntimeAdapter");
|
||||||
|
async function tryHandleAssistantLivingChatRuntime(input) {
|
||||||
|
const runLivingChatRuntimeSafe = input.runLivingChatRuntime ?? assistantLivingChatRuntimeAdapter_1.runAssistantLivingChatRuntime;
|
||||||
|
const finalizeLivingChatTurnSafe = input.finalizeLivingChatTurn ?? assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn;
|
||||||
|
try {
|
||||||
|
const runtime = await runLivingChatRuntimeSafe({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
sessionItems: input.sessionItems,
|
||||||
|
modeDecision: input.modeDecision,
|
||||||
|
sessionScope: input.sessionScope,
|
||||||
|
addressRuntimeMeta: input.addressRuntimeMeta,
|
||||||
|
traceIdFactory: input.traceIdFactory,
|
||||||
|
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,
|
||||||
|
executeLlmChat: input.executeLlmChat,
|
||||||
|
applyScriptGuard: input.applyScriptGuard,
|
||||||
|
applyGroundingGuard: input.applyGroundingGuard,
|
||||||
|
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
|
||||||
|
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
|
||||||
|
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
|
||||||
|
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
|
||||||
|
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
|
||||||
|
buildAssistantCapabilityContractReply: input.buildAssistantCapabilityContractReply
|
||||||
|
});
|
||||||
|
if (!runtime.handled || !runtime.debug) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const finalization = finalizeLivingChatTurnSafe({
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
assistantReply: runtime.chatText,
|
||||||
|
replyType: "factual_with_explanation",
|
||||||
|
debug: runtime.debug,
|
||||||
|
modeDecision: input.modeDecision,
|
||||||
|
appendItem: input.appendItem,
|
||||||
|
getSession: input.getSession,
|
||||||
|
persistSession: input.persistSession,
|
||||||
|
cloneConversation: input.cloneConversation,
|
||||||
|
logEvent: input.logEvent,
|
||||||
|
messageIdFactory: input.messageIdFactory
|
||||||
|
});
|
||||||
|
return finalization.response;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
input.logEvent({
|
||||||
|
timestamp: input.nowIso(),
|
||||||
|
level: "warn",
|
||||||
|
service: "assistant_loop",
|
||||||
|
message: "assistant_living_chat_failed_fallback_to_deep",
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
details: {
|
||||||
|
session_id: input.sessionId,
|
||||||
|
user_message: input.userMessage,
|
||||||
|
reason: error instanceof Error ? error.message : String(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -79,8 +79,7 @@ const assistantDeepTurnNormalizationRuntimeAdapter_1 = __importStar(require("./a
|
||||||
const assistantDeepTurnResponseRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnResponseRuntimeAdapter"));
|
const assistantDeepTurnResponseRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnResponseRuntimeAdapter"));
|
||||||
const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnRetrievalRuntimeAdapter"));
|
const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnRetrievalRuntimeAdapter"));
|
||||||
const assistantAddressRuntimeAdapter_1 = __importStar(require("./assistantAddressRuntimeAdapter"));
|
const assistantAddressRuntimeAdapter_1 = __importStar(require("./assistantAddressRuntimeAdapter"));
|
||||||
const assistantLivingChatTurnFinalizeRuntimeAdapter_1 = __importStar(require("./assistantLivingChatTurnFinalizeRuntimeAdapter"));
|
const assistantLivingChatHandlerRuntimeAdapter_1 = __importStar(require("./assistantLivingChatHandlerRuntimeAdapter"));
|
||||||
const assistantLivingChatRuntimeAdapter_1 = __importStar(require("./assistantLivingChatRuntimeAdapter"));
|
|
||||||
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"));
|
||||||
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||||||
|
|
@ -4454,102 +4453,73 @@ class AssistantService {
|
||||||
return runtime.response;
|
return runtime.response;
|
||||||
};
|
};
|
||||||
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => {
|
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => {
|
||||||
try {
|
return (0, assistantLivingChatHandlerRuntimeAdapter_1.tryHandleAssistantLivingChatRuntime)({
|
||||||
const runtime = await (0, assistantLivingChatRuntimeAdapter_1.runAssistantLivingChatRuntime)({
|
sessionId,
|
||||||
userMessage,
|
userMessage,
|
||||||
sessionItems: session.items,
|
sessionItems: session.items,
|
||||||
modeDecision,
|
modeDecision,
|
||||||
sessionScope: {
|
sessionScope: {
|
||||||
knownOrganizations: sessionOrganizationScope.knownOrganizations,
|
knownOrganizations: sessionOrganizationScope.knownOrganizations,
|
||||||
selectedOrganization: sessionOrganizationScope.selectedOrganization,
|
selectedOrganization: sessionOrganizationScope.selectedOrganization,
|
||||||
activeOrganization: sessionOrganizationScope.activeOrganization
|
activeOrganization: sessionOrganizationScope.activeOrganization
|
||||||
},
|
},
|
||||||
addressRuntimeMeta,
|
addressRuntimeMeta,
|
||||||
traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`,
|
traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`,
|
||||||
toNonEmptyString,
|
toNonEmptyString,
|
||||||
mergeKnownOrganizations,
|
mergeKnownOrganizations,
|
||||||
hasAssistantDataScopeMetaQuestionSignal,
|
hasAssistantDataScopeMetaQuestionSignal,
|
||||||
shouldHandleAsAssistantCapabilityMetaQuery,
|
shouldHandleAsAssistantCapabilityMetaQuery,
|
||||||
hasDestructiveDataActionSignal,
|
hasDestructiveDataActionSignal,
|
||||||
hasDangerOrCoercionSignal,
|
hasDangerOrCoercionSignal,
|
||||||
hasOperationalAdminActionRequestSignal,
|
hasOperationalAdminActionRequestSignal,
|
||||||
hasOrganizationFactLookupSignal,
|
hasOrganizationFactLookupSignal,
|
||||||
hasOrganizationFactFollowupSignal,
|
hasOrganizationFactFollowupSignal,
|
||||||
shouldEmitOrganizationSelectionReply,
|
shouldEmitOrganizationSelectionReply,
|
||||||
hasAssistantCapabilityQuestionSignal,
|
hasAssistantCapabilityQuestionSignal,
|
||||||
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
||||||
executeLlmChat: async () => {
|
executeLlmChat: async () => {
|
||||||
const conversationWindow = buildLivingChatContextWindow(session.items);
|
const conversationWindow = buildLivingChatContextWindow(session.items);
|
||||||
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
|
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
|
||||||
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
|
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
|
||||||
const chatResponse = await this.chatClient.chat({
|
const chatResponse = await this.chatClient.chat({
|
||||||
llmProvider: payload.llmProvider,
|
llmProvider: payload.llmProvider,
|
||||||
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''),
|
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''),
|
||||||
model: String(payload.model ?? config_1.DEFAULT_MODEL),
|
model: String(payload.model ?? config_1.DEFAULT_MODEL),
|
||||||
baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL,
|
baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL,
|
||||||
temperature: payload.temperature ?? 0.35,
|
temperature: payload.temperature ?? 0.35,
|
||||||
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900))
|
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900))
|
||||||
}, {
|
}, {
|
||||||
systemPrompt: [
|
systemPrompt: [
|
||||||
'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.',
|
'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.',
|
||||||
'Работай честно: не заявляй действия, которые недоступны в этом рантайме.',
|
'Работай честно: не заявляй действия, которые недоступны в этом рантайме.',
|
||||||
'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.',
|
'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.',
|
||||||
'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.',
|
'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.',
|
||||||
'Если пользователь спрашивает про возможности, отвечай только по этому контракту.',
|
'Если пользователь спрашивает про возможности, отвечай только по этому контракту.',
|
||||||
`Канон поведения: ${canonExcerpt}`
|
`Канон поведения: ${canonExcerpt}`
|
||||||
].join(' '),
|
].join(' '),
|
||||||
developerPrompt: 'Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.',
|
developerPrompt: 'Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.',
|
||||||
userMessage: userPrompt,
|
userMessage: userPrompt,
|
||||||
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)),
|
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)),
|
||||||
temperature: payload.temperature ?? 0.35
|
temperature: payload.temperature ?? 0.35
|
||||||
});
|
});
|
||||||
return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.');
|
return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.');
|
||||||
},
|
},
|
||||||
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
|
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
|
||||||
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
|
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
|
||||||
buildAssistantSafetyRefusalReply,
|
buildAssistantSafetyRefusalReply,
|
||||||
buildAssistantDataScopeContractReply,
|
buildAssistantDataScopeContractReply,
|
||||||
buildAssistantOrganizationFactBoundaryReply,
|
buildAssistantOrganizationFactBoundaryReply,
|
||||||
buildAssistantDataScopeSelectionReply,
|
buildAssistantDataScopeSelectionReply,
|
||||||
buildAssistantOperationalBoundaryReply,
|
buildAssistantOperationalBoundaryReply,
|
||||||
buildAssistantCapabilityContractReply
|
buildAssistantCapabilityContractReply,
|
||||||
});
|
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
|
||||||
if (!runtime.handled || !runtime.debug) {
|
getSession: (targetSessionId) => this.sessions.getSession(targetSessionId),
|
||||||
return null;
|
persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState),
|
||||||
}
|
cloneConversation: (items) => cloneItems(items),
|
||||||
const chatText = runtime.chatText;
|
logEvent: (payload) => (0, log_1.logJson)(payload),
|
||||||
const debug = runtime.debug;
|
messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||||||
const finalization = (0, assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn)({
|
nowIso: () => new Date().toISOString()
|
||||||
sessionId,
|
});
|
||||||
userMessage,
|
|
||||||
assistantReply: chatText,
|
|
||||||
replyType: 'factual_with_explanation',
|
|
||||||
debug,
|
|
||||||
modeDecision,
|
|
||||||
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
|
|
||||||
getSession: (targetSessionId) => this.sessions.getSession(targetSessionId),
|
|
||||||
persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState),
|
|
||||||
cloneConversation: (items) => cloneItems(items),
|
|
||||||
logEvent: (payload) => (0, log_1.logJson)(payload),
|
|
||||||
messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`
|
|
||||||
});
|
|
||||||
return finalization.response;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
(0, log_1.logJson)({
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
level: 'warn',
|
|
||||||
service: 'assistant_loop',
|
|
||||||
message: 'assistant_living_chat_failed_fallback_to_deep',
|
|
||||||
sessionId,
|
|
||||||
details: {
|
|
||||||
session_id: sessionId,
|
|
||||||
user_message: userMessage,
|
|
||||||
reason: error instanceof Error ? error.message : String(error)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let addressRuntimeMetaForDeep = null;
|
let addressRuntimeMetaForDeep = null;
|
||||||
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => {
|
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
import {
|
||||||
|
runAssistantLivingChatRuntime,
|
||||||
|
type AssistantLivingChatRuntimeInput,
|
||||||
|
type AssistantLivingChatRuntimeOutput
|
||||||
|
} from "./assistantLivingChatRuntimeAdapter";
|
||||||
|
import {
|
||||||
|
finalizeAssistantLivingChatTurn,
|
||||||
|
type FinalizeAssistantLivingChatTurnInput
|
||||||
|
} from "./assistantLivingChatTurnFinalizeRuntimeAdapter";
|
||||||
|
|
||||||
|
export interface TryHandleAssistantLivingChatRuntimeInput<ResponseType = unknown> {
|
||||||
|
sessionId: string;
|
||||||
|
userMessage: string;
|
||||||
|
sessionItems: unknown[];
|
||||||
|
modeDecision?: AssistantLivingChatRuntimeInput["modeDecision"];
|
||||||
|
sessionScope: AssistantLivingChatRuntimeInput["sessionScope"];
|
||||||
|
addressRuntimeMeta?: AssistantLivingChatRuntimeInput["addressRuntimeMeta"];
|
||||||
|
traceIdFactory: AssistantLivingChatRuntimeInput["traceIdFactory"];
|
||||||
|
toNonEmptyString: AssistantLivingChatRuntimeInput["toNonEmptyString"];
|
||||||
|
mergeKnownOrganizations: AssistantLivingChatRuntimeInput["mergeKnownOrganizations"];
|
||||||
|
hasAssistantDataScopeMetaQuestionSignal: AssistantLivingChatRuntimeInput["hasAssistantDataScopeMetaQuestionSignal"];
|
||||||
|
shouldHandleAsAssistantCapabilityMetaQuery: AssistantLivingChatRuntimeInput["shouldHandleAsAssistantCapabilityMetaQuery"];
|
||||||
|
hasDestructiveDataActionSignal: AssistantLivingChatRuntimeInput["hasDestructiveDataActionSignal"];
|
||||||
|
hasDangerOrCoercionSignal: AssistantLivingChatRuntimeInput["hasDangerOrCoercionSignal"];
|
||||||
|
hasOperationalAdminActionRequestSignal: AssistantLivingChatRuntimeInput["hasOperationalAdminActionRequestSignal"];
|
||||||
|
hasOrganizationFactLookupSignal: AssistantLivingChatRuntimeInput["hasOrganizationFactLookupSignal"];
|
||||||
|
hasOrganizationFactFollowupSignal: AssistantLivingChatRuntimeInput["hasOrganizationFactFollowupSignal"];
|
||||||
|
shouldEmitOrganizationSelectionReply: AssistantLivingChatRuntimeInput["shouldEmitOrganizationSelectionReply"];
|
||||||
|
hasAssistantCapabilityQuestionSignal: AssistantLivingChatRuntimeInput["hasAssistantCapabilityQuestionSignal"];
|
||||||
|
resolveDataScopeProbe: AssistantLivingChatRuntimeInput["resolveDataScopeProbe"];
|
||||||
|
executeLlmChat: AssistantLivingChatRuntimeInput["executeLlmChat"];
|
||||||
|
applyScriptGuard: AssistantLivingChatRuntimeInput["applyScriptGuard"];
|
||||||
|
applyGroundingGuard: AssistantLivingChatRuntimeInput["applyGroundingGuard"];
|
||||||
|
buildAssistantSafetyRefusalReply: AssistantLivingChatRuntimeInput["buildAssistantSafetyRefusalReply"];
|
||||||
|
buildAssistantDataScopeContractReply: AssistantLivingChatRuntimeInput["buildAssistantDataScopeContractReply"];
|
||||||
|
buildAssistantOrganizationFactBoundaryReply: AssistantLivingChatRuntimeInput["buildAssistantOrganizationFactBoundaryReply"];
|
||||||
|
buildAssistantDataScopeSelectionReply: AssistantLivingChatRuntimeInput["buildAssistantDataScopeSelectionReply"];
|
||||||
|
buildAssistantOperationalBoundaryReply: AssistantLivingChatRuntimeInput["buildAssistantOperationalBoundaryReply"];
|
||||||
|
buildAssistantCapabilityContractReply: AssistantLivingChatRuntimeInput["buildAssistantCapabilityContractReply"];
|
||||||
|
appendItem: FinalizeAssistantLivingChatTurnInput["appendItem"];
|
||||||
|
getSession: FinalizeAssistantLivingChatTurnInput["getSession"];
|
||||||
|
persistSession: FinalizeAssistantLivingChatTurnInput["persistSession"];
|
||||||
|
cloneConversation: FinalizeAssistantLivingChatTurnInput["cloneConversation"];
|
||||||
|
logEvent: (payload: Record<string, unknown>) => void;
|
||||||
|
messageIdFactory: FinalizeAssistantLivingChatTurnInput["messageIdFactory"];
|
||||||
|
nowIso: () => string;
|
||||||
|
runLivingChatRuntime?: (
|
||||||
|
input: AssistantLivingChatRuntimeInput
|
||||||
|
) => Promise<AssistantLivingChatRuntimeOutput>;
|
||||||
|
finalizeLivingChatTurn?: (
|
||||||
|
input: FinalizeAssistantLivingChatTurnInput
|
||||||
|
) => {
|
||||||
|
response: ResponseType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tryHandleAssistantLivingChatRuntime<ResponseType = unknown>(
|
||||||
|
input: TryHandleAssistantLivingChatRuntimeInput<ResponseType>
|
||||||
|
): Promise<ResponseType | null> {
|
||||||
|
const runLivingChatRuntimeSafe = input.runLivingChatRuntime ?? runAssistantLivingChatRuntime;
|
||||||
|
const finalizeLivingChatTurnSafe = input.finalizeLivingChatTurn ?? finalizeAssistantLivingChatTurn;
|
||||||
|
try {
|
||||||
|
const runtime = await runLivingChatRuntimeSafe({
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
sessionItems: input.sessionItems,
|
||||||
|
modeDecision: input.modeDecision,
|
||||||
|
sessionScope: input.sessionScope,
|
||||||
|
addressRuntimeMeta: input.addressRuntimeMeta,
|
||||||
|
traceIdFactory: input.traceIdFactory,
|
||||||
|
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,
|
||||||
|
executeLlmChat: input.executeLlmChat,
|
||||||
|
applyScriptGuard: input.applyScriptGuard,
|
||||||
|
applyGroundingGuard: input.applyGroundingGuard,
|
||||||
|
buildAssistantSafetyRefusalReply: input.buildAssistantSafetyRefusalReply,
|
||||||
|
buildAssistantDataScopeContractReply: input.buildAssistantDataScopeContractReply,
|
||||||
|
buildAssistantOrganizationFactBoundaryReply: input.buildAssistantOrganizationFactBoundaryReply,
|
||||||
|
buildAssistantDataScopeSelectionReply: input.buildAssistantDataScopeSelectionReply,
|
||||||
|
buildAssistantOperationalBoundaryReply: input.buildAssistantOperationalBoundaryReply,
|
||||||
|
buildAssistantCapabilityContractReply: input.buildAssistantCapabilityContractReply
|
||||||
|
});
|
||||||
|
if (!runtime.handled || !runtime.debug) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const finalization = finalizeLivingChatTurnSafe({
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
userMessage: input.userMessage,
|
||||||
|
assistantReply: runtime.chatText,
|
||||||
|
replyType: "factual_with_explanation",
|
||||||
|
debug: runtime.debug,
|
||||||
|
modeDecision: input.modeDecision,
|
||||||
|
appendItem: input.appendItem,
|
||||||
|
getSession: input.getSession,
|
||||||
|
persistSession: input.persistSession,
|
||||||
|
cloneConversation: input.cloneConversation,
|
||||||
|
logEvent: input.logEvent as FinalizeAssistantLivingChatTurnInput["logEvent"],
|
||||||
|
messageIdFactory: input.messageIdFactory
|
||||||
|
});
|
||||||
|
return finalization.response as ResponseType;
|
||||||
|
} catch (error) {
|
||||||
|
input.logEvent({
|
||||||
|
timestamp: input.nowIso(),
|
||||||
|
level: "warn",
|
||||||
|
service: "assistant_loop",
|
||||||
|
message: "assistant_living_chat_failed_fallback_to_deep",
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
details: {
|
||||||
|
session_id: input.sessionId,
|
||||||
|
user_message: input.userMessage,
|
||||||
|
reason: error instanceof Error ? error.message : String(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,8 +33,7 @@ import * as assistantDeepTurnNormalizationRuntimeAdapter_1 from "./assistantDeep
|
||||||
import * as assistantDeepTurnResponseRuntimeAdapter_1 from "./assistantDeepTurnResponseRuntimeAdapter";
|
import * as assistantDeepTurnResponseRuntimeAdapter_1 from "./assistantDeepTurnResponseRuntimeAdapter";
|
||||||
import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter";
|
import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter";
|
||||||
import * as assistantAddressRuntimeAdapter_1 from "./assistantAddressRuntimeAdapter";
|
import * as assistantAddressRuntimeAdapter_1 from "./assistantAddressRuntimeAdapter";
|
||||||
import * as assistantLivingChatTurnFinalizeRuntimeAdapter_1 from "./assistantLivingChatTurnFinalizeRuntimeAdapter";
|
import * as assistantLivingChatHandlerRuntimeAdapter_1 from "./assistantLivingChatHandlerRuntimeAdapter";
|
||||||
import * as assistantLivingChatRuntimeAdapter_1 from "./assistantLivingChatRuntimeAdapter";
|
|
||||||
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
|
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
|
||||||
import iconv from "iconv-lite";
|
import iconv from "iconv-lite";
|
||||||
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||||||
|
|
@ -4409,102 +4408,73 @@ export class AssistantService {
|
||||||
return runtime.response;
|
return runtime.response;
|
||||||
};
|
};
|
||||||
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => {
|
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => {
|
||||||
try {
|
return (0, assistantLivingChatHandlerRuntimeAdapter_1.tryHandleAssistantLivingChatRuntime)({
|
||||||
const runtime = await (0, assistantLivingChatRuntimeAdapter_1.runAssistantLivingChatRuntime)({
|
sessionId,
|
||||||
userMessage,
|
userMessage,
|
||||||
sessionItems: session.items,
|
sessionItems: session.items,
|
||||||
modeDecision,
|
modeDecision,
|
||||||
sessionScope: {
|
sessionScope: {
|
||||||
knownOrganizations: sessionOrganizationScope.knownOrganizations,
|
knownOrganizations: sessionOrganizationScope.knownOrganizations,
|
||||||
selectedOrganization: sessionOrganizationScope.selectedOrganization,
|
selectedOrganization: sessionOrganizationScope.selectedOrganization,
|
||||||
activeOrganization: sessionOrganizationScope.activeOrganization
|
activeOrganization: sessionOrganizationScope.activeOrganization
|
||||||
},
|
},
|
||||||
addressRuntimeMeta,
|
addressRuntimeMeta,
|
||||||
traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`,
|
traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`,
|
||||||
toNonEmptyString,
|
toNonEmptyString,
|
||||||
mergeKnownOrganizations,
|
mergeKnownOrganizations,
|
||||||
hasAssistantDataScopeMetaQuestionSignal,
|
hasAssistantDataScopeMetaQuestionSignal,
|
||||||
shouldHandleAsAssistantCapabilityMetaQuery,
|
shouldHandleAsAssistantCapabilityMetaQuery,
|
||||||
hasDestructiveDataActionSignal,
|
hasDestructiveDataActionSignal,
|
||||||
hasDangerOrCoercionSignal,
|
hasDangerOrCoercionSignal,
|
||||||
hasOperationalAdminActionRequestSignal,
|
hasOperationalAdminActionRequestSignal,
|
||||||
hasOrganizationFactLookupSignal,
|
hasOrganizationFactLookupSignal,
|
||||||
hasOrganizationFactFollowupSignal,
|
hasOrganizationFactFollowupSignal,
|
||||||
shouldEmitOrganizationSelectionReply,
|
shouldEmitOrganizationSelectionReply,
|
||||||
hasAssistantCapabilityQuestionSignal,
|
hasAssistantCapabilityQuestionSignal,
|
||||||
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
||||||
executeLlmChat: async () => {
|
executeLlmChat: async () => {
|
||||||
const conversationWindow = buildLivingChatContextWindow(session.items);
|
const conversationWindow = buildLivingChatContextWindow(session.items);
|
||||||
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
|
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
|
||||||
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
|
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
|
||||||
const chatResponse = await this.chatClient.chat({
|
const chatResponse = await this.chatClient.chat({
|
||||||
llmProvider: payload.llmProvider,
|
llmProvider: payload.llmProvider,
|
||||||
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''),
|
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''),
|
||||||
model: String(payload.model ?? config_1.DEFAULT_MODEL),
|
model: String(payload.model ?? config_1.DEFAULT_MODEL),
|
||||||
baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL,
|
baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL,
|
||||||
temperature: payload.temperature ?? 0.35,
|
temperature: payload.temperature ?? 0.35,
|
||||||
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900))
|
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900))
|
||||||
}, {
|
}, {
|
||||||
systemPrompt: [
|
systemPrompt: [
|
||||||
'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.',
|
'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.',
|
||||||
'Работай честно: не заявляй действия, которые недоступны в этом рантайме.',
|
'Работай честно: не заявляй действия, которые недоступны в этом рантайме.',
|
||||||
'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.',
|
'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.',
|
||||||
'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.',
|
'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.',
|
||||||
'Если пользователь спрашивает про возможности, отвечай только по этому контракту.',
|
'Если пользователь спрашивает про возможности, отвечай только по этому контракту.',
|
||||||
`Канон поведения: ${canonExcerpt}`
|
`Канон поведения: ${canonExcerpt}`
|
||||||
].join(' '),
|
].join(' '),
|
||||||
developerPrompt: 'Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.',
|
developerPrompt: 'Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.',
|
||||||
userMessage: userPrompt,
|
userMessage: userPrompt,
|
||||||
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)),
|
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)),
|
||||||
temperature: payload.temperature ?? 0.35
|
temperature: payload.temperature ?? 0.35
|
||||||
});
|
});
|
||||||
return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.');
|
return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.');
|
||||||
},
|
},
|
||||||
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
|
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
|
||||||
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
|
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
|
||||||
buildAssistantSafetyRefusalReply,
|
buildAssistantSafetyRefusalReply,
|
||||||
buildAssistantDataScopeContractReply,
|
buildAssistantDataScopeContractReply,
|
||||||
buildAssistantOrganizationFactBoundaryReply,
|
buildAssistantOrganizationFactBoundaryReply,
|
||||||
buildAssistantDataScopeSelectionReply,
|
buildAssistantDataScopeSelectionReply,
|
||||||
buildAssistantOperationalBoundaryReply,
|
buildAssistantOperationalBoundaryReply,
|
||||||
buildAssistantCapabilityContractReply
|
buildAssistantCapabilityContractReply,
|
||||||
});
|
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
|
||||||
if (!runtime.handled || !runtime.debug) {
|
getSession: (targetSessionId) => this.sessions.getSession(targetSessionId),
|
||||||
return null;
|
persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState),
|
||||||
}
|
cloneConversation: (items) => cloneItems(items),
|
||||||
const chatText = runtime.chatText;
|
logEvent: (payload) => (0, log_1.logJson)(payload),
|
||||||
const debug = runtime.debug;
|
messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`,
|
||||||
const finalization = (0, assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn)({
|
nowIso: () => new Date().toISOString()
|
||||||
sessionId,
|
});
|
||||||
userMessage,
|
|
||||||
assistantReply: chatText,
|
|
||||||
replyType: 'factual_with_explanation',
|
|
||||||
debug,
|
|
||||||
modeDecision,
|
|
||||||
appendItem: (targetSessionId, item) => this.sessions.appendItem(targetSessionId, item),
|
|
||||||
getSession: (targetSessionId) => this.sessions.getSession(targetSessionId),
|
|
||||||
persistSession: (sessionState) => this.sessionLogger.persistSession(sessionState),
|
|
||||||
cloneConversation: (items) => cloneItems(items),
|
|
||||||
logEvent: (payload) => (0, log_1.logJson)(payload),
|
|
||||||
messageIdFactory: () => `msg-${(0, nanoid_1.nanoid)(10)}`
|
|
||||||
});
|
|
||||||
return finalization.response;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
(0, log_1.logJson)({
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
level: 'warn',
|
|
||||||
service: 'assistant_loop',
|
|
||||||
message: 'assistant_living_chat_failed_fallback_to_deep',
|
|
||||||
sessionId,
|
|
||||||
details: {
|
|
||||||
session_id: sessionId,
|
|
||||||
user_message: userMessage,
|
|
||||||
reason: error instanceof Error ? error.message : String(error)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let addressRuntimeMetaForDeep = null;
|
let addressRuntimeMetaForDeep = null;
|
||||||
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => {
|
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { tryHandleAssistantLivingChatRuntime } from "../src/services/assistantLivingChatHandlerRuntimeAdapter";
|
||||||
|
|
||||||
|
function buildInput(overrides: Record<string, unknown> = {}) {
|
||||||
|
return {
|
||||||
|
sessionId: "asst-1",
|
||||||
|
userMessage: "question",
|
||||||
|
sessionItems: [],
|
||||||
|
modeDecision: { mode: "chat", reason: "living_chat_signal_detected" },
|
||||||
|
sessionScope: {
|
||||||
|
knownOrganizations: [],
|
||||||
|
selectedOrganization: null,
|
||||||
|
activeOrganization: null
|
||||||
|
},
|
||||||
|
addressRuntimeMeta: null,
|
||||||
|
traceIdFactory: () => "chat-trace-1",
|
||||||
|
toNonEmptyString: (value: unknown) => (typeof value === "string" && value.trim() ? value.trim() : null),
|
||||||
|
mergeKnownOrganizations: (values: unknown[]) => values.map((v) => String(v)),
|
||||||
|
hasAssistantDataScopeMetaQuestionSignal: () => false,
|
||||||
|
shouldHandleAsAssistantCapabilityMetaQuery: () => false,
|
||||||
|
hasDestructiveDataActionSignal: () => false,
|
||||||
|
hasDangerOrCoercionSignal: () => false,
|
||||||
|
hasOperationalAdminActionRequestSignal: () => false,
|
||||||
|
hasOrganizationFactLookupSignal: () => false,
|
||||||
|
hasOrganizationFactFollowupSignal: () => false,
|
||||||
|
shouldEmitOrganizationSelectionReply: () => false,
|
||||||
|
hasAssistantCapabilityQuestionSignal: () => false,
|
||||||
|
resolveDataScopeProbe: async () => null,
|
||||||
|
executeLlmChat: async () => "chat answer",
|
||||||
|
applyScriptGuard: (text: string) => ({ text, applied: false, reason: null }),
|
||||||
|
applyGroundingGuard: (payload: { chatText: string }) => ({
|
||||||
|
text: payload.chatText,
|
||||||
|
applied: false,
|
||||||
|
reason: null
|
||||||
|
}),
|
||||||
|
buildAssistantSafetyRefusalReply: () => "safety",
|
||||||
|
buildAssistantDataScopeContractReply: () => "scope",
|
||||||
|
buildAssistantOrganizationFactBoundaryReply: () => "boundary",
|
||||||
|
buildAssistantDataScopeSelectionReply: () => "selection",
|
||||||
|
buildAssistantOperationalBoundaryReply: () => "operational",
|
||||||
|
buildAssistantCapabilityContractReply: () => "capability",
|
||||||
|
appendItem: () => {},
|
||||||
|
getSession: () => ({ session_id: "asst-1", updated_at: "", items: [], investigation_state: null } as any),
|
||||||
|
persistSession: () => {},
|
||||||
|
cloneConversation: (items: any[]) => items,
|
||||||
|
logEvent: vi.fn(),
|
||||||
|
messageIdFactory: () => "msg-1",
|
||||||
|
nowIso: () => "2026-04-10T00:00:00.000Z",
|
||||||
|
...overrides
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("assistant living chat handler runtime adapter", () => {
|
||||||
|
it("returns finalized response when runtime is handled", async () => {
|
||||||
|
const runLivingChatRuntime = vi.fn(async () => ({
|
||||||
|
handled: true,
|
||||||
|
chatText: "chat",
|
||||||
|
debug: { trace_id: "chat-1" }
|
||||||
|
}));
|
||||||
|
const finalizeLivingChatTurn = vi.fn(() => ({
|
||||||
|
response: { ok: true, lane: "chat" }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await tryHandleAssistantLivingChatRuntime(
|
||||||
|
buildInput({
|
||||||
|
runLivingChatRuntime,
|
||||||
|
finalizeLivingChatTurn
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(runLivingChatRuntime).toHaveBeenCalledTimes(1);
|
||||||
|
expect(finalizeLivingChatTurn).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
assistantReply: "chat",
|
||||||
|
replyType: "factual_with_explanation"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(response).toEqual({ ok: true, lane: "chat" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when runtime is not handled", async () => {
|
||||||
|
const finalizeLivingChatTurn = vi.fn();
|
||||||
|
const response = await tryHandleAssistantLivingChatRuntime(
|
||||||
|
buildInput({
|
||||||
|
runLivingChatRuntime: async () => ({
|
||||||
|
handled: false,
|
||||||
|
chatText: "",
|
||||||
|
debug: null
|
||||||
|
}),
|
||||||
|
finalizeLivingChatTurn
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toBeNull();
|
||||||
|
expect(finalizeLivingChatTurn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logs warn and returns null on runtime error", async () => {
|
||||||
|
const logEvent = vi.fn();
|
||||||
|
const response = await tryHandleAssistantLivingChatRuntime(
|
||||||
|
buildInput({
|
||||||
|
runLivingChatRuntime: async () => {
|
||||||
|
throw new Error("boom");
|
||||||
|
},
|
||||||
|
logEvent
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toBeNull();
|
||||||
|
expect(logEvent).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
level: "warn",
|
||||||
|
message: "assistant_living_chat_failed_fallback_to_deep"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue