Files
ai-agent/internal/services/knowledge_base_service.go
T

332 lines
10 KiB
Go
Raw Normal View History

2026-04-09 10:01:23 +08:00
package services
import (
"context"
"encoding/json"
"fmt"
"strings"
2026-04-09 10:01:23 +08:00
"time"
"agent-desk/internal/ai/rag"
"agent-desk/internal/ai/workflow/dsl"
workflowregistry "agent-desk/internal/ai/workflow/registry"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
2026-04-09 10:01:23 +08:00
"github.com/mlogclub/simple/sqls"
)
var KnowledgeBaseService = newKnowledgeBaseService()
func newKnowledgeBaseService() *knowledgeBaseService {
return &knowledgeBaseService{}
}
type knowledgeBaseService struct {
}
func (s *knowledgeBaseService) Get(id int64) *models.KnowledgeBase {
return repositories.KnowledgeBaseRepository.Get(sqls.DB(), id)
}
func (s *knowledgeBaseService) Take(where ...interface{}) *models.KnowledgeBase {
return repositories.KnowledgeBaseRepository.Take(sqls.DB(), where...)
}
func (s *knowledgeBaseService) Find(cnd *sqls.Cnd) []models.KnowledgeBase {
return repositories.KnowledgeBaseRepository.Find(sqls.DB(), cnd)
}
func (s *knowledgeBaseService) FindOne(cnd *sqls.Cnd) *models.KnowledgeBase {
return repositories.KnowledgeBaseRepository.FindOne(sqls.DB(), cnd)
}
func (s *knowledgeBaseService) FindPageByParams(params *params.QueryParams) (list []models.KnowledgeBase, paging *sqls.Paging) {
return repositories.KnowledgeBaseRepository.FindPageByParams(sqls.DB(), params)
}
func (s *knowledgeBaseService) FindPageByCnd(cnd *sqls.Cnd) (list []models.KnowledgeBase, paging *sqls.Paging) {
return repositories.KnowledgeBaseRepository.FindPageByCnd(sqls.DB(), cnd)
}
func (s *knowledgeBaseService) Count(cnd *sqls.Cnd) int64 {
return repositories.KnowledgeBaseRepository.Count(sqls.DB(), cnd)
}
func (s *knowledgeBaseService) Create(t *models.KnowledgeBase) error {
return repositories.KnowledgeBaseRepository.Create(sqls.DB(), t)
}
func (s *knowledgeBaseService) Update(t *models.KnowledgeBase) error {
return repositories.KnowledgeBaseRepository.Update(sqls.DB(), t)
}
func (s *knowledgeBaseService) Updates(id int64, columns map[string]interface{}) error {
return repositories.KnowledgeBaseRepository.Updates(sqls.DB(), id, columns)
}
func (s *knowledgeBaseService) UpdateColumn(id int64, name string, value interface{}) error {
return repositories.KnowledgeBaseRepository.UpdateColumn(sqls.DB(), id, name, value)
}
func (s *knowledgeBaseService) Delete(id int64) {
repositories.KnowledgeBaseRepository.Delete(sqls.DB(), id)
}
func (s *knowledgeBaseService) CreateKnowledgeBase(req request.CreateKnowledgeBaseRequest, operator *dto.AuthPrincipal) (*models.KnowledgeBase, error) {
if operator == nil {
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
2026-04-09 10:01:23 +08:00
}
item, err := s.buildKnowledgeBaseModel(req)
if err != nil {
return nil, err
}
item.Status = enums.StatusOk
item.AuditFields = utils.BuildAuditFields(operator)
if err := repositories.KnowledgeBaseRepository.Create(sqls.DB(), item); err != nil {
return nil, err
}
return item, nil
}
func (s *knowledgeBaseService) UpdateKnowledgeBase(req request.UpdateKnowledgeBaseRequest, operator *dto.AuthPrincipal) error {
if operator == nil {
return errorsx.UnauthorizedI18n("error.auth.expired")
2026-04-09 10:01:23 +08:00
}
current := s.Get(req.ID)
if current == nil {
return errorsx.InvalidParamI18n("error.e0283")
2026-04-09 10:01:23 +08:00
}
item, err := s.buildKnowledgeBaseModel(req.CreateKnowledgeBaseRequest)
if err != nil {
return err
}
return repositories.KnowledgeBaseRepository.Updates(sqls.DB(), req.ID, map[string]any{
"name": item.Name,
"description": item.Description,
"knowledge_type": item.KnowledgeType,
"default_top_k": item.DefaultTopK,
"default_score_threshold": item.DefaultScoreThreshold,
"default_rerank_limit": item.DefaultRerankLimit,
"chunk_provider": item.ChunkProvider,
"chunk_target_tokens": item.ChunkTargetTokens,
"chunk_max_tokens": item.ChunkMaxTokens,
"chunk_overlap_tokens": item.ChunkOverlapTokens,
"answer_mode": item.AnswerMode,
"remark": item.Remark,
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": time.Now(),
})
}
func (s *knowledgeBaseService) DeleteKnowledgeBase(id int64) error {
current := s.Get(id)
if current == nil {
return errorsx.InvalidParamI18n("error.e0283")
2026-04-09 10:01:23 +08:00
}
referencingWorkflows := s.findWorkflowReferencesByKnowledgeBaseID(id)
if len(referencingWorkflows) > 0 {
if len(referencingWorkflows) == 1 {
return errorsx.Forbidden(fmt.Sprintf("知识库正在被流程「%s」使用,请先从知识检索节点中移除", referencingWorkflows[0]))
}
return errorsx.Forbidden(fmt.Sprintf("知识库正在被 %d 个流程使用,请先从知识检索节点中移除", len(referencingWorkflows)))
2026-04-09 10:01:23 +08:00
}
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
if err := repositories.KnowledgeDocumentRepository.DeleteByKnowledgeBaseID(ctx.Tx, id); err != nil {
return err
}
if err := repositories.KnowledgeFAQRepository.DeleteByKnowledgeBaseID(ctx.Tx, id); err != nil {
return err
}
return ctx.Tx.Delete(&models.KnowledgeBase{}, "id = ?", id).Error
}); err != nil {
return err
2026-04-09 10:01:23 +08:00
}
return rag.Index.RemoveKnowledgeBaseIndex(context.Background(), id)
2026-04-09 10:01:23 +08:00
}
func (s *knowledgeBaseService) findWorkflowReferencesByKnowledgeBaseID(id int64) []string {
names := make(map[string]struct{})
workflows := repositories.AIWorkflowRepository.Find(sqls.DB(), sqls.NewCnd().Eq("status", enums.StatusOk))
workflowNames := make(map[int64]string, len(workflows))
for _, workflow := range workflows {
name := strings.TrimSpace(workflow.Name)
if name == "" {
name = fmt.Sprintf("ID %d", workflow.ID)
}
workflowNames[workflow.ID] = name
if workflowDefinitionUsesKnowledgeBase(workflow.DraftDefinition, id) {
names[name] = struct{}{}
}
}
versions := repositories.AIWorkflowVersionRepository.Find(sqls.DB(), sqls.NewCnd())
for _, version := range versions {
if !workflowDefinitionUsesKnowledgeBase(version.Definition, id) {
continue
}
name := workflowNames[version.WorkflowID]
if strings.TrimSpace(name) == "" {
name = fmt.Sprintf("ID %d", version.WorkflowID)
}
names[name] = struct{}{}
}
ret := make([]string, 0, len(names))
for name := range names {
ret = append(ret, name)
}
return ret
}
func workflowDefinitionUsesKnowledgeBase(definition string, id int64) bool {
definition = strings.TrimSpace(definition)
if definition == "" {
return false
}
var def dsl.Definition
if err := json.Unmarshal([]byte(definition), &def); err != nil {
return false
}
for _, node := range def.Nodes {
if strings.TrimSpace(node.Type) != workflowregistry.NodeTypeKnowledgeRetrieve {
continue
}
for _, knowledgeBaseID := range knowledgeBaseIDsFromWorkflowNodeConfig(node.Data.Config) {
if knowledgeBaseID == id {
return true
}
}
}
return false
}
func knowledgeBaseIDsFromWorkflowNodeConfig(raw json.RawMessage) []int64 {
if len(raw) == 0 {
return nil
}
var cfg map[string]any
if err := json.Unmarshal(raw, &cfg); err != nil {
return nil
}
items, ok := cfg["knowledgeBaseIds"].([]any)
if !ok {
return nil
}
ret := make([]int64, 0, len(items))
for _, item := range items {
switch value := item.(type) {
case float64:
ret = append(ret, int64(value))
case int64:
ret = append(ret, value)
case int:
ret = append(ret, int64(value))
}
}
return ret
}
2026-04-09 10:01:23 +08:00
func (s *knowledgeBaseService) UpdateSort(ids []int64) error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
for i, id := range ids {
if err := repositories.KnowledgeBaseRepository.UpdateColumn(ctx.Tx, id, "sort_no", i); err != nil {
return err
}
}
return nil
})
}
func (s *knowledgeBaseService) buildKnowledgeBaseModel(req request.CreateKnowledgeBaseRequest) (*models.KnowledgeBase, error) {
item := &models.KnowledgeBase{
Name: req.Name,
Description: req.Description,
KnowledgeType: req.KnowledgeType,
DefaultTopK: req.DefaultTopK,
DefaultScoreThreshold: req.DefaultScoreThreshold,
DefaultRerankLimit: req.DefaultRerankLimit,
ChunkProvider: req.ChunkProvider,
ChunkTargetTokens: req.ChunkTargetTokens,
ChunkMaxTokens: req.ChunkMaxTokens,
ChunkOverlapTokens: req.ChunkOverlapTokens,
AnswerMode: req.AnswerMode,
Remark: req.Remark,
}
if item.DefaultTopK == 0 {
item.DefaultTopK = 10
}
if item.KnowledgeType == "" {
item.KnowledgeType = string(enums.KnowledgeBaseTypeDocument)
}
if !isValidKnowledgeType(item.KnowledgeType) {
return nil, errorsx.InvalidParamI18n("error.e0290")
2026-04-09 10:01:23 +08:00
}
if item.DefaultScoreThreshold == 0 {
item.DefaultScoreThreshold = 0.2
}
if item.DefaultRerankLimit == 0 {
item.DefaultRerankLimit = 5
}
if item.ChunkProvider == "" {
item.ChunkProvider = string(enums.KnowledgeChunkProviderStructured)
}
if item.KnowledgeType == string(enums.KnowledgeBaseTypeFAQ) {
item.ChunkProvider = string(enums.KnowledgeChunkProviderFAQ)
item.ChunkTargetTokens = 0
item.ChunkMaxTokens = 0
item.ChunkOverlapTokens = 0
} else if item.ChunkProvider == string(enums.KnowledgeChunkProviderFAQ) {
return nil, errorsx.InvalidParamI18n("error.e0219")
2026-04-09 10:01:23 +08:00
}
if !isValidChunkProvider(item.ChunkProvider) {
return nil, errorsx.InvalidParamI18n("error.e0130")
2026-04-09 10:01:23 +08:00
}
if item.KnowledgeType != string(enums.KnowledgeBaseTypeFAQ) && item.ChunkTargetTokens == 0 {
item.ChunkTargetTokens = 300
}
if item.KnowledgeType != string(enums.KnowledgeBaseTypeFAQ) && item.ChunkMaxTokens == 0 {
item.ChunkMaxTokens = 400
}
if item.KnowledgeType != string(enums.KnowledgeBaseTypeFAQ) && item.ChunkMaxTokens < item.ChunkTargetTokens {
item.ChunkMaxTokens = item.ChunkTargetTokens
}
if item.KnowledgeType != string(enums.KnowledgeBaseTypeFAQ) && item.ChunkOverlapTokens == 0 {
item.ChunkOverlapTokens = 40
}
if item.AnswerMode == 0 {
item.AnswerMode = 1
}
return item, nil
}
func isValidChunkProvider(provider string) bool {
switch provider {
case string(enums.KnowledgeChunkProviderFixed),
string(enums.KnowledgeChunkProviderStructured),
string(enums.KnowledgeChunkProviderFAQ),
string(enums.KnowledgeChunkProviderSemantic):
return true
default:
return false
}
}
func isValidKnowledgeType(knowledgeType string) bool {
switch knowledgeType {
case string(enums.KnowledgeBaseTypeDocument), string(enums.KnowledgeBaseTypeFAQ):
return true
default:
return false
}
}