ai_scheduler/internal/biz/ding_talk_bot.go

408 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package biz
import (
"ai_scheduler/internal/biz/do"
"ai_scheduler/internal/biz/handle/dingtalk"
"ai_scheduler/internal/biz/tools_regis"
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/tools"
"strconv"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/gofiber/fiber/v2/log"
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
"xorm.io/builder"
)
// AiRouterBiz 智能路由服务
type DingTalkBotBiz struct {
do *do.Do
handle *do.Handle
botConfigImpl *impl.BotConfigImpl
replier *chatbot.ChatbotReplier
log log.Logger
dingTalkUser *dingtalk.User
botTools []model.AiBotTool
botGroupImpl *impl.BotGroupImpl
toolManager *tools.Manager
chatHis *impl.BotChatHisImpl
}
// NewDingTalkBotBiz
func NewDingTalkBotBiz(
do *do.Do,
handle *do.Handle,
botConfigImpl *impl.BotConfigImpl,
botGroupImpl *impl.BotGroupImpl,
dingTalkUser *dingtalk.User,
tools *tools_regis.ToolRegis,
chatHis *impl.BotChatHisImpl,
toolManager *tools.Manager,
) *DingTalkBotBiz {
return &DingTalkBotBiz{
do: do,
handle: handle,
botConfigImpl: botConfigImpl,
replier: chatbot.NewChatbotReplier(),
dingTalkUser: dingTalkUser,
botTools: tools.BootTools,
botGroupImpl: botGroupImpl,
toolManager: toolManager,
chatHis: chatHis,
}
}
func (d *DingTalkBotBiz) GetDingTalkBotCfgList() (dingBotList []entitys.DingTalkBot, err error) {
botConfig := make([]model.AiBotConfig, 0)
cond := builder.NewCond()
cond = cond.And(builder.Eq{"status": constants.Enable})
cond = cond.And(builder.Eq{"bot_type": constants.BotTypeDingTalk})
err = d.botConfigImpl.GetRangeToMapStruct(&cond, &botConfig)
for _, v := range botConfig {
var config entitys.DingTalkBot
err = json.Unmarshal([]byte(v.BotConfig), &config)
if err != nil {
d.log.Info("初始化“%s”失败:%s", v.BotName, err.Error())
}
config.BotIndex = v.BotIndex
dingBotList = append(dingBotList, config)
}
return
}
func (d *DingTalkBotBiz) InitRequire(ctx context.Context, data *chatbot.BotCallbackDataModel) (requireData *entitys.RequireDataDingTalkBot, err error) {
requireData = &entitys.RequireDataDingTalkBot{
Req: data,
Ch: make(chan entitys.Response, 2),
}
return
}
func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
entitys.ResLoading(requireData.Ch, "", "收到消息,正在处理中,请稍等")
defer close(requireData.Ch)
switch constants.ConversationType(requireData.Req.ConversationType) {
case constants.ConversationTypeSingle:
err = d.handleSingleChat(ctx, requireData)
case constants.ConversationTypeGroup:
err = d.handleGroupChat(ctx, requireData)
default:
err = errors.New("未知的聊天类型:" + requireData.Req.ConversationType)
}
if err != nil {
entitys.ResText(requireData.Ch, "", err.Error())
}
return
}
func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
entitys.ResLog(requireData.Ch, "", "个人聊天暂未开启,请期待后续更新")
return
//requireData.UserInfo, err = d.dingTalkUser.GetUserInfoFromBot(ctx, requireData.Req.SenderStaffId, dingtalk.WithId(1))
//if err != nil {
// return
//}
//requireData.ID=requireData.UserInfo.UserID
////如果不是管理或者不是老板,则进行权限判断
//if requireData.UserInfo.IsSenior == constants.IsSeniorFalse && requireData.UserInfo.IsBoss == constants.IsBossFalse {
//
//}
//return
}
func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
group, err := d.initGroup(ctx, requireData.Req.ConversationId, requireData.Req.ConversationTitle)
if err != nil {
return
}
requireData.ID = group.GroupID
groupTools, err := d.getGroupTools(ctx, group)
if err != nil {
return
}
rec, err := d.recognize(ctx, requireData, groupTools)
if err != nil {
return
}
return d.handleMatch(ctx, rec)
}
func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string) (group *model.AiBotGroup, err error) {
group, err = d.botGroupImpl.GetByConversationId(conversationId)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return
}
}
if group.GroupID == 0 {
group = &model.AiBotGroup{
ConversationID: conversationId,
Title: conversationTitle,
ToolList: "",
}
//如果不存在则创建
d.botGroupImpl.Add(group)
}
return
}
func (d *DingTalkBotBiz) getGroupTools(ctx context.Context, group *model.AiBotGroup) (tools []model.AiBotTool, err error) {
if len(d.botTools) == 0 {
return
}
var (
groupRegisTools = make(map[int]struct{})
)
if group.ToolList != "" {
groupToolList := strings.Split(group.ToolList, ",")
for _, tool := range groupToolList {
if tool == "" {
continue
}
num, _err := strconv.Atoi(tool)
if _err != nil {
continue
}
groupRegisTools[num] = struct{}{}
}
}
for _, v := range d.botTools {
if v.PermissionType == constants.PermissionTypeNone {
tools = append(tools, v)
continue
}
if _, ex := groupRegisTools[int(v.ToolID)]; ex {
tools = append(tools, v)
}
}
return
}
func (d *DingTalkBotBiz) recognize(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, tools []model.AiBotTool) (rec *entitys.Recognize, err error) {
userContent, err := d.getUserContent(requireData.Req.Msgtype, requireData.Req.Text.Content)
if err != nil {
return nil, err
}
rec = &entitys.Recognize{
Ch: requireData.Ch,
SystemPrompt: d.defaultPrompt(),
UserContent: userContent,
}
if len(tools) > 0 {
rec.Tasks = make([]entitys.RegistrationTask, 0, len(tools))
for _, task := range tools {
taskConfig := entitys.TaskConfigDetail{}
if err = json.Unmarshal([]byte(task.Config), &taskConfig); err != nil {
log.Errorf("解析任务配置失败: %s, 任务ID: %s", err.Error(), task.Index)
continue // 解析失败时跳过该任务,而不是直接返回错误
}
rec.Tasks = append(rec.Tasks, entitys.RegistrationTask{
Name: task.Index,
Desc: task.Desc,
TaskConfigDetail: taskConfig, // 直接使用解析后的配置,避免重复构建
})
}
}
err = d.handle.Recognize(ctx, rec, &do.WithDingTalkBot{})
return
}
func (d *DingTalkBotBiz) getUserContent(msgType string, msgContent interface{}) (content *entitys.RecognizeUserContent, err error) {
switch constants.BotMsgType(msgType) {
case constants.BotMsgTypeText:
content = &entitys.RecognizeUserContent{
Text: msgContent.(string),
}
default:
return nil, errors.New("未知的消息类型:" + msgType)
}
return
}
func (d *DingTalkBotBiz) handleMatch(ctx context.Context, rec *entitys.Recognize) (err error) {
if !rec.Match.IsMatch {
if len(rec.Match.Chat) != 0 {
entitys.ResText(rec.Ch, "", rec.Match.Chat)
} else {
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
}
return
}
var pointTask *model.AiBotTool
for _, task := range d.botTools {
if task.Index == rec.Match.Index {
pointTask = &task
break
}
}
if pointTask == nil || pointTask.Index == "other" {
return d.otherTask(ctx, rec)
}
switch constants.TaskType(pointTask.Type) {
//case constants.TaskTypeApi:
//return d.handleApiTask(ctx, requireData, pointTask)
case constants.TaskTypeFunc:
return d.handleTask(ctx, rec, pointTask)
default:
return d.otherTask(ctx, rec)
}
return
}
func (d *DingTalkBotBiz) handleTask(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool) (err error) {
var configData entitys.ConfigDataTool
err = json.Unmarshal([]byte(task.Config), &configData)
if err != nil {
return
}
err = d.toolManager.ExecuteTool(ctx, configData.Tool, rec)
if err != nil {
return
}
return
}
func (d *DingTalkBotBiz) otherTask(ctx context.Context, rec *entitys.Recognize) (err error) {
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
return
}
func (d *DingTalkBotBiz) HandleRes(ctx context.Context, data *chatbot.BotCallbackDataModel, resp entitys.Response) error {
switch resp.Type {
case entitys.ResponseText:
return d.replyText(ctx, data.SessionWebhook, resp.Content)
case entitys.ResponseStream:
return d.replySteam(ctx, data.SessionWebhook, resp.Content)
case entitys.ResponseImg:
return d.replyImg(ctx, data.SessionWebhook, resp.Content)
case entitys.ResponseFile:
return d.replyFile(ctx, data.SessionWebhook, resp.Content)
case entitys.ResponseMarkdown:
return d.replyMarkdown(ctx, data.SessionWebhook, resp.Content)
case entitys.ResponseActionCard:
return d.replyActionCard(ctx, data.SessionWebhook, resp.Content)
default:
return nil
}
}
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 {
msg := content
if len(arg) > 0 {
msg = fmt.Sprintf(content, arg)
}
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
}
func (d *DingTalkBotBiz) replyText(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
msg := content
if len(arg) > 0 {
msg = fmt.Sprintf(content, arg)
}
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
}
func (d *DingTalkBotBiz) replyImg(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
msg := content
if len(arg) > 0 {
msg = fmt.Sprintf(content, arg)
}
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
}
func (d *DingTalkBotBiz) replyFile(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
msg := content
if len(arg) > 0 {
msg = fmt.Sprintf(content, arg)
}
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
}
func (d *DingTalkBotBiz) replyMarkdown(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
msg := content
if len(arg) > 0 {
msg = fmt.Sprintf(content, arg)
}
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
}
func (d *DingTalkBotBiz) replyActionCard(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
msg := content
if len(arg) > 0 {
msg = fmt.Sprintf(content, arg)
}
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\": \"京东月卡\"}")。`
}