ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.36 вынос tryHandleLivingChat в отдельный runtime-handler чтобы убрать еще один большой локальный блок. новый living-chat handler-адаптер с try/catch + finalize + warn fallback

This commit is contained in:
2026-04-10 22:34:10 +03:00
parent 9c22460e8b
commit be116dcbde
6 changed files with 482 additions and 197 deletions
@@ -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 assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter";
import * as assistantAddressRuntimeAdapter_1 from "./assistantAddressRuntimeAdapter";
import * as assistantLivingChatTurnFinalizeRuntimeAdapter_1 from "./assistantLivingChatTurnFinalizeRuntimeAdapter";
import * as assistantLivingChatRuntimeAdapter_1 from "./assistantLivingChatRuntimeAdapter";
import * as assistantLivingChatHandlerRuntimeAdapter_1 from "./assistantLivingChatHandlerRuntimeAdapter";
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
import iconv from "iconv-lite";
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
@@ -4409,102 +4408,73 @@ export class AssistantService {
return runtime.response;
};
const tryHandleLivingChat = async (modeDecision, addressRuntimeMeta = null) => {
try {
const runtime = await (0, assistantLivingChatRuntimeAdapter_1.runAssistantLivingChatRuntime)({
userMessage,
sessionItems: session.items,
modeDecision,
sessionScope: {
knownOrganizations: sessionOrganizationScope.knownOrganizations,
selectedOrganization: sessionOrganizationScope.selectedOrganization,
activeOrganization: sessionOrganizationScope.activeOrganization
},
addressRuntimeMeta,
traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`,
toNonEmptyString,
mergeKnownOrganizations,
hasAssistantDataScopeMetaQuestionSignal,
shouldHandleAsAssistantCapabilityMetaQuery,
hasDestructiveDataActionSignal,
hasDangerOrCoercionSignal,
hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal,
shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
executeLlmChat: async () => {
const conversationWindow = buildLivingChatContextWindow(session.items);
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
const chatResponse = await this.chatClient.chat({
llmProvider: payload.llmProvider,
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''),
model: String(payload.model ?? config_1.DEFAULT_MODEL),
baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL,
temperature: payload.temperature ?? 0.35,
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900))
}, {
systemPrompt: [
'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.',
'Работай честно: не заявляй действия, которые недоступны в этом рантайме.',
'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.',
'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.',
'Если пользователь спрашивает про возможности, отвечай только по этому контракту.',
`Канон поведения: ${canonExcerpt}`
].join(' '),
developerPrompt: 'Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.',
userMessage: userPrompt,
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)),
temperature: payload.temperature ?? 0.35
});
return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.');
},
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply,
buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply,
buildAssistantCapabilityContractReply
});
if (!runtime.handled || !runtime.debug) {
return null;
}
const chatText = runtime.chatText;
const debug = runtime.debug;
const finalization = (0, assistantLivingChatTurnFinalizeRuntimeAdapter_1.finalizeAssistantLivingChatTurn)({
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;
}
return (0, assistantLivingChatHandlerRuntimeAdapter_1.tryHandleAssistantLivingChatRuntime)({
sessionId,
userMessage,
sessionItems: session.items,
modeDecision,
sessionScope: {
knownOrganizations: sessionOrganizationScope.knownOrganizations,
selectedOrganization: sessionOrganizationScope.selectedOrganization,
activeOrganization: sessionOrganizationScope.activeOrganization
},
addressRuntimeMeta,
traceIdFactory: () => `chat-${(0, nanoid_1.nanoid)(10)}`,
toNonEmptyString,
mergeKnownOrganizations,
hasAssistantDataScopeMetaQuestionSignal,
shouldHandleAsAssistantCapabilityMetaQuery,
hasDestructiveDataActionSignal,
hasDangerOrCoercionSignal,
hasOperationalAdminActionRequestSignal,
hasOrganizationFactLookupSignal,
hasOrganizationFactFollowupSignal,
shouldEmitOrganizationSelectionReply,
hasAssistantCapabilityQuestionSignal,
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
executeLlmChat: async () => {
const conversationWindow = buildLivingChatContextWindow(session.items);
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
const chatResponse = await this.chatClient.chat({
llmProvider: payload.llmProvider,
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ''),
model: String(payload.model ?? config_1.DEFAULT_MODEL),
baseUrl: payload.baseUrl ?? config_1.DEFAULT_OPENAI_BASE_URL,
temperature: payload.temperature ?? 0.35,
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900))
}, {
systemPrompt: [
'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.',
'Работай честно: не заявляй действия, которые недоступны в этом рантайме.',
'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.',
'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.',
'Если пользователь спрашивает про возможности, отвечай только по этому контракту.',
`Канон поведения: ${canonExcerpt}`
].join(' '),
developerPrompt: 'Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.',
userMessage: userPrompt,
maxOutputTokens: Math.max(120, Math.min(Number(payload.maxOutputTokens ?? 420), 900)),
temperature: payload.temperature ?? 0.35
});
return sanitizeOutgoingAssistantText(chatResponse?.outputText ?? '', 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.');
},
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
buildAssistantSafetyRefusalReply,
buildAssistantDataScopeContractReply,
buildAssistantOrganizationFactBoundaryReply,
buildAssistantDataScopeSelectionReply,
buildAssistantOperationalBoundaryReply,
buildAssistantCapabilityContractReply,
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)}`,
nowIso: () => new Date().toISOString()
});
};
let addressRuntimeMetaForDeep = null;
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint) => {