feat: 添加网页搜索功能,优化意图识别模式,更新依赖项

This commit is contained in:
zhaoyingbo 2025-02-06 13:06:27 +00:00
parent 33b0c1bec1
commit 266eb89c5a
12 changed files with 261 additions and 72 deletions

View File

@ -1,5 +1,7 @@
{ {
"cSpell.words": [ "cSpell.words": [
"BOCHA",
"bochaai",
"bunx", "bunx",
"CEINTL", "CEINTL",
"Chakroun", "Chakroun",

BIN
bun.lockb

Binary file not shown.

View File

@ -29,6 +29,11 @@ const functionMap = {
xAuthor: "wangyifei15 🕹️ Yingbo", xAuthor: "wangyifei15 🕹️ Yingbo",
xIcon: "🕹️", xIcon: "🕹️",
}, },
searchAgent: {
xName: "小煎蛋 Search Agent",
xAuthor: "Yingbo",
xIcon: "🔍",
},
} }
export default functionMap export default functionMap

View File

@ -22,6 +22,9 @@ const BriefingSchema = BaseIntentSchema.extend({
userDescription: z.string().min(1), userDescription: z.string().min(1),
}) })
/**
*
*/
const BriefingLinkSchema = z.object({ const BriefingLinkSchema = z.object({
intent: z.literal(14), intent: z.literal(14),
link: z.string().url(), link: z.string().url(),
@ -35,6 +38,14 @@ const CommonResponseSchema = BaseIntentSchema.extend({
message: z.string().min(1), 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, CommonResponseSchema,
BaseIntentSchema, BaseIntentSchema,
BriefingLinkSchema, BriefingLinkSchema,
NetSearchSchema,
]) ])
type BaseIntent = z.infer<typeof BaseIntentSchema>
type Briefing = z.infer<typeof BriefingSchema> type Briefing = z.infer<typeof BriefingSchema>
type BriefingLink = z.infer<typeof BriefingLinkSchema> type BriefingLink = z.infer<typeof BriefingLinkSchema>
type CommonResponse = z.infer<typeof CommonResponseSchema> type CommonResponse = z.infer<typeof CommonResponseSchema>
type NetSearch = z.infer<typeof NetSearchSchema>
export type Intent = z.infer<typeof IntentSchema> export type Intent = z.infer<typeof IntentSchema>
/** /**
@ -58,8 +70,8 @@ const jsonSchema = zodToJsonSchema(IntentSchema)
/** /**
* *
* @param {Context} ctx - * @param ctx -
* @returns {Promise<Intent>} - * @returns
*/ */
const agent = async (ctx: Context): Promise<Intent> => { const agent = async (ctx: Context): Promise<Intent> => {
const { const {
@ -71,7 +83,7 @@ const agent = async (ctx: Context): Promise<Intent> => {
let attempts = 0 let attempts = 0
while (attempts < 3) { while (attempts < 3) {
const res = await llm.invoke( const res = await llm.invoke(
"intentRecognitionNext", "intentRecognition",
{ {
userInput: msgText, userInput: msgText,
time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
@ -105,31 +117,51 @@ const agent = async (ctx: Context): Promise<Intent> => {
} }
} }
const isBaseIntent = (intent: Intent): intent is BaseIntent => { /**
return intent.intent !== 5 && intent.intent !== 2 *
} * @param intent -
* @returns truefalse
*/
const isBriefing = (intent: Intent): intent is Briefing => { const isBriefing = (intent: Intent): intent is Briefing => {
return intent.intent === 5 return intent.intent === 5
} }
/**
*
* @param intent -
* @returns truefalse
*/
const isBriefingLink = (intent: Intent): intent is BriefingLink => { const isBriefingLink = (intent: Intent): intent is BriefingLink => {
return intent.intent === 14 return intent.intent === 14
} }
/**
*
* @param intent -
* @returns truefalse
*/
const isCommonResponse = (intent: Intent): intent is CommonResponse => { const isCommonResponse = (intent: Intent): intent is CommonResponse => {
return intent.intent === 2 return intent.intent === 2
} }
/**
*
* @param intent -
* @returns truefalse
*/
const isNetSearch = (intent: Intent): intent is NetSearch => {
return intent.intent === 15
}
/** /**
* *
*/ */
const intentAgent = { const intentAgent = {
agent, agent,
isBaseIntent,
isBriefing, isBriefing,
isCommonResponse, isCommonResponse,
isBriefingLink, isBriefingLink,
isNetSearch,
} }
export default intentAgent export default intentAgent

View File

@ -154,7 +154,7 @@ const agent = async (ctx: Context, link: string, userDescription: string) => {
validateLink(link) validateLink(link)
// 发送一个loading卡片 // 发送一个loading卡片
await message.updateOrReply( await message.updateOrReply(
cardGender.genSuccessCard("正在为您收集简报,请稍等片刻~") cardGender.genPendingCard("正在为您收集简报,请稍等片刻~")
) )
// // 抓取网页 // // 抓取网页
// const crawRes = await crawlWebPage(ctx, link) // const crawRes = await crawlWebPage(ctx, link)

View File

@ -0,0 +1,70 @@
import { Context } from "../../types"
import llm from "../../utils/llm"
/**
*
* @param {Context} ctx -
* @param {string} userDescription -
* @param {any} content -
* @returns {Promise<string>} -
* @throws {Error} -
*/
export const generateAnswer = async (
ctx: Context,
webSearchResults:
| {
siteName: string
summary: string
}[]
| null
): Promise<string> => {
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

View File

@ -17,12 +17,12 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.6.1", "@commitlint/cli": "^19.7.1",
"@commitlint/config-conventional": "^19.6.0", "@commitlint/config-conventional": "^19.7.1",
"@eslint/js": "^9.19.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.1", "bun-types": "^1.2.2",
"eslint": "^9.19.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",
@ -30,7 +30,7 @@
"lint-staged": "^15.4.3", "lint-staged": "^15.4.3",
"oxlint": "^0.13.2", "oxlint": "^0.13.2",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript-eslint": "^8.22.0" "typescript-eslint": "^8.23.0"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.5.4" "typescript": "^5.5.4"
@ -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.31.1", "@egg/net-tool": "^1.31.2",
"@egg/path-tool": "^1.4.1", "@egg/path-tool": "^1.4.1",
"@langchain/core": "^0.3.36", "@langchain/core": "^0.3.38",
"@langchain/langgraph": "^0.2.41", "@langchain/langgraph": "^0.2.44",
"@langchain/openai": "^0.3.17", "@langchain/openai": "^0.3.17",
"joi": "^17.13.3", "joi": "^17.13.3",
"langfuse-langchain": "^3.32.3", "langfuse-langchain": "^3.35.1",
"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,6 +1,7 @@
import groupAgent from "../../controller/groupAgent" import groupAgent from "../../controller/groupAgent"
import intentAgent from "../../controller/intentAgent" import intentAgent from "../../controller/intentAgent"
import reportAgent from "../../controller/reportAgent" import reportAgent from "../../controller/reportAgent"
import searchAgent from "../../controller/searchAgent"
import soupAgent from "../../controller/soupAgent" import soupAgent from "../../controller/soupAgent"
import { Context } from "../../types" import { Context } from "../../types"
import { isNotP2POrAtBot } from "../../utils/message" import { isNotP2POrAtBot } from "../../utils/message"
@ -68,69 +69,69 @@ const manageIntent = async (ctx: Context) => {
try { try {
const intentRes = await intentAgent.agent(ctx) const intentRes = await intentAgent.agent(ctx)
logger.info(`intentRes: ${JSON.stringify(intentRes)}`) logger.info(`intentRes: ${JSON.stringify(intentRes)}`)
// 链接总结
if (intentAgent.isBriefing(intentRes)) { if (intentAgent.isBriefing(intentRes)) {
reportAgent reportAgent
.agent(ctx, intentRes.link, intentRes.userDescription) .agent(ctx, intentRes.link, intentRes.userDescription)
.catch(() => null) .catch(() => null)
return return
} }
// 设置链接总结的结果存储表格
if (intentAgent.isBriefingLink(intentRes)) { if (intentAgent.isBriefingLink(intentRes)) {
reportAgent.setSheet(ctx, intentRes.link).catch(() => null) reportAgent.setSheet(ctx, intentRes.link).catch(() => null)
return return
} }
// 网络检索
if (intentAgent.isNetSearch(intentRes)) {
searchAgent.agent(ctx, intentRes.query).catch(() => null)
return
}
// 通用回复
if (intentAgent.isCommonResponse(intentRes)) { if (intentAgent.isCommonResponse(intentRes)) {
await message.updateOrReply(larkCard.genSuccessCard(intentRes.message)) await message.updateOrReply(larkCard.genSuccessCard(intentRes.message))
return return
} }
if (intentAgent.isBaseIntent(intentRes)) { switch (intentRes.intent) {
switch (intentRes.intent) { case 3:
case 3: await message.updateOrReply(
await message.updateOrReply( larkCard.genTempCard("chatId", { chat_id: chatId }) as string
larkCard.genTempCard("chatId", { chat_id: chatId }) as string )
) break
break case 4:
case 4: await attachService.ciMonitor(chatId)
await attachService.ciMonitor(chatId) break
break case 6:
case 6: await message.updateOrReply(
await message.updateOrReply( larkCard.genTempCard("eggGuide", { chat_id: chatId }) as string
larkCard.genTempCard("eggGuide", { chat_id: chatId }) as string )
) break
break case 7:
case 7: groupAgent.report.setSubscription(ctx, "daily", true).catch(() => null)
groupAgent.report break
.setSubscription(ctx, "daily", true) case 8:
.catch(() => null) groupAgent.report.setSubscription(ctx, "weekly", true).catch(() => null)
break break
case 8: case 9:
groupAgent.report groupAgent.report.setSubscription(ctx, "daily", false).catch(() => null)
.setSubscription(ctx, "weekly", true) break
.catch(() => null) case 10:
break groupAgent.report
case 9: .setSubscription(ctx, "weekly", false)
groupAgent.report .catch(() => null)
.setSubscription(ctx, "daily", false) break
.catch(() => null) case 11:
break groupAgent.report.gen4Test(ctx, "daily").catch(() => null)
case 10: break
groupAgent.report case 12:
.setSubscription(ctx, "weekly", false) groupAgent.report.gen4Test(ctx, "weekly").catch(() => null)
.catch(() => null) break
break case 13:
case 11: soupAgent.startOrStopGame(ctx, true, "manual").catch(() => null)
groupAgent.report.gen4Test(ctx, "daily").catch(() => null) break
break case 1:
case 12: default:
groupAgent.report.gen4Test(ctx, "weekly").catch(() => null) groupAgent.agent(ctx).catch(() => null)
break break
case 13:
soupAgent.startOrStopGame(ctx, true, "manual").catch(() => null)
break
case 1:
default:
groupAgent.agent(ctx).catch(() => null)
break
}
} }
} catch (error) { } catch (error) {
logger.error(`manageIntent error: ${error}`) logger.error(`manageIntent error: ${error}`)

View File

@ -142,6 +142,37 @@ class AttachService extends NetToolBase {
throw new Error("MIFY爬虫请求失败") 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 export default AttachService

View File

@ -8,34 +8,63 @@ import { cleanLLMRes } from "../../utils/llm/base"
await initAppConfig() await initAppConfig()
/**
*
*/
const BaseIntentSchema = z.object({ const BaseIntentSchema = z.object({
intent: z.number().min(1).max(13), intent: z.number().min(1).max(13),
}) })
/**
*
*/
const BriefingSchema = BaseIntentSchema.extend({ const BriefingSchema = BaseIntentSchema.extend({
intent: z.literal(3), intent: z.literal(5),
link: z.string().url(), link: z.string().url(),
userDescription: z.string().min(1), userDescription: z.string().min(1),
}) })
/**
*
*/
const BriefingLinkSchema = z.object({
intent: z.literal(14),
link: z.string().url(),
})
/**
*
*/
const CommonResponseSchema = BaseIntentSchema.extend({ const CommonResponseSchema = BaseIntentSchema.extend({
intent: z.literal(12), intent: z.literal(2),
message: z.string().min(1), message: z.string().min(1),
}) })
/**
*
*/
const NetSearchSchema = BaseIntentSchema.extend({
intent: z.literal(15),
query: z.string().min(1),
})
/**
*
*/
const IntentSchema = z.union([ const IntentSchema = z.union([
BriefingSchema, BriefingSchema,
CommonResponseSchema, CommonResponseSchema,
BaseIntentSchema, BaseIntentSchema,
BriefingLinkSchema,
NetSearchSchema,
]) ])
const jsonSchema = zodToJsonSchema(IntentSchema) const jsonSchema = zodToJsonSchema(IntentSchema)
const res = await llm.invoke( const res = await llm.invoke(
"intentRecognitionNext", "intentRecognition",
{ {
userInput: userInput: "今天是哪天",
"https://mp.weixin.qq.com/s/-0J8XbXJU6Bu-UihRtgGAQ Airbnb死磕React Native惨败微软却玩出花Office、Outlook全线接入Copilot成最大赢家 推荐大家看一下rn助力微软copilot 跨平台实现",
time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
jsonSchema, jsonSchema,
}, },

View File

@ -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)

8
test/server/webSearch.ts Normal file
View File

@ -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)