ai_scheduler/internal/biz/callback.go

291 lines
9.4 KiB
Go
Raw Permalink 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/config"
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/domain/tools/common/knowledge_base"
"ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/internal/pkg/util"
"ai_scheduler/internal/pkg/utils_ollama"
"context"
"encoding/json"
"fmt"
"io"
"strings"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"github.com/alibabacloud-go/dingtalk/card_1_0"
"github.com/alibabacloud-go/tea/tea"
"github.com/gofiber/fiber/v2/log"
"github.com/ollama/ollama/api"
)
type CallbackBiz struct {
cfg *config.Config
ollamaClient *utils_ollama.Client
dingtalkCardClient *dingtalk.CardClient
botConfigImpl *impl.BotConfigImpl
}
func NewCallbackBiz(
cfg *config.Config,
ollamaClient *utils_ollama.Client,
dingtalkCardClient *dingtalk.CardClient,
botConfigImpl *impl.BotConfigImpl,
) *CallbackBiz {
return &CallbackBiz{
cfg: cfg,
ollamaClient: ollamaClient,
dingtalkCardClient: dingtalkCardClient,
botConfigImpl: botConfigImpl,
}
}
// IssueHandlingGroup 问题处理群机器人回调
// 能力1 通过[内容提取] 宏分析用户QA问题调出QA表单卡片
// 能力2 通过[QA收集] 宏,收集用户反馈,写入知识库
// 能力3 通过[知识库查询] 宏,查询知识库,返回答案
func (c *CallbackBiz) IssueHandlingGroup(data chatbot.BotCallbackDataModel) error {
// 能力1、2分析用户QA问题写入知识库
if strings.Contains(data.Text.Content, "[内容提取]") || strings.Contains(data.Text.Content, "[QA收集]") {
c.issueHandlingExtractContent(data)
}
// 能力3查询知识库返回答案
if strings.Contains(data.Text.Content, "[知识库查询]") {
c.issueHandlingQueryKnowledgeBase(data)
}
return nil
}
// 问题处理群机器人内容提取
func (c *CallbackBiz) issueHandlingExtractContent(data chatbot.BotCallbackDataModel) {
// 1.提取用户输入
prompt := fmt.Sprintf(constants.IssueHandlingExtractContentPrompt, data.Text.Content)
log.Infof("问题提取提示词: %s", prompt)
// LLM 提取
generateResp, err := c.ollamaClient.Generation(context.Background(), &api.GenerateRequest{
Model: c.cfg.Ollama.GenerateModel,
Prompt: prompt,
Stream: util.AnyToPoint(false),
})
if err != nil {
log.Errorf("问题提取失败: %v", err)
return
}
// 解析 JSON 响应
var resp struct {
Items []struct {
Question string `json:"question"`
Answer string `json:"answer"`
Confidence string `json:"confidence"`
} `json:"items"`
}
if err := json.Unmarshal([]byte(generateResp.Response), &resp); err != nil {
log.Errorf("解析 JSON 响应失败: %v", err)
return
}
// 2.构建文本域内容
cardContentTpl := "问题:%s \n答案%s"
var cardContentList []string
for _, item := range resp.Items {
cardContentList = append(cardContentList, fmt.Sprintf(cardContentTpl, item.Question, item.Answer))
}
cardContent := strings.Join(cardContentList, "\n\n")
// 3.获取应用AppKey
appKey, err := c.botConfigImpl.GetRobotAppKey(data.RobotCode)
if err != nil {
log.Errorf("获取应用配置失败: %v", err)
return
}
// 4.创建并投放卡片
outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode) // 构建卡片 OutTrackId
_, err = c.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.ContentCollect),
OutTrackId: tea.String(outTrackId),
CallbackType: tea.String("HTTP"),
CallbackRouteKey: tea.String(c.cfg.Dingtalk.Card.CallbackRouteKey),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"title": tea.String("QA知识收集"),
"button_display": tea.String("true"),
"QA_details_now": tea.String(cardContent),
"textarea_display": tea.String("normal"),
"action_id": tea.String("collect_qa"),
"tenant_id": tea.String(constants.KnowledgeTenantIdDefault),
"_CARD_DEBUG_TOOL_ENTRY": tea.String(c.cfg.Dingtalk.Card.DebugToolEntryShow), // 调试字段
},
},
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + data.ConversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(c.cfg.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling),
},
},
)
}
// 问题处理群机器人查询知识库
func (c *CallbackBiz) issueHandlingQueryKnowledgeBase(data chatbot.BotCallbackDataModel) {
// 获取应用配置
appKey, err := c.botConfigImpl.GetRobotAppKey(data.RobotCode)
if err != nil {
log.Errorf("应用机器人配置不存在: %s, err: %v", data.RobotCode, err)
return
}
// 创建卡片
outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode)
_, err = c.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.BaseMsg),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"title": tea.String(data.Text.Content),
"markdown": tea.String("知识库检索中..."),
},
},
OutTrackId: tea.String(outTrackId),
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + data.ConversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(data.RobotCode),
},
},
)
// 查询知识库
knowledgeBase := knowledge_base.New(c.cfg.KnowledgeConfig)
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
TenantID: constants.KnowledgeTenantIdDefault,
Query: data.Text.Content,
Mode: constants.KnowledgeModeMix,
Stream: false,
Think: false,
OnlyRAG: true,
})
if err != nil {
log.Errorf("查询知识库失败: %v", err)
return
}
knowledgeRespBytes, err := io.ReadAll(knowledgeResp)
if err != nil {
log.Errorf("读取知识库响应失败: %v", err)
return
}
// 卡片更新
message, isRetrieved, err := knowledge_base.ParseOpenAIHTTPData(string(knowledgeRespBytes))
if err != nil {
log.Errorf("读取知识库 SSE 数据失败: %v", err)
return
}
content := message.Content
if !isRetrieved {
content = "知识库未检测到匹配信息,请核查知识库数据是否正确。"
}
// 卡片更新
_, err = c.dingtalkCardClient.UpdateCard(
appKey,
&card_1_0.UpdateCardRequest{
OutTrackId: tea.String(outTrackId),
CardData: &card_1_0.UpdateCardRequestCardData{
CardParamMap: map[string]*string{
"markdown": tea.String(content),
},
},
CardUpdateOptions: &card_1_0.UpdateCardRequestCardUpdateOptions{
UpdateCardDataByKey: tea.Bool(true),
},
},
)
if err != nil {
log.Errorf("更新卡片失败: %v", err)
return
}
}
// IssueHandlingCollectQA 问题处理群机器人 QA 收集回调
func (c *CallbackBiz) IssueHandlingCollectQA(data card.CardRequest) *card.CardResponse {
// 确认提交,文本写入知识库
if data.CardActionData.CardPrivateData.Params["submit"] == "submit" {
content := data.CardActionData.CardPrivateData.Params["QA_details"].(string)
tenantID := data.CardActionData.CardPrivateData.Params["tenant_id"].(string)
// 协程执行耗时操作,防止阻塞
util.SafeGo("inject_knowledge_base", func() {
knowledgeBase := knowledge_base.New(c.cfg.KnowledgeConfig)
err := knowledgeBase.IngestText(&knowledge_base.IngestTextRequest{
TenantID: tenantID,
Text: content,
})
if err != nil {
log.Errorf("注入知识库失败: %v", err)
} else {
log.Infof("注入知识库成功: tenantID=%s", tenantID)
}
// 解析当前卡片的 ConversationId 和 robotCode
conversationId, robotCode := constants.ParseCardOutTrackId(data.OutTrackId)
// 获取应用配置
appKey, err := c.botConfigImpl.GetRobotAppKey(robotCode)
if err != nil {
log.Errorf("获取应用机器人配置失败: %v", err)
return
}
// 发送卡片通知用户注入成功
outTrackId := constants.BuildCardOutTrackId(conversationId, robotCode)
c.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.BaseMsg),
OutTrackId: tea.String(outTrackId),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"title": tea.String("QA知识收集结果"),
"markdown": tea.String("[Get] **成功**"),
},
},
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + conversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(robotCode),
},
},
)
})
}
// 取消提交,禁用输入框
resp := &card.CardResponse{
CardUpdateOptions: &card.CardUpdateOptions{
UpdateCardDataByKey: true,
},
CardData: &card.CardDataDto{
CardParamMap: map[string]string{
"textarea_display": "disabled",
},
},
}
return resp
}