diff --git a/.vscode/settings.json b/.vscode/settings.json index 8252710..ce81483 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "cSpell.words": [ + "BOCHA", + "bochaai", "bunx", "CEINTL", "Chakroun", diff --git a/bun.lockb b/bun.lockb index 244d5e1..e0497f3 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/constant/function.ts b/constant/function.ts index 5d852a7..8649484 100644 --- a/constant/function.ts +++ b/constant/function.ts @@ -29,6 +29,11 @@ const functionMap = { xAuthor: "wangyifei15 🕹️ Yingbo", xIcon: "🕹️", }, + searchAgent: { + xName: "小煎蛋 Search Agent", + xAuthor: "Yingbo", + xIcon: "🔍", + }, } export default functionMap diff --git a/controller/intentAgent/index.ts b/controller/intentAgent/index.ts index 2c7c5d0..65ca3fc 100644 --- a/controller/intentAgent/index.ts +++ b/controller/intentAgent/index.ts @@ -22,6 +22,9 @@ const BriefingSchema = BaseIntentSchema.extend({ userDescription: z.string().min(1), }) +/** + * 简报存储链接模式 + */ const BriefingLinkSchema = z.object({ intent: z.literal(14), link: z.string().url(), @@ -35,6 +38,14 @@ const CommonResponseSchema = BaseIntentSchema.extend({ message: z.string().min(1), }) +/** + * 联网检索模式 + */ +const NetSearchSchema = BaseIntentSchema.extend({ + intent: z.literal(15), + query: z.string().min(1), +}) + /** * 意图模式 */ @@ -43,12 +54,13 @@ const IntentSchema = z.union([ CommonResponseSchema, BaseIntentSchema, BriefingLinkSchema, + NetSearchSchema, ]) -type BaseIntent = z.infer type Briefing = z.infer type BriefingLink = z.infer type CommonResponse = z.infer +type NetSearch = z.infer export type Intent = z.infer /** @@ -58,8 +70,8 @@ const jsonSchema = zodToJsonSchema(IntentSchema) /** * 代理函数 - * @param {Context} ctx - 上下文对象 - * @returns {Promise} - 返回意图对象 + * @param ctx - 上下文对象 + * @returns 返回意图对象 */ const agent = async (ctx: Context): Promise => { const { @@ -71,7 +83,7 @@ const agent = async (ctx: Context): Promise => { let attempts = 0 while (attempts < 3) { const res = await llm.invoke( - "intentRecognitionNext", + "intentRecognition", { userInput: msgText, time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), @@ -105,31 +117,51 @@ const agent = async (ctx: Context): Promise => { } } -const isBaseIntent = (intent: Intent): intent is BaseIntent => { - return intent.intent !== 5 && intent.intent !== 2 -} - +/** + * 判断是否为简报意图 + * @param intent - 意图对象 + * @returns 如果是简报意图则返回true,否则返回false + */ const isBriefing = (intent: Intent): intent is Briefing => { return intent.intent === 5 } +/** + * 判断是否为简报链接意图 + * @param intent - 意图对象 + * @returns 如果是简报链接意图则返回true,否则返回false + */ const isBriefingLink = (intent: Intent): intent is BriefingLink => { return intent.intent === 14 } +/** + * 判断是否为通用响应意图 + * @param intent - 意图对象 + * @returns 如果是通用响应意图则返回true,否则返回false + */ const isCommonResponse = (intent: Intent): intent is CommonResponse => { return intent.intent === 2 } +/** + * 判断是否为联网检索意图 + * @param intent - 意图对象 + * @returns 如果是联网检索意图则返回true,否则返回false + */ +const isNetSearch = (intent: Intent): intent is NetSearch => { + return intent.intent === 15 +} + /** * 意图代理对象 */ const intentAgent = { agent, - isBaseIntent, isBriefing, isCommonResponse, isBriefingLink, + isNetSearch, } export default intentAgent diff --git a/controller/reportAgent/index.ts b/controller/reportAgent/index.ts index dd44497..afd6127 100644 --- a/controller/reportAgent/index.ts +++ b/controller/reportAgent/index.ts @@ -154,7 +154,7 @@ const agent = async (ctx: Context, link: string, userDescription: string) => { validateLink(link) // 发送一个loading卡片 await message.updateOrReply( - cardGender.genSuccessCard("正在为您收集简报,请稍等片刻~") + cardGender.genPendingCard("正在为您收集简报,请稍等片刻~") ) // // 抓取网页 // const crawRes = await crawlWebPage(ctx, link) diff --git a/controller/searchAgent/index.ts b/controller/searchAgent/index.ts new file mode 100644 index 0000000..ca17816 --- /dev/null +++ b/controller/searchAgent/index.ts @@ -0,0 +1,70 @@ +import { Context } from "../../types" +import llm from "../../utils/llm" + +/** + * 生成网页简报 + * @param {Context} ctx - 上下文对象 + * @param {string} userDescription - 用户描述 + * @param {any} content - 网页内容 + * @returns {Promise} - 返回简报内容 + * @throws {Error} - 当生成简报失败时抛出错误 + */ +export const generateAnswer = async ( + ctx: Context, + webSearchResults: + | { + siteName: string + summary: string + }[] + | null +): Promise => { + const { + requestId, + larkBody: { msgText }, + } = ctx + const llmRes = (await llm.invoke( + "searchAgent", + { + webSearchResults, + time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), + userInput: msgText, + }, + requestId, + 1 + )) as string + if (!llmRes) throw new Error("模型侧错误") + return llmRes +} + +const agent = async (ctx: Context, query: string) => { + const { + larkService: { message }, + attachService, + larkCard, + logger, + } = ctx + const cardGender = larkCard.child("searchAgent") + try { + // 发送一个loading卡片 + await message.updateOrReply( + cardGender.genPendingCard("正在检索网络信息,请稍等片刻~") + ) + const searchRes = await attachService.webSearch(query) + await message.updateOrReply( + cardGender.genPendingCard("LLM输出中,请稍等...") + ) + const answer = await generateAnswer(ctx, searchRes) + await message.updateOrReply(cardGender.genSuccessCard(answer)) + } catch (error: any) { + logger.error(`searchAgent error: ${error}`) + await message.updateOrReply( + cardGender.genErrorCard(`检索失败: ${error.message}`) + ) + } +} + +const searchAgent = { + agent, +} + +export default searchAgent diff --git a/package.json b/package.json index ac06a14..0b009dd 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,12 @@ ] }, "devDependencies": { - "@commitlint/cli": "^19.6.1", - "@commitlint/config-conventional": "^19.6.0", + "@commitlint/cli": "^19.7.1", + "@commitlint/config-conventional": "^19.7.1", "@eslint/js": "^9.19.0", "@types/node-schedule": "^2.1.7", "@types/uuid": "^10.0.0", - "bun-types": "^1.2.1", + "bun-types": "^1.2.2", "eslint": "^9.19.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", @@ -30,7 +30,7 @@ "lint-staged": "^15.4.3", "oxlint": "^0.13.2", "prettier": "^3.4.2", - "typescript-eslint": "^8.22.0" + "typescript-eslint": "^8.23.0" }, "peerDependencies": { "typescript": "^5.5.4" @@ -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.31.1", + "@egg/net-tool": "^1.31.2", "@egg/path-tool": "^1.4.1", - "@langchain/core": "^0.3.36", - "@langchain/langgraph": "^0.2.41", + "@langchain/core": "^0.3.38", + "@langchain/langgraph": "^0.2.44", "@langchain/openai": "^0.3.17", "joi": "^17.13.3", - "langfuse-langchain": "^3.32.3", + "langfuse-langchain": "^3.35.1", "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 1b18901..ea681b0 100644 --- a/routes/bot/eventMsg.ts +++ b/routes/bot/eventMsg.ts @@ -1,6 +1,7 @@ import groupAgent from "../../controller/groupAgent" import intentAgent from "../../controller/intentAgent" import reportAgent from "../../controller/reportAgent" +import searchAgent from "../../controller/searchAgent" import soupAgent from "../../controller/soupAgent" import { Context } from "../../types" import { isNotP2POrAtBot } from "../../utils/message" @@ -68,69 +69,69 @@ const manageIntent = async (ctx: Context) => { try { const intentRes = await intentAgent.agent(ctx) logger.info(`intentRes: ${JSON.stringify(intentRes)}`) + // 链接总结 if (intentAgent.isBriefing(intentRes)) { reportAgent .agent(ctx, intentRes.link, intentRes.userDescription) .catch(() => null) return } + // 设置链接总结的结果存储表格 if (intentAgent.isBriefingLink(intentRes)) { reportAgent.setSheet(ctx, intentRes.link).catch(() => null) return } + // 网络检索 + if (intentAgent.isNetSearch(intentRes)) { + searchAgent.agent(ctx, intentRes.query).catch(() => null) + return + } + // 通用回复 if (intentAgent.isCommonResponse(intentRes)) { await message.updateOrReply(larkCard.genSuccessCard(intentRes.message)) return } - if (intentAgent.isBaseIntent(intentRes)) { - switch (intentRes.intent) { - case 3: - await message.updateOrReply( - larkCard.genTempCard("chatId", { chat_id: chatId }) as string - ) - break - case 4: - await attachService.ciMonitor(chatId) - break - case 6: - await message.updateOrReply( - larkCard.genTempCard("eggGuide", { chat_id: chatId }) as string - ) - break - case 7: - groupAgent.report - .setSubscription(ctx, "daily", true) - .catch(() => null) - break - case 8: - groupAgent.report - .setSubscription(ctx, "weekly", true) - .catch(() => null) - break - case 9: - groupAgent.report - .setSubscription(ctx, "daily", false) - .catch(() => null) - break - case 10: - groupAgent.report - .setSubscription(ctx, "weekly", false) - .catch(() => null) - break - case 11: - groupAgent.report.gen4Test(ctx, "daily").catch(() => null) - break - case 12: - groupAgent.report.gen4Test(ctx, "weekly").catch(() => null) - break - case 13: - soupAgent.startOrStopGame(ctx, true, "manual").catch(() => null) - break - case 1: - default: - groupAgent.agent(ctx).catch(() => null) - break - } + switch (intentRes.intent) { + case 3: + await message.updateOrReply( + larkCard.genTempCard("chatId", { chat_id: chatId }) as string + ) + break + case 4: + await attachService.ciMonitor(chatId) + break + case 6: + await message.updateOrReply( + larkCard.genTempCard("eggGuide", { chat_id: chatId }) as string + ) + break + case 7: + groupAgent.report.setSubscription(ctx, "daily", true).catch(() => null) + break + case 8: + groupAgent.report.setSubscription(ctx, "weekly", true).catch(() => null) + break + case 9: + groupAgent.report.setSubscription(ctx, "daily", false).catch(() => null) + break + case 10: + groupAgent.report + .setSubscription(ctx, "weekly", false) + .catch(() => null) + break + case 11: + groupAgent.report.gen4Test(ctx, "daily").catch(() => null) + break + case 12: + groupAgent.report.gen4Test(ctx, "weekly").catch(() => null) + break + case 13: + soupAgent.startOrStopGame(ctx, true, "manual").catch(() => null) + break + case 1: + default: + groupAgent.agent(ctx).catch(() => null) + break } } catch (error) { logger.error(`manageIntent error: ${error}`) diff --git a/services/attach/index.ts b/services/attach/index.ts index 74864d8..4665b4b 100644 --- a/services/attach/index.ts +++ b/services/attach/index.ts @@ -142,6 +142,37 @@ class AttachService extends NetToolBase { throw new Error("MIFY爬虫请求失败") }) } + + /** + * 使用bochaai搜索网页 + * @param {string} query - 搜索关键词 + * @returns 返回搜索结果 + */ + async webSearch(query: string) { + const URL = "https://api.bochaai.com/v1/web-search" + return this.post( + URL, + { + query, + summary: true, + }, + {}, + { + Authorization: `Bearer ${APP_CONFIG.BOCHA_SK}`, + } + ) + .then((res) => { + const { value } = res.data.webPages + return value.map(({ siteName, summary }: any) => ({ + siteName, + summary, + })) as { + siteName: string + summary: string + }[] + }) + .catch(() => null) + } } export default AttachService diff --git a/test/llm/intentRecognition.ts b/test/llm/intentRecognition.ts index 1a72294..bc524d9 100644 --- a/test/llm/intentRecognition.ts +++ b/test/llm/intentRecognition.ts @@ -8,34 +8,63 @@ import { cleanLLMRes } from "../../utils/llm/base" await initAppConfig() +/** + * 基础意图模式 + */ const BaseIntentSchema = z.object({ intent: z.number().min(1).max(13), }) +/** + * 简报模式 + */ const BriefingSchema = BaseIntentSchema.extend({ - intent: z.literal(3), + intent: z.literal(5), link: z.string().url(), userDescription: z.string().min(1), }) +/** + * 简报存储链接模式 + */ +const BriefingLinkSchema = z.object({ + intent: z.literal(14), + link: z.string().url(), +}) + +/** + * 通用响应模式 + */ const CommonResponseSchema = BaseIntentSchema.extend({ - intent: z.literal(12), + intent: z.literal(2), message: z.string().min(1), }) +/** + * 联网检索模式 + */ +const NetSearchSchema = BaseIntentSchema.extend({ + intent: z.literal(15), + query: z.string().min(1), +}) + +/** + * 意图模式 + */ const IntentSchema = z.union([ BriefingSchema, CommonResponseSchema, BaseIntentSchema, + BriefingLinkSchema, + NetSearchSchema, ]) const jsonSchema = zodToJsonSchema(IntentSchema) const res = await llm.invoke( - "intentRecognitionNext", + "intentRecognition", { - userInput: - "https://mp.weixin.qq.com/s/-0J8XbXJU6Bu-UihRtgGAQ Airbnb死磕React Native惨败,微软却玩出花!Office、Outlook全线接入,Copilot成最大赢家 推荐大家看一下,rn助力微软copilot 跨平台实现", + userInput: "今天是哪天", time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), jsonSchema, }, diff --git a/test/server/getBatchUserInfo.ts b/test/server/getBatchUserInfo.ts new file mode 100644 index 0000000..95356f6 --- /dev/null +++ b/test/server/getBatchUserInfo.ts @@ -0,0 +1,11 @@ +import initAppConfig from "../../constant/config" +import genLarkService from "../../utils/genLarkService" + +await initAppConfig() + +const larkService = genLarkService("egg", "test") + +larkService.user + .batchGet(["ou_5d9c2da2870802fc47fc2066f28b1b49"], "open_id") + .then(console.log) + .catch(console.error) diff --git a/test/server/webSearch.ts b/test/server/webSearch.ts new file mode 100644 index 0000000..e1fcb6a --- /dev/null +++ b/test/server/webSearch.ts @@ -0,0 +1,8 @@ +import initAppConfig from "../../constant/config" +import { AttachService } from "../../services" + +await initAppConfig() + +const service = new AttachService() + +service.webSearch("北京今天天气").then(console.log).catch(console.error)