121 lines
5.8 KiB
TypeScript
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);
|
|
}
|