refactor: enhance bot chat history and error handling

This commit is contained in:
renzhiyuan 2025-12-16 21:13:24 +08:00
parent 33b6363233
commit 9d95106b03
4 changed files with 90 additions and 17 deletions

View File

@ -9,6 +9,7 @@ import (
"ai_scheduler/internal/data/model" "ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/tools" "ai_scheduler/internal/tools"
"strconv"
"context" "context"
"database/sql" "database/sql"
@ -34,6 +35,7 @@ type DingTalkBotBiz struct {
botTools []model.AiBotTool botTools []model.AiBotTool
botGroupImpl *impl.BotGroupImpl botGroupImpl *impl.BotGroupImpl
toolManager *tools.Manager toolManager *tools.Manager
chatHis *impl.BotChatHisImpl
} }
// NewDingTalkBotBiz // NewDingTalkBotBiz
@ -44,6 +46,7 @@ func NewDingTalkBotBiz(
botGroupImpl *impl.BotGroupImpl, botGroupImpl *impl.BotGroupImpl,
dingTalkUser *dingtalk.User, dingTalkUser *dingtalk.User,
tools *tools_regis.ToolRegis, tools *tools_regis.ToolRegis,
chatHis *impl.BotChatHisImpl,
toolManager *tools.Manager, toolManager *tools.Manager,
) *DingTalkBotBiz { ) *DingTalkBotBiz {
return &DingTalkBotBiz{ return &DingTalkBotBiz{
@ -55,6 +58,7 @@ func NewDingTalkBotBiz(
botTools: tools.BootTools, botTools: tools.BootTools,
botGroupImpl: botGroupImpl, botGroupImpl: botGroupImpl,
toolManager: toolManager, toolManager: toolManager,
chatHis: chatHis,
} }
} }
@ -88,7 +92,7 @@ func (d *DingTalkBotBiz) InitRequire(ctx context.Context, data *chatbot.BotCallb
} }
func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
entitys.ResText(requireData.Ch, "", "收到消息,正在处理中,请稍等") entitys.ResLoading(requireData.Ch, "", "收到消息,正在处理中,请稍等")
defer close(requireData.Ch) defer close(requireData.Ch)
switch constants.ConversationType(requireData.Req.ConversationType) { switch constants.ConversationType(requireData.Req.ConversationType) {
case constants.ConversationTypeSingle: case constants.ConversationTypeSingle:
@ -98,6 +102,9 @@ func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDat
default: default:
err = errors.New("未知的聊天类型:" + requireData.Req.ConversationType) err = errors.New("未知的聊天类型:" + requireData.Req.ConversationType)
} }
if err != nil {
entitys.ResText(requireData.Ch, "", err.Error())
}
return return
} }
@ -108,6 +115,7 @@ func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *enti
//if err != nil { //if err != nil {
// return // return
//} //}
//requireData.ID=requireData.UserInfo.UserID
////如果不是管理或者不是老板,则进行权限判断 ////如果不是管理或者不是老板,则进行权限判断
//if requireData.UserInfo.IsSenior == constants.IsSeniorFalse && requireData.UserInfo.IsBoss == constants.IsBossFalse { //if requireData.UserInfo.IsSenior == constants.IsSeniorFalse && requireData.UserInfo.IsBoss == constants.IsBossFalse {
// //
@ -117,9 +125,11 @@ func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *enti
func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
group, err := d.initGroup(ctx, requireData.Req.ConversationId, requireData.Req.ConversationTitle) group, err := d.initGroup(ctx, requireData.Req.ConversationId, requireData.Req.ConversationTitle)
if err != nil { if err != nil {
return return
} }
requireData.ID = group.GroupID
groupTools, err := d.getGroupTools(ctx, group) groupTools, err := d.getGroupTools(ctx, group)
if err != nil { if err != nil {
return return
@ -158,12 +168,19 @@ func (d *DingTalkBotBiz) getGroupTools(ctx context.Context, group *model.AiBotGr
return return
} }
var ( var (
groupRegisTools map[string]struct{} groupRegisTools = make(map[int]struct{})
) )
if group.ToolList != "" { if group.ToolList != "" {
groupList := strings.Split(group.ToolList, ",") groupToolList := strings.Split(group.ToolList, ",")
for _, tool := range groupList { for _, tool := range groupToolList {
groupRegisTools[tool] = struct{}{} if tool == "" {
continue
}
num, _err := strconv.Atoi(tool)
if _err != nil {
continue
}
groupRegisTools[num] = struct{}{}
} }
} }
@ -172,7 +189,7 @@ func (d *DingTalkBotBiz) getGroupTools(ctx context.Context, group *model.AiBotGr
tools = append(tools, v) tools = append(tools, v)
continue continue
} }
if _, ex := groupRegisTools[v.Index]; ex { if _, ex := groupRegisTools[int(v.ToolID)]; ex {
tools = append(tools, v) tools = append(tools, v)
} }
} }
@ -221,11 +238,6 @@ func (d *DingTalkBotBiz) getUserContent(msgType string, msgContent interface{})
return return
} }
func (d *DingTalkBotBiz) defaultPrompt() string {
return `{"system":"智能路由系统,精准解析用户意图并路由至任务模块,遵循以下规则:","rule":{"返回格式":"{\\"index\\":\\"工具索引\\",\\"confidence\\":\\"0.0-1.0\\",\\"reasoning\\":\\"判断理由\\",\\"parameters\\":\\"转义JSON参数\\",\\"is_match\\":true|false,\\"chat\\":\\"追问内容\\"}","工具匹配":["用工具parameters匹配区分必选required和可选optional参数","无法匹配时is_match=falsechat提醒用户适用工具'请问您要查询订单还是商品?'"],"参数提取":["从用户输入提取parameters中明确提及的参数","必须参数仅用用户直接提及的缺失时is_match=falsechat提醒补充'需补充XX信息'"],"格式要求":["所有字段值为字符串含confidence","parameters为转义JSON字符串如\\"{\\\\"key\\\\":\\\\"value\\\\"}\\""]}}`
}
func (d *DingTalkBotBiz) handleMatch(ctx context.Context, rec *entitys.Recognize) (err error) { func (d *DingTalkBotBiz) handleMatch(ctx context.Context, rec *entitys.Recognize) (err error) {
if !rec.Match.IsMatch { if !rec.Match.IsMatch {
@ -297,6 +309,28 @@ func (d *DingTalkBotBiz) HandleRes(ctx context.Context, data *chatbot.BotCallbac
} }
} }
func (d *DingTalkBotBiz) SaveHis(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, chat []string) (err error) {
if len(chat) == 0 {
return
}
his := []*model.AiBotChatHi{
{
HisType: requireData.Req.ConversationType,
ID: requireData.ID,
Role: "user",
Content: requireData.Req.Text.Content,
},
{
HisType: requireData.Req.ConversationType,
ID: requireData.ID,
Role: "system",
Content: strings.Join(chat, "\n"),
},
}
_, err = d.chatHis.Add(his)
return err
}
func (d *DingTalkBotBiz) replySteam(ctx context.Context, SessionWebhook string, content string, arg ...string) error { func (d *DingTalkBotBiz) replySteam(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
msg := content msg := content
if len(arg) > 0 { if len(arg) > 0 {
@ -344,3 +378,30 @@ func (d *DingTalkBotBiz) replyActionCard(ctx context.Context, SessionWebhook str
} }
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg)) return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
} }
func (d *DingTalkBotBiz) defaultPrompt() string {
return `[system] 你是一个智能路由系统核心职责是 **精准解析用户意图并路由至对应任务模块**请严格遵循以下规则
[rule]
1. **返回格式**
仅输出以下 **严格格式化的 JSON 字符串**禁用 Markdown
{ "index": "工具索引index", "confidence": 0.0-1.0,"reasoning": "判断理由","parameters":"jsonstring |提取参数","is_match":true||false,"chat": "追问内容"}
关键规则按优先级排序
2. **工具匹配**
- 若匹配到工具使用工具的 parameters 作为模板做参数匹配
- 注意区分 parameters 中的 必须参数required 可选参数optional按下述参数提取规则处理
- **完全无法匹配**立即设置 is_match: false并在 chat 中已第一人称的角度提醒用户需要适用何种工具"请问您是要查询订单还是商品呢"
1. **参数提取**
- 根据 parameters 字段列出的参数名从用户输入中提取对应值
- **仅提取**明确提及的参数忽略未列出的内容
- 必须参数仅使用用户直接提及的参数不允许从上下文推断
- 若必须参数缺失立即设置 is_match: false并在 chat 中已第一人称的角度提醒用户提供缺少的参数追问"需要您补充XX信息"
4. 格式强制要求
-所有字段值必须是**字符串**包括 confidence
-parameters 必须是 **转义后的 JSON 字符串** "{\"product_name\": \"京东月卡\"}"`
}

View File

@ -13,7 +13,8 @@ const TableNameAiBotChatHi = "ai_bot_chat_his"
// AiBotChatHi mapped from table <ai_bot_chat_his> // AiBotChatHi mapped from table <ai_bot_chat_his>
type AiBotChatHi struct { type AiBotChatHi struct {
HisID int64 `gorm:"column:his_id;primaryKey;autoIncrement:true" json:"his_id"` HisID int64 `gorm:"column:his_id;primaryKey;autoIncrement:true" json:"his_id"`
SessionID string `gorm:"column:session_id;not null" json:"session_id"` HisType string `gorm:"column:his_type;not null;default:1;comment:1为个人2为群聊" json:"his_type"` // 1为个人2为群聊
ID int32 `gorm:"column:id;not null;comment:对应的id" json:"id"` // 对应的id
Role string `gorm:"column:role;not null;comment:system系统输出assistant助手输出,user用户输入" json:"role"` // system系统输出assistant助手输出,user用户输入 Role string `gorm:"column:role;not null;comment:system系统输出assistant助手输出,user用户输入" json:"role"` // system系统输出assistant助手输出,user用户输入
Content string `gorm:"column:content;not null" json:"content"` Content string `gorm:"column:content;not null" json:"content"`
Files string `gorm:"column:files;not null" json:"files"` Files string `gorm:"column:files;not null" json:"files"`

View File

@ -13,6 +13,7 @@ type RequireDataDingTalkBot struct {
Match *Match Match *Match
Req *chatbot.BotCallbackDataModel Req *chatbot.BotCallbackDataModel
Ch chan Response Ch chan Response
ID int32
} }
type DingTalkBot struct { type DingTalkBot struct {

View File

@ -27,20 +27,27 @@ func (d *DingBotService) GetServiceCfg() ([]entitys.DingTalkBot, error) {
} }
func (d *DingBotService) OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) (content []byte, err error) { func (d *DingBotService) OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) (content []byte, err error) {
var (
lastErr error
chat []string
)
requireData, err := d.dingTalkBotBiz.InitRequire(ctx, data) requireData, err := d.dingTalkBotBiz.InitRequire(ctx, data)
if err != nil { if err != nil {
return return
} }
// 使用 ctx.Done() 通知 Do 方法提前终止 // 使用 ctx.Done() 通知 Do 方法提前终止
subCtx, cancel := context.WithCancel(ctx) subCtx, cancel := context.WithCancel(ctx)
defer cancel() defer func() {
cancel()
_ = d.dingTalkBotBiz.SaveHis(ctx, requireData, chat)
}()
// 异步执行 Do 方法 // 异步执行 Do 方法
done := make(chan error, 1) done := make(chan error, 1)
go func() { go func() {
done <- d.dingTalkBotBiz.Do(subCtx, requireData) done <- d.dingTalkBotBiz.Do(subCtx, requireData)
}() }()
var lastErr error
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -53,6 +60,9 @@ func (d *DingBotService) OnChatBotMessageReceived(ctx context.Context, data *cha
if resp.Type == entitys.ResponseLog { if resp.Type == entitys.ResponseLog {
continue continue
} }
if resp.Type == entitys.ResponseText || resp.Type == entitys.ResponseStream || resp.Type == entitys.ResponseJson {
chat = append(chat, resp.Content)
}
if err := d.dingTalkBotBiz.HandleRes(ctx, data, resp); err != nil { if err := d.dingTalkBotBiz.HandleRes(ctx, data, resp); err != nil {
log.Printf("HandleRes 失败: %v", err) log.Printf("HandleRes 失败: %v", err)
} }
@ -60,9 +70,9 @@ func (d *DingBotService) OnChatBotMessageReceived(ctx context.Context, data *cha
} }
cleanup: cleanup:
select { select {
case err := <-done: case _err := <-done:
if err != nil { if _err != nil {
log.Printf("Do 方法执行失败: %v", err) panic(_err)
} }
case <-time.After(1 * time.Second): case <-time.After(1 * time.Second):
log.Println("警告:等待 Do 方法超时,可能发生 goroutine 泄漏") log.Println("警告:等待 Do 方法超时,可能发生 goroutine 泄漏")