fix: 1.开放机器人单聊处理 2. 增加问题->负责人路由相关方法 3.多轮对话获取用户实际问题 4.知识库调用 5.建群(无效)
This commit is contained in:
parent
b3b09f184b
commit
3b6471a196
1
go.mod
1
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
|
||||
|
|
|
|||
2
go.sum
2
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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,4 +18,5 @@ var ProviderImpl = wire.NewSet(
|
|||
NewBotGroupConfigImpl,
|
||||
NewBotGroupQywxImpl,
|
||||
NewReportDailyCacheImpl,
|
||||
NewIssueImpl,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"` // 创建时间
|
||||
|
|
|
|||
|
|
@ -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"` // 创建时间
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"` // 创建时间
|
||||
|
|
|
|||
Loading…
Reference in New Issue