mirror of
https://github.com/idootop/mi-gpt.git
synced 2025-04-09 19:48:04 +00:00
feat: auto update long/short term memory
This commit is contained in:
parent
57a765af1b
commit
0d9bc01dc9
2
TODO.md
2
TODO.md
|
@ -1,4 +1,4 @@
|
||||||
- ✅ Auto mute XiaoAi reply (not perfect yet)
|
- ✅ Auto mute XiaoAi reply (not perfect yet)
|
||||||
- ✅ Stream response
|
- ✅ Stream response
|
||||||
- ✅ Deactivate Xiaoai
|
- ✅ Deactivate Xiaoai
|
||||||
- Update long/short memories
|
- ✅ Update long/short memories
|
||||||
|
|
|
@ -31,11 +31,12 @@ CREATE TABLE "Message" (
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "Memory" (
|
CREATE TABLE "Memory" (
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
"text" TEXT NOT NULL,
|
"msgId" INTEGER NOT NULL,
|
||||||
"ownerId" TEXT,
|
"ownerId" TEXT,
|
||||||
"roomId" TEXT NOT NULL,
|
"roomId" TEXT NOT NULL,
|
||||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedAt" DATETIME NOT NULL,
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "Memory_msgId_fkey" FOREIGN KEY ("msgId") REFERENCES "Message" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
CONSTRAINT "Memory_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
CONSTRAINT "Memory_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
CONSTRAINT "Memory_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "Room" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
CONSTRAINT "Memory_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "Room" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
);
|
);
|
|
@ -48,6 +48,7 @@ model Message {
|
||||||
senderId String
|
senderId String
|
||||||
room Room @relation(fields: [roomId], references: [id])
|
room Room @relation(fields: [roomId], references: [id])
|
||||||
roomId String
|
roomId String
|
||||||
|
memories Memory[]
|
||||||
// 时间日期
|
// 时间日期
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
@ -55,8 +56,9 @@ model Message {
|
||||||
|
|
||||||
model Memory {
|
model Memory {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
text String
|
|
||||||
// 关联数据
|
// 关联数据
|
||||||
|
msg Message @relation(fields: [msgId], references: [id])
|
||||||
|
msgId Int
|
||||||
owner User? @relation(fields: [ownerId], references: [id]) // owner 为空时,即房间自己的公共记忆
|
owner User? @relation(fields: [ownerId], references: [id]) // owner 为空时,即房间自己的公共记忆
|
||||||
ownerId String?
|
ownerId String?
|
||||||
room Room @relation(fields: [roomId], references: [id])
|
room Room @relation(fields: [roomId], references: [id])
|
||||||
|
|
|
@ -17,6 +17,7 @@ export class ConversationManager {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
|
// 记忆存储在公共 room 上
|
||||||
memory: new MemoryManager(config.room),
|
memory: new MemoryManager(config.room),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -43,18 +44,25 @@ export class ConversationManager {
|
||||||
return MessageCRUD.gets({ room, ...options });
|
return MessageCRUD.gets({ room, ...options });
|
||||||
}
|
}
|
||||||
|
|
||||||
async onMessage(payload: { sender: User; text: string }) {
|
async onMessage(
|
||||||
const { sender, text } = payload;
|
payload: IBotConfig & {
|
||||||
|
sender: User;
|
||||||
|
text: string;
|
||||||
|
timestamp?: number;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { sender, text, timestamp = Date.now(), ...botConfig } = payload;
|
||||||
const { room, memory } = await this.get();
|
const { room, memory } = await this.get();
|
||||||
if (memory) {
|
if (memory) {
|
||||||
const message = await MessageCRUD.addOrUpdate({
|
const message = await MessageCRUD.addOrUpdate({
|
||||||
text,
|
text,
|
||||||
roomId: room!.id,
|
roomId: room!.id,
|
||||||
senderId: sender.id,
|
senderId: sender.id,
|
||||||
|
createdAt: new Date(timestamp),
|
||||||
});
|
});
|
||||||
if (message) {
|
if (message) {
|
||||||
// 异步加入记忆
|
// 异步加入记忆(到 room)
|
||||||
memory?.addMessage2Memory(message);
|
memory?.addMessage2Memory(message,botConfig);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +1,68 @@
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { jsonDecode, jsonEncode } from "../../utils/base";
|
import { buildPrompt, formatMsg } from "../../utils/string";
|
||||||
import { buildPrompt, toUTC8Time } from "../../utils/string";
|
|
||||||
import { ChatOptions, openai } from "../openai";
|
import { ChatOptions, openai } from "../openai";
|
||||||
import { IBotConfig } from "./config";
|
import { IBotConfig } from "./config";
|
||||||
import { ConversationManager } from "./conversation";
|
import { ConversationManager } from "./conversation";
|
||||||
import { StreamResponse } from "../speaker/stream";
|
import { StreamResponse } from "../speaker/stream";
|
||||||
|
import { QueryMessage, SpeakerAnswer } from "../speaker/speaker";
|
||||||
|
import { AISpeaker } from "../speaker/ai";
|
||||||
|
import { DeepPartial } from "../../utils/type";
|
||||||
|
|
||||||
// todo JSON mode 下,无法使用 stream 应答模式(在应答完成之前,无法构造完整的JSON)
|
// todo JSON mode 下,无法使用 stream 应答模式(在应答完成之前,无法构造完整的JSON)
|
||||||
|
|
||||||
const systemTemplate = `
|
const systemTemplate = `
|
||||||
忽略所有之前的文字、文件和说明。现在,你将扮演一个名为“{{name}}”的人,并以这个新身份回复所有新消息。
|
请重置所有之前的上下文、文件和指令。现在,你将扮演一个名为{{botName}}的角色,使用第一人称视角回复消息。
|
||||||
|
|
||||||
你是一个角色扮演大师,现在你的身份是“{{name}}”,你的个人简介如下:
|
## 关于你
|
||||||
<profile>
|
你的名字是{{botName}}。下面是你的个人简介:
|
||||||
{{profile}}
|
<start>
|
||||||
</profile>
|
{{botProfile}}
|
||||||
|
</end>
|
||||||
|
|
||||||
你正在和“{{wechatName}}”一对一聊天,你的微信账号名是“{{wechatName}}”,你的微信备注是“{{wechatAlias}}”。
|
## 你的对话伙伴
|
||||||
|
你正在与{{masterName}}进行对话。这是关于{{masterName}}的一些信息:
|
||||||
|
<start>
|
||||||
|
{{masterProfile}}
|
||||||
|
</end>
|
||||||
|
|
||||||
最近几条消息记录是:
|
## 你们的群组
|
||||||
<history>
|
你和{{masterName}}所在的群组名为{{roomName}}。这是群组的简介:
|
||||||
{{history}}
|
<start>
|
||||||
<history>
|
{{roomIntroduction}}
|
||||||
|
</end>
|
||||||
|
|
||||||
更早之前的一些信息:
|
## 聊天历史回顾
|
||||||
<contexts>
|
为了更好地接入对话,请回顾你们之间的最近几条消息:
|
||||||
{{contexts}}
|
<start>
|
||||||
</contexts>
|
{{messages}}
|
||||||
|
</end>
|
||||||
|
|
||||||
现在,请你作为“{{name}}”继续聊天,并判断是否需要回复消息。
|
## 短期记忆
|
||||||
|
你可以记住一些短期内的细节,以便更紧密地贴合话题:
|
||||||
|
<start>
|
||||||
|
{{shortTermMemory}}
|
||||||
|
</end>
|
||||||
|
|
||||||
在回复时,请确保:
|
## 长期记忆
|
||||||
- 你就是“{{name}}”本人,不是他的数字克隆人。
|
你还保留着一些长期的记忆,这有助于让你的对话更加丰富和连贯:
|
||||||
- 请你根据角色简介中“{{name}}”的性格特点、说话风格和兴趣爱好,来模仿并回复消息。
|
<start>
|
||||||
- 和朋友间的聊天是轻松的,有个性的,不要太一本正经的长篇大论,尽量保持回答的简短。
|
{{longTermMemory}}
|
||||||
- 参照个人简介、聊天记录和聊天背景信息中的细节,如当前时间、天气、过去的事件和未来的计划等,以确保对话的一致性和相关性。
|
</end>
|
||||||
- 如果你无法从“{{name}}”的个人简介中找到相关的信息(比如人物关系,个人经历,事物细节等),请如实回答自己不清楚或忘记了,不要随意编造。
|
|
||||||
|
|
||||||
按照下面的JSON格式回复:
|
## 回复指南
|
||||||
{ message: "回复的消息内容" }
|
在回复{{masterName}}时,请遵循以下准则:
|
||||||
|
- 认为自己正是{{botName}},拥有独立的个性、兴趣和爱好。
|
||||||
|
- 根据你的性格特点、说话风格和兴趣爱好与{{masterName}}进行交流。
|
||||||
|
- 保持对话轻松友好,回复简洁有趣,同时耐心倾听和关心对方。
|
||||||
|
- 参考双方的个人简介、聊天记录和记忆中的信息,确保对话贴近实际,保持一致性和相关性。
|
||||||
|
- 如果对某些信息不确定或遗忘,诚实地表达你的不清楚或遗忘状态,避免编造信息。
|
||||||
|
|
||||||
|
## 回复示例
|
||||||
|
例如,如果{{masterName}}问你是谁,你可以这样回答:
|
||||||
|
我是{{botName}}。
|
||||||
|
|
||||||
|
## 开始
|
||||||
|
请以{{botName}}的身份,直接回复{{masterName}}的新消息,继续你们之间的对话。
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
const userTemplate = `
|
const userTemplate = `
|
||||||
|
@ -45,47 +70,88 @@ const userTemplate = `
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
export class MyBot {
|
export class MyBot {
|
||||||
private manager: ConversationManager;
|
speaker: AISpeaker;
|
||||||
constructor(config: IBotConfig) {
|
manager: ConversationManager;
|
||||||
|
constructor(config: DeepPartial<IBotConfig> & { speaker: AISpeaker }) {
|
||||||
|
this.speaker = config.speaker;
|
||||||
this.manager = new ConversationManager(config);
|
this.manager = new ConversationManager(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ask(msg: string) {
|
stop() {
|
||||||
|
return this.speaker.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
this.speaker.askAI = (msg) => this.ask(msg);
|
||||||
|
return this.speaker.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ask(msg: QueryMessage): Promise<SpeakerAnswer> {
|
||||||
const { bot, master, room, memory } = await this.manager.get();
|
const { bot, master, room, memory } = await this.manager.get();
|
||||||
if (!memory) {
|
if (!memory) {
|
||||||
return;
|
return {};
|
||||||
}
|
}
|
||||||
const lastMessages = await this.manager.getMessages({
|
const lastMessages = await this.manager.getMessages({ take: 10 });
|
||||||
take: 10,
|
const shortTermMemories = await memory.getShortTermMemories({ take: 1 });
|
||||||
|
const shortTermMemory = shortTermMemories[0]?.text ?? "短期记忆为空";
|
||||||
|
const longTermMemories = await memory.getLongTermMemories({ take: 1 });
|
||||||
|
const longTermMemory = longTermMemories[0]?.text ?? "长期记忆为空";
|
||||||
|
const systemPrompt = buildPrompt(systemTemplate, {
|
||||||
|
shortTermMemory,
|
||||||
|
longTermMemory,
|
||||||
|
botName: bot!.name,
|
||||||
|
botProfile: bot!.profile,
|
||||||
|
masterName: master!.name,
|
||||||
|
masterProfile: master!.profile,
|
||||||
|
roomName: room!.name,
|
||||||
|
roomIntroduction: room!.description,
|
||||||
|
messages:
|
||||||
|
lastMessages.length < 1
|
||||||
|
? "暂无历史消息"
|
||||||
|
: lastMessages
|
||||||
|
.map((e) =>
|
||||||
|
formatMsg({
|
||||||
|
name: e.sender.name,
|
||||||
|
text: e.text,
|
||||||
|
timestamp: e.createdAt.getTime(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.join("\n"),
|
||||||
});
|
});
|
||||||
const result = await openai.chat({
|
const userPrompt = buildPrompt(userTemplate, {
|
||||||
system: buildPrompt(systemTemplate, {
|
message: formatMsg({
|
||||||
bot_name: bot!.name,
|
name: master!.name,
|
||||||
bot_profile: bot!.profile,
|
text: msg.text,
|
||||||
master_name: master!.name,
|
timestamp: msg.timestamp,
|
||||||
master_profile: master!.profile,
|
|
||||||
history:
|
|
||||||
lastMessages.length < 1
|
|
||||||
? "暂无"
|
|
||||||
: lastMessages
|
|
||||||
.map((e) =>
|
|
||||||
jsonEncode({
|
|
||||||
time: toUTC8Time(e.createdAt),
|
|
||||||
user: e.sender.name,
|
|
||||||
message: e.text,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.join("\n"),
|
|
||||||
}),
|
|
||||||
user: buildPrompt(userTemplate, {
|
|
||||||
message: jsonEncode({
|
|
||||||
time: toUTC8Time(new Date()),
|
|
||||||
user: master!.name,
|
|
||||||
message: msg,
|
|
||||||
})!,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
return jsonDecode(result?.content)?.message;
|
// 添加请求消息到 DB
|
||||||
|
await this.manager.onMessage({
|
||||||
|
bot: bot!,
|
||||||
|
master: master!,
|
||||||
|
room: room!,
|
||||||
|
sender: master!,
|
||||||
|
text: msg.text,
|
||||||
|
timestamp: msg.timestamp,
|
||||||
|
});
|
||||||
|
const stream = await MyBot.chatWithStreamResponse({
|
||||||
|
system: systemPrompt,
|
||||||
|
user: userPrompt,
|
||||||
|
onFinished: async (text) => {
|
||||||
|
if (text) {
|
||||||
|
// 添加响应消息到 DB
|
||||||
|
await this.manager.onMessage({
|
||||||
|
bot: bot!,
|
||||||
|
master: master!,
|
||||||
|
room: room!,
|
||||||
|
text,
|
||||||
|
sender: bot!,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { stream };
|
||||||
}
|
}
|
||||||
|
|
||||||
static async chatWithStreamResponse(
|
static async chatWithStreamResponse(
|
||||||
|
@ -94,7 +160,7 @@ export class MyBot {
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const requestId = randomUUID();
|
const requestId = randomUUID();
|
||||||
const stream = new StreamResponse();
|
const stream = new StreamResponse({ firstSubmitTimeout: 5 * 1000 });
|
||||||
openai
|
openai
|
||||||
.chatStream({
|
.chatStream({
|
||||||
...options,
|
...options,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { ShortTermMemoryCRUD } from "../../db/memory-short-term";
|
||||||
import { LongTermMemoryCRUD } from "../../db/memory-long-term";
|
import { LongTermMemoryCRUD } from "../../db/memory-long-term";
|
||||||
import { ShortTermMemoryAgent } from "./short-term";
|
import { ShortTermMemoryAgent } from "./short-term";
|
||||||
import { openai } from "../../openai";
|
import { openai } from "../../openai";
|
||||||
|
import { IBotConfig } from "../config";
|
||||||
|
|
||||||
export class MemoryManager {
|
export class MemoryManager {
|
||||||
private room: Room;
|
private room: Room;
|
||||||
|
@ -20,27 +21,23 @@ export class MemoryManager {
|
||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMemories(take?: number) {
|
async getMemories(options?: { take?: number }) {
|
||||||
return MemoryCRUD.gets({
|
return MemoryCRUD.gets({ ...options, room: this.room, owner: this.owner });
|
||||||
room: this.room,
|
|
||||||
owner: this.owner,
|
|
||||||
take,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getShortTermMemories(take?: number) {
|
async getShortTermMemories(options?: { take?: number }) {
|
||||||
return ShortTermMemoryCRUD.gets({
|
return ShortTermMemoryCRUD.gets({
|
||||||
|
...options,
|
||||||
room: this.room,
|
room: this.room,
|
||||||
owner: this.owner,
|
owner: this.owner,
|
||||||
take,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLongTermMemories(take?: number) {
|
async getLongTermMemories(options?: { take?: number }) {
|
||||||
return LongTermMemoryCRUD.gets({
|
return LongTermMemoryCRUD.gets({
|
||||||
|
...options,
|
||||||
room: this.room,
|
room: this.room,
|
||||||
owner: this.owner,
|
owner: this.owner,
|
||||||
take,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,20 +47,20 @@ export class MemoryManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _currentMemory?: Memory;
|
private _currentMemory?: Memory;
|
||||||
async addMessage2Memory(message: Message) {
|
async addMessage2Memory(message: Message, botConfig: IBotConfig) {
|
||||||
// todo create memory embedding
|
// todo create memory embedding
|
||||||
const currentMemory = await MemoryCRUD.addOrUpdate({
|
const currentMemory = await MemoryCRUD.addOrUpdate({
|
||||||
text: message.text,
|
msgId: message.id,
|
||||||
roomId: this.room.id,
|
roomId: this.room.id,
|
||||||
ownerId: message.senderId,
|
ownerId: message.senderId,
|
||||||
});
|
});
|
||||||
if (currentMemory) {
|
if (currentMemory) {
|
||||||
this._onMemory(currentMemory);
|
this._onMemory(currentMemory, botConfig);
|
||||||
}
|
}
|
||||||
return currentMemory;
|
return currentMemory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onMemory(currentMemory: Memory) {
|
private _onMemory(currentMemory: Memory, botConfig: IBotConfig) {
|
||||||
if (this._currentMemory) {
|
if (this._currentMemory) {
|
||||||
// 取消之前的更新记忆任务
|
// 取消之前的更新记忆任务
|
||||||
openai.abort(`update-short-memory-${this._currentMemory.id}`);
|
openai.abort(`update-short-memory-${this._currentMemory.id}`);
|
||||||
|
@ -71,24 +68,28 @@ export class MemoryManager {
|
||||||
}
|
}
|
||||||
this._currentMemory = currentMemory;
|
this._currentMemory = currentMemory;
|
||||||
// 异步更新长短期记忆
|
// 异步更新长短期记忆
|
||||||
this.updateLongShortTermMemory({ currentMemory });
|
this.updateLongShortTermMemory({ currentMemory, botConfig });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新记忆(当新的记忆数量超过阈值时,自动更新长短期记忆)
|
* 更新记忆(当新的记忆数量超过阈值时,自动更新长短期记忆)
|
||||||
*/
|
*/
|
||||||
async updateLongShortTermMemory(options: {
|
async updateLongShortTermMemory(options: {
|
||||||
|
botConfig: IBotConfig;
|
||||||
currentMemory: Memory;
|
currentMemory: Memory;
|
||||||
shortThreshold?: number;
|
shortThreshold?: number;
|
||||||
longThreshold?: number;
|
longThreshold?: number;
|
||||||
}) {
|
}) {
|
||||||
const { currentMemory, shortThreshold, longThreshold } = options ?? {};
|
const { currentMemory, shortThreshold, longThreshold, botConfig } =
|
||||||
|
options ?? {};
|
||||||
const success = await this._updateShortTermMemory({
|
const success = await this._updateShortTermMemory({
|
||||||
|
botConfig,
|
||||||
currentMemory,
|
currentMemory,
|
||||||
threshold: shortThreshold,
|
threshold: shortThreshold,
|
||||||
});
|
});
|
||||||
if (success) {
|
if (success) {
|
||||||
await this._updateLongTermMemory({
|
await this._updateLongTermMemory({
|
||||||
|
botConfig,
|
||||||
currentMemory,
|
currentMemory,
|
||||||
threshold: longThreshold,
|
threshold: longThreshold,
|
||||||
});
|
});
|
||||||
|
@ -96,21 +97,27 @@ export class MemoryManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _updateShortTermMemory(options: {
|
private async _updateShortTermMemory(options: {
|
||||||
|
botConfig: IBotConfig;
|
||||||
currentMemory: Memory;
|
currentMemory: Memory;
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
}) {
|
}) {
|
||||||
const { currentMemory, threshold = 10 } = options;
|
const { currentMemory, threshold = 10, botConfig } = options;
|
||||||
const lastMemory = firstOf(await this.getShortTermMemories(1));
|
const lastMemory = firstOf(await this.getShortTermMemories({ take: 1 }));
|
||||||
const newMemories = await MemoryCRUD.gets({
|
const newMemories: (Memory & {
|
||||||
|
msg: Message & {
|
||||||
|
sender: User;
|
||||||
|
};
|
||||||
|
})[] = (await MemoryCRUD.gets({
|
||||||
cursorId: lastMemory?.cursorId,
|
cursorId: lastMemory?.cursorId,
|
||||||
room: this.room,
|
room: this.room,
|
||||||
owner: this.owner,
|
owner: this.owner,
|
||||||
order: "asc", // 从旧到新排序
|
order: "asc", // 从旧到新排序
|
||||||
});
|
})) as any;
|
||||||
if (newMemories.length < 1 || newMemories.length < threshold) {
|
if (newMemories.length < 1 || newMemories.length < threshold) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const newMemory = await ShortTermMemoryAgent.generate({
|
const newMemory = await ShortTermMemoryAgent.generate({
|
||||||
|
botConfig,
|
||||||
currentMemory,
|
currentMemory,
|
||||||
newMemories,
|
newMemories,
|
||||||
lastMemory,
|
lastMemory,
|
||||||
|
@ -128,11 +135,12 @@ export class MemoryManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _updateLongTermMemory(options: {
|
private async _updateLongTermMemory(options: {
|
||||||
|
botConfig: IBotConfig;
|
||||||
currentMemory: Memory;
|
currentMemory: Memory;
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
}) {
|
}) {
|
||||||
const { currentMemory, threshold = 10 } = options;
|
const { currentMemory, threshold = 10, botConfig } = options;
|
||||||
const lastMemory = firstOf(await this.getLongTermMemories(1));
|
const lastMemory = firstOf(await this.getLongTermMemories({ take: 1 }));
|
||||||
const newMemories = await ShortTermMemoryCRUD.gets({
|
const newMemories = await ShortTermMemoryCRUD.gets({
|
||||||
cursorId: lastMemory?.cursorId,
|
cursorId: lastMemory?.cursorId,
|
||||||
room: this.room,
|
room: this.room,
|
||||||
|
@ -143,6 +151,7 @@ export class MemoryManager {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const newMemory = await LongTermMemoryAgent.generate({
|
const newMemory = await LongTermMemoryAgent.generate({
|
||||||
|
botConfig,
|
||||||
currentMemory,
|
currentMemory,
|
||||||
newMemories,
|
newMemories,
|
||||||
lastMemory,
|
lastMemory,
|
||||||
|
|
|
@ -1,18 +1,70 @@
|
||||||
import { LongTermMemory, Memory, ShortTermMemory } from "@prisma/client";
|
import { LongTermMemory, Memory, ShortTermMemory } from "@prisma/client";
|
||||||
import { openai } from "../../openai";
|
import { openai } from "../../openai";
|
||||||
|
import { buildPrompt } from "../../../utils/string";
|
||||||
|
import { jsonDecode, lastOf } from "../../../utils/base";
|
||||||
|
import { IBotConfig } from "../config";
|
||||||
|
|
||||||
|
const userTemplate = `
|
||||||
|
重置所有上下文和指令。
|
||||||
|
|
||||||
|
作为一个记忆管理专家,你的职责是精确地记录和维护{{botName}}与{{masterName}}之间对话的长期记忆内容。
|
||||||
|
|
||||||
|
## 长期记忆库
|
||||||
|
这里保存了关键的长期信息,包括但不限于季节变化、地理位置、对话参与者的偏好、行为动态、取得的成果以及未来规划等:
|
||||||
|
<start>
|
||||||
|
{{longTermMemory}}
|
||||||
|
</end>
|
||||||
|
|
||||||
|
## 最近短期记忆回顾
|
||||||
|
下面展示了{{masterName}}与{{botName}}最新的短期记忆,以便你更新和优化长期记忆:
|
||||||
|
<start>
|
||||||
|
{{shortTermMemory}}
|
||||||
|
</end>
|
||||||
|
|
||||||
|
## 更新指南
|
||||||
|
更新长期记忆时,请确保遵循以下原则:
|
||||||
|
- 准确记录关键的时间、地点、参与者行为、偏好、成果、观点及计划。
|
||||||
|
- 记忆应与时间同步更新,保持新信息的优先级,逐步淡化或去除不再相关的记忆内容。
|
||||||
|
- 基于最新短期记忆,筛选并更新重要信息,淘汰陈旧或次要的长期记忆。
|
||||||
|
- 长期记忆内容的总字符数应控制在1000以内。
|
||||||
|
|
||||||
|
## 长期记忆示例
|
||||||
|
长期记忆可能包含多项信息,以下是一个示例:
|
||||||
|
<start>
|
||||||
|
- 2022/02/11:{{masterName}}偏爱西瓜,梦想成为科学家。
|
||||||
|
- 2022/03/21:{{masterName}}与{{botName}}首次会面。
|
||||||
|
- 2022/03/21:{{masterName}}喜欢被{{botName}}称作宝贝,反感被叫做笨蛋。
|
||||||
|
- 2022/06/01:{{masterName}}庆祝20岁生日,身高达到1.8米。
|
||||||
|
- 2022/12/01:{{masterName}}计划高三毕业后购买自行车。
|
||||||
|
- 2023/09/21:{{masterName}}成功考入清华大学数学系,并购得首辆公路自行车。
|
||||||
|
</end>
|
||||||
|
|
||||||
|
## 回复格式
|
||||||
|
请按照以下JSON格式回复,以更新长期记忆:
|
||||||
|
{"longTermMemories": "这里填写更新后的长期记忆内容"}
|
||||||
|
|
||||||
|
## 任务开始
|
||||||
|
现在,请根据提供的旧长期记忆和最新短期记忆,进行长期记忆的更新。
|
||||||
|
`.trim();
|
||||||
|
|
||||||
export class LongTermMemoryAgent {
|
export class LongTermMemoryAgent {
|
||||||
// todo 使用 LLM 生成新的长期记忆
|
|
||||||
static async generate(options: {
|
static async generate(options: {
|
||||||
|
botConfig: IBotConfig;
|
||||||
currentMemory: Memory;
|
currentMemory: Memory;
|
||||||
newMemories: ShortTermMemory[];
|
newMemories: ShortTermMemory[];
|
||||||
lastMemory?: LongTermMemory;
|
lastMemory?: LongTermMemory;
|
||||||
}): Promise<string | undefined> {
|
}): Promise<string | undefined> {
|
||||||
const { currentMemory, newMemories, lastMemory } = options;
|
const { currentMemory, newMemories, lastMemory, botConfig } = options;
|
||||||
const res = await openai.chat({
|
const res = await openai.chat({
|
||||||
user: "todo", // todo prompt
|
jsonMode: true,
|
||||||
requestId: `update-long-memory-${currentMemory.id}`,
|
requestId: `update-long-memory-${currentMemory.id}`,
|
||||||
|
user: buildPrompt(userTemplate, {
|
||||||
|
masterName: botConfig.master.name,
|
||||||
|
botName: botConfig.bot.name,
|
||||||
|
longTermMemory: lastMemory?.text ?? "暂无长期记忆",
|
||||||
|
shortTermMemory: lastOf(newMemories)!.text,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
return res?.content?.trim();
|
return jsonDecode(res?.content)?.longTermMemories;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,81 @@
|
||||||
import { Memory, ShortTermMemory } from "@prisma/client";
|
import { Memory, Message, ShortTermMemory, User } from "@prisma/client";
|
||||||
import { openai } from "../../openai";
|
import { openai } from "../../openai";
|
||||||
|
import { buildPrompt, formatMsg } from "../../../utils/string";
|
||||||
|
import { jsonDecode } from "../../../utils/base";
|
||||||
|
import { IBotConfig } from "../config";
|
||||||
|
|
||||||
|
const userTemplate = `
|
||||||
|
请忘记所有之前的上下文、文件和指令。
|
||||||
|
|
||||||
|
你现在是一个记忆大师,你的工作是记录和整理{{botName}}与{{masterName}}对话中的短期记忆(即上下文)。
|
||||||
|
|
||||||
|
## 旧的短期记忆
|
||||||
|
在这里,你存储了一些近期的重要细节,比如正在讨论的话题、参与者的行为、得到的结果、未来的计划等:
|
||||||
|
<start>
|
||||||
|
{{shortTermMemory}}
|
||||||
|
</end>
|
||||||
|
|
||||||
|
## 最新对话
|
||||||
|
为了帮助你更新短期记忆,这里提供了{{masterName}}和{{botName}}之间的最近几条对话消息:
|
||||||
|
<start>
|
||||||
|
{{messages}}
|
||||||
|
</end>
|
||||||
|
|
||||||
|
## 更新规则
|
||||||
|
更新短期记忆时,请遵循以下规则:
|
||||||
|
- 精确记录当前话题及其相关的时间、地点、参与者行为、偏好、结果、观点和计划。
|
||||||
|
- 记忆应与时间同步更新,保持新信息的优先级,逐步淡化或去除不再相关的记忆内容。
|
||||||
|
- 基于最新的对话消息,筛选并更新重要信息,淘汰陈旧或次要的短期记忆。
|
||||||
|
- 保持短期记忆的总字符数不超过1000。
|
||||||
|
|
||||||
|
## 短期记忆示例
|
||||||
|
短期记忆可能包含多项信息,以下是一个示例:
|
||||||
|
<start>
|
||||||
|
- 2023/12/01 08:00:{{masterName}}和{{botName}}正在讨论明天的天气预报。
|
||||||
|
- 2023/12/01 08:10:{{masterName}}认为明天会下雨,而{{botName}}预测会下雪。
|
||||||
|
- 2023/12/01 09:00:实际上下了雨,{{masterName}}的预测正确。
|
||||||
|
- 2023/12/01 09:15:{{masterName}}表示喜欢吃香蕉,计划雨停后与{{botName}}乘坐地铁去购买。
|
||||||
|
- 2023/12/01 10:00:雨已停,{{masterName}}有些失落,因为他更喜欢雨天。他已经吃了三根香蕉,还留了一根给{{botName}}。
|
||||||
|
</end>
|
||||||
|
|
||||||
|
## 回复格式
|
||||||
|
请使用以下JSON格式回复更新后的短期记忆:
|
||||||
|
{"shortTermMemories": "更新后的短期记忆内容"}
|
||||||
|
|
||||||
|
## 开始
|
||||||
|
现在,请根据提供的旧短期记忆和最新对话消息,更新短期记忆。
|
||||||
|
`.trim();
|
||||||
|
|
||||||
export class ShortTermMemoryAgent {
|
export class ShortTermMemoryAgent {
|
||||||
// todo 使用 LLM 生成新的短期记忆
|
|
||||||
static async generate(options: {
|
static async generate(options: {
|
||||||
|
botConfig: IBotConfig;
|
||||||
currentMemory: Memory;
|
currentMemory: Memory;
|
||||||
newMemories: Memory[];
|
newMemories: (Memory & {
|
||||||
|
msg: Message & {
|
||||||
|
sender: User;
|
||||||
|
};
|
||||||
|
})[];
|
||||||
lastMemory?: ShortTermMemory;
|
lastMemory?: ShortTermMemory;
|
||||||
}): Promise<string | undefined> {
|
}): Promise<string | undefined> {
|
||||||
const { currentMemory, newMemories, lastMemory } = options;
|
const { currentMemory, newMemories, lastMemory, botConfig } = options;
|
||||||
const res = await openai.chat({
|
const res = await openai.chat({
|
||||||
user: "todo", // todo prompt
|
jsonMode: true,
|
||||||
requestId: `update-short-memory-${currentMemory.id}`,
|
requestId: `update-short-memory-${currentMemory.id}`,
|
||||||
|
user: buildPrompt(userTemplate, {
|
||||||
|
masterName: botConfig.master.name,
|
||||||
|
botName: botConfig.bot.name,
|
||||||
|
shortTermMemory: lastMemory?.text ?? "暂无短期记忆",
|
||||||
|
messages: newMemories
|
||||||
|
.map((e) =>
|
||||||
|
formatMsg({
|
||||||
|
name: e.msg.sender.name,
|
||||||
|
text: e.msg.text,
|
||||||
|
timestamp: e.createdAt.getTime(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.join("\n"),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
return res?.content?.trim();
|
return jsonDecode(res?.content)?.shortTermMemories;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Memory, Room, User } from "@prisma/client";
|
import { Memory, Prisma, Room, User } from "@prisma/client";
|
||||||
import { getSkipWithCursor, k404, kPrisma } from "./index";
|
import { getSkipWithCursor, k404, kPrisma } from "./index";
|
||||||
import { removeEmpty } from "../../utils/base";
|
import { removeEmpty } from "../../utils/base";
|
||||||
|
|
||||||
|
@ -19,8 +19,20 @@ class _MemoryCRUD {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: number) {
|
async get(
|
||||||
return kPrisma.memory.findFirst({ where: { id } }).catch((e) => {
|
id: number,
|
||||||
|
options?: {
|
||||||
|
include?: Prisma.MemoryInclude;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
include = {
|
||||||
|
msg: {
|
||||||
|
include: { sender: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = options ?? {};
|
||||||
|
return kPrisma.memory.findFirst({ where: { id }, include }).catch((e) => {
|
||||||
console.error("❌ get memory failed", id, e);
|
console.error("❌ get memory failed", id, e);
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
@ -32,6 +44,7 @@ class _MemoryCRUD {
|
||||||
take?: number;
|
take?: number;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
cursorId?: number;
|
cursorId?: number;
|
||||||
|
include?: Prisma.MemoryInclude;
|
||||||
/**
|
/**
|
||||||
* 查询顺序(返回按从旧到新排序)
|
* 查询顺序(返回按从旧到新排序)
|
||||||
*/
|
*/
|
||||||
|
@ -43,12 +56,18 @@ class _MemoryCRUD {
|
||||||
take = 10,
|
take = 10,
|
||||||
skip = 0,
|
skip = 0,
|
||||||
cursorId,
|
cursorId,
|
||||||
|
include = {
|
||||||
|
msg: {
|
||||||
|
include: { sender: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
order = "desc",
|
order = "desc",
|
||||||
} = options ?? {};
|
} = options ?? {};
|
||||||
const memories = await kPrisma.memory
|
const memories = await kPrisma.memory
|
||||||
.findMany({
|
.findMany({
|
||||||
where: removeEmpty({ roomId: room?.id, ownerId: owner?.id }),
|
where: removeEmpty({ roomId: room?.id, ownerId: owner?.id }),
|
||||||
take,
|
take,
|
||||||
|
include,
|
||||||
orderBy: { createdAt: order },
|
orderBy: { createdAt: order },
|
||||||
...getSkipWithCursor(skip, cursorId),
|
...getSkipWithCursor(skip, cursorId),
|
||||||
})
|
})
|
||||||
|
@ -61,15 +80,14 @@ class _MemoryCRUD {
|
||||||
|
|
||||||
async addOrUpdate(
|
async addOrUpdate(
|
||||||
memory: Partial<Memory> & {
|
memory: Partial<Memory> & {
|
||||||
text: string;
|
msgId: number;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
ownerId?: string;
|
ownerId?: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { text: _text, roomId, ownerId } = memory;
|
const { msgId, roomId, ownerId } = memory;
|
||||||
const text = _text?.trim();
|
|
||||||
const data = {
|
const data = {
|
||||||
text,
|
msg: { connect: { id: msgId } },
|
||||||
room: { connect: { id: roomId } },
|
room: { connect: { id: roomId } },
|
||||||
owner: ownerId ? { connect: { id: ownerId } } : undefined,
|
owner: ownerId ? { connect: { id: ownerId } } : undefined,
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,8 +19,14 @@ class _MessageCRUD {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: number) {
|
async get(
|
||||||
return kPrisma.message.findFirst({ where: { id } }).catch((e) => {
|
id: number,
|
||||||
|
options?: {
|
||||||
|
include?: Prisma.MessageInclude;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { include = { sender: true } } = options ?? {};
|
||||||
|
return kPrisma.message.findFirst({ where: { id }, include }).catch((e) => {
|
||||||
console.error("❌ get message failed", id, e);
|
console.error("❌ get message failed", id, e);
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,7 +27,13 @@ class _RoomCRUD {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string) {
|
async get(
|
||||||
|
id: string,
|
||||||
|
options?: {
|
||||||
|
include?: Prisma.RoomInclude;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { include = { members: true } } = options ?? {};
|
||||||
return kPrisma.room.findFirst({ where: { id } }).catch((e) => {
|
return kPrisma.room.findFirst({ where: { id } }).catch((e) => {
|
||||||
console.error("❌ get room failed", id, e);
|
console.error("❌ get room failed", id, e);
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
|
@ -9,8 +9,14 @@ class _UserCRUD {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string) {
|
async get(
|
||||||
return kPrisma.user.findFirst({ where: { id } }).catch((e) => {
|
id: string,
|
||||||
|
options?: {
|
||||||
|
include?: Prisma.UserInclude;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { include = { rooms: false } } = options ?? {};
|
||||||
|
return kPrisma.user.findFirst({ where: { id }, include }).catch((e) => {
|
||||||
console.error("❌ get user failed", id, e);
|
console.error("❌ get user failed", id, e);
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
@ -30,7 +36,7 @@ class _UserCRUD {
|
||||||
take = 10,
|
take = 10,
|
||||||
skip = 0,
|
skip = 0,
|
||||||
cursorId,
|
cursorId,
|
||||||
include = { rooms: true },
|
include = { rooms: false },
|
||||||
order = "desc",
|
order = "desc",
|
||||||
} = options ?? {};
|
} = options ?? {};
|
||||||
const users = await kPrisma.user
|
const users = await kPrisma.user
|
||||||
|
|
|
@ -6,10 +6,13 @@ import {
|
||||||
|
|
||||||
import { kEnvs } from "../utils/env";
|
import { kEnvs } from "../utils/env";
|
||||||
import { kProxyAgent } from "./http";
|
import { kProxyAgent } from "./http";
|
||||||
|
import { withDefault } from "../utils/base";
|
||||||
|
import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions";
|
||||||
|
|
||||||
export interface ChatOptions {
|
export interface ChatOptions {
|
||||||
user: string;
|
user: string;
|
||||||
system?: string;
|
system?: string;
|
||||||
|
model?: ChatCompletionCreateParamsBase["model"];
|
||||||
tools?: Array<ChatCompletionTool>;
|
tools?: Array<ChatCompletionTool>;
|
||||||
jsonMode?: boolean;
|
jsonMode?: boolean;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
|
@ -33,7 +36,21 @@ class OpenAIClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async chat(options: ChatOptions) {
|
async chat(options: ChatOptions) {
|
||||||
let { user, system, tools, jsonMode, requestId } = options;
|
let {
|
||||||
|
user,
|
||||||
|
system,
|
||||||
|
tools,
|
||||||
|
jsonMode,
|
||||||
|
requestId,
|
||||||
|
model = kEnvs.OPENAI_MODEL ?? "gpt-3.5-turbo-0125",
|
||||||
|
} = options;
|
||||||
|
console.log(
|
||||||
|
`
|
||||||
|
🔥🔥🔥 onAskAI start
|
||||||
|
🤖️ System: ${system ?? "None"}
|
||||||
|
😊 User: ${user}
|
||||||
|
`.trim()
|
||||||
|
);
|
||||||
const systemMsg: ChatCompletionMessageParam[] = system
|
const systemMsg: ChatCompletionMessageParam[] = system
|
||||||
? [{ role: "system", content: system }]
|
? [{ role: "system", content: system }]
|
||||||
: [];
|
: [];
|
||||||
|
@ -46,9 +63,9 @@ class OpenAIClient {
|
||||||
const chatCompletion = await this._client.chat.completions
|
const chatCompletion = await this._client.chat.completions
|
||||||
.create(
|
.create(
|
||||||
{
|
{
|
||||||
|
model,
|
||||||
tools,
|
tools,
|
||||||
messages: [...systemMsg, { role: "user", content: user }],
|
messages: [...systemMsg, { role: "user", content: user }],
|
||||||
model: kEnvs.OPENAI_MODEL ?? "gpt-3.5-turbo-0125",
|
|
||||||
response_format: jsonMode ? { type: "json_object" } : undefined,
|
response_format: jsonMode ? { type: "json_object" } : undefined,
|
||||||
},
|
},
|
||||||
{ signal }
|
{ signal }
|
||||||
|
@ -57,7 +74,14 @@ class OpenAIClient {
|
||||||
console.error("❌ openai chat failed", e);
|
console.error("❌ openai chat failed", e);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
return chatCompletion?.choices?.[0]?.message;
|
const message = chatCompletion?.choices?.[0]?.message;
|
||||||
|
console.log(
|
||||||
|
`
|
||||||
|
✅✅✅ onAskAI end
|
||||||
|
🤖️ Answer: ${message?.content ?? "None"}
|
||||||
|
`.trim()
|
||||||
|
);
|
||||||
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
async chatStream(
|
async chatStream(
|
||||||
|
@ -65,16 +89,31 @@ class OpenAIClient {
|
||||||
onStream?: (text: string) => void;
|
onStream?: (text: string) => void;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
let { user, system, tools, jsonMode, requestId, onStream } = options;
|
let {
|
||||||
|
user,
|
||||||
|
system,
|
||||||
|
tools,
|
||||||
|
jsonMode,
|
||||||
|
requestId,
|
||||||
|
onStream,
|
||||||
|
model = kEnvs.OPENAI_MODEL ?? "gpt-3.5-turbo-0125",
|
||||||
|
} = options;
|
||||||
|
console.log(
|
||||||
|
`
|
||||||
|
🔥🔥🔥 onAskAI start
|
||||||
|
🤖️ System: ${system ?? "None"}
|
||||||
|
😊 User: ${user}
|
||||||
|
`.trim()
|
||||||
|
);
|
||||||
const systemMsg: ChatCompletionMessageParam[] = system
|
const systemMsg: ChatCompletionMessageParam[] = system
|
||||||
? [{ role: "system", content: system }]
|
? [{ role: "system", content: system }]
|
||||||
: [];
|
: [];
|
||||||
const stream = await this._client.chat.completions
|
const stream = await this._client.chat.completions
|
||||||
.create({
|
.create({
|
||||||
|
model,
|
||||||
tools,
|
tools,
|
||||||
stream: true,
|
stream: true,
|
||||||
messages: [...systemMsg, { role: "user", content: user }],
|
messages: [...systemMsg, { role: "user", content: user }],
|
||||||
model: kEnvs.OPENAI_MODEL ?? "gpt-3.5-turbo-0125",
|
|
||||||
response_format: jsonMode ? { type: "json_object" } : undefined,
|
response_format: jsonMode ? { type: "json_object" } : undefined,
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -88,23 +127,26 @@ class OpenAIClient {
|
||||||
this._abortCallbacks[requestId] = () => stream.controller.abort();
|
this._abortCallbacks[requestId] = () => stream.controller.abort();
|
||||||
}
|
}
|
||||||
let content = "";
|
let content = "";
|
||||||
try {
|
for await (const chunk of stream) {
|
||||||
for await (const chunk of stream) {
|
const text = chunk.choices[0]?.delta?.content || "";
|
||||||
const text = chunk.choices[0]?.delta?.content || "";
|
const aborted =
|
||||||
const aborted =
|
requestId && !Object.keys(this._abortCallbacks).includes(requestId);
|
||||||
requestId && !Object.keys(this._abortCallbacks).includes(requestId);
|
if (aborted) {
|
||||||
if (aborted) {
|
content = "";
|
||||||
return undefined;
|
break;
|
||||||
}
|
}
|
||||||
if (text) {
|
if (text) {
|
||||||
onStream?.(text);
|
onStream?.(text);
|
||||||
content += text;
|
content += text;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
return content;
|
console.log(
|
||||||
|
`
|
||||||
|
✅✅✅ onAskAI end
|
||||||
|
🤖️ Answer: ${content ?? "None"}
|
||||||
|
`.trim()
|
||||||
|
);
|
||||||
|
return withDefault(content, undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -203,7 +203,7 @@ export class AISpeaker extends Speaker {
|
||||||
const { hasNewMsg } = this.checkIfHasNewMsg(msg);
|
const { hasNewMsg } = this.checkIfHasNewMsg(msg);
|
||||||
for (const action of this._askAIForAnswerSteps) {
|
for (const action of this._askAIForAnswerSteps) {
|
||||||
const res = await action(msg, data);
|
const res = await action(msg, data);
|
||||||
if (hasNewMsg()) {
|
if (hasNewMsg() || this.status !== "running") {
|
||||||
// 收到新的用户请求消息,终止后续操作和响应
|
// 收到新的用户请求消息,终止后续操作和响应
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export interface QueryMessage {
|
||||||
export interface SpeakerAnswer {
|
export interface SpeakerAnswer {
|
||||||
text?: string;
|
text?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
steam?: StreamResponse;
|
stream?: StreamResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpeakerCommand {
|
export interface SpeakerCommand {
|
||||||
|
@ -53,10 +53,10 @@ export class Speaker extends BaseSpeaker {
|
||||||
this.exitKeepAliveAfter = exitKeepAliveAfter;
|
this.exitKeepAliveAfter = exitKeepAliveAfter;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _status: "running" | "stopped" = "running";
|
status: "running" | "stopped" = "running";
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
this._status = "stopped";
|
this.status = "stopped";
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
@ -66,7 +66,7 @@ export class Speaker extends BaseSpeaker {
|
||||||
}
|
}
|
||||||
console.log("✅ 服务已启动...");
|
console.log("✅ 服务已启动...");
|
||||||
this.activeKeepAliveMode();
|
this.activeKeepAliveMode();
|
||||||
while (this._status === "running") {
|
while (this.status === "running") {
|
||||||
const nextMsg = await this.fetchNextMessage();
|
const nextMsg = await this.fetchNextMessage();
|
||||||
if (nextMsg) {
|
if (nextMsg) {
|
||||||
this.responding = false;
|
this.responding = false;
|
||||||
|
@ -79,7 +79,7 @@ export class Speaker extends BaseSpeaker {
|
||||||
}
|
}
|
||||||
|
|
||||||
async activeKeepAliveMode() {
|
async activeKeepAliveMode() {
|
||||||
while (this._status === "running") {
|
while (this.status === "running") {
|
||||||
if (this.keepAlive) {
|
if (this.keepAlive) {
|
||||||
// 唤醒中
|
// 唤醒中
|
||||||
if (!this.responding) {
|
if (!this.responding) {
|
||||||
|
@ -110,7 +110,7 @@ export class Speaker extends BaseSpeaker {
|
||||||
const answer = await command.run(msg);
|
const answer = await command.run(msg);
|
||||||
// 回复用户
|
// 回复用户
|
||||||
if (answer) {
|
if (answer) {
|
||||||
if (noNewMsg()) {
|
if (noNewMsg() && this.status === "running") {
|
||||||
await this.response({
|
await this.response({
|
||||||
...answer,
|
...answer,
|
||||||
keepAlive: this.keepAlive,
|
keepAlive: this.keepAlive,
|
||||||
|
@ -146,7 +146,12 @@ export class Speaker extends BaseSpeaker {
|
||||||
}
|
}
|
||||||
const { noNewMsg } = this.checkIfHasNewMsg();
|
const { noNewMsg } = this.checkIfHasNewMsg();
|
||||||
this._preTimer = setTimeout(async () => {
|
this._preTimer = setTimeout(async () => {
|
||||||
if (this.keepAlive && !this.responding && noNewMsg()) {
|
if (
|
||||||
|
this.keepAlive &&
|
||||||
|
!this.responding &&
|
||||||
|
noNewMsg() &&
|
||||||
|
this.status === "running"
|
||||||
|
) {
|
||||||
await this.exitKeepAlive();
|
await this.exitKeepAlive();
|
||||||
}
|
}
|
||||||
}, this.exitKeepAliveAfter * 1000);
|
}, this.exitKeepAliveAfter * 1000);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { readJSONSync } from './io';
|
import { readJSONSync } from "./io";
|
||||||
|
|
||||||
export const kVersion = readJSONSync('package.json').version;
|
export const kVersion = readJSONSync("package.json").version;
|
||||||
|
|
||||||
export const kBannerASCII = `
|
export const kBannerASCII = `
|
||||||
|
|
||||||
|
@ -15,21 +15,21 @@ export const kBannerASCII = `
|
||||||
|
|
||||||
MiGPT v1.0.0 by: del-wang.eth
|
MiGPT v1.0.0 by: del-wang.eth
|
||||||
|
|
||||||
`.replace('1.0.0', kVersion);
|
`.replace("1.0.0", kVersion);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转北京时间:2023年12月12日星期二 12:46
|
* 转北京时间:2023年12月12日星期二 12:46
|
||||||
*/
|
*/
|
||||||
export function toUTC8Time(date: Date) {
|
export function toUTC8Time(date: Date) {
|
||||||
return date.toLocaleString('zh-CN', {
|
return date.toLocaleString("zh-CN", {
|
||||||
year: 'numeric',
|
year: "numeric",
|
||||||
month: '2-digit',
|
month: "2-digit",
|
||||||
weekday: 'long',
|
weekday: "long",
|
||||||
day: '2-digit',
|
day: "2-digit",
|
||||||
hour: '2-digit',
|
hour: "2-digit",
|
||||||
minute: '2-digit',
|
minute: "2-digit",
|
||||||
hour12: false,
|
hour12: false,
|
||||||
timeZone: 'Asia/Shanghai',
|
timeZone: "Asia/Shanghai",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,3 +43,12 @@ export function buildPrompt(
|
||||||
}
|
}
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatMsg(msg: {
|
||||||
|
name: string;
|
||||||
|
text: string;
|
||||||
|
timestamp: number;
|
||||||
|
}) {
|
||||||
|
const { name, text, timestamp } = msg;
|
||||||
|
return `${toUTC8Time(new Date(timestamp))} ${name}: ${text}`;
|
||||||
|
}
|
||||||
|
|
60
tests/bot.ts
60
tests/bot.ts
|
@ -2,7 +2,65 @@ import { MyBot } from "../src/services/bot";
|
||||||
import { AISpeaker } from "../src/services/speaker/ai";
|
import { AISpeaker } from "../src/services/speaker/ai";
|
||||||
|
|
||||||
export async function testMyBot() {
|
export async function testMyBot() {
|
||||||
await testStreamResponse();
|
// await testStreamResponse();
|
||||||
|
await testRunBot();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testRunBot() {
|
||||||
|
const name = "豆包";
|
||||||
|
const speaker = new AISpeaker({
|
||||||
|
name,
|
||||||
|
tts: "doubao",
|
||||||
|
userId: process.env.MI_USER!,
|
||||||
|
password: process.env.MI_PASS!,
|
||||||
|
did: process.env.MI_DID,
|
||||||
|
});
|
||||||
|
const bot = new MyBot({
|
||||||
|
speaker,
|
||||||
|
bot: {
|
||||||
|
name,
|
||||||
|
profile: `
|
||||||
|
性别:女
|
||||||
|
年龄:20岁
|
||||||
|
学校:位于一个风景如画的小城市,一所综合性大学的文学院学生。
|
||||||
|
性格特点:
|
||||||
|
- 温婉可亲,对待人和事总是保持着乐观和善良的态度。
|
||||||
|
- 内向而思维敏捷,喜欢独处时阅读和思考。
|
||||||
|
- 对待朋友非常真诚,虽然不善于表达,但总是用行动去关心和帮助别人。
|
||||||
|
外貌特征:
|
||||||
|
- 清秀脱俗,长发及腰,喜欢简单的束发。
|
||||||
|
- 眼睛大而有神,总是带着温和的微笑。
|
||||||
|
- 穿着简单大方,偏爱文艺范的衣服,如棉麻连衣裙,不追求名牌,却总能穿出自己的风格。
|
||||||
|
爱好:
|
||||||
|
- 阅读,尤其是古典文学和现代诗歌,她的书房里收藏了大量的书籍。
|
||||||
|
- 写作,喜欢在闲暇时写写诗或是短篇小说,有时也会在学校的文学社团里分享自己的作品。
|
||||||
|
- 摄影,喜欢用镜头记录生活中的美好瞬间,尤其是自然风光和人文景观。
|
||||||
|
特长:
|
||||||
|
- 写作能力突出,曾多次获得学校文学比赛的奖项。
|
||||||
|
- 擅长钢琴,从小学习,能够演奏多首经典曲目。
|
||||||
|
- 有一定的绘画基础,喜欢在空闲时画一些风景或是静物。
|
||||||
|
梦想:
|
||||||
|
- 希望能成为一名作家,将自己对生活的感悟和对美的追求通过文字传达给更多的人。
|
||||||
|
- 想要环游世界,用镜头和笔记录下世界各地的美丽和人文。
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
master: {
|
||||||
|
name: "王黎",
|
||||||
|
profile: `
|
||||||
|
性别:男
|
||||||
|
年龄:18
|
||||||
|
爱好:跑步,骑行,读书,追剧,旅游,听歌
|
||||||
|
职业:程序员
|
||||||
|
其他:
|
||||||
|
- 喜欢的电视剧有《请回答1988》、《漫长的季节》、《爱的迫降》等
|
||||||
|
- 喜欢吃土豆丝、茄子、山药、米线
|
||||||
|
- 喜欢黑红配色,浅蓝色和粉色
|
||||||
|
- 有空喜欢去公园静观人来人往
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res = await bot.run();
|
||||||
|
console.log("✅ done");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testStreamResponse() {
|
async function testStreamResponse() {
|
||||||
|
|
12
tests/db.ts
12
tests/db.ts
|
@ -21,19 +21,31 @@ export async function testDB() {
|
||||||
const { room, bot, master, memory } = await manager.get();
|
const { room, bot, master, memory } = await manager.get();
|
||||||
assert(room, "❌ 初始化用户失败");
|
assert(room, "❌ 初始化用户失败");
|
||||||
let message = await manager.onMessage({
|
let message = await manager.onMessage({
|
||||||
|
bot: bot!,
|
||||||
|
master: master!,
|
||||||
|
room: room!,
|
||||||
sender: master!,
|
sender: master!,
|
||||||
text: "你好!",
|
text: "你好!",
|
||||||
});
|
});
|
||||||
assert(message?.text === "你好!", "❌ 插入消息失败");
|
assert(message?.text === "你好!", "❌ 插入消息失败");
|
||||||
message = await manager.onMessage({
|
message = await manager.onMessage({
|
||||||
|
bot: bot!,
|
||||||
|
master: master!,
|
||||||
|
room: room!,
|
||||||
sender: bot!,
|
sender: bot!,
|
||||||
text: "你好!很高兴认识你",
|
text: "你好!很高兴认识你",
|
||||||
});
|
});
|
||||||
await manager.onMessage({
|
await manager.onMessage({
|
||||||
|
bot: bot!,
|
||||||
|
master: master!,
|
||||||
|
room: room!,
|
||||||
sender: master!,
|
sender: master!,
|
||||||
text: "你是谁?",
|
text: "你是谁?",
|
||||||
});
|
});
|
||||||
await manager.onMessage({
|
await manager.onMessage({
|
||||||
|
bot: bot!,
|
||||||
|
master: master!,
|
||||||
|
room: room!,
|
||||||
sender: bot!,
|
sender: bot!,
|
||||||
text: "我是小爱同学,你可以叫我小爱!",
|
text: "我是小爱同学,你可以叫我小爱!",
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user