feat: 支持使用对话唤醒机器人

This commit is contained in:
zhaoyingbo 2024-12-01 08:30:39 +00:00
parent 07b5df8689
commit f439db8832
15 changed files with 301 additions and 679 deletions

BIN
bun.lockb

Binary file not shown.

32
cache.ts Normal file
View File

@ -0,0 +1,32 @@
import { z } from "zod"
const groupAgentConfig = z
.object({
isGroupChat: z.boolean().describe("是否是群聊问答"),
startTime: z
.string()
.optional()
.describe("开始时间,格式为 YYYY-MM-DD HH:mm:ss"),
endTime: z
.string()
.optional()
.describe("结束时间,格式为 YYYY-MM-DD HH:mm:ss"),
userQueryResponse: z
.string()
.optional()
.describe("如果不是群聊问答的对用户Query的回复"),
})
.refine(
(data) => {
if (data.isGroupChat) {
return data.startTime && data.endTime
} else {
return data.userQueryResponse
}
},
{
message:
"isGroupChat 为 true 时需要 startTime 和 endTime为 false 时需要 userQueryResponse",
path: ["isGroupChat"],
}
)

View File

@ -1,130 +1,5 @@
import { cardComponent } from "@egg/lark-msg-tool"
const groupSelector = {
config: {
update_multi: true,
},
elements: [
{
tag: "markdown",
content: "请选择要对话的群聊",
},
{
tag: "action",
actions: [
{
tag: "select_static",
placeholder: {
tag: "plain_text",
content: "请选择群名称",
},
value: {
cardGroup: "groupAgent",
cardName: "groupSelector",
},
options: "${groupOptions}",
},
],
},
{
tag: "hr",
},
cardComponent.commonNote,
],
header: cardComponent.pendingHeader,
}
const functionSelector = {
config: {
update_multi: true,
},
elements: [
{
tag: "action",
actions: [
{
tag: "select_static",
placeholder: {
tag: "plain_text",
content: "请选择功能",
},
value: {
cardGroup: "groupAgent",
cardName: "functionSelector",
},
options: "${functionOptions}",
},
],
},
{
tag: "hr",
},
cardComponent.commonNote,
],
header: cardComponent.pendingHeader,
}
const timeScopeSelector = {
config: {
update_multi: true,
},
elements: [
{
tag: "markdown",
content: "请选择时间范围",
},
{
tag: "action",
actions: [
{
tag: "button",
text: {
tag: "plain_text",
content: "过去一天",
},
type: "default",
value: {
cardGroup: "groupAgent",
cardName: "timeScopeSelector",
timeScope: "1",
},
},
{
tag: "button",
text: {
tag: "plain_text",
content: "过去三天",
},
type: "default",
value: {
cardGroup: "groupAgent",
cardName: "timeScopeSelector",
timeScope: "3",
},
},
{
tag: "button",
text: {
tag: "plain_text",
content: "过去七天",
},
type: "default",
value: {
cardGroup: "groupAgent",
cardName: "timeScopeSelector",
timeScope: "7",
},
},
],
},
{
tag: "hr",
},
cardComponent.commonNote,
],
header: cardComponent.pendingHeader,
}
const resultReport = {
config: {
update_multi: true,
@ -172,18 +47,8 @@ const autoReport = {
},
}
export const functionOptionList = [
{
id: "summary-qwen-72b-instruct-int4",
name: "总结消息",
},
]
const cardMap = {
functionSelector,
timeScopeSelector,
resultReport,
groupSelector,
autoReport,
}

View File

@ -0,0 +1,64 @@
import { Context } from "../../types"
import llm from "../../utils/llm"
import getChatHistory from "./chatHistory"
const agent = async (ctx: Context.Data) => {
const {
logger,
requestId,
larkCard,
larkService,
larkBody: { messageId, msgText, chatId, mentions },
} = ctx
const cardGender = larkCard.child("groupAgent")
// 回复一个loading的卡片
const {
data: { message_id },
} = await larkService.message.replyCard(
messageId,
cardGender.genPendingCard("分析中,请稍等...")
)
const updateCard = (content: any) =>
larkService.message.update(message_id, content)
// 使用大模型解析用户输入
const { startTime, endTime } = await llm.timeParser(msgText, requestId)
logger.info(`Parsed time: startTime: ${startTime}, endTime: ${endTime}`)
// 更新卡片
updateCard(cardGender.genPendingCard("正在爬楼中,请稍等..."))
// 获取聊天记录
const chatHistory = await getChatHistory(ctx, {
chatId,
startTime,
endTime,
mentions,
})
// 如果没有聊天记录,返回错误信息
if (chatHistory.length === 0) {
logger.info("No chat history found")
return await updateCard(cardGender.genErrorCard("未找到聊天记录"))
}
logger.debug(`Chat history: ${JSON.stringify(chatHistory)}`)
// 调用大模型
try {
const llmRes = await llm.invoke(
"groupAgent",
{
userInput: msgText,
chatHistory: JSON.stringify(chatHistory),
time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
},
requestId
)
logger.info(
`LLM invoked successfully, see detail: http://langfuse.ai.srv/project/cm1j2tkj9001gukrgdvc1swuw/sessions/${requestId}`
)
await updateCard(cardGender.genSuccessCard(llmRes))
} catch (error: any) {
logger.error(`Failed to invoke llm: ${error.message}`)
await updateCard(cardGender.genErrorCard("LLM调用失败: " + error.message))
}
}
export default agent

View File

@ -46,15 +46,13 @@ const getChatHistory = async (
{ larkService, logger }: Context.Data,
{
chatId,
timeScope,
startTime,
endTime,
mentions: targetUsers,
}: {
chatId: string
timeScope?: string
startTime?: string
endTime?: string
startTime: string
endTime: string
mentions?: LarkEvent.Mention[]
}
): Promise<Message[]> => {
@ -63,27 +61,15 @@ const getChatHistory = async (
// 上海时区的偏移量UTC+8以分钟为单位
const shanghaiTimezoneOffset = -8 * 60
let startTimeTimestamp: number
let endTimeTimestamp: number
if (startTime && endTime) {
// 将startTime和endTime转换为时间戳并调整为上海时区
startTimeTimestamp = Math.round(
new Date(startTime).getTime() / 1000 +
(shanghaiTimezoneOffset - serverTimezoneOffset) * 60
)
endTimeTimestamp = Math.round(
new Date(endTime).getTime() / 1000 +
(shanghaiTimezoneOffset - serverTimezoneOffset) * 60
)
} else {
// 计算时间戳,调整为上海时区
endTimeTimestamp =
Math.round(new Date().getTime() / 1000) +
// 将startTime和endTime转换为时间戳并调整为上海时区
const startTimeTimestamp = Math.round(
new Date(startTime).getTime() / 1000 +
(shanghaiTimezoneOffset - serverTimezoneOffset) * 60
startTimeTimestamp = endTimeTimestamp - Number(timeScope) * 24 * 60 * 60
}
)
const endTimeTimestamp = Math.round(
new Date(endTime).getTime() / 1000 +
(shanghaiTimezoneOffset - serverTimezoneOffset) * 60
)
// 获取群聊中的历史记录
const { data: chatHistory } = await larkService.message.getHistory(
chatId,

View File

@ -1,8 +1,8 @@
import manual from "./manual"
import agent from "./agent"
import report from "./report"
const groupAgent = {
manual,
agent,
report,
}

View File

@ -1,412 +0,0 @@
import { genCardOptions, LarkEvent } from "@egg/lark-msg-tool"
import { functionOptionList } from "../../constant/card"
import { Context, LarkServer } from "../../types"
import llm from "../../utils/llm"
import getChatHistory from "./chatHistory"
/**
*
* @param ctx -
* @param innerList -
* @param commonVal -
*/
const genGroupSelector = (
{ larkCard }: Context.Data,
innerList: LarkServer.ChatGroupData[],
commonVal: Record<string, any> = {}
) => {
const cardGender = larkCard.child("groupAgent")
// 组织群组数据
const groupOptions = genCardOptions(
innerList.reduce(
(acc, item) => {
acc[item.name] = `${item.chat_id}|${item.name}`
return acc
},
{} as Record<string, string>
)
)
return cardGender.genCard("groupSelector", {
groupOptions,
...commonVal,
})
}
/**
*
* @param ctx -
* @param commonVal -
*/
const genFunctionSelector = (
{ larkCard }: Context.Data,
commonVal: Record<string, any> = {}
) => {
const cardGender = larkCard.child("groupAgent")
// 组织功能数据
const functionOptions = genCardOptions(
functionOptionList.reduce(
(acc, item) => {
acc[item.name] = `${item.id}|${item.name}`
return acc
},
{} as Record<string, string>
)
)
return cardGender.genCard("functionSelector", {
functionOptions,
...commonVal,
})
}
/**
*
* @param ctx - body和larkService
*/
const genTimeScopeSelector = (
{ larkCard }: Context.Data,
commonVal: Record<string, any> = {}
) => {
return larkCard.child("groupAgent").genCard("timeScopeSelector", commonVal)
}
const sendGroupReport = async (
ctx: Context.Data,
messageId: string,
{
chatId,
chatName,
functionId,
functionName,
timeScope,
startTime,
endTime,
mentions,
}: {
chatId: string
chatName: string
functionId: string
functionName: string
timeScope?: string
startTime?: string
endTime?: string
mentions?: LarkEvent.Mention[]
}
) => {
const { larkService, logger, larkCard, requestId } = ctx
const cardGender = larkCard.child("groupAgent")
const updateCard = (content: any) =>
larkService.message.update(messageId, content)
// action需要返回loading的消息event需要主动update卡片所以loading就放外边了
// 记录发送loading消息后的时间戳
const processStart = Date.now()
// 获取聊天记录
const chatHistory = await getChatHistory(ctx, {
chatId,
timeScope,
startTime,
endTime,
mentions,
})
// 如果没有历史记录则返回错误消息
if (chatHistory.length === 0) {
logger.error("Chat history is empty")
await updateCard(cardGender.genErrorCard("未找到聊天记录"))
return
}
logger.debug(`Chat history: ${JSON.stringify(chatHistory)}`)
try {
const llmRes = await llm.invoke(
functionId,
{
chatHistory: JSON.stringify(chatHistory),
time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
},
requestId
)
// 记录大模型返回结果后的时间戳
const processEnd = Date.now()
// 计算时间差并存储在processingTime变量中以秒为单位
const processingTime = ((processEnd - processStart) / 1000).toFixed(2)
logger.info(`LLM takes time: ${processingTime}s, result: ${llmRes}`)
// 动态生成content内容
const timeRange = timeScope
? `总结天数:**${timeScope}**`
: `时间范围:**${startTime}** 至 **${endTime}**`
const mentionsList = mentions
? `圈选的用户:\n${mentions
.filter((v) => v.id.user_id)
.map((mention: any) => `- ${mention.name}`)
.join("\n")}`
: ""
const content = `群聊:**${chatName}**,功能:**${functionName}**${timeRange}\n\n${mentionsList}\n\n以下内容由AI模型生成耗时**${processingTime}s**`
// 更新消息卡片
await larkService.message.update(
messageId,
cardGender.genCard("resultReport", {
content,
llmRes,
})
)
} catch (error: any) {
logger.error(`LLM error: ${error.message}`)
await larkService.message.update(
messageId,
cardGender.genErrorCard("LLM调用失败: " + error.message)
)
}
}
/**
*
* @param ctx -
* @param innerList -
*/
const parseGroupAgentQuery = async (
ctx: Context.Data,
innerList: LarkServer.ChatGroupData[],
mentions?: LarkEvent.Mention[]
) => {
// TODO处理在群聊里的情况不用获取群组名称
const {
larkBody: { msgText, chatId: rawChatId },
larkService,
larkCard,
logger,
requestId,
} = ctx
const cardGender = larkCard.child("groupAgent")
// 发送一个loading的消息
const {
data: { message_id },
} = await larkService.message.sendCard2Chat(
rawChatId,
cardGender.genPendingCard("分析中,请稍等...")
)
const updateCard = (content: any) =>
larkService.message.update(message_id, content)
// 组织群组数据
const groupInfo = JSON.stringify(
innerList.map((v) => ({
name: v.name,
id: v.chat_id,
}))
)
// 获取功能信息
const functionInfo = JSON.stringify(functionOptionList)
// 使用大模型解析用户输入
const { chatId, chatName, functionName, functionId, startTime, endTime } =
await llm.parseGroupAgentQuery(msgText, groupInfo, functionInfo, requestId)
logger.info(
`Parsed group agent query: chatId: ${chatId}, chatName: ${chatName}, functionName: ${functionName}, functionId: ${functionId}, startTime: ${startTime}, endTime: ${endTime}`
)
// 判断顺序是 群组 -> 功能 -> 时间范围
// 返回群组选择器,其他的值往里边丢就行
if (!chatId || !chatName) {
logger.info("Send group selector")
updateCard(
genGroupSelector(ctx, innerList, {
functionName,
functionId,
startTime,
endTime,
mentions,
})
)
return
}
// 返回功能选择器,其他的值往里边丢就行
if (!functionId || !functionName) {
logger.info("Send function selector")
updateCard(
genFunctionSelector(ctx, {
chatId,
chatName,
startTime,
endTime,
mentions,
})
)
return
}
// 返回时间范围选择器,其他的值往里边丢就行
if (!startTime || !endTime) {
logger.info("Send time scope selector")
updateCard(
genTimeScopeSelector(ctx, {
chatId,
chatName,
functionId,
functionName,
mentions,
})
)
return
}
logger.info("Send group report")
// 设置齐全,返回结果报告
updateCard(cardGender.genPendingCard("正在爬楼中,请稍等..."))
sendGroupReport(ctx, message_id, {
chatId,
chatName,
functionId,
functionName,
startTime,
endTime,
mentions,
})
}
/**
*
* @param ctx -
*/
const manageEventMsg = async (ctx: Context.Data) => {
const {
larkBody: { msgText, chatType, chatId: rawChatId, mentions },
larkService,
logger,
} = ctx
// 获取群组信息
const { data: innerList } = await larkService.chat.getInnerList()
logger.info(`Inner list: ${JSON.stringify(innerList)}`)
const sendCard = (content: string) =>
larkService.message.sendCard2Chat(rawChatId, content)
// 去过去掉所有非必要的信息为空的话
if (msgText.replace("/groupchat", "").replaceAll(" ", "") === "") {
// 私聊发送正常的群组选择器
if (chatType === "p2p") {
logger.info("Send group selector to p2p chat")
await sendCard(genGroupSelector(ctx, innerList, { mentions }))
return
}
// 如果是群聊,获取群聊名称并发送功能
const {
data: { name: chatName },
} = await larkService.chat.getChatInfo(rawChatId)
logger.info(`Send function selector to group chat: ${chatName}`)
await sendCard(genFunctionSelector(ctx, { chatName, mentions }))
return
}
logger.info(`User input: ${msgText}, chatType: ${chatType}, use llm to parse`)
// 用户有输入,使用大模型进行解析发送对应卡片
await parseGroupAgentQuery(ctx, innerList, mentions)
return
}
/**
* Action消息
* @param ctx -
*/
const manageActionMsg = async (ctx: Context.Data) => {
const {
larkBody: { actionOption, actionValue, messageId },
larkCard,
logger,
} = ctx
const cardGender = larkCard.child("groupAgent")
logger.debug(`Action option: ${JSON.stringify(actionOption)}`)
logger.debug(`Action value: ${JSON.stringify(actionValue)}`)
let { chatId, chatName, functionId, functionName } = actionValue
const { timeScope, startTime, endTime, cardName, mentions } = actionValue
// 如果是群组选择器返回值
if (cardName === "groupSelector") {
const [newChatId, newChatName] = (actionOption ?? "").split("|")
if (!newChatId || !newChatName) {
logger.error(
`Invalid targetChatId or targetChatName: ${JSON.stringify(actionOption)}`
)
return cardGender.genErrorCard("Invalid targetChatId or targetChatName")
}
chatId = newChatId
chatName = newChatName
}
// 如果是功能选择器返回值
if (cardName === "functionSelector") {
const [newFunctionId, newFunctionName] = (actionOption ?? "").split("|")
if (!newFunctionId || !newFunctionName) {
logger.error(
`Invalid functionId or functionName: ${JSON.stringify(actionOption)}`
)
return cardGender.genErrorCard("Invalid functionId or functionName")
}
functionId = newFunctionId
functionName = newFunctionName
}
// 时间返回的返回值就会带在timeScope里不需要再处理
// 理论上来说这里的chatId, chatName肯定是有值的不需要再判断
// 判断是否需要返回功能选择器
if (!functionId || !functionName) {
logger.info("Send function selector")
return genFunctionSelector(ctx, {
chatId,
chatName,
startTime,
endTime,
timeScope,
mentions,
})
}
// 判断是否需要返回时间范围选择器
if (!(timeScope || (startTime && endTime))) {
logger.info("Send time scope selector")
return genTimeScopeSelector(ctx, {
chatId,
chatName,
functionId,
functionName,
mentions,
})
}
logger.info("Send group report")
// 设置齐全,返回结果报告
sendGroupReport(ctx, messageId, {
chatId,
chatName,
functionId,
functionName,
timeScope,
startTime,
endTime,
mentions,
})
return cardGender.genPendingCard("正在爬楼中,请稍等...")
}
/**
* Agent的主入口
* @param ctx -
*/
const manual = async (ctx: Context.Data) => {
const {
larkBody: { isEvent, isAction },
logger,
} = ctx
try {
// 如果是Event则解析自然语言并发送对应的卡片
if (isEvent) return await manageEventMsg(ctx)
// 如果是Action则取出用户选的值并判断是否需要继续发送表单卡片或者开始大模型推理
if (isAction) return await manageActionMsg(ctx)
} catch (e: any) {
logger.error(`Group agent error: ${e.message}`)
return ctx.larkCard.child("groupAgent").genErrorCard("Group agent error")
}
}
export default manual

View File

@ -39,13 +39,13 @@
"@egg/hooks": "^1.2.0",
"@egg/lark-msg-tool": "^1.17.0",
"@egg/logger": "^1.6.0",
"@egg/net-tool": "^1.16.1",
"@egg/net-tool": "^1.19.0",
"@egg/path-tool": "^1.4.1",
"@langchain/core": "^0.3.19",
"@langchain/openai": "^0.3.14",
"@prisma/client": "^5.22.0",
"joi": "^17.13.3",
"langfuse-langchain": "^3.31.0",
"langfuse-langchain": "^3.31.1",
"node-schedule": "^2.1.1",
"p-limit": "^6.1.0",
"pocketbase": "^0.21.5",

View File

@ -1,9 +1,6 @@
import groupAgent from "../../controller/groupAgent"
import { Context } from "../../types"
const GROUP_MAP = {
groupAgent: groupAgent.manual,
}
const GROUP_MAP = {}
/**
*
@ -19,7 +16,7 @@ const manageAction = async (ctx: Context.Data) => {
}
logger.info(`Got lark action cardGroup: ${cardGroup}`)
if (!cardGroup) return
const func = GROUP_MAP[cardGroup]
const func = GROUP_MAP[cardGroup] as (ctx: Context.Data) => Promise<any>
if (!func) return
return func(ctx)
}

View File

@ -136,14 +136,6 @@ const manageCMDMsg = (ctx: Context.Data) => {
}
}
// michat专属功能
if (app === "michat") {
// 帮助
logger.info(`bot command is /help, chatId: ${chatId}`)
manageHelpMsg(ctx, "miChatGuide")
return
}
// 小煎蛋专属功能
if (app === "egg") {
// CI监控
@ -173,13 +165,6 @@ const manageCMDMsg = (ctx: Context.Data) => {
return
}
// 选择群组信息
if (msgText.startsWith("/g")) {
logger.info(`bot command is /groupchat, chatId: ${chatId}`)
groupAgent.manual(ctx)
return
}
// 帮助
if (msgText === "/help") {
logger.info(`bot command is /help, chatId: ${chatId}`)
@ -188,6 +173,12 @@ const manageCMDMsg = (ctx: Context.Data) => {
}
}
// 如果是群聊使用groupAgent兜底
if (isInGroup) {
groupAgent.agent(ctx)
return
}
return
}

View File

@ -0,0 +1,4 @@
POST http://localhost:3000/bot?app=egg HTTP/1.1
content-type: application/json
{"schema":"2.0","header":{"event_id":"c94518fbcb9d66cc93b4144cc69e4f0c","token":"tV9djUKSjzVnekV7xTg2Od06NFTcsBnj","create_time":"1732612280153","event_type":"im.message.receive_v1","tenant_key":"2ee61fe50f4f1657","app_id":"cli_a1eff35b43b89063"},"event":{"message":{"chat_id":"oc_8c789ce8f4ecc6695bb63ca6ec4c61ea","chat_type":"group","content":"{\"text\":\"@_user_1 今天的重点消息是什么\"}","create_time":"1732612279943","mentions":[{"id":{"open_id":"ou_032f507d08f9a7f28b042fcd086daef5","union_id":"on_7111660fddd8302ce47bf1999147c011","user_id":""},"key":"@_user_1","name":"小煎蛋","tenant_key":"2ee61fe50f4f1657"}],"message_id":"om_4ab57cb6e889eeb81ca061b137238189","message_type":"text","update_time":"1732612279943"},"sender":{"sender_id":{"open_id":"ou_470ac13b8b50fc472d9d8ee71e03de26","union_id":"on_9dacc59a539023df8b168492f5e5433c","user_id":"zhaoyingbo"},"sender_type":"user","tenant_key":"2ee61fe50f4f1657"}}}

View File

@ -0,0 +1,54 @@
import { describe, expect, test } from "bun:test"
import llm from "../utils/llm"
describe("timeParser", () => {
const testCases = [
{
input: "过去五天赵英博说了什么",
expected: { s: "2024-11-26 00:00:00", e: "2024-11-30 23:59:59" },
},
{
input: "昨天的会议记录",
expected: { s: "2024-11-30 00:00:00", e: "2024-11-30 23:59:59" },
},
{
input: "上周的销售数据",
expected: { s: "2024-11-18 00:00:00", e: "2024-11-24 23:59:59" },
},
{
input: "今天的天气",
expected: { s: "2024-12-01 00:00:00", e: "2024-12-01 23:59:59" },
},
{
input: "上个月的财务报表",
expected: { s: "2024-11-01 00:00:00", e: "2024-11-30 23:59:59" },
},
{
input: "今年的计划",
expected: { s: "2024-01-01 00:00:00", e: "2024-12-31 23:59:59" },
},
{
input: "明天的安排",
expected: { s: "2024-12-02 00:00:00", e: "2024-12-02 23:59:59" },
},
{
input: "下周的会议",
expected: { s: "2024-12-02 00:00:00", e: "2024-12-08 23:59:59" },
},
{
input: "下个月的活动",
expected: { s: "2024-12-01 00:00:00", e: "2024-12-31 23:59:59" },
},
{
input: "明年的目标",
expected: { s: "2025-01-01 00:00:00", e: "2025-12-31 23:59:59" },
},
]
testCases.forEach(({ input, expected }, index) => {
test(`Test case ${index + 1}: ${input}`, async () => {
const result = await llm.timeParser(input, `testRequestId${index}`)
expect(result).toEqual(expected)
})
})
})

View File

@ -1,25 +1,5 @@
import llm from "../utils/llm"
const groupInfo = JSON.stringify([
{
id: "oc_ef98c2a9229657f99d4ef573a30fe91c",
name: "MIAI-FE 人工智能部-前端组",
},
{
id: "oc_433b1cb7a9dbb7ebe70a4e1a59cb8bb1",
name: "方糖の家",
},
])
const userInput = "下个月份的活动"
const functionInfo = JSON.stringify([
{
id: "summary-qwen-72b-instruct-int4",
name: "总结消息",
},
])
const userInput = "你好"
llm
.parseGroupAgentQuery(userInput, groupInfo, functionInfo, "localTest")
.then(console.log)
llm.timeParser(userInput, "localTest").then(console.log)

View File

@ -1,68 +1,81 @@
import { PromptTemplate } from "@langchain/core/prompts"
import { z } from "zod"
import { adjustTimeRange, getTimeRange } from "../time"
import { getLangfuse, getModel } from "./base"
const groupAgentConfig = z.object({
chatId: z.string().describe("群聊ID"),
chatName: z.string().describe("群聊名称"),
functionId: z.string().describe("功能ID"),
functionName: z.string().describe("功能名称"),
startTime: z.string().describe("开始时间,格式为 YYYY-MM-DD HH:mm:ss"),
endTime: z.string().describe("结束时间,格式为 YYYY-MM-DD HH:mm:ss"),
})
/**
* GroupAgent用户输入
* timeParser用户输入
*
* @param userInput
* @param groupInfo
* @param functionInfo
* @param requestId ID
* @returns
*/
const parseGroupAgentQuery = async (
userInput: string,
groupInfo: string,
functionInfo: string,
requestId: string
) => {
const timeParser = async (userInput: string, requestId: string) => {
const { langfuseHandler } = await getLangfuse(
"parseGroupAgentQuery",
requestId
)
const model = await getModel()
const structuredLlm = model.withStructuredOutput(groupAgentConfig, {
name: "groupAgent",
})
return await structuredLlm.invoke(
`
${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}
${groupInfo}
${functionInfo}
IDID
\`\`\`
const structuredLlm = model.withStructuredOutput(
z.object({
s: z.string().describe("开始时间,格式为 YYYY-MM-DD HH:mm:ss"),
e: z.string().describe("结束时间,格式为 YYYY-MM-DD HH:mm:ss"),
}),
{
"chatId": "oc_ef98c2a9229657f99d4ef573a30fe91c",
"chatName": "MIAI-FE 人工智能部-前端组",
"functionId": "summary-qwen-72b-instruct-int4",
"functionName": "总结消息",
"startTime": "2022-01-01 00:00:00",
"endTime": "2022-01-01 23:59:59"
}
\`\`\`
\`\`\`
${userInput.replaceAll("`", " ")}
\`\`\`
`,
{
callbacks: [langfuseHandler],
name: "timeParser",
}
)
const invokeParser = async () => {
try {
return await structuredLlm.invoke(
`
${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}
00:00:00 23:59:59
\`\`\`ts
interface GroupAgent {
s: string // 开始时间,格式为 YYYY-MM-DD HH:mm:ss
e: string // 结束时间,格式为 YYYY-MM-DD HH:mm:ss
}
\`\`\`
\`\`\`
${userInput.replaceAll("`", " ")}
\`\`\`
`,
{
callbacks: [langfuseHandler],
}
)
} catch {
// 如果解析失败,则返回空字符串
return { s: "", e: "" }
}
}
const validateResult = (result: any) => {
const regex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/
return regex.test(result.s) && regex.test(result.e)
}
let result
let attempts = 0
do {
result = await invokeParser()
attempts++
} while (!validateResult(result) && attempts < 3)
// 如果解析失败,则返回过去三天的时间范围
if (!validateResult(result)) {
return getTimeRange("threeDays")
}
// 解析成功,调整时间范围
return adjustTimeRange(result.s, result.e)
}
/**
@ -98,7 +111,7 @@ const invoke = async (
}
const llm = {
parseGroupAgentQuery,
timeParser,
invoke,
}

View File

@ -1,30 +1,78 @@
/**
*
* @param {Date} date -
* @returns {string} - YYYY-MM-DD HH:mm:ss
*/
const formatDate = (date: Date) => {
const pad = (num: number) => (num < 10 ? "0" + num : num)
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
/**
*
* @param {string} dateStr - YYYY-MM-DD HH:mm:ss
* @returns {Date} -
*/
const parseDate = (dateStr: string) => {
const [datePart, timePart] = dateStr.split(" ")
const [year, month, day] = datePart.split("-").map(Number)
const [hours, minutes, seconds] = timePart.split(":").map(Number)
return new Date(year, month - 1, day, hours, minutes, seconds)
}
/**
*
* @param {("daily" | "weekly")} timeScope - "daily" "weekly"
* @param {("daily" | "weekly" | "threeDays")} timeScope - "daily""weekly" "threeDays"
* @returns {{ startTime: string, endTime: string }} -
*/
export const getTimeRange = (timeScope: "daily" | "weekly") => {
const formatDate = (date: Date) => {
const pad = (num: number) => (num < 10 ? "0" + num : num)
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
export const getTimeRange = (timeScope: "daily" | "weekly" | "threeDays") => {
const now = new Date()
const startTime = new Date(now)
if (timeScope === "daily") {
startTime.setHours(0, 0, 0, 0)
} else {
startTime.setHours(0, 0, 0, 0)
const endTime = new Date(now)
endTime.setHours(23, 59, 59, 999)
if (timeScope === "weekly") {
const day = now.getDay()
const diff = day === 0 ? 6 : day - 1 // adjust when day is sunday
startTime.setDate(now.getDate() - diff)
startTime.setHours(0, 0, 0, 0)
} else if (timeScope === "threeDays") {
startTime.setDate(now.getDate() - 2)
}
const endTime = new Date(now)
endTime.setHours(23, 59, 59, 999)
return {
startTime: formatDate(startTime),
endTime: formatDate(endTime),
}
}
/**
*
*
*
*
* @param {string} startTime - YYYY-MM-DD HH:mm:ss
* @param {string} endTime - YYYY-MM-DD HH:mm:ss
* @returns {{ startTime: string, endTime: string }} -
*/
export const adjustTimeRange = (startTime: string, endTime: string) => {
const now = new Date()
let start = parseDate(startTime)
let end = parseDate(endTime)
if (start > end) {
;[start, end] = [end, start]
}
if (start > now && end > now) {
return getTimeRange("threeDays")
} else if (start < now && end > now) {
end = now
end.setHours(23, 59, 59, 999)
}
return {
startTime: formatDate(start),
endTime: formatDate(end),
}
}