diff --git a/go.mod b/go.mod index 095f736..30a9fdb 100644 --- a/go.mod +++ b/go.mod @@ -62,6 +62,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/duke-git/lancet/v2 v2.3.8 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/eino-contrib/jsonschema v1.0.3 // indirect github.com/eino-contrib/ollama v0.1.0 // indirect diff --git a/go.sum b/go.sum index 7d1f8b1..1c24ee2 100644 --- a/go.sum +++ b/go.sum @@ -155,6 +155,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/duke-git/lancet/v2 v2.3.8 h1:dlkqn6Nj2LRWFuObNxttkMHxrFeaV6T26JR8jbEVbPg= +github.com/duke-git/lancet/v2 v2.3.8/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0= diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index 0d8dd0f..4820961 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -12,6 +12,7 @@ import ( "ai_scheduler/internal/tools" "ai_scheduler/internal/tools/bbxt" "ai_scheduler/tmpl/dataTemp" + "ai_scheduler/utils" "context" "database/sql" "encoding/json" @@ -27,6 +28,7 @@ import ( "github.com/alibabacloud-go/dingtalk/card_1_0" "github.com/alibabacloud-go/tea/tea" + "github.com/duke-git/lancet/v2/slice" "github.com/gofiber/fiber/v2/log" "xorm.io/builder" ) @@ -44,6 +46,7 @@ type DingTalkBotBiz struct { botGroupQywxImpl *impl.BotGroupQywxImpl toolManager *tools.Manager chatHis *impl.BotChatHisImpl + botUserImpl *impl.BotUserImpl conf *config.Config cardSend *dingtalk.SendCardClient qywxGroupHandle *qywx.Group @@ -53,6 +56,9 @@ type DingTalkBotBiz struct { dingtalkOauth2Client *dingtalkPkg.Oauth2Client dingTalkOld *dingtalkPkg.OldClient dingtalkCardClient *dingtalkPkg.CardClient + rdb *utils.Rdb + issueImpl *impl.IssueImpl + sysImpl *impl.SysImpl } // NewDingTalkBotBiz @@ -64,6 +70,7 @@ func NewDingTalkBotBiz( botGroupConfigImpl *impl.BotGroupConfigImpl, dingTalkUser *dingtalk.User, chatHis *impl.BotChatHisImpl, + botUserImpl *impl.BotUserImpl, reportDailyCacheImpl *impl.ReportDailyCacheImpl, toolManager *tools.Manager, conf *config.Config, @@ -73,6 +80,9 @@ func NewDingTalkBotBiz( dingtalkOauth2Client *dingtalkPkg.Oauth2Client, dingTalkOld *dingtalkPkg.OldClient, dingtalkCardClient *dingtalkPkg.CardClient, + rdb *utils.Rdb, + issueImpl *impl.IssueImpl, + sysImpl *impl.SysImpl, ) *DingTalkBotBiz { return &DingTalkBotBiz{ do: do, @@ -85,6 +95,7 @@ func NewDingTalkBotBiz( botGroupConfigImpl: botGroupConfigImpl, toolManager: toolManager, chatHis: chatHis, + botUserImpl: botUserImpl, conf: conf, cardSend: cardSend, reportDailyCacheImpl: reportDailyCacheImpl, @@ -92,6 +103,9 @@ func NewDingTalkBotBiz( dingtalkOauth2Client: dingtalkOauth2Client, dingTalkOld: dingTalkOld, dingtalkCardClient: dingtalkCardClient, + rdb: rdb, + issueImpl: issueImpl, + sysImpl: sysImpl, } } @@ -141,19 +155,249 @@ func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDat return } +// handleSingleChat 单聊处理 +// 先不接意图识别-仅提供问题处理 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 + // 1. 获取用户信息 + requireData.UserInfo, err = d.dingTalkUser.GetUserInfoFromBot(ctx, requireData.Req.SenderStaffId, dingtalk.WithId(1)) + if err != nil { + return + } + requireData.ID = int32(requireData.UserInfo.UserId) + + // 2. 检查会话状态 (Redis) + statusKey := fmt.Sprintf("ai_bot:session:status:%s", requireData.Req.SenderStaffId) + status, _ := d.rdb.Rdb.Get(ctx, statusKey).Result() + + if status == "WAITING_FOR_SYS_CONFIRM" { + // 用户回复了系统名称 + sysName := requireData.Req.Text.Content + d.rdb.Rdb.Del(ctx, statusKey) + return d.handleWithSpecificSys(ctx, requireData, sysName) + } + + // 3. 获取历史记录 (最近6轮用户输入) + userHist, err := d.getRecentUserHistory(ctx, constants.ConversationTypeSingle, requireData.ID, 6) + if err != nil { + return err + } + + // 4. 改写 Query (Query Rewriting) + rewrittenQuery, err := d.handle.RewriteQuery(ctx, userHist, requireData.Req.Text.Content) + var queryText = requireData.Req.Text.Content + if err == nil && rewrittenQuery != "" { + queryText = rewrittenQuery + } + + // 构造识别对象 + rec := &entitys.Recognize{ + Ch: requireData.Ch, + SystemPrompt: d.defaultPrompt(), + UserContent: &entitys.RecognizeUserContent{ + Text: queryText, + }, + } + + // 5. 调用知识库 + isRetrieved, err := d.groupConfigBiz.handleKnowledge(ctx, rec, nil, requireData.Req) + if err != nil { + return err + } + + if isRetrieved { + return nil + } + + // 6. Fallback: 分类 -> 规则 -> 拉群 + return d.fallbackToGroupCreation(ctx, requireData) +} + +// handleWithSpecificSys 处理用户明确指定的系统 +func (d *DingTalkBotBiz) handleWithSpecificSys(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, sysName string) error { + // 1. 查找系统 + var sys model.AiSy + cond := builder.NewCond().And(builder.Eq{"sys_name": sysName}) + err := d.sysImpl.GetOneBySearchToStrut(&cond, &sys) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + entitys.ResText(requireData.Ch, "", "抱歉,我还是没有找到名为“"+sysName+"”的系统。请联系管理员确认系统名称。") + return nil + } + return err + } + + // 2. 既然已经明确了系统,直接尝试拉群(这里假设问题类型为“其他”或由LLM再次分析) + // 为简化,这里再次调用分类逻辑,但带上已确定的系统 + return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys) +} + +// getRecentUserHistory 获取最近的用户输入历史 +func (d *DingTalkBotBiz) getRecentUserHistory(ctx context.Context, conversationType constants.ConversationType, id int32, limit int) ([]model.AiBotChatHi, error) { + var his []model.AiBotChatHi + cond := builder.NewCond(). + And(builder.Eq{"his_type": conversationType}). + And(builder.Eq{"id": id}). + And(builder.Eq{"role": "user"}) + + _, err := d.chatHis.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: limit}, &his, "his_id desc") + if err != nil { + return nil, err + } + return his, nil +} + +// fallbackToGroupCreation 分类并拉群 +func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) error { + // 1. 获取所有系统和问题类型用于分类 + allSys, err := d.sysImpl.FindAll() + if err != nil { + return err + } + sysNames := slice.Map(allSys, func(_ int, sys model.AiSy) string { + return sys.SysName + }) + allIssueTypes, err := d.issueImpl.IssueType.FindAll() + if err != nil { + return err + } + issueTypeNames := slice.Map(allIssueTypes, func(_ int, it model.AiIssueType) string { + return it.Name + }) + + // 2. LLM 分类 + classification, err := d.handle.ClassifyIssue(ctx, sysNames, issueTypeNames, requireData.Req.Text.Content) + if err != nil { + // 分类失败,使用兜底 + return d.createDefaultGroup(ctx, requireData, "系统无法识别") + } + + // 3. 匹配系统 + var sys model.AiSy + for _, s := range allSys { + if s.SysName == classification.SysName { + sys = s + break + } + } + + if sys.SysID == 0 { + // 无法明确系统,询问用户 + statusKey := fmt.Sprintf("ai_bot:session:status:%s", requireData.Req.SenderStaffId) + d.rdb.Rdb.Set(ctx, statusKey, "WAITING_FOR_SYS_CONFIRM", time.Hour) + entitys.ResText(requireData.Ch, "", "抱歉,我无法确定您咨询的是哪个系统。请告诉我具体系统名称(如:直连天下系统、货易通系统),以便我为您安排对应的技术支持。") + return nil + } + + return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys) +} + +// fallbackToGroupCreationWithSys 在已知系统的情况下进行分类并拉群 +func (d *DingTalkBotBiz) fallbackToGroupCreationWithSys(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, sys *model.AiSy) error { + // 1. 获取所有问题类型 + allIssueTypes, err := d.issueImpl.IssueType.FindAll() + if err != nil { + return err + } + issueTypeNames := slice.Map(allIssueTypes, func(_ int, it model.AiIssueType) string { + return it.Name + }) + + // 2. LLM 再次分类(确定问题类型和简述) + classification, err := d.handle.ClassifyIssue(ctx, []string{sys.SysName}, issueTypeNames, requireData.Req.Text.Content) + if err != nil { + return d.createDefaultGroup(ctx, requireData, "问题类型识别失败") + } + issueType, found, err := d.issueImpl.IssueType.FindOne(d.issueImpl.WithName(classification.IssueTypeName)) + if !found { + log.Errorf("issue type %s not found; err: %v", classification.IssueTypeName, err) + return fmt.Errorf("问题类型 %s 不存在", classification.IssueTypeName) + } + + // 3. 查找分配规则 + rule, found, err := d.issueImpl.IssueAssignRule.FindOne( + d.issueImpl.WithSysID(sys.SysID), + d.issueImpl.WithIssueTypeID(issueType.ID), + d.issueImpl.WithStatus(1), + ) + if !found { + log.Errorf("assign rule not found for sys %s and issue type %s; err: %v", sys.SysName, issueType.Name, err) + return fmt.Errorf("分配规则 %s-%s 不存在", sys.SysName, issueType.Name) + } + + var staffIds []string + if rule.ID != 0 { + // 获取配置的用户 + assignUsers, err := d.issueImpl.IssueAssignUser.FindAll(d.issueImpl.WithRuleID(rule.ID)) + if len(assignUsers) == 0 { + log.Errorf("assign user not found for rule %d; err: %v", rule.ID, err) + return fmt.Errorf("分配用户 %d 不存在", rule.ID) + } + userIds := slice.Map(assignUsers, func(_ int, au model.AiIssueAssignUser) int32 { + return au.UserID + }) + // 获取有效用户 + botUsers, err := d.botUserImpl.GetByUserIds(userIds) + if err != nil { + return err + } + + // 仅获取有效用户的 staff_id + for _, au := range assignUsers { + botUser, found := slice.Find(botUsers, func(_ int, bu model.AiBotUser) bool { + return bu.UserID == au.UserID + }) + if found && botUser.StaffID != "" { + staffIds = append(staffIds, botUser.StaffID) + } + } + } + + // 兜底处理人 + if len(staffIds) == 0 { + staffIds = []string{"17415698414368678"} + } + + // 合并提问者 + staffIds = append([]string{requireData.Req.SenderStaffId}, staffIds...) + + // 4. 拉群 + groupName := fmt.Sprintf("[%s]-%s", classification.IssueTypeName, classification.Summary) + return d.createIssueHandlingGroupAndInitSingle(ctx, requireData.Req.RobotCode, groupName, staffIds, classification.Summary) +} + +// createDefaultGroup 兜底拉群 +func (d *DingTalkBotBiz) createDefaultGroup(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, reason string) error { + userIds := []string{requireData.Req.SenderStaffId, "17415698414368678"} + groupName := fmt.Sprintf("[未知]-%s", reason) + return d.createIssueHandlingGroupAndInitSingle(ctx, requireData.Req.RobotCode, groupName, userIds, "由于无法识别具体问题类型,已为您拉起默认技术支持群。") +} + +// createIssueHandlingGroupAndInitSingle 创建群聊并初始化(单聊专用) +func (d *DingTalkBotBiz) createIssueHandlingGroupAndInitSingle(ctx context.Context, robotCode string, groupName string, userIds []string, summary string) error { + // 获取机器人配置 + appKey, err := d.botConfigImpl.GetRobotAppKey(robotCode) + if err != nil { + log.Errorf("get robot app key failed; err: %v", err) + return fmt.Errorf("未找到机器人配置") + } + + // 获取 access_token + accessToken, err := d.dingtalkOauth2Client.GetAccessToken(appKey) + if err != nil { + log.Errorf("get access token failed; err: %v", err) + return fmt.Errorf("获取 access token 失败") + } + appKey.AccessToken = accessToken + + // 创建群聊 + _, openConversationId, err := d.createIssueHandlingGroup(ctx, accessToken, groupName, userIds) + if err != nil { + log.Errorf("create issue handling group failed; err: %v", err) + return fmt.Errorf("创建群聊失败") + } + + // 初始化群聊 + return d.initIssueHandlingGroup(appKey, openConversationId, summary) } func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { @@ -504,7 +748,7 @@ func (d *DingTalkBotBiz) createIssueHandlingGroupAndInit(ctx context.Context, ca appKey.AccessToken = accessToken // 创建群聊 - _, openConversationId, err := d.createIssueHandlingGroup(ctx, accessToken, userIds) + _, openConversationId, err := d.createIssueHandlingGroup(ctx, accessToken, "问题处理群", userIds) if err != nil { return err } @@ -534,18 +778,22 @@ func (d *DingTalkBotBiz) createIssueHandlingGroupAndInit(ctx context.Context, ca } // createIssueHandlingGroup 创建问题处理群聊会话 -func (d *DingTalkBotBiz) createIssueHandlingGroup(ctx context.Context, accessToken string, userIds []string) (chatId, openConversationId string, err error) { +func (d *DingTalkBotBiz) createIssueHandlingGroup(ctx context.Context, accessToken string, groupName string, userIds []string) (chatId, openConversationId string, err error) { // 是否使用模板群开关 var useTemplateGroup bool = true + if groupName == "" { + groupName = "问题处理群" + } + // 创建内部群会话 if !useTemplateGroup { - return d.dingTalkOld.CreateInternalGroupConversation(ctx, accessToken, "问题处理群", userIds) + return d.dingTalkOld.CreateInternalGroupConversation(ctx, accessToken, groupName, userIds) } // 根据群模板ID创建群 if useTemplateGroup { - return d.dingTalkOld.CreateSceneGroupConversation(ctx, accessToken, "问题处理群", userIds, d.conf.Dingtalk.SceneGroup.GroupTemplateIDIssueHandling) + return d.dingTalkOld.CreateSceneGroupConversation(ctx, accessToken, groupName, userIds, d.conf.Dingtalk.SceneGroup.GroupTemplateIDIssueHandling) } return diff --git a/internal/biz/do/handle.go b/internal/biz/do/handle.go index b9432ea..fa6bd57 100644 --- a/internal/biz/do/handle.go +++ b/internal/biz/do/handle.go @@ -12,14 +12,12 @@ import ( "ai_scheduler/internal/domain/workflow/runtime" "ai_scheduler/internal/entitys" "ai_scheduler/internal/gateway" - "ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg/dingtalk" "ai_scheduler/internal/pkg/l_request" "ai_scheduler/internal/pkg/mapstructure" "ai_scheduler/internal/pkg/rec_extra" "ai_scheduler/internal/pkg/util" "ai_scheduler/internal/tools" - "ai_scheduler/internal/tools/public" "bufio" errorsSpecial "errors" "io" @@ -33,6 +31,7 @@ import ( "github.com/coze-dev/coze-go" "github.com/gofiber/fiber/v2/log" + "github.com/ollama/ollama/api" "gorm.io/gorm/utils" ) @@ -93,6 +92,79 @@ func (r *Handle) Recognize(ctx context.Context, rec *entitys.Recognize, promptPr return } +// RewriteQuery 改写查询词,支持多轮对话 +func (r *Handle) RewriteQuery(ctx context.Context, history []model.AiBotChatHi, currentQuery string) (string, error) { + if len(history) == 0 { + return currentQuery, nil + } + + var histStr strings.Builder + for _, h := range history { + role := "用户" + if h.Role != "user" { + role = "助手" + } + histStr.WriteString(fmt.Sprintf("%s: %s\n", role, h.Content)) + } + + systemPrompt := `你是一个搜索查询改写专家。请结合用户的历史对话上下文,将用户当前的输入改写为一个独立的、语义完整的、适合知识库检索的中文查询词。 +要求: +1. 保持原意,补全指代(如“它”、“刚才那个问题”)。 +2. 只返回改写后的查询词,不要有任何解释。 +3. 如果当前输入已经很完整,直接返回原句。` + + userPrompt := fmt.Sprintf("### 历史对话:\n%s\n### 当前输入:\n%s\n### 改写后的查询词:", histStr.String(), currentQuery) + + messages := []api.Message{ + {Role: "system", Content: systemPrompt}, + {Role: "user", Content: userPrompt}, + } + + return r.Ollama.Chat(ctx, messages) +} + +type IssueClassification struct { + SysName string `json:"sys_name"` + IssueTypeName string `json:"issue_type_name"` + Summary string `json:"summary"` + Reason string `json:"reason"` +} + +// ClassifyIssue 问题分类分析 +func (r *Handle) ClassifyIssue(ctx context.Context, systems []string, issueTypes []string, userInput string) (*IssueClassification, error) { + systemPrompt := fmt.Sprintf(`你是一个技术支持路由专家。请分析用户的输入,并将其归类到最合适的系统和问题类型中。 +可用系统列表: [%s] +可用问题类型: [%s] + +请仅以 JSON 格式回复,包含以下字段: +- sys_name: 系统名称 +- issue_type_name: 问题类型名称 +- summary: 15字以内的问题简述(用于群命名) +- reason: 分类理由`, strings.Join(systems, ", "), strings.Join(issueTypes, ", ")) + + messages := []api.Message{ + {Role: "system", Content: systemPrompt}, + {Role: "user", Content: userInput}, + } + + resp, err := r.Ollama.Chat(ctx, messages) + if err != nil { + return nil, err + } + + // 尝试清理 JSON 内容(有时模型会返回 markdown 块) + resp = strings.TrimPrefix(resp, "```json") + resp = strings.TrimSuffix(resp, "```") + resp = strings.TrimSpace(resp) + + var result IssueClassification + if err := json.Unmarshal([]byte(resp), &result); err != nil { + return nil, fmt.Errorf("解析分类结果失败: %w, 原文: %s", err, resp) + } + + return &result, nil +} + func (r *Handle) handleOtherTask(ctx context.Context, requireData *entitys.RequireData) (err error) { entitys.ResText(requireData.Ch, "", requireData.Match.Reasoning) return @@ -166,87 +238,87 @@ func (r *Handle) handleTask(ctx context.Context, rec *entitys.Recognize, task *m } // 知识库 -func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) { +// func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) { - var ( - configData entitys.ConfigDataTool - sessionIdKnowledge string - query string - host string - ) - err = json.Unmarshal([]byte(task.Config), &configData) - if err != nil { - return - } - ext, err := rec_extra.GetTaskRecExt(rec) - if err != nil { - return - } - // 通过session 找到知识库session - var has bool - if len(ext.Session) == 0 { - return errors.SessionNotFound - } - ext.SessionInfo, has, err = r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(ext.Session)) - if err != nil { - return - } else if !has { - return errors.SessionNotFound - } +// var ( +// configData entitys.ConfigDataTool +// sessionIdKnowledge string +// query string +// host string +// ) +// err = json.Unmarshal([]byte(task.Config), &configData) +// if err != nil { +// return +// } +// ext, err := rec_extra.GetTaskRecExt(rec) +// if err != nil { +// return +// } +// // 通过session 找到知识库session +// var has bool +// if len(ext.Session) == 0 { +// return errors.SessionNotFound +// } +// ext.SessionInfo, has, err = r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(ext.Session)) +// if err != nil { +// return +// } else if !has { +// return errors.SessionNotFound +// } - // 找到知识库的host - { - tool, exists := r.toolManager.GetTool(configData.Tool) - if !exists { - return fmt.Errorf("tool not found: %s", configData.Tool) - } +// // 找到知识库的host +// { +// tool, exists := r.toolManager.GetTool(configData.Tool) +// if !exists { +// return fmt.Errorf("tool not found: %s", configData.Tool) +// } - if knowledgeTool, ok := tool.(*public.KnowledgeBaseTool); !ok { - return fmt.Errorf("未找到知识库Tool: %s", configData.Tool) - } else { - host = knowledgeTool.GetConfig().BaseURL - } +// if knowledgeTool, ok := tool.(*public.KnowledgeBaseTool); !ok { +// return fmt.Errorf("未找到知识库Tool: %s", configData.Tool) +// } else { +// host = knowledgeTool.GetConfig().BaseURL +// } - } +// } - // 知识库的session为空,请求知识库获取, 并绑定 - if ext.SessionInfo.KnowlegeSessionID == "" { - // 请求知识库 - if sessionIdKnowledge, err = public.GetKnowledgeBaseSession(host, ext.Sys.KnowlegeBaseID, ext.Sys.KnowlegeTenantKey); err != nil { - return - } +// // 知识库的session为空,请求知识库获取, 并绑定 +// if ext.SessionInfo.KnowlegeSessionID == "" { +// // 请求知识库 +// if sessionIdKnowledge, err = public.GetKnowledgeBaseSession(host, ext.Sys.KnowlegeBaseID, ext.Sys.KnowlegeTenantKey); err != nil { +// return +// } - // 绑定知识库session,下次可以使用 - ext.SessionInfo.KnowlegeSessionID = sessionIdKnowledge - if err = r.sessionImpl.Update(&ext.SessionInfo, r.sessionImpl.WithSessionId(ext.SessionInfo.SessionID)); err != nil { - return - } - } +// // 绑定知识库session,下次可以使用 +// ext.SessionInfo.KnowlegeSessionID = sessionIdKnowledge +// if err = r.sessionImpl.Update(&ext.SessionInfo, r.sessionImpl.WithSessionId(ext.SessionInfo.SessionID)); err != nil { +// return +// } +// } - // 用户输入解析 - var ok bool - input := make(map[string]string) - if err = json.Unmarshal([]byte(rec.Match.Parameters), &input); err != nil { - return - } - if query, ok = input["query"]; !ok { - return fmt.Errorf("query不能为空") - } +// // 用户输入解析 +// var ok bool +// input := make(map[string]string) +// if err = json.Unmarshal([]byte(rec.Match.Parameters), &input); err != nil { +// return +// } +// if query, ok = input["query"]; !ok { +// return fmt.Errorf("query不能为空") +// } - ext.KnowledgeConf = entitys.KnowledgeBaseRequest{ - Session: ext.SessionInfo.KnowlegeSessionID, - ApiKey: ext.Sys.KnowlegeTenantKey, - Query: query, - } - rec.Ext = pkg.JsonByteIgonErr(ext) - // 执行工具 - err = r.toolManager.ExecuteTool(ctx, configData.Tool, rec) - if err != nil { - return - } +// ext.KnowledgeConf = entitys.KnowledgeBaseRequest{ +// Session: ext.SessionInfo.KnowlegeSessionID, +// ApiKey: ext.Sys.KnowlegeTenantKey, +// Query: query, +// } +// rec.Ext = pkg.JsonByteIgonErr(ext) +// // 执行工具 +// err = r.toolManager.ExecuteTool(ctx, configData.Tool, rec) +// if err != nil { +// return +// } - return -} +// return +// } // 知识库V2 - lightRAG自建 func (r *Handle) handleKnowleV2(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) { diff --git a/internal/biz/group_config.go b/internal/biz/group_config.go index 5c2146b..8e97c5e 100644 --- a/internal/biz/group_config.go +++ b/internal/biz/group_config.go @@ -270,7 +270,8 @@ func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize case constants.TaskTypeCozeWorkflow: return g.handleCozeWorkflow(ctx, rec, pointTask) case constants.TaskTypeKnowle: // 知识库lightRAG版本 - return g.handleKnowledge(ctx, rec, groupConfig, callback) + _, err = g.handleKnowledge(ctx, rec, groupConfig, callback) + return err default: return g.otherTask(ctx, rec) } @@ -477,7 +478,7 @@ func (g *GroupConfigBiz) GetReportCache(ctx context.Context, day time.Time, tota } // handleKnowledge 处理知识库V2版本 -func (g *GroupConfigBiz) handleKnowledge(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (err error) { +func (g *GroupConfigBiz) handleKnowledge(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (isRetrieved bool, err error) { // 请求知识库工具 knowledgeBase := knowledge_base.New(g.conf.KnowledgeConfig) knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{ @@ -489,19 +490,19 @@ func (g *GroupConfigBiz) handleKnowledge(ctx context.Context, rec *entitys.Recog OnlyRAG: true, }) if err != nil { - return fmt.Errorf("请求知识库工具失败,err: %v", err) + return false, fmt.Errorf("请求知识库工具失败,err: %v", err) } // 读取知识库SSE数据 - isRetrieved, err := g.readKnowledgeSSE(knowledgeResp, rec.Ch, true) + isRetrieved, err = g.readKnowledgeSSE(knowledgeResp, rec.Ch, true) if err != nil { return } - // 未检索到匹配信息,询问是否拉群 - if !isRetrieved { + // 未检索到匹配信息,群聊时询问是否拉群 + if !isRetrieved && callback.ConversationType == constants.ConversationTypeGroup { g.shouldCreateIssueHandlingGroup(ctx, rec, groupConfig, callback) - return nil + return false, nil } return diff --git a/internal/biz/llm_service/ollama.go b/internal/biz/llm_service/ollama.go index 7333de5..eb74adc 100644 --- a/internal/biz/llm_service/ollama.go +++ b/internal/biz/llm_service/ollama.go @@ -62,6 +62,14 @@ func (r *OllamaService) IntentRecognize(ctx context.Context, req *entitys.ToolSe return } +func (r *OllamaService) Chat(ctx context.Context, messages []api.Message) (string, error) { + res, err := r.client.Chat(ctx, r.config.Ollama.Model, messages) + if err != nil { + return "", err + } + return res.Message.Content, nil +} + //func (r *OllamaService) RecognizeWithImg(ctx context.Context, imgByte []api.ImageData, ch chan entitys.Response) (desc api.GenerateResponse, err error) { // if imgByte == nil { // return diff --git a/internal/data/impl/ai_issue_manager.go b/internal/data/impl/ai_issue_manager.go new file mode 100644 index 0000000..c827bd1 --- /dev/null +++ b/internal/data/impl/ai_issue_manager.go @@ -0,0 +1,64 @@ +package impl + +import ( + "ai_scheduler/internal/data/model" + "ai_scheduler/utils" + + "gorm.io/gorm" +) + +type IssueImpl struct { + IssueType BaseRepository[model.AiIssueType] + IssueAssignRule BaseRepository[model.AiIssueAssignRule] + IssueAssignUser BaseRepository[model.AiIssueAssignUser] +} + +func NewIssueImpl(db *utils.Db) *IssueImpl { + return &IssueImpl{ + IssueType: NewBaseModel[model.AiIssueType](db.Client), + IssueAssignRule: NewBaseModel[model.AiIssueAssignRule](db.Client), + IssueAssignUser: NewBaseModel[model.AiIssueAssignUser](db.Client), + } +} + +// WithName 名称查询 +func (a *IssueImpl) WithName(name string) CondFunc { + return func(db *gorm.DB) *gorm.DB { + return db.Where("name = ?", name) + } +} + +// WithCode 编码查询 +func (a *IssueImpl) WithCode(code string) CondFunc { + return func(db *gorm.DB) *gorm.DB { + return db.Where("code = ?", code) + } +} + +// WithSysID 系统ID查询 +func (a *IssueImpl) WithSysID(sysID any) CondFunc { + return func(db *gorm.DB) *gorm.DB { + return db.Where("sys_id = ?", sysID) + } +} + +// WithIssueTypeID 问题类型ID查询 +func (a *IssueImpl) WithIssueTypeID(issueTypeID any) CondFunc { + return func(db *gorm.DB) *gorm.DB { + return db.Where("issue_type_id = ?", issueTypeID) + } +} + +// WithRuleID 规则ID查询 +func (a *IssueImpl) WithRuleID(ruleID any) CondFunc { + return func(db *gorm.DB) *gorm.DB { + return db.Where("rule_id = ?", ruleID) + } +} + +// WithStatus 状态查询 +func (a *IssueImpl) WithStatus(status any) CondFunc { + return func(db *gorm.DB) *gorm.DB { + return db.Where("status = ?", status) + } +} diff --git a/internal/data/impl/base.go b/internal/data/impl/base.go index 501f710..f33159f 100644 --- a/internal/data/impl/base.go +++ b/internal/data/impl/base.go @@ -22,7 +22,8 @@ BaseModel 是一个泛型结构体,用于封装GORM数据库通用操作。 // 定义受支持的PO类型集合(可根据需要扩展), 只有包含表结构才能使用BaseModel,避免使用出现问题 type PO interface { model.AiChatHi | - model.AiSy | model.AiSession | model.AiTask | model.AiBotConfig + model.AiSy | model.AiSession | model.AiTask | model.AiBotConfig | + model.AiIssueType | model.AiIssueAssignRule | model.AiIssueAssignUser } type BaseModel[P PO] struct { diff --git a/internal/data/impl/bot_user.go b/internal/data/impl/bot_user.go index 862292f..ce47281 100644 --- a/internal/data/impl/bot_user.go +++ b/internal/data/impl/bot_user.go @@ -5,6 +5,8 @@ import ( "ai_scheduler/tmpl/dataTemp" "ai_scheduler/utils" "database/sql" + + "xorm.io/builder" ) type BotUserImpl struct { @@ -25,3 +27,12 @@ func (k BotUserImpl) GetByStaffId(staffId string) (*model.AiBotUser, error) { } return &data, err } + +func (k BotUserImpl) GetByUserIds(userIds []int32) ([]model.AiBotUser, error) { + var data []model.AiBotUser + cond := builder.NewCond() + cond = cond.And(builder.In("user_id", userIds)) + _, err := k.GetListToStruct2(&cond, nil, &data) + + return data, err +} diff --git a/internal/data/impl/provider_set.go b/internal/data/impl/provider_set.go index c200234..21320f0 100644 --- a/internal/data/impl/provider_set.go +++ b/internal/data/impl/provider_set.go @@ -18,4 +18,5 @@ var ProviderImpl = wire.NewSet( NewBotGroupConfigImpl, NewBotGroupQywxImpl, NewReportDailyCacheImpl, + NewIssueImpl, ) diff --git a/internal/data/model/ai_issue_assign_rule.gen.go b/internal/data/model/ai_issue_assign_rule.gen.go index a1dfbbc..067e578 100644 --- a/internal/data/model/ai_issue_assign_rule.gen.go +++ b/internal/data/model/ai_issue_assign_rule.gen.go @@ -12,9 +12,9 @@ const TableNameAiIssueAssignRule = "ai_issue_assign_rule" // AiIssueAssignRule AI问题分配规则表,指定系统+问题类型对应分配规则 type AiIssueAssignRule struct { - ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID - SysID int64 `gorm:"column:sys_id;not null;comment:系统ID,关联 ai_sys.id" json:"sys_id"` // 系统ID,关联 ai_sys.id - IssueTypeID int64 `gorm:"column:issue_type_id;not null;comment:问题类型ID,关联 ai_issue_type.id" json:"issue_type_id"` // 问题类型ID,关联 ai_issue_type.id + ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID + SysID int32 `gorm:"column:sys_id;not null;comment:系统ID,关联 ai_sys.id" json:"sys_id"` // 系统ID,关联 ai_sys.id + IssueTypeID int32 `gorm:"column:issue_type_id;not null;comment:问题类型ID,关联 ai_issue_type.id" json:"issue_type_id"` // 问题类型ID,关联 ai_issue_type.id Status int32 `gorm:"column:status;not null;default:1;comment:规则状态:1=启用,0=停用" json:"status"` // 规则状态:1=启用,0=停用 Description string `gorm:"column:description;comment:规则描述,用于说明规则用途" json:"description"` // 规则描述,用于说明规则用途 CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间 diff --git a/internal/data/model/ai_issue_assign_user.gen.go b/internal/data/model/ai_issue_assign_user.gen.go index c05f88d..b45bf28 100644 --- a/internal/data/model/ai_issue_assign_user.gen.go +++ b/internal/data/model/ai_issue_assign_user.gen.go @@ -12,9 +12,9 @@ const TableNameAiIssueAssignUser = "ai_issue_assign_user" // AiIssueAssignUser 规则对应的用户表,命中规则时需要通知的钉钉用户 type AiIssueAssignUser struct { - ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID - RuleID int64 `gorm:"column:rule_id;not null;comment:规则ID,关联 ai_issue_assign_rule.id" json:"rule_id"` // 规则ID,关联 ai_issue_assign_rule.id - UserID int64 `gorm:"column:user_id;not null;comment:钉钉用户ID,关联 ai_bot_user.id" json:"user_id"` // 钉钉用户ID,关联 ai_bot_user.id + ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID + RuleID int32 `gorm:"column:rule_id;not null;comment:规则ID,关联 ai_issue_assign_rule.id" json:"rule_id"` // 规则ID,关联 ai_issue_assign_rule.id + UserID int32 `gorm:"column:user_id;not null;comment:钉钉用户ID,关联 ai_bot_user.id" json:"user_id"` // 钉钉用户ID,关联 ai_bot_user.id CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间 } diff --git a/internal/data/model/ai_issue_type.gen.go b/internal/data/model/ai_issue_type.gen.go index 2a91fe5..13c6eb6 100644 --- a/internal/data/model/ai_issue_type.gen.go +++ b/internal/data/model/ai_issue_type.gen.go @@ -12,7 +12,7 @@ const TableNameAiIssueType = "ai_issue_type" // AiIssueType AI问题类型表 type AiIssueType struct { - ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID + ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID Code string `gorm:"column:code;not null;comment:问题类型编码,例如: ui, bug, demand" json:"code"` // 问题类型编码,例如: ui, bug, demand Name string `gorm:"column:name;not null;comment:问题类型名称,例如: UI问题, Bug, 需求" json:"name"` // 问题类型名称,例如: UI问题, Bug, 需求 CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间