diff --git a/.vscode/settings.json b/.vscode/settings.json index 82ea997..42ea0a8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,7 @@ "langchain", "langfuse", "langgraph", + "LLMRes", "metas", "MIAI", "michat", diff --git a/bun.lockb b/bun.lockb index fe556d2..d3b8a97 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/controller/groupAgent/agent.ts b/controller/groupAgent/agent.ts index 262eed4..61741b7 100644 --- a/controller/groupAgent/agent.ts +++ b/controller/groupAgent/agent.ts @@ -1,5 +1,6 @@ import { Context } from "../../types" import llm from "../../utils/llm" +import { cleanLLMRes } from "../../utils/llm/base" import getChatHistory from "./chatHistory" const agent = async (ctx: Context) => { @@ -7,20 +8,22 @@ const agent = async (ctx: Context) => { logger, requestId, larkCard, - larkService, + larkService: { message }, appInfo, larkBody: { messageId, msgText, chatId, mentions, rawMsgText, openId }, } = ctx const cardGender = larkCard.child("groupAgent") - const replyCard = larkService.message.updateReplyMessage(messageId) - const loadingMessageId = await replyCard( - cardGender.genPendingCard("分析中,请稍等...") + const loadingMessageId = await message.updateOrReply( + cardGender.genPendingCard("正在分析时间区间,请稍等...") ) // 使用大模型解析用户输入 const { startTime, endTime } = await llm.timeParser(msgText, requestId) logger.info(`Parsed time: startTime: ${startTime}, endTime: ${endTime}`) // 更新卡片 - await replyCard(cardGender.genPendingCard("正在爬楼中,请稍等...")) + await message.updateOrReply( + cardGender.genPendingCard("正在爬楼中,请稍等...") + ) + // 获取聊天记录 const { messages: chatHistory, mentions: historyMentions } = await getChatHistory(ctx, { @@ -35,7 +38,7 @@ const agent = async (ctx: Context) => { // 如果没有聊天记录,返回错误信息 if (chatHistory.length === 0) { logger.info("No chat history found") - await replyCard(cardGender.genErrorCard("未找到聊天记录")) + await message.updateOrReply(cardGender.genErrorCard("未找到聊天记录")) return } logger.debug(`Chat history: ${JSON.stringify(chatHistory)}`) @@ -52,7 +55,9 @@ const agent = async (ctx: Context) => { // 调用大模型 try { - await replyCard(cardGender.genPendingCard("LLM输出中,请稍等...")) + await message.updateOrReply( + cardGender.genPendingCard("LLM输出中,请稍等...") + ) const llmRes = (await llm.invoke( "groupAgent", { @@ -66,13 +71,13 @@ const agent = async (ctx: Context) => { logger.info( `LLM invoked successfully, see detail: http://langfuse.ai.srv/project/cm1j2tkj9001gukrgdvc1swuw/sessions/${requestId}` ) - const cleanedLlmRes = llmRes - .replace(/```(\w+)?\n([\s\S]*?)```/g, "$2") - .trim() - await replyCard(cardGender.genSuccessCard(cleanedLlmRes)) + const cleanedLlmRes = cleanLLMRes(llmRes) + await message.updateOrReply(cardGender.genSuccessCard(cleanedLlmRes)) } catch (error: any) { logger.error(`Failed to invoke llm: ${error.message}`) - await replyCard(cardGender.genErrorCard("LLM调用失败: " + error.message)) + await message.updateOrReply( + cardGender.genErrorCard("LLM调用失败: " + error.message) + ) } } diff --git a/controller/groupAgent/report.ts b/controller/groupAgent/report.ts index 2461fde..76aaabd 100644 --- a/controller/groupAgent/report.ts +++ b/controller/groupAgent/report.ts @@ -18,13 +18,21 @@ const genSummary = async ( timeScope: "daily" | "weekly", trigger: "auto" | "manual" ) => { - const { logger, requestId, larkCard, larkService, appInfo, larkBody } = ctx + const { + logger, + requestId, + larkCard, + larkService, + appInfo, + larkService: { message }, + } = ctx logger.info(`genSummary ${timeScope} by ${trigger}`) const cardGender = larkCard.child("groupAgent") - const replyCard = larkService.message.updateReplyMessage(larkBody.messageId) try { if (trigger === "manual") { - await replyCard(cardGender.genPendingCard("总结中,请稍等...")) + await message.updateOrReply( + cardGender.genPendingCard("总结中,请稍等...") + ) } // 获取群聊信息 const chat = await db.chat.getAndCreate(ctx) @@ -86,7 +94,7 @@ const genSummary = async ( // 发送卡片消息,手动触发时回复原消息 if (trigger === "manual") { - await replyCard(cardContent) + await message.updateOrReply(cardContent) } else { await larkService.message.sendCard2Chat(chatId, cardContent) } @@ -104,7 +112,7 @@ const genSummary = async ( ) // 手动触发时回复原消息 if (trigger === "manual") { - await replyCard(errorCard) + await message.updateOrReply(errorCard) } // 自动触发发送给自己的订阅群 else { @@ -175,15 +183,19 @@ const setSubscription = async ( timeScope: "daily" | "weekly", value: boolean ) => { - const { larkService, logger, larkBody, larkCard } = ctx + const { + larkService: { message }, + logger, + larkBody, + larkCard, + } = ctx const cardGender = larkCard.child("groupAgent") - const sendErrorMsg = (message: string) => - larkService.message.replyCard( - larkBody.messageId, + const sendErrorMsg = (msg: string) => + message.updateOrReply( cardGender.genErrorCard( `${ value ? RespMessage.registerFailed : RespMessage.cancelFailed - }: ${message}` + }: ${msg}` ) ) try { @@ -207,28 +219,25 @@ const setSubscription = async ( } hasUpdate = true } - let message = "" + let msg = "" if (!hasUpdate && value) { - message = + msg = timeScope === "daily" ? RespMessage.hasRegisteredDaily : RespMessage.hasRegisteredWeekly } else if (!value) { - message = + msg = timeScope === "daily" ? RespMessage.cancelDailySuccess : RespMessage.cancelWeeklySuccess } else { - message = + msg = timeScope === "daily" ? RespMessage.registerDailySuccess : RespMessage.registerWeeklySuccess } // 发送成功消息 - await larkService.message.replyCard( - larkBody.messageId, - cardGender.genSuccessCard(message) - ) + await message.updateOrReply(cardGender.genSuccessCard(msg)) } catch (e: any) { logger.error(`Subscribe error: ${e.message}`) await sendErrorMsg(e.message) diff --git a/controller/soupAgent/index.ts b/controller/soupAgent/index.ts index 2c82d21..a1e88f4 100644 --- a/controller/soupAgent/index.ts +++ b/controller/soupAgent/index.ts @@ -140,16 +140,16 @@ const chat2Soup = async (ctx: Context) => { logger, attachService, larkCard, - larkService, + larkService: { message }, } = ctx const cardGender = larkCard.child("soupAgent") - const replyCard = larkService.message.updateReplyMessage(messageId, "text") + message.setReplyMessage(messageId, "text") const activeGame = await db.soupGame.getActiveOneByChatId(chatId) if (!activeGame) { logger.info(`chatId: ${chatId} has no active game`) return } - await replyCard("模型生成中...") + await message.updateOrReply("模型生成中...") const res = await attachService.chat2Soup({ user_query: msgText, @@ -158,7 +158,7 @@ const chat2Soup = async (ctx: Context) => { }) if (!res) { logger.error(`chatId: ${chatId} failed to get soup result`) - await larkService.message.replyCard( + await message.replyCard( messageId, cardGender.genErrorCard(SoupGameMessage.chatFailed) ) @@ -176,30 +176,27 @@ const chat2Soup = async (ctx: Context) => { ]) // 回复用户模型的消息 - await replyCard(res.content) + await message.updateOrReply(res.content) } /** * 海龟汤游戏 * @param ctx */ -const soupAgent = async (ctx: Context) => { +const agent = async (ctx: Context) => { const { - larkBody: { msgText, chatId }, + larkBody: { chatId }, } = ctx if (!chatId) return - if (msgText === "开始游戏") { - startOrStopGame(ctx, true) - return true - } - if (msgText === "结束游戏" || msgText === "停止游戏") { - startOrStopGame(ctx, false) - return true - } const activeGame = await db.soupGame.getActiveOneByChatId(chatId) if (!activeGame) return false chat2Soup(ctx) return true } +const soupAgent = { + startOrStopGame, + agent, +} + export default soupAgent diff --git a/package.json b/package.json index 4c0db4d..f5c21b6 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,11 @@ "devDependencies": { "@commitlint/cli": "^19.6.1", "@commitlint/config-conventional": "^19.6.0", - "@eslint/js": "^9.18.0", + "@eslint/js": "^9.19.0", "@types/node-schedule": "^2.1.7", "@types/uuid": "^10.0.0", "bun-types": "^1.2.0", - "eslint": "^9.18.0", + "eslint": "^9.19.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", "husky": "^9.1.7", @@ -39,13 +39,13 @@ "@egg/hooks": "^1.2.0", "@egg/lark-msg-tool": "^1.21.0", "@egg/logger": "^1.6.0", - "@egg/net-tool": "^1.27.0", + "@egg/net-tool": "^1.28.2", "@egg/path-tool": "^1.4.1", - "@langchain/core": "^0.3.33", + "@langchain/core": "^0.3.36", "@langchain/langgraph": "^0.2.41", "@langchain/openai": "^0.3.17", "joi": "^17.13.3", - "langfuse-langchain": "^3.32.1", + "langfuse-langchain": "^3.32.3", "node-schedule": "^2.1.1", "p-limit": "^6.2.0", "pocketbase": "^0.23.0", diff --git a/routes/bot/eventMsg.ts b/routes/bot/eventMsg.ts index 77ce12d..20e90cd 100644 --- a/routes/bot/eventMsg.ts +++ b/routes/bot/eventMsg.ts @@ -1,9 +1,8 @@ -import tempMap from "../../constant/template" -import gitlabEvent from "../../controller/gitlabEvent" import groupAgent from "../../controller/groupAgent" -import createKVTemp from "../../controller/sheet/createKVTemp" import soupAgent from "../../controller/soupAgent" import { Context } from "../../types" +import llm from "../../utils/llm" +import { cleanLLMRes } from "../../utils/llm/base" import { isNotP2POrAtBot } from "../../utils/message" /** @@ -65,167 +64,105 @@ const manageIdMsg = ({ ) /** - * 回复引导消息 + * 处理意图消息 * @param {Context} ctx - 上下文数据 */ -const manageHelpMsg = ( - { larkBody: { chatId }, larkCard, larkService }: Context, - tempKey: keyof typeof tempMap -) => - larkService.message.sendCard2Chat( - chatId, - larkCard.genTempCard(tempKey, { chat_id: chatId }) as string - ) - -/** - * 处理命令消息 - * @param {Context} ctx - 上下文数据 - */ -const manageCMDMsg = async (ctx: Context) => { +const manageIntent = async (ctx: Context) => { const { body, logger, - larkService, + larkService: { message }, attachService, - larkBody: { msgText, chatId, isInGroup, isP2P }, + larkBody: { msgText, chatId }, + larkCard, + requestId, } = ctx logger.info(`bot req text: ${msgText}`) - - // 处理命令消息 - if (msgText === "/id") { - logger.info(`bot command is /id, chatId: ${chatId}`) - await manageIdMsg(ctx) - return - } - - // CI监控 - if (msgText === "/ci monitor") { - logger.info(`bot command is /ci, chatId: ${chatId}`) - await attachService.ciMonitor(chatId) - return - } - // 简报 - if ( - msgText.startsWith("简报") || - msgText.startsWith("share") || - (msgText.includes("share") && msgText.includes("简报")) - ) { - logger.info(`bot command is share report, chatId: ${chatId}`) - // 这个用时比较久,先发一条提醒用户收到了请求 - // TODO: 迁移到简报服务中 - await larkService.message.sendText2Chat( - chatId, - "正在为您收集简报,请稍等片刻~" - ) - await attachService.reportCollector(body) - return - } - // 创建Sheet DB - if (msgText === "/gen db") { - logger.info(`bot command is /gen db, chatId: ${chatId}`) - await createKVTemp.createFromEvent(ctx) - return - } - - // 关闭CICD成功提醒 - if (msgText.startsWith("/ci off")) { - logger.info(`bot command is /notify ci off, chatId: ${chatId}`) - await gitlabEvent.register.closeCICDNotify( - ctx, - Number(msgText.replace("/ci off ", "")) - ) - return - } - - // 开启CICD成功提醒 - if (msgText.startsWith("/ci")) { - logger.info(`bot command is /ci, chatId: ${chatId}`) - await gitlabEvent.register.openCICDNotify( - ctx, - Number(msgText.replace("/ci ", "")) - ) - return - } - - // 关闭MR自动总结 - if (msgText.startsWith("/mr off")) { - logger.info(`bot command is /mr off, chatId: ${chatId}`) - await gitlabEvent.register.closeMRSummary( - ctx, - Number(msgText.replace("/mr off ", "")) - ) - return - } - - // 开启MR自动总结 - if (msgText.startsWith("/mr")) { - logger.info(`bot command is /mr, chatId: ${chatId}`) - await gitlabEvent.register.openMRSummary( - ctx, - Number(msgText.replace("/mr ", "")) - ) - return - } - - // 私聊场景下或者/help命令 - if (msgText === "/help" || isP2P) { - logger.info(`bot command is /help, chatId: ${chatId}`) - await manageHelpMsg(ctx, "eggGuide") - return - } - - // 仅限群组功能 - if (isInGroup) { - // 注册群组日报 - if (msgText === "开启日报") { - logger.info( - `bot command is register, chatId: ${chatId}, timeScope: daily` - ) - groupAgent.report.setSubscription(ctx, "daily", true) - return - } - // 注册群组周报 - if (msgText === "开启周报") { - logger.info( - `bot command is register, chatId: ${chatId}, timeScope: weekly` - ) - groupAgent.report.setSubscription(ctx, "weekly", true) + await message.updateOrReply( + larkCard.genPendingCard("正在理解您的意图,请稍等...") + ) + try { + const llmRes = (await llm.invoke( + "intentRecognition", + { + userInput: msgText, + time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), + }, + requestId + )) as string + const cleanedLlmRes = cleanLLMRes(llmRes) + logger.info(`intentRecognition llm res: ${cleanedLlmRes}`) + // 返回值不是数字,说明是通识回答 + const intent = Number(cleanedLlmRes) + if (isNaN(intent)) { + await message.updateOrReply(larkCard.genSuccessCard(cleanedLlmRes)) return } - // 注销群组日报 - if (msgText === "关闭日报") { - logger.info( - `bot command is unregister, chatId: ${chatId}, timeScope: daily` - ) - groupAgent.report.setSubscription(ctx, "daily", false) - return + switch (intent) { + // 获取聊天ID + case 1: + await manageIdMsg(ctx) + break + // CI监控 + case 2: + await attachService.ciMonitor(chatId) + break + // 生成简报 + case 3: + await message.updateOrReply( + larkCard.genSuccessCard("正在为您收集简报,请稍等片刻~") + ) + await attachService.reportCollector(body) + break + // 获取帮助 + case 4: + await message.updateOrReply( + larkCard.genTempCard("eggGuide", { chat_id: chatId }) as string + ) + break + // 开启日报订阅 + case 5: + groupAgent.report.setSubscription(ctx, "daily", true) + break + // 开启周报订阅 + case 6: + groupAgent.report.setSubscription(ctx, "weekly", true) + break + // 关闭日报订阅 + case 7: + groupAgent.report.setSubscription(ctx, "daily", false) + break + // 关闭周报订阅 + case 8: + groupAgent.report.setSubscription(ctx, "weekly", false) + break + // 立即发送日报 + case 9: + groupAgent.report.gen4Test(ctx, "daily") + break + // 立即发送周报 + case 10: + groupAgent.report.gen4Test(ctx, "weekly") + break + // 开始海龟汤游戏 + case 11: + soupAgent.startOrStopGame(ctx, true, "manual") + break + // 结束海龟汤游戏 + case 12: + soupAgent.startOrStopGame(ctx, false, "manual") + break + // 通识回答 + case 13: + default: + groupAgent.agent(ctx) + break } - // 注销群组周报 - if (msgText === "关闭周报") { - logger.info( - `bot command is unregister, chatId: ${chatId}, timeScope: weekly` - ) - groupAgent.report.setSubscription(ctx, "weekly", false) - return - } - // 立即发送日简报 - if (msgText === "总结日报") { - logger.info(`bot command is summary, chatId: ${chatId}`) - groupAgent.report.gen4Test(ctx, "daily") - return - } - // 立即发送周简报 - if (msgText === "总结周报") { - logger.info(`bot command is summary, chatId: ${chatId}`) - groupAgent.report.gen4Test(ctx, "weekly") - return - } - // 使用GroupAgent兜底 - groupAgent.agent(ctx) - return + } catch (error) { + logger.error(`manageIntent error: ${error}`) + await message.updateOrReply(larkCard.genErrorCard(`意图分析失败`)) } - return } /** @@ -238,7 +175,7 @@ export const manageEventMsg = async (ctx: Context) => { // 过滤非法消息 if (await filterIllegalMsg(ctx)) return // 海龟汤 - if (await soupAgent(ctx)) return + if (await soupAgent.agent(ctx)) return // 处理命令消息 - await manageCMDMsg(ctx) + await manageIntent(ctx) } diff --git a/utils/genContext.ts b/utils/genContext.ts index f7cebb2..ef8dfee 100644 --- a/utils/genContext.ts +++ b/utils/genContext.ts @@ -48,6 +48,8 @@ const genContext = async (req: Request, rId?: string) => { const logger = loggerIns.child({ requestId }) const genResp = new NetTool({ requestId }) const larkService = genLarkService("egg", requestId) + // 设置回复消息功能 + larkService.message.setReplyMessage(larkBody.messageId) const gitlabService = new GitlabService({ baseUrl: APP_CONFIG.GITLAB_BASE_URL, authKey: APP_CONFIG.GITLAB_AUTH_KEY, diff --git a/utils/llm/base.ts b/utils/llm/base.ts index c792511..8d2179e 100644 --- a/utils/llm/base.ts +++ b/utils/llm/base.ts @@ -38,3 +38,12 @@ export const getLangfuse = async (name: string, requestId: string) => { langfuse: new Langfuse(langfuseParams), } } + +/** + * 清理LLM输出 + * @param llmRes LLM输出 + * @returns + */ +export const cleanLLMRes = (llmRes: string) => { + return llmRes.replace(/```(\w+)?\n([\s\S]*?)```/g, "$2").trim() +}