koishi-plugin-sus-chat
Version:
超简单超棒的AI聊天, 启动!
313 lines (312 loc) • 12.2 kB
JavaScript
"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;
}