feat(group-agent): 优化对话形式
All checks were successful
Egg Server MIflow / build-image (push) Successful in 35s

This commit is contained in:
zhaoyingbo 2024-09-26 03:37:38 +00:00
parent b992ee0b21
commit 629d25287c
14 changed files with 292 additions and 253 deletions

View File

@ -9,14 +9,18 @@
"devcontainer",
"devcontainers",
"eamodio",
"EOPK",
"esbenp",
"Euqi",
"Gruntfuggly",
"langchain",
"langfuse",
"metas",
"mina",
"mindnote",
"openai",
"openchat",
"PWTHWP",
"tseslint",
"userid",
"wlpbbgiky",

BIN
bun.lockb

Binary file not shown.

View File

@ -1,5 +1,4 @@
import logger from "@egg/logger"
import { makeCheckPathTool } from "@egg/path-tool"
import { manageBotReq } from "./routes/bot"
import { manageMessageReq } from "./routes/message"
@ -12,39 +11,36 @@ initSchedule()
const server = Bun.serve({
async fetch(req) {
// 路由处理
const { exactCheck, startsWithCheck, fullCheck } = makeCheckPathTool(
req.url
)
// 生成上下文
const ctx = await genContext(req)
const { path, genResp, logger } = ctx
// 非健康检查由打印必要信息
if (
exactCheck("/bot") ||
exactCheck("/message") ||
exactCheck("/sheet") ||
startsWithCheck("/micro_app")
path.exact("/bot") ||
path.exact("/message") ||
path.exact("/sheet") ||
path.startsWith("/micro_app")
) {
ctx.logger.info(`${req.method} ${req.url}`)
ctx.logger.debug(`req body: ${ctx.text}`)
logger.info(`${req.method} ${req.url}`)
logger.debug(`req body: ${ctx.text}`)
}
// 逻辑处理
try {
// 机器人
if (exactCheck("/bot")) return await manageBotReq(ctx)
if (path.exact("/bot")) return await manageBotReq(ctx)
// 消息代理发送
if (exactCheck("/message")) return await manageMessageReq(ctx)
if (path.exact("/message")) return await manageMessageReq(ctx)
// 表格代理操作
if (exactCheck("/sheet")) return await manageSheetReq(ctx)
if (path.exact("/sheet")) return await manageSheetReq(ctx)
// 小程序
if (startsWithCheck("/micro_app")) return await manageMicroAppReq(ctx)
if (path.startsWith("/micro_app")) return await manageMicroAppReq(ctx)
// 健康检查
if (fullCheck("/health")) return ctx.genResp.healthCheck()
if (path.full("/health")) return genResp.healthCheck()
// 其他
return ctx.genResp.healthCheck("hello, there is egg, glade to serve you!")
return genResp.healthCheck("hello, there is egg, glade to serve you!")
} catch (error: any) {
// 错误处理
return ctx.genResp.serverError(error.message || "server error")
return genResp.serverError(error.message || "server error")
}
},
port: 3000,

View File

@ -35,12 +35,13 @@
},
"dependencies": {
"@egg/hooks": "^1.2.0",
"@egg/lark-msg-tool": "^1.2.1",
"@egg/lark-msg-tool": "^1.4.0",
"@egg/logger": "^1.4.3",
"@egg/net-tool": "^1.6.5",
"@egg/path-tool": "^1.3.0",
"@langchain/openai": "^0.3.0",
"@egg/path-tool": "^1.4.1",
"@langchain/openai": "^0.3.2",
"joi": "^17.13.3",
"langfuse-langchain": "^3.26.0",
"node-schedule": "^2.1.1",
"p-limit": "^6.1.0",
"pocketbase": "^0.21.5",

View File

@ -1,33 +1,12 @@
import { getActionType, getIsActionMsg } from "@egg/lark-msg-tool"
import { sleep } from "bun"
import { Context } from "../../types"
import groupAgent from "./groupAgent"
/**
* ChatId卡片
* @param {LarkAction.Data} body
* @returns {Promise<string>} ChatId卡片的JSON字符串
*/
const makeChatIdCard = async ({ body }: Context.Data): Promise<string> => {
await sleep(500)
return JSON.stringify({
type: "template",
data: {
config: {
update_multi: true,
},
template_id: "ctp_AAi3NnHb6zgK",
template_variable: {
chat_id: body.open_chat_id,
},
},
})
}
const ACTION_MAP = {
chat_id: makeChatIdCard,
group_selector: groupAgent.setChatGroupContext,
sendFunctionSelector: groupAgent.sendFunctionSelector,
sendTimeScopeSelector: groupAgent.sendTimeScopeSelector,
manageGroupMsg: groupAgent.manageGroupMsg,
}
/**
@ -36,18 +15,15 @@ const ACTION_MAP = {
* @returns {Promise<void>}
*/
const manageBtnClick = async (ctx: Context.Data): Promise<void> => {
const { body, larkService, logger } = ctx
const { body, logger } = ctx
const { action } = body?.action?.value as {
action: keyof typeof ACTION_MAP
}
logger.info(`got button click action: ${action}`)
logger.info(`Got lark action: ${action}`)
if (!action) return
const func = ACTION_MAP[action]
if (!func) return
const card = await func(ctx)
if (!card) return
// 更新飞书的卡片
await larkService.message.update(body.open_message_id, card)
func(ctx)
}
/**

View File

@ -132,6 +132,7 @@ const manageCMDMsg = (ctx: Context.Data): boolean => {
if (text.includes("share") && text.includes("简报")) {
logger.info(`bot command is share report, chatId: ${chatId}`)
// 这个用时比较久,先发一条提醒用户收到了请求
// TODO: 迁移到简报服务中
larkService.message.send(
"chat_id",
chatId,
@ -148,17 +149,11 @@ const manageCMDMsg = (ctx: Context.Data): boolean => {
return true
}
// 选择群组信息
if (text.trim() === "/sg") {
logger.info(`bot command is /sg, chatId: ${chatId}`)
if (text.trim() === "@群聊助手") {
logger.info(`bot command is @群聊助手, chatId: ${chatId}`)
groupAgent.sendGroupSelector(ctx)
return true
}
// 获取当前群组信息
if (text.trim() === "/cg") {
logger.info(`bot command is /cg, chatId: ${chatId}`)
groupAgent.getCurrentGroup(ctx)
return true
}
return false
}

View File

@ -1,74 +0,0 @@
import { getChatId, LarkAction, LarkEvent } from "@egg/lark-msg-tool"
import db from "../../../db"
import { Context } from "../../../types"
import { genGroupAgentSuccessMsg } from "../../../utils/genMsg"
/**
*
* @param ctx - body和larkService
*/
const sendGroupSelector = async ({ larkService, body }: Context.Data) => {
const chatId = getChatId(body)!
const { data: innerList } = await larkService.chat.getInnerList()
// 组织群组数据
const groups = innerList.map((v) => ({
text: v.name,
value: `${v.chat_id}|${v.name}`,
}))
larkService.message.sendTemp("chat_id", chatId, "ctp_AA00oqPWPTdc", {
groups,
})
}
/**
*
* @param ctx - body和larkService
*/
const getCurrentGroup = async (ctx: Context.Data, needSendMsg = true) => {
const body = ctx.body as LarkEvent.Data
const chatId = getChatId(body)!
const group = await db.groupAgentConfig.get(
body.event.sender.sender_id.user_id
)
if (!needSendMsg) return group
if (!group) {
await sendGroupSelector(ctx)
return
}
const msg = genGroupAgentSuccessMsg(`当前群组:${group.chat_name}`)
ctx.larkService.message.send("chat_id", chatId, "interactive", msg)
}
/**
*
* @param ctx - body, larkService和logger
*/
const setChatGroupContext = async (ctx: Context.Data) => {
const { larkService, logger } = ctx
const body = ctx.body as LarkAction.Data
const targetId = body?.action?.option?.split?.("|")[0]
const targetName = body?.action?.option?.split?.("|")[1]
if (!targetId || !targetName) {
logger.error(
`invalid targetId or targetName: ${JSON.stringify(body?.action)}`
)
}
// 更新群组数据
await db.groupAgentConfig.upsert({
user_id: body.user_id,
chat_id: targetId,
chat_name: targetName,
})
// 更新成功消息
const successMsg = genGroupAgentSuccessMsg(`已将群组切换至 ${targetName}`)
larkService.message.update(body.open_message_id, successMsg)
}
const groupManager = {
sendGroupSelector,
setChatGroupContext,
getCurrentGroup,
}
export default groupManager

View File

@ -1,117 +1,216 @@
import { getChatId, getMsgText, LarkEvent } from "@egg/lark-msg-tool"
import db from "../../../db"
import { Context } from "../../../types"
import {
genGroupAgentErrorMsg,
genGroupAgentSuccessMsg,
} from "../../../utils/genMsg"
getActionOption,
getActionValue,
getChatId,
getMessageId,
LarkAction,
} from "@egg/lark-msg-tool"
import { Context } from "../../../types"
import llm from "../../../utils/llm"
import groupManager from "./groupManager"
enum CardTemplate {
groupSelector = "ctp_AA0LgXOkJpIn",
functionSelector = "ctp_AA0xqIITeUex",
timeScopeSelector = "ctp_AA00oqPWT1Hq",
resultReport = "ctp_AA00oqPWTHWP",
successMsg = "ctp_AA0LgXOkEOPK",
errorMsg = "ctp_AA0LgXOkEuqi",
}
/**
*
* @param ctx -
* @param userInput -
* @param targetChatId - ID
* @param loadingMsgId - loading消息ID
*
* @param ctx - body和larkService
*/
const chat2llm = async (
ctx: Context.Data,
userInput: string,
targetChatId: string,
loadingMsgId?: string
) => {
const { logger, body } = ctx
// 发送消息给Deepseek模型解析时间返回格式为 YYYY-MM-DD HH:mm:ss
const { startTime, endTime } = await llm.parseTime(userInput)
logger.info(`Parsed result: startTime = ${startTime}, endTime = ${endTime}`)
const sendGroupSelector = async ({
larkService,
body,
logger,
requestId,
}: Context.Data) => {
const { data: innerList } = await larkService.chat.getInnerList()
logger.info(`Inner list: ${JSON.stringify(innerList)}`)
// 组织群组数据
const groups = innerList.map((v) => ({
text: v.name,
value: `${v.chat_id}|${v.name}`,
}))
larkService.message.sendTemp(
"chat_id",
getChatId(body),
CardTemplate.groupSelector,
{
groups,
requestId,
}
)
}
/**
*
* @param ctx - body和larkService
*/
const sendFunctionSelector = async (ctx: Context.Data) => {
const { larkService, logger, requestId } = ctx
const body = ctx.body as LarkAction.Data
const option = getActionOption(body)
logger.debug(`Action option: ${JSON.stringify(option)}`)
const [chatId, chatName] = option.split("|")
if (!chatId || !chatName) {
logger.error(
`Invalid targetChatId or targetChatName: ${JSON.stringify(option)}`
)
return
}
// 组织功能数据
const functions = [
{
text: "总结消息",
value: "summary|总结消息",
},
]
larkService.message.updateTemp(
getMessageId(body),
CardTemplate.functionSelector,
{
functions,
chatId,
chatName,
requestId,
}
)
}
/**
*
* @param ctx - body和larkService
*/
const sendTimeScopeSelector = async (ctx: Context.Data) => {
const { larkService, logger, requestId } = ctx
const body = ctx.body as LarkAction.Data
const option = getActionOption(body)
logger.debug(`Action option: ${JSON.stringify(option)}`)
const [functionId, functionName] = option.split("|")
if (!functionId || !functionName) {
logger.error(`Invalid functionId or functionName: ${option}`)
return
}
const value = getActionValue(body)
logger.debug(`Action value: ${JSON.stringify(value)}`)
const { chatId, chatName } = value
if (!chatName || !chatId) {
logger.error(`Invalid chatName or chatId: ${JSON.stringify(value)}`)
return
}
larkService.message.updateTemp(
getMessageId(body),
CardTemplate.timeScopeSelector,
{
chatId,
chatName,
functionId,
functionName,
requestId,
}
)
}
/**
*
* @param ctx - body和larkService
*/
const manageGroupMsg = async (ctx: Context.Data) => {
const { larkService, logger, requestId } = ctx
const body = ctx.body as LarkAction.Data
const value = getActionValue(body)
logger.debug(`Action value: ${JSON.stringify(value)}`)
const { chatId, chatName, functionId, functionName, timeScope } = value
if (!chatId || !chatName || !functionId || !functionName || !timeScope) {
logger.error(`Invalid value: ${JSON.stringify(value)}`)
return
}
// 发送一个loading的消息
await larkService.message.updateTemp(
getMessageId(body),
CardTemplate.successMsg,
{
content: "Group Agent 正在爬楼中,请稍等...",
requestId,
}
)
// 记录发送loading消息后的时间戳
const startTime = Date.now()
// 获取历史消息timeScope为1、3、7分别代表1天、3天、7天
// 获取服务器的时区偏移量(以分钟为单位)
const serverTimezoneOffset = new Date().getTimezoneOffset()
// 上海时区的偏移量UTC+8以分钟为单位
const shanghaiTimezoneOffset = -8 * 60
// 计算时间戳,调整为上海时区
const startTimeTimestamp =
Math.round(new Date(startTime).getTime() / 1000) +
(shanghaiTimezoneOffset - serverTimezoneOffset) * 60
const endTimeTimestamp =
Math.round(new Date(endTime).getTime() / 1000) +
Math.round(new Date().getTime() / 1000) +
(shanghaiTimezoneOffset - serverTimezoneOffset) * 60
const startTimeTimestamp = endTimeTimestamp - Number(timeScope) * 24 * 60 * 60
// 获取群聊中的历史记录
const { data: chatHistory } = await ctx.larkService.message.getHistory(
targetChatId,
const { data: chatHistory } = await larkService.message.getHistory(
chatId,
String(startTimeTimestamp),
String(endTimeTimestamp)
)
// 如果没有历史记录则返回错误消息
if (chatHistory.length === 0) {
logger.error("Chat history is empty")
const content = genGroupAgentErrorMsg("未找到聊天记录")
if (loadingMsgId) {
await ctx.larkService.message.update(loadingMsgId, content)
} else {
await ctx.larkService.message.sendInteractive2Chat(
getChatId(body),
content
)
}
await larkService.message.updateTemp(
getMessageId(body),
CardTemplate.errorMsg,
{
content: "未找到聊天记录",
requestId,
}
)
return
}
logger.debug(`Chat history: ${JSON.stringify(chatHistory)}`)
const llmRes = await llm.queryWithChatHistory(
userInput,
JSON.stringify(chatHistory)
)
logger.info(`LLM result: ${llmRes.content}`)
const successMsg = genGroupAgentSuccessMsg(llmRes.content as string)
// 发送LLM结果
if (loadingMsgId) {
await ctx.larkService.message.update(loadingMsgId, successMsg)
} else {
await ctx.larkService.message.sendInteractive2Chat(
getChatId(body),
successMsg
try {
const llmRes = await llm.invokeLLM4ChatHistory(
functionId,
JSON.stringify(chatHistory)
)
// 记录大模型返回结果后的时间戳
const endTime = Date.now()
// 计算时间差并存储在processingTime变量中以秒为单位
const processingTime = ((endTime - startTime) / 1000).toFixed(2)
logger.info(`LLM takes time: ${processingTime}s, result: ${llmRes}`)
await larkService.message.updateTemp(
getMessageId(body),
CardTemplate.resultReport,
{
chatName,
functionName,
llmRes,
timeScope,
requestId,
processingTime,
}
)
} catch (error: any) {
logger.error(`LLM error: ${error.message}`)
await larkService.message.updateTemp(
getMessageId(body),
CardTemplate.errorMsg,
{
content: "LLM调用失败: " + error.message,
requestId,
}
)
}
}
/**
*
* @param ctx -
*/
const manageGroupMsg = async (ctx: Context.Data) => {
const { logger } = ctx
logger.info("Start to manage group message")
const body = ctx.body as LarkEvent.Data
// 获取用户输入
const userInput = getMsgText(body)
// 先获取当前对话的目标群组ID
const group = await groupManager.getCurrentGroup(ctx, false)
// 没有目标群组ID则发送群组选择器并保存当前问题在选择后立即处理
if (!group || !group.chat_id) {
logger.info("No group found, send group selector")
groupManager.sendGroupSelector(ctx)
db.groupAgentConfig.upsert({
user_id: body.event.sender.sender_id.user_id,
pre_query: userInput,
})
return
}
// 发送一个loading的消息
const loadingRes = await ctx.larkService.message.sendInteractive2Chat(
getChatId(body),
genGroupAgentSuccessMsg("小煎蛋正在爬楼中,请稍等...")
)
if (loadingRes.code !== 0) {
logger.error("Failed to send loading message")
}
const { message_id } = loadingRes.data
await chat2llm(ctx, userInput, group.chat_id, message_id)
}
const groupAgent = {
...groupManager,
sendGroupSelector,
sendFunctionSelector,
sendTimeScopeSelector,
manageGroupMsg,
}

View File

@ -1,5 +1,3 @@
import { makeCheckPathTool } from "@egg/path-tool"
import { Context } from "../../types"
/**
@ -67,13 +65,13 @@ const manageBatchUser = async (ctx: Context.Data) => {
* @returns
*/
export const manageMicroAppReq = async (ctx: Context.Data) => {
const { exactCheck } = makeCheckPathTool(ctx.req.url, "/micro_app")
const path = ctx.path.child("/micro_app")
// 处理登录请求
if (exactCheck("/login")) {
if (path.exact("/login")) {
return manageLogin(ctx)
}
// 处理批量获取用户信息请求
if (exactCheck("/batch_user")) {
if (path.exact("/batch_user")) {
return manageBatchUser(ctx)
}
return ctx.genResp.ok()

View File

@ -76,6 +76,16 @@ class LarkMessageService extends LarkBaseService {
return this.patch<LarkServer.BaseRes>(path, { content })
}
/**
*
* @param messageId id
* @param templateId ID
* @param variable
*/
async updateTemp(messageId: string, templateId: string, variable: any) {
return this.update(messageId, genTempMsg(templateId, variable))
}
/**
*
* @param chatId ID

View File

@ -1,4 +1,5 @@
import { NetTool } from "@egg/net-tool"
import { PathCheckTool } from "@egg/path-tool"
import { Logger } from "winston"
import { AttachService, LarkService } from "../services"
@ -13,5 +14,7 @@ export namespace Context {
text: string
larkService: LarkService
attachService: AttachService
path: PathCheckTool
searchParams: URLSearchParams
}
}

View File

@ -1,10 +1,25 @@
import { getActionValue } from "@egg/lark-msg-tool"
import loggerIns from "@egg/logger"
import { NetTool } from "@egg/net-tool"
import { PathCheckTool } from "@egg/path-tool"
import { v4 as uuid } from "uuid"
import { AttachService, LarkService } from "../services"
import { Context } from "../types"
/**
* requestId
*
* @param {any} body -
* @returns {string} requestId
*/
const getPreRequestId = (body: any) => {
// 在Action请求中会带上之前的requestId
const value = getActionValue(body)
if (!value || !value.requestId) return ""
return value.requestId
}
/**
*
*
@ -12,12 +27,6 @@ import { Context } from "../types"
* @returns {Promise<Context.Data>}
*/
const genContext = async (req: Request) => {
const requestId = uuid()
const logger = loggerIns.child({ requestId })
const genResp = new NetTool({ requestId })
const larkService = new LarkService("egg", requestId)
const attachService = new AttachService({ requestId })
let body: any = null
let text: string = ""
try {
@ -26,8 +35,16 @@ const genContext = async (req: Request) => {
} catch {
/* empty */
}
const requestId = getPreRequestId(body) || uuid()
const logger = loggerIns.child({ requestId })
const genResp = new NetTool({ requestId })
const larkService = new LarkService("egg", requestId)
const attachService = new AttachService({ requestId })
const path = new PathCheckTool(req.url)
return {
req,
path,
requestId,
logger,
genResp,
@ -35,6 +52,7 @@ const genContext = async (req: Request) => {
text,
larkService,
attachService,
searchParams: new URL(req.url).searchParams,
} as Context.Data
}

View File

@ -85,7 +85,7 @@ export const genSheetDbSuccessMsg = (content: string) =>
* @returns {string} JSON
*/
export const genGroupAgentErrorMsg = (content: string) =>
genErrorMsg("🧑‍💻 小煎蛋 Group Agent 错误提醒", content)
genErrorMsg("🧑‍💻 Group Agent 错误提醒", content)
/**
* Group Agent JSON
@ -93,4 +93,4 @@ export const genGroupAgentErrorMsg = (content: string) =>
* @returns {string} JSON
*/
export const genGroupAgentSuccessMsg = (content: string) =>
genSuccessMsg("🧑‍💻 感谢使用小煎蛋 Group Agent", content)
genSuccessMsg("🧑‍💻 感谢使用 Group Agent", content)

View File

@ -1,8 +1,19 @@
import { ChatOpenAI } from "@langchain/openai"
import { CallbackHandler, Langfuse } from "langfuse-langchain"
import { z } from "zod"
import db from "../db"
const langfuseParams = {
publicKey: "pk-lf-7328c2f1-595a-4023-994b-78762d3dde9e",
secretKey: "sk-lf-bdf20cd5-ff2c-45c3-9021-8292dbb1bef3",
baseUrl: "https://langfuse.yingbo.im:333",
}
const langfuseHandler = new CallbackHandler(langfuseParams)
const langfuse = new Langfuse(langfuseParams)
/**
* Deepseek模型
* @param temperature
@ -40,36 +51,38 @@ const parseTime = async (userInput: string) => {
)
}
export enum LlmPromptType {
summary = "summary",
}
/**
*
* @param userInput
* LLM模型
* @param promptType
* @param chatHistory
* @returns
*/
const queryWithChatHistory = async (userInput: string, chatHistory: string) => {
const invokeLLM4ChatHistory = async (
promptType: LlmPromptType,
chatHistory: string
) => {
const prompt = await langfuse.getPrompt(promptType)
const compiled_prompt = prompt.compile({
chatHistory,
time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
})
const model = await getDeepseekModel(0.5)
return await model.invoke(
`
${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}
\`\`\`
${userInput.replaceAll("`", " ")}
\`\`\`
\`\`\`
${chatHistory.replaceAll("`", " ")}
\`\`\`
`
)
const { content } = (await model.invoke(compiled_prompt, {
callbacks: [langfuseHandler],
})) as {
content: string
}
return content
}
const llm = {
getDeepseekModel,
parseTime,
queryWithChatHistory,
invokeLLM4ChatHistory,
}
export default llm