Files

203 lines
6.7 KiB
Go
Raw Permalink Normal View History

package workbench
import (
"context"
"fmt"
"slices"
"sort"
"strings"
"github.com/Tencent/WeKnora/internal/types"
)
type SegmentCategory struct {
ID string `json:"id"`
KnowledgeBaseID string `json:"knowledge_base_id"`
Name string `json:"name"`
}
// Resolve the permitted task scope before enumerating its categories. Explicit
// files remain explicit: selecting a category must never open the entire base.
func (s *Service) segmentCategories(ctx context.Context, d TaskData) ([]SegmentCategory, error) {
result := []SegmentCategory{}
if s.knowledgeBases == nil || s.knowledge == nil {
return result, nil
}
if err := s.validateKnowledgeScope(ctx, d.KBIDs, d.KnowledgeIDs, d.KnowledgeTags); err != nil {
return nil, err
}
tenant, _, _, err := identity(ctx)
if err != nil {
return nil, err
}
baseIDs := slices.Clone(d.KBIDs)
for id := range d.KnowledgeTags {
baseIDs = append(baseIDs, id)
}
files, err := s.knowledge.GetKnowledgeBatchWithSharedAccess(ctx, tenant, d.KnowledgeIDs)
if err != nil {
return nil, err
}
for _, file := range files {
if file != nil {
baseIDs = append(baseIDs, file.KnowledgeBaseID)
}
}
sort.Strings(baseIDs)
for _, id := range slices.Compact(baseIDs) {
base, err := s.knowledgeBases.GetKnowledgeBaseByID(ctx, id)
if err != nil || base == nil {
return nil, ErrForbidden
}
if err := s.validateKnowledgeScope(ctx, []string{id}, nil, nil); err != nil {
return nil, err
}
q := s.db.WithContext(ctx).Model(&types.KnowledgeTag{}).Where("tenant_id=? AND knowledge_base_id=?", base.TenantID, id)
if tags := d.KnowledgeTags[id]; len(tags) > 0 && len(d.KnowledgeFolders[id]) == 0 {
q = q.Where("id IN ?", tags)
}
allowedFiles := []string{}
for _, file := range files {
if file != nil && file.KnowledgeBaseID == id {
allowedFiles = append(allowedFiles, file.ID)
}
}
wholeBase := slices.Contains(d.KBIDs, id) && len(d.KnowledgeTags[id]) == 0 && len(d.KnowledgeFolders[id]) == 0
if !wholeBase && len(allowedFiles) > 0 {
q = q.Where("id IN (SELECT tag_id FROM knowledge_tag_relations WHERE knowledge_id IN ?)", allowedFiles)
}
var rows []types.KnowledgeTag
if err := q.Order("sort_order, name, id").Find(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
result = append(result, SegmentCategory{row.ID, row.KnowledgeBaseID, row.Name})
}
}
return result, nil
}
func normalizeCategoryRecommendations(segments []Segment, options []SegmentCategory) error {
allowed := map[string]bool{}
for _, option := range options {
allowed[option.ID] = true
}
for i := range segments {
seg := &segments[i]
if seg.RecommendedCategoryID != "" && !allowed[seg.RecommendedCategoryID] {
return fmt.Errorf("%w: AI 推荐了当前 Agent 资料范围以外的分类,请重新拆解", ErrInvalid)
}
seg.CategoryID = seg.RecommendedCategoryID
if seg.CategoryID == "" {
seg.CategoryReason = "当前范围没有合适分类,沿用本段 Agent 的资料范围。"
} else if strings.TrimSpace(seg.CategoryReason) == "" {
return fmt.Errorf("%w: AI 未说明分类推荐理由,请重新拆解", ErrInvalid)
}
}
return nil
}
func (s *Service) agentCategoryOptions(ctx context.Context, d TaskData, flow FlowData) (map[string][]SegmentCategory, error) {
result := map[string][]SegmentCategory{}
for _, node := range flow.Nodes {
if _, exists := result[node.AgentID]; exists {
continue
}
scope := d
selected := flow
selected.Nodes = []FlowNode{{ID: node.ID, AgentID: node.AgentID}}
if err := s.inheritFlow(ctx, &scope, selected); err != nil {
return nil, err
}
options, err := s.segmentCategories(ctx, scope)
if err != nil {
return nil, err
}
result[node.AgentID] = options
}
return result, nil
}
func validateAgentCategoryRecommendations(segments []Segment, options map[string][]SegmentCategory) error {
for _, segment := range segments {
if segment.RecommendedCategoryID != "" && !slices.ContainsFunc(options[segment.AgentID], func(option SegmentCategory) bool {
return option.ID == segment.RecommendedCategoryID
}) {
return fmt.Errorf("%w: 本段推荐分类不属于负责 Agent 的资料范围,请重新拆解", ErrInvalid)
}
}
return nil
}
// A recommendation is optional metadata. Discard invalid AI suggestions without
// widening the assigned Agent's scope or losing an otherwise valid decomposition.
func reconcileCategoryRecommendations(segments []Segment, options []SegmentCategory, byAgent map[string][]SegmentCategory) error {
for i := range segments {
segment := &segments[i]
if segment.RecommendedCategoryID == "" {
continue
}
allowed := func(option SegmentCategory) bool { return option.ID == segment.RecommendedCategoryID }
if !slices.ContainsFunc(options, allowed) || !slices.ContainsFunc(byAgent[segment.AgentID], allowed) || strings.TrimSpace(segment.CategoryReason) == "" {
segment.RecommendedCategoryID = ""
segment.CategoryReason = ""
}
}
if err := normalizeCategoryRecommendations(segments, options); err != nil {
return err
}
return validateAgentCategoryRecommendations(segments, byAgent)
}
func (s *Service) applySegmentCategory(ctx context.Context, scope *TaskData, categoryID string) error {
if categoryID == "" {
return nil
}
options, err := s.segmentCategories(ctx, *scope)
if err != nil {
return err
}
var selected *SegmentCategory
for i := range options {
if options[i].ID == categoryID {
selected = &options[i]
break
}
}
if selected == nil {
return fmt.Errorf("%w: 本段分类不在当前 Agent 的知识范围内,请重新选择", ErrInvalid)
}
baseID := selected.KnowledgeBaseID
tenant, _, _, err := identity(ctx)
if err != nil {
return err
}
files, err := s.knowledge.GetKnowledgeBatchWithSharedAccess(ctx, tenant, scope.KnowledgeIDs)
if err != nil {
return err
}
fileIDs := []string{}
for _, file := range files {
if file != nil && file.KnowledgeBaseID == baseID {
fileIDs = append(fileIDs, file.ID)
}
}
wholeBase := slices.Contains(scope.KBIDs, baseID) && len(scope.KnowledgeTags[baseID]) == 0 && len(scope.KnowledgeFolders[baseID]) == 0
if wholeBase || len(fileIDs) == 0 {
scope.KBIDs, scope.KnowledgeIDs = nil, nil
scope.KnowledgeFolders = nil
scope.KnowledgeTags = map[string][]string{baseID: {categoryID}}
return nil
}
var ids []string
if err := s.db.WithContext(ctx).Table("knowledge_tag_relations").Where("tag_id=? AND knowledge_id IN ?", categoryID, fileIDs).Pluck("knowledge_id", &ids).Error; err != nil {
return err
}
if len(ids) == 0 {
return fmt.Errorf("%w: 所选分类在当前资料范围内没有文件", ErrInvalid)
}
scope.KBIDs, scope.KnowledgeFolders, scope.KnowledgeTags = nil, nil, nil
scope.KnowledgeIDs = ids
return nil
}