ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.37 - вынос LLM-вызов живого чата из assistantService (большой prompt-блок) в отдельный адаптер, чтобы assistantService стал заметно чище.

This commit is contained in:
2026-04-10 22:56:32 +03:00
parent be116dcbde
commit 0cc8f71068
6 changed files with 340 additions and 103 deletions
@@ -0,0 +1,69 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runAssistantLivingChatLlmRuntime = runAssistantLivingChatLlmRuntime;
const DEFAULT_LIVING_CHAT_TEMPERATURE = 0.35;
const DEFAULT_LIVING_CHAT_MAX_OUTPUT_TOKENS = 420;
const LIVING_CHAT_MAX_TOKENS_MIN = 120;
const LIVING_CHAT_MAX_TOKENS_MAX = 900;
const LIVING_CHAT_SYSTEM_PROMPT_PARTS = [
'Ты живой русскоязычный ассистент для чтения и анализа данных 1С.',
'Работай честно: не заявляй действия, которые недоступны в этом рантайме.',
'Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.',
'Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.',
'Если пользователь спрашивает про возможности, отвечай только по этому контракту.'
];
const LIVING_CHAT_DEVELOPER_PROMPT = 'Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.';
const LIVING_CHAT_FALLBACK_REPLY = 'Понял. Сформулируйте, что именно нужно по данным 1С, и я помогу по шагам.';
function clampLivingChatMaxOutputTokens(value) {
const numeric = Number(value ?? DEFAULT_LIVING_CHAT_MAX_OUTPUT_TOKENS);
return Math.max(LIVING_CHAT_MAX_TOKENS_MIN, Math.min(numeric, LIVING_CHAT_MAX_TOKENS_MAX));
}
function compactWhitespace(value) {
return String(value ?? "")
.replace(/\s+/g, " ")
.trim();
}
function buildLivingChatContextWindow(items) {
const source = Array.isArray(items) ? items.slice(-6) : [];
const lines = [];
for (const item of source) {
if (!item || typeof item !== "object") {
continue;
}
const role = String(item.role ?? "").trim();
const text = compactWhitespace(String(item.text ?? ""));
if (!role || !text) {
continue;
}
const clipped = text.length > 220 ? `${text.slice(0, 220)}...` : text;
lines.push(`${role}: ${clipped}`);
}
return lines.join("\n");
}
function buildLivingChatPrompt(userMessage, conversationWindow) {
const contextBlock = conversationWindow ? `Контекст последних сообщений:\n${conversationWindow}\n\n` : "";
return `${contextBlock}Сообщение пользователя:\n${userMessage}`;
}
async function runAssistantLivingChatLlmRuntime(input) {
const conversationWindow = buildLivingChatContextWindow(input.sessionItems);
const userPrompt = buildLivingChatPrompt(input.userMessage, conversationWindow);
const canonExcerpt = input.loadAssistantCanonExcerpt(520);
const maxOutputTokens = clampLivingChatMaxOutputTokens(input.payload.maxOutputTokens);
const temperature = input.payload.temperature ?? DEFAULT_LIVING_CHAT_TEMPERATURE;
const systemPrompt = [...LIVING_CHAT_SYSTEM_PROMPT_PARTS, `Канон поведения: ${canonExcerpt}`].join(" ");
const chatResponse = await input.chatClient.chat({
llmProvider: input.payload.llmProvider,
apiKey: String(input.payload.apiKey ?? input.defaultApiKey ?? ""),
model: String(input.payload.model ?? input.defaultModel),
baseUrl: input.payload.baseUrl ?? input.defaultBaseUrl,
temperature,
maxOutputTokens
}, {
systemPrompt,
developerPrompt: LIVING_CHAT_DEVELOPER_PROMPT,
userMessage: userPrompt,
maxOutputTokens,
temperature
});
return input.sanitizeOutgoingAssistantText(chatResponse?.outputText ?? "", LIVING_CHAT_FALLBACK_REPLY);
}
+12 -50
View File
@@ -80,6 +80,7 @@ const assistantDeepTurnResponseRuntimeAdapter_1 = __importStar(require("./assist
const assistantDeepTurnRetrievalRuntimeAdapter_1 = __importStar(require("./assistantDeepTurnRetrievalRuntimeAdapter"));
const assistantAddressRuntimeAdapter_1 = __importStar(require("./assistantAddressRuntimeAdapter"));
const assistantLivingChatHandlerRuntimeAdapter_1 = __importStar(require("./assistantLivingChatHandlerRuntimeAdapter"));
const assistantLivingChatLlmRuntimeAdapter_1 = __importStar(require("./assistantLivingChatLlmRuntimeAdapter"));
const assistantQueryPlanning_1 = __importStar(require("./assistantQueryPlanning"));
const iconv_lite_1 = __importDefault(require("iconv-lite"));
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
@@ -3469,29 +3470,6 @@ function hasLivingChatSignal(text) {
}
return hasSmallTalkSignal(lower);
}
function buildLivingChatContextWindow(items) {
const source = Array.isArray(items) ? items.slice(-6) : [];
const lines = [];
for (const item of source) {
if (!item || typeof item !== "object") {
continue;
}
const role = String(item.role ?? "").trim();
const text = compactWhitespace(String(item.text ?? ""));
if (!role || !text) {
continue;
}
const clipped = text.length > 220 ? `${text.slice(0, 220)}...` : text;
lines.push(`${role}: ${clipped}`);
}
return lines.join("\n");
}
function buildLivingChatPrompt(userMessage, conversationWindow) {
const contextBlock = conversationWindow
? `Контекст последних сообщений:\n${conversationWindow}\n\n`
: "";
return `${contextBlock}Сообщение пользователя:\n${userMessage}`;
}
function buildAssistantCapabilityContractReply() {
return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)();
}
@@ -4477,33 +4455,17 @@ class AssistantService {
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С, и я помогу по шагам.');
},
executeLlmChat: async () => (0, assistantLivingChatLlmRuntimeAdapter_1.runAssistantLivingChatLlmRuntime)({
userMessage,
sessionItems: session.items,
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 ?? ""
}),
applyScriptGuard: (chatText, runtimeUserMessage) => applyLivingChatScriptGuard(chatText, runtimeUserMessage),
applyGroundingGuard: (guardInput) => applyLivingChatGroundingGuard(guardInput),
buildAssistantSafetyRefusalReply,