feat: 添加LLM输出清理函数;优化消息回复逻辑,统一使用更新回复消息的方法;更新依赖版本

This commit is contained in:
zhaoyingbo 2025-01-25 10:43:13 +00:00
parent 5085da7e12
commit c5fd3da73c
9 changed files with 163 additions and 203 deletions

View File

@ -20,6 +20,7 @@
"langchain", "langchain",
"langfuse", "langfuse",
"langgraph", "langgraph",
"LLMRes",
"metas", "metas",
"MIAI", "MIAI",
"michat", "michat",

BIN
bun.lockb

Binary file not shown.

View File

@ -1,5 +1,6 @@
import { Context } from "../../types" import { Context } from "../../types"
import llm from "../../utils/llm" import llm from "../../utils/llm"
import { cleanLLMRes } from "../../utils/llm/base"
import getChatHistory from "./chatHistory" import getChatHistory from "./chatHistory"
const agent = async (ctx: Context) => { const agent = async (ctx: Context) => {
@ -7,20 +8,22 @@ const agent = async (ctx: Context) => {
logger, logger,
requestId, requestId,
larkCard, larkCard,
larkService, larkService: { message },
appInfo, appInfo,
larkBody: { messageId, msgText, chatId, mentions, rawMsgText, openId }, larkBody: { messageId, msgText, chatId, mentions, rawMsgText, openId },
} = ctx } = ctx
const cardGender = larkCard.child("groupAgent") const cardGender = larkCard.child("groupAgent")
const replyCard = larkService.message.updateReplyMessage(messageId) const loadingMessageId = await message.updateOrReply(
const loadingMessageId = await replyCard( cardGender.genPendingCard("正在分析时间区间,请稍等...")
cardGender.genPendingCard("分析中,请稍等...")
) )
// 使用大模型解析用户输入 // 使用大模型解析用户输入
const { startTime, endTime } = await llm.timeParser(msgText, requestId) const { startTime, endTime } = await llm.timeParser(msgText, requestId)
logger.info(`Parsed time: startTime: ${startTime}, endTime: ${endTime}`) logger.info(`Parsed time: startTime: ${startTime}, endTime: ${endTime}`)
// 更新卡片 // 更新卡片
await replyCard(cardGender.genPendingCard("正在爬楼中,请稍等...")) await message.updateOrReply(
cardGender.genPendingCard("正在爬楼中,请稍等...")
)
// 获取聊天记录 // 获取聊天记录
const { messages: chatHistory, mentions: historyMentions } = const { messages: chatHistory, mentions: historyMentions } =
await getChatHistory(ctx, { await getChatHistory(ctx, {
@ -35,7 +38,7 @@ const agent = async (ctx: Context) => {
// 如果没有聊天记录,返回错误信息 // 如果没有聊天记录,返回错误信息
if (chatHistory.length === 0) { if (chatHistory.length === 0) {
logger.info("No chat history found") logger.info("No chat history found")
await replyCard(cardGender.genErrorCard("未找到聊天记录")) await message.updateOrReply(cardGender.genErrorCard("未找到聊天记录"))
return return
} }
logger.debug(`Chat history: ${JSON.stringify(chatHistory)}`) logger.debug(`Chat history: ${JSON.stringify(chatHistory)}`)
@ -52,7 +55,9 @@ const agent = async (ctx: Context) => {
// 调用大模型 // 调用大模型
try { try {
await replyCard(cardGender.genPendingCard("LLM输出中请稍等...")) await message.updateOrReply(
cardGender.genPendingCard("LLM输出中请稍等...")
)
const llmRes = (await llm.invoke( const llmRes = (await llm.invoke(
"groupAgent", "groupAgent",
{ {
@ -66,13 +71,13 @@ const agent = async (ctx: Context) => {
logger.info( logger.info(
`LLM invoked successfully, see detail: http://langfuse.ai.srv/project/cm1j2tkj9001gukrgdvc1swuw/sessions/${requestId}` `LLM invoked successfully, see detail: http://langfuse.ai.srv/project/cm1j2tkj9001gukrgdvc1swuw/sessions/${requestId}`
) )
const cleanedLlmRes = llmRes const cleanedLlmRes = cleanLLMRes(llmRes)
.replace(/```(\w+)?\n([\s\S]*?)```/g, "$2") await message.updateOrReply(cardGender.genSuccessCard(cleanedLlmRes))
.trim()
await replyCard(cardGender.genSuccessCard(cleanedLlmRes))
} catch (error: any) { } catch (error: any) {
logger.error(`Failed to invoke llm: ${error.message}`) logger.error(`Failed to invoke llm: ${error.message}`)
await replyCard(cardGender.genErrorCard("LLM调用失败: " + error.message)) await message.updateOrReply(
cardGender.genErrorCard("LLM调用失败: " + error.message)
)
} }
} }

View File

@ -18,13 +18,21 @@ const genSummary = async (
timeScope: "daily" | "weekly", timeScope: "daily" | "weekly",
trigger: "auto" | "manual" 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}`) logger.info(`genSummary ${timeScope} by ${trigger}`)
const cardGender = larkCard.child("groupAgent") const cardGender = larkCard.child("groupAgent")
const replyCard = larkService.message.updateReplyMessage(larkBody.messageId)
try { try {
if (trigger === "manual") { if (trigger === "manual") {
await replyCard(cardGender.genPendingCard("总结中,请稍等...")) await message.updateOrReply(
cardGender.genPendingCard("总结中,请稍等...")
)
} }
// 获取群聊信息 // 获取群聊信息
const chat = await db.chat.getAndCreate(ctx) const chat = await db.chat.getAndCreate(ctx)
@ -86,7 +94,7 @@ const genSummary = async (
// 发送卡片消息,手动触发时回复原消息 // 发送卡片消息,手动触发时回复原消息
if (trigger === "manual") { if (trigger === "manual") {
await replyCard(cardContent) await message.updateOrReply(cardContent)
} else { } else {
await larkService.message.sendCard2Chat(chatId, cardContent) await larkService.message.sendCard2Chat(chatId, cardContent)
} }
@ -104,7 +112,7 @@ const genSummary = async (
) )
// 手动触发时回复原消息 // 手动触发时回复原消息
if (trigger === "manual") { if (trigger === "manual") {
await replyCard(errorCard) await message.updateOrReply(errorCard)
} }
// 自动触发发送给自己的订阅群 // 自动触发发送给自己的订阅群
else { else {
@ -175,15 +183,19 @@ const setSubscription = async (
timeScope: "daily" | "weekly", timeScope: "daily" | "weekly",
value: boolean value: boolean
) => { ) => {
const { larkService, logger, larkBody, larkCard } = ctx const {
larkService: { message },
logger,
larkBody,
larkCard,
} = ctx
const cardGender = larkCard.child("groupAgent") const cardGender = larkCard.child("groupAgent")
const sendErrorMsg = (message: string) => const sendErrorMsg = (msg: string) =>
larkService.message.replyCard( message.updateOrReply(
larkBody.messageId,
cardGender.genErrorCard( cardGender.genErrorCard(
`${ `${
value ? RespMessage.registerFailed : RespMessage.cancelFailed value ? RespMessage.registerFailed : RespMessage.cancelFailed
}: ${message}` }: ${msg}`
) )
) )
try { try {
@ -207,28 +219,25 @@ const setSubscription = async (
} }
hasUpdate = true hasUpdate = true
} }
let message = "" let msg = ""
if (!hasUpdate && value) { if (!hasUpdate && value) {
message = msg =
timeScope === "daily" timeScope === "daily"
? RespMessage.hasRegisteredDaily ? RespMessage.hasRegisteredDaily
: RespMessage.hasRegisteredWeekly : RespMessage.hasRegisteredWeekly
} else if (!value) { } else if (!value) {
message = msg =
timeScope === "daily" timeScope === "daily"
? RespMessage.cancelDailySuccess ? RespMessage.cancelDailySuccess
: RespMessage.cancelWeeklySuccess : RespMessage.cancelWeeklySuccess
} else { } else {
message = msg =
timeScope === "daily" timeScope === "daily"
? RespMessage.registerDailySuccess ? RespMessage.registerDailySuccess
: RespMessage.registerWeeklySuccess : RespMessage.registerWeeklySuccess
} }
// 发送成功消息 // 发送成功消息
await larkService.message.replyCard( await message.updateOrReply(cardGender.genSuccessCard(msg))
larkBody.messageId,
cardGender.genSuccessCard(message)
)
} catch (e: any) { } catch (e: any) {
logger.error(`Subscribe error: ${e.message}`) logger.error(`Subscribe error: ${e.message}`)
await sendErrorMsg(e.message) await sendErrorMsg(e.message)

View File

@ -140,16 +140,16 @@ const chat2Soup = async (ctx: Context) => {
logger, logger,
attachService, attachService,
larkCard, larkCard,
larkService, larkService: { message },
} = ctx } = ctx
const cardGender = larkCard.child("soupAgent") const cardGender = larkCard.child("soupAgent")
const replyCard = larkService.message.updateReplyMessage(messageId, "text") message.setReplyMessage(messageId, "text")
const activeGame = await db.soupGame.getActiveOneByChatId(chatId) const activeGame = await db.soupGame.getActiveOneByChatId(chatId)
if (!activeGame) { if (!activeGame) {
logger.info(`chatId: ${chatId} has no active game`) logger.info(`chatId: ${chatId} has no active game`)
return return
} }
await replyCard("模型生成中...") await message.updateOrReply("模型生成中...")
const res = await attachService.chat2Soup({ const res = await attachService.chat2Soup({
user_query: msgText, user_query: msgText,
@ -158,7 +158,7 @@ const chat2Soup = async (ctx: Context) => {
}) })
if (!res) { if (!res) {
logger.error(`chatId: ${chatId} failed to get soup result`) logger.error(`chatId: ${chatId} failed to get soup result`)
await larkService.message.replyCard( await message.replyCard(
messageId, messageId,
cardGender.genErrorCard(SoupGameMessage.chatFailed) cardGender.genErrorCard(SoupGameMessage.chatFailed)
) )
@ -176,30 +176,27 @@ const chat2Soup = async (ctx: Context) => {
]) ])
// 回复用户模型的消息 // 回复用户模型的消息
await replyCard(res.content) await message.updateOrReply(res.content)
} }
/** /**
* *
* @param ctx * @param ctx
*/ */
const soupAgent = async (ctx: Context) => { const agent = async (ctx: Context) => {
const { const {
larkBody: { msgText, chatId }, larkBody: { chatId },
} = ctx } = ctx
if (!chatId) return 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) const activeGame = await db.soupGame.getActiveOneByChatId(chatId)
if (!activeGame) return false if (!activeGame) return false
chat2Soup(ctx) chat2Soup(ctx)
return true return true
} }
const soupAgent = {
startOrStopGame,
agent,
}
export default soupAgent export default soupAgent

View File

@ -19,11 +19,11 @@
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.6.1", "@commitlint/cli": "^19.6.1",
"@commitlint/config-conventional": "^19.6.0", "@commitlint/config-conventional": "^19.6.0",
"@eslint/js": "^9.18.0", "@eslint/js": "^9.19.0",
"@types/node-schedule": "^2.1.7", "@types/node-schedule": "^2.1.7",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"bun-types": "^1.2.0", "bun-types": "^1.2.0",
"eslint": "^9.18.0", "eslint": "^9.19.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"husky": "^9.1.7", "husky": "^9.1.7",
@ -39,13 +39,13 @@
"@egg/hooks": "^1.2.0", "@egg/hooks": "^1.2.0",
"@egg/lark-msg-tool": "^1.21.0", "@egg/lark-msg-tool": "^1.21.0",
"@egg/logger": "^1.6.0", "@egg/logger": "^1.6.0",
"@egg/net-tool": "^1.27.0", "@egg/net-tool": "^1.28.2",
"@egg/path-tool": "^1.4.1", "@egg/path-tool": "^1.4.1",
"@langchain/core": "^0.3.33", "@langchain/core": "^0.3.36",
"@langchain/langgraph": "^0.2.41", "@langchain/langgraph": "^0.2.41",
"@langchain/openai": "^0.3.17", "@langchain/openai": "^0.3.17",
"joi": "^17.13.3", "joi": "^17.13.3",
"langfuse-langchain": "^3.32.1", "langfuse-langchain": "^3.32.3",
"node-schedule": "^2.1.1", "node-schedule": "^2.1.1",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",
"pocketbase": "^0.23.0", "pocketbase": "^0.23.0",

View File

@ -1,9 +1,8 @@
import tempMap from "../../constant/template"
import gitlabEvent from "../../controller/gitlabEvent"
import groupAgent from "../../controller/groupAgent" import groupAgent from "../../controller/groupAgent"
import createKVTemp from "../../controller/sheet/createKVTemp"
import soupAgent from "../../controller/soupAgent" import soupAgent from "../../controller/soupAgent"
import { Context } from "../../types" import { Context } from "../../types"
import llm from "../../utils/llm"
import { cleanLLMRes } from "../../utils/llm/base"
import { isNotP2POrAtBot } from "../../utils/message" import { isNotP2POrAtBot } from "../../utils/message"
/** /**
@ -65,167 +64,105 @@ const manageIdMsg = ({
) )
/** /**
* *
* @param {Context} ctx - * @param {Context} ctx -
*/ */
const manageHelpMsg = ( const manageIntent = async (ctx: Context) => {
{ 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 { const {
body, body,
logger, logger,
larkService, larkService: { message },
attachService, attachService,
larkBody: { msgText, chatId, isInGroup, isP2P }, larkBody: { msgText, chatId },
larkCard,
requestId,
} = ctx } = ctx
logger.info(`bot req text: ${msgText}`) logger.info(`bot req text: ${msgText}`)
await message.updateOrReply(
// 处理命令消息 larkCard.genPendingCard("正在理解您的意图,请稍等...")
if (msgText === "/id") { )
logger.info(`bot command is /id, chatId: ${chatId}`) try {
await manageIdMsg(ctx) const llmRes = (await llm.invoke(
return "intentRecognition",
} {
userInput: msgText,
// CI监控 time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
if (msgText === "/ci monitor") { },
logger.info(`bot command is /ci, chatId: ${chatId}`) requestId
await attachService.ciMonitor(chatId) )) as string
return const cleanedLlmRes = cleanLLMRes(llmRes)
} logger.info(`intentRecognition llm res: ${cleanedLlmRes}`)
// 简报 // 返回值不是数字,说明是通识回答
if ( const intent = Number(cleanedLlmRes)
msgText.startsWith("简报") || if (isNaN(intent)) {
msgText.startsWith("share") || await message.updateOrReply(larkCard.genSuccessCard(cleanedLlmRes))
(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)
return return
} }
// 注销群组日报 switch (intent) {
if (msgText === "关闭日报") { // 获取聊天ID
logger.info( case 1:
`bot command is unregister, chatId: ${chatId}, timeScope: daily` await manageIdMsg(ctx)
) break
groupAgent.report.setSubscription(ctx, "daily", false) // CI监控
return 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
} }
// 注销群组周报 } catch (error) {
if (msgText === "关闭周报") { logger.error(`manageIntent error: ${error}`)
logger.info( await message.updateOrReply(larkCard.genErrorCard(`意图分析失败`))
`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
} }
return
} }
/** /**
@ -238,7 +175,7 @@ export const manageEventMsg = async (ctx: Context) => {
// 过滤非法消息 // 过滤非法消息
if (await filterIllegalMsg(ctx)) return if (await filterIllegalMsg(ctx)) return
// 海龟汤 // 海龟汤
if (await soupAgent(ctx)) return if (await soupAgent.agent(ctx)) return
// 处理命令消息 // 处理命令消息
await manageCMDMsg(ctx) await manageIntent(ctx)
} }

View File

@ -48,6 +48,8 @@ const genContext = async (req: Request, rId?: string) => {
const logger = loggerIns.child({ requestId }) const logger = loggerIns.child({ requestId })
const genResp = new NetTool({ requestId }) const genResp = new NetTool({ requestId })
const larkService = genLarkService("egg", requestId) const larkService = genLarkService("egg", requestId)
// 设置回复消息功能
larkService.message.setReplyMessage(larkBody.messageId)
const gitlabService = new GitlabService({ const gitlabService = new GitlabService({
baseUrl: APP_CONFIG.GITLAB_BASE_URL, baseUrl: APP_CONFIG.GITLAB_BASE_URL,
authKey: APP_CONFIG.GITLAB_AUTH_KEY, authKey: APP_CONFIG.GITLAB_AUTH_KEY,

View File

@ -38,3 +38,12 @@ export const getLangfuse = async (name: string, requestId: string) => {
langfuse: new Langfuse(langfuseParams), langfuse: new Langfuse(langfuseParams),
} }
} }
/**
* LLM输出
* @param llmRes LLM输出
* @returns
*/
export const cleanLLMRes = (llmRes: string) => {
return llmRes.replace(/```(\w+)?\n([\s\S]*?)```/g, "$2").trim()
}