Files
NODEDC_1C/llm_normalizer/backend/src/services/assistantLivingChatLlmRuntimeAdapter.ts
T

121 lines
5.8 KiB
TypeScript

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);
}