ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Рефакторинг этапов 2.37 - вынос LLM-вызов живого чата из assistantService (большой prompt-блок) в отдельный адаптер, чтобы assistantService стал заметно чище.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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С, и я помогу по шагам.';
|
||||
|
||||
export interface RunAssistantLivingChatLlmRuntimeInput {
|
||||
userMessage: string;
|
||||
sessionItems: unknown[];
|
||||
payload: {
|
||||
llmProvider?: unknown;
|
||||
apiKey?: unknown;
|
||||
model?: unknown;
|
||||
baseUrl?: unknown;
|
||||
temperature?: number;
|
||||
maxOutputTokens?: number;
|
||||
};
|
||||
chatClient: {
|
||||
chat: (
|
||||
config: {
|
||||
llmProvider: unknown;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
baseUrl: unknown;
|
||||
temperature: number;
|
||||
maxOutputTokens: number;
|
||||
},
|
||||
prompt: {
|
||||
systemPrompt: string;
|
||||
developerPrompt: string;
|
||||
userMessage: string;
|
||||
maxOutputTokens: number;
|
||||
temperature: number;
|
||||
}
|
||||
) => Promise<{ outputText?: string | null } | null>;
|
||||
};
|
||||
loadAssistantCanonExcerpt: (maxChars: number) => string;
|
||||
sanitizeOutgoingAssistantText: (text: unknown, fallback?: string) => string;
|
||||
defaultModel: string;
|
||||
defaultBaseUrl: string;
|
||||
defaultApiKey?: string;
|
||||
}
|
||||
|
||||
function clampLivingChatMaxOutputTokens(value: unknown): number {
|
||||
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: unknown): string {
|
||||
return String(value ?? "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function buildLivingChatContextWindow(items: unknown[]): string {
|
||||
const source = Array.isArray(items) ? items.slice(-6) : [];
|
||||
const lines: string[] = [];
|
||||
for (const item of source) {
|
||||
if (!item || typeof item !== "object") {
|
||||
continue;
|
||||
}
|
||||
const role = String((item as { role?: unknown }).role ?? "").trim();
|
||||
const text = compactWhitespace(String((item as { text?: unknown }).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: string, conversationWindow: string): string {
|
||||
const contextBlock = conversationWindow ? `Контекст последних сообщений:\n${conversationWindow}\n\n` : "";
|
||||
return `${contextBlock}Сообщение пользователя:\n${userMessage}`;
|
||||
}
|
||||
|
||||
export async function runAssistantLivingChatLlmRuntime(
|
||||
input: RunAssistantLivingChatLlmRuntimeInput
|
||||
): Promise<string> {
|
||||
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);
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import * as assistantDeepTurnResponseRuntimeAdapter_1 from "./assistantDeepTurnR
|
||||
import * as assistantDeepTurnRetrievalRuntimeAdapter_1 from "./assistantDeepTurnRetrievalRuntimeAdapter";
|
||||
import * as assistantAddressRuntimeAdapter_1 from "./assistantAddressRuntimeAdapter";
|
||||
import * as assistantLivingChatHandlerRuntimeAdapter_1 from "./assistantLivingChatHandlerRuntimeAdapter";
|
||||
import * as assistantLivingChatLlmRuntimeAdapter_1 from "./assistantLivingChatLlmRuntimeAdapter";
|
||||
import * as assistantQueryPlanning_1 from "./assistantQueryPlanning";
|
||||
import iconv from "iconv-lite";
|
||||
const DATA_SCOPE_CACHE_TTL_MS = 60_000;
|
||||
@@ -3425,29 +3426,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)();
|
||||
}
|
||||
@@ -4432,33 +4410,17 @@ export 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,
|
||||
@@ -4690,5 +4652,3 @@ export class AssistantService {
|
||||
return deepTurnResponseRuntime.response;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user