UNPKG

koishi-plugin-sus-chat

Version:
313 lines (312 loc) 12.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChatServer = exports.Prompts = void 0; const node_fs_1 = __importDefault(require("node:fs")); const liquidjs_1 = require("liquidjs"); const node_path_1 = __importDefault(require("node:path")); const js_yaml_1 = __importDefault(require("js-yaml")); const index_1 = require("./index"); class Prompts { origin_config; directory; prompts_map = {}; init_func; get_liquid(ctx, session) { const liquid = new liquidjs_1.Liquid(); if (!!ctx && !!this.init_func) { const init_func = this.init_func; liquid.plugin(function (Liquid) { init_func.call(this, ctx, Liquid); }); } liquid.registerTag("send", { *render(ctx, emitter, hash) { const str = yield this.value.value(ctx); session?.send(str); }, parse(token, remainingTokens) { this.value = new liquidjs_1.Value(token.args, this.liquid); }, }); return liquid; } reload(ctx, directory) { index_1.logger.info("load prompts"); const init_file_path = node_path_1.default.join(directory, "init.js"); if (node_fs_1.default.existsSync(init_file_path)) { this.init_func = require(node_path_1.default.resolve(node_path_1.default.join(init_file_path))); } let files = node_fs_1.default .readdirSync(directory) .filter((file) => file.toLowerCase().endsWith(".yml")); let prompts = files.map((file) => { const content_string = node_fs_1.default.readFileSync(node_path_1.default.join(directory, file), "utf-8"); const content = js_yaml_1.default.load(content_string); return content; }); const liquid = this.get_liquid(ctx); prompts.forEach((prompt) => { const prompts = prompt.prompts?.map((prompt) => { return { role: prompt.role, content: liquid.parse(prompt.content) }; }); this.prompts_map[prompt.name] = Object.assign({}, prompt, { prompts: prompts, }); }); } constructor(ctx, directory, config) { this.origin_config = config; this.reload(ctx, directory); } get names() { return Object.keys(this.prompts_map); } get_keywords(name) { return this.prompts_map[name]?.keywords ?? []; } get(name, ctx, session) { const liquid = this.get_liquid(ctx, session); if (!this.prompts_map[name]) { throw "prompt not found"; } const temp = this.prompts_map[name]; const messages = temp.prompts?.map((message) => { const content = liquid.renderSync(message.content, { session: JSON.parse(JSON.stringify(session)), }); return { role: message.role, content }; }); let postprocessing; if (temp.postprocessing) { postprocessing = (message) => { const content = liquid.parseAndRenderSync(temp.postprocessing, { message: message, session: JSON.parse(JSON.stringify(session)), }); return { role: message.role, content: content.trim() }; }; } else { postprocessing = (message) => message; } let target = { prompts: messages ?? [], postprocessing, follow: !!temp.follow, config: temp.config, preamble: temp.preamble, prologue: temp.prologue, }; if (typeof temp.extend == "string") { target = Object.assign({}, this.get(temp.extend, ctx, session), filterUndefined(target)); } return target; } } exports.Prompts = Prompts; class ChatServer { prompts; prompt_str; #liquid; #recollect; max_length; persistence; origin_config; get recollect() { return this.#recollect; } set recollect(value) { this.#recollect = value; this.#limit_length(); } get_recollect(session, prompt_name) { return this.recollect[session.cid]?.[prompt_name] ?? []; } #limit_length() { for (const cid in this.#recollect) { for (const prompt_name in this.#recollect[cid]) { if (this.#recollect[cid][prompt_name].length <= this.max_length) { continue; } const new_arr = this.#recollect[cid][prompt_name].slice(this.#recollect[cid][prompt_name].length - this.max_length); while (new_arr[0]?.role != "user") { new_arr.shift(); } this.#recollect[cid][prompt_name] = new_arr; } } } update_recollect(ctx, session, prompt_name, callback) { if (typeof this.#recollect[session.cid] == "undefined") { this.#recollect[session.cid] = {}; } if (typeof this.#recollect[session.cid][prompt_name] == "undefined") { this.#recollect[session.cid][prompt_name] = []; } this.#recollect[session.cid][prompt_name] = callback(this.#recollect[session.cid][prompt_name]); this.#limit_length(); if (this.persistence) { const dir = node_path_1.default.join(ctx.baseDir, "data", "sus-recollect", encodeURIComponent(session.cid)); const file = `${encodeURIComponent(prompt_name)}.json`; try { node_fs_1.default.mkdirSync(dir, { recursive: true }); } catch { } node_fs_1.default.writeFileSync(`${dir}/${file}`, JSON.stringify(this.#recollect[session.cid][prompt_name]), { encoding: "utf-8", }); } } load_recollect(ctx) { const dir = node_path_1.default.join(ctx.baseDir, "data", "sus-recollect"); let items; try { items = node_fs_1.default.readdirSync(dir, { withFileTypes: true }); } catch { index_1.logger.info("no recollect data"); return; } const subdirectories = items?.filter((item) => item.isDirectory()); for (const subdirectory of subdirectories) { const cid = decodeURIComponent(subdirectory.name); const files = node_fs_1.default .readdirSync(`${dir}/${subdirectory.name}`) .filter((file) => file.toLowerCase().endsWith(".json")); for (const file of files) { const prompt_name = decodeURIComponent(file.slice(0, -5)); const content = node_fs_1.default.readFileSync(`${dir}/${subdirectory.name}/${file}`, "utf-8"); this.update_recollect(ctx, { cid }, prompt_name, (_messages) => { return JSON.parse(content); }); } } } constructor(config, prompts) { this.#recollect = {}; this.max_length = config.max_length; if (typeof prompts === "string") { this.#liquid = new liquidjs_1.Liquid(); this.prompt_str = this.#liquid.parse(prompts); } else { this.prompts = prompts; } this.origin_config = config; } get_liquid(ctx, session) { return this.#liquid ?? this.prompts.get_liquid(ctx, session); } async evaluate(ctx, session, content) { return ((await this.get_liquid(ctx, session).parseAndRender(content, { session: JSON.parse(JSON.stringify(session)), })) ?? content); } async get_prompt(prompt_name, ctx, session) { if (typeof this.prompts === "undefined") { return { prompts: [ { role: "system", content: await this.#liquid.render(this.prompt_str, { session: JSON.parse(JSON.stringify(session)), }), }, ], preamble: null, postprocessing: (message) => message, follow: false, config: undefined, prologue: null, }; } else { return this.prompts.get(prompt_name, ctx, session); } } async chat(message, prompt_name, ctx, session) { const recall = this.get_recollect(session, prompt_name); if (message.content.trim() == "" || typeof message.content != "string") { return undefined; } const prompt_real = await this.get_prompt(prompt_name, ctx, session); let messages; if (prompt_real?.follow) { messages = [...recall, ...(prompt_real?.prompts ?? [])]; } else { messages = [...(prompt_real?.prompts ?? []), ...recall]; } const liquid = this.prompts?.get_liquid?.(ctx, session); if (prompt_real?.preamble && liquid) { const preamble = liquid.parseAndRenderSync(prompt_real.preamble, { session: JSON.parse(JSON.stringify(session)), }); messages.push({ role: "system", content: preamble, }); } let prologue = ""; if (typeof liquid != "undefined" && prompt_real.prologue) { prologue = liquid.parseAndRenderSync(prompt_real.prologue, { session: JSON.parse(JSON.stringify(session)), }); } messages.push({ role: message.role, content: prologue + message.content }); if (this.origin_config.functionality.logging) { index_1.logger.info(`${session.cid}:`, prologue + message.content); } const url = prompt_real.config?.["apiUrl"] ?? this.origin_config.api; const req = Object.assign({}, { model: this.origin_config.model, messages: messages, temperature: this.origin_config.temperature, stream: false, }, filterUndefined({ model: prompt_real.config?.["model"], max_tokens: prompt_real.config?.["max_tokens"], temperature: prompt_real.config?.["temperature"], top_p: prompt_real.config?.["top_p"], frequency_penalty: prompt_real.config?.["frequency_penalty"], presence_penalty: prompt_real.config?.["presence_penalty"], stop: prompt_real.config?.["stop"], logit_bias: prompt_real.config?.["logit_bias"], prompt: prompt_real.config?.["prompt"], ...prompt_real.config?.["extra"], })); const res = await ctx.http(`${url}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${(prompt_real.config?.["apiToken"] ?? this.origin_config.api_key).trim()}`, }, data: JSON.stringify(req), }); const result_p = prompt_real.postprocessing(res.data.choices[0].message); const result = result_p.content.trim() == "" ? undefined : result_p; this.update_recollect(ctx, session, prompt_name, (messages) => { messages.push(message); messages.push(result ?? res.data.choices[0].message); return messages; }); if (this.origin_config.functionality.logging) { index_1.logger.info("assistant:", res.data.choices[0]?.message?.content); } return result; } } exports.ChatServer = ChatServer; function filterUndefined(obj) { const filtered = {}; Object.keys(obj).forEach((key) => { if (obj[key] !== undefined) { // 如果值不是 undefined,则将其添加到过滤后的对象中 filtered[key] = obj[key]; } }); return filtered; }