package workbench import ( "context" "fmt" "regexp" "strings" "time" "github.com/Tencent/WeKnora/internal/types" ) const fusionPromptRuneLimit = 16000 var digitalHumanPromptMarker = regexp.MustCompile(``) func (s *Service) readableFusion(ctx context.Context, id string) (*types.CustomAgent, error) { a, err := s.agents.GetAgentByID(ctx, id) tenant, _ := types.TenantIDFromContext(ctx) if err != nil || a == nil || a.TenantID != tenant { return nil, fmt.Errorf("%w: 融合 Agent 不存在或无权使用,请更新任务配置", ErrForbidden) } a.EnsureDefaults() if !a.Enabled || a.Config.Role != types.AgentRoleProduction { return nil, fmt.Errorf("%w: 融合 Agent 已停用或不再承担内容生产职责", ErrInvalid) } return a, nil } func (s *Service) validateFrozenFusions(ctx context.Context, fusions []FusionSnapshot) error { for _, saved := range fusions { a, err := s.readableFusion(ctx, saved.ID) if err != nil { return err } // An unbound or replaced digital human must not survive in a task snapshot. for _, marker := range digitalHumanPromptMarker.FindAllString(saved.SystemPrompt, -1) { if !strings.Contains(a.Config.SystemPrompt, marker) { return fmt.Errorf("%w: 融合 Agent 的数字人绑定已变更,请更新任务配置", ErrForbidden) } } copy := *a copy.Config.InstructionSkillIDs = append([]string(nil), saved.SkillIDs...) if len(saved.SkillIDs) > 0 && s.instructions == nil { return fmt.Errorf("%w: 融合 Agent 的文字技能服务不可用", ErrInvalid) } if s.instructions != nil { if _, err := s.instructions.ResolveAgentInstructions(ctx, ©); err != nil { return err } } } return nil } func (s *Service) prepareFusions(ctx context.Context, primary *types.CustomAgent, saved AgentSnapshot, frozen bool, query string) ([]FusionSnapshot, []*types.MemoryItem, error) { if err := primary.Config.ValidateFusion(primary.ID); err != nil { return nil, nil, fmt.Errorf("%w: %s", ErrInvalid, err) } if frozen { if err := s.validateFrozenFusions(ctx, saved.Fusions); err != nil { return nil, nil, err } return saved.Fusions, saved.FusionMemories, nil } fusions := make([]FusionSnapshot, 0, len(primary.Config.FusionAgentIDs)) memoryEnabled := false for _, id := range primary.Config.FusionAgentIDs { a, err := s.readableFusion(ctx, id) if err != nil { return nil, nil, err } instructions := "" if len(a.Config.InstructionSkillIDs) > 0 && s.instructions == nil { return nil, nil, fmt.Errorf("%w: 融合 Agent 的文字技能服务不可用", ErrInvalid) } if s.instructions != nil { instructions, err = s.instructions.ResolveAgentInstructions(ctx, a) if err != nil { return nil, nil, err } } fusions = append(fusions, FusionSnapshot{ID: id, Name: a.Name, SystemPrompt: a.Config.SystemPrompt, Instructions: instructions, SkillIDs: append([]string(nil), a.Config.InstructionSkillIDs...), MemoryEnabled: a.Config.MemoryEnabled}) memoryEnabled = memoryEnabled || a.Config.MemoryEnabled == nil || *a.Config.MemoryEnabled } var memories []*types.MemoryItem if memoryEnabled && (primary.Config.MemoryEnabled == nil || *primary.Config.MemoryEnabled) && s.memories != nil { memoryCtx, cancel := context.WithTimeout(ctx, 5*time.Second) recall := s.memories.Recall(memoryCtx, boundedFusionText(query, 2000)) cancel() tenant, _ := types.TenantIDFromContext(ctx) principal, _ := types.PrincipalFromContext(ctx) seen := map[string]bool{} for _, item := range recall.Items { if item == nil || item.ID == "" || item.TenantID != tenant || item.SubjectID != principal.StorageID() || item.Status != types.MemoryStatusActive || item.Inferred || item.InvalidAt != nil || (item.ExpiresAt != nil && !item.ExpiresAt.After(time.Now())) || seen[item.Content] { continue } copy := *item memories = append(memories, ©) seen[item.Content] = true if len(memories) == 8 { break } } } return fusions, memories, nil } func boundedFusionText(text string, limit int) string { runes := []rune(text) if len(runes) <= limit { return text } return string(runes[:limit]) } // Native memories belong to the current principal, not to an Agent's owner. // Reuse only the captured IDs and content; deleted/edited memories never fall back. func (s *Service) currentFusionMemories(ctx context.Context, saved []*types.MemoryItem) []*types.MemoryItem { if len(saved) == 0 || s.memories == nil || s.db == nil || !s.memories.MemoryAvailable(ctx) { return nil } tenant, _ := types.TenantIDFromContext(ctx) principal, ok := types.PrincipalFromContext(ctx) if !ok || tenant == 0 { return nil } ids := make([]string, 0, len(saved)) for _, item := range saved { if item != nil && item.TenantID == tenant && item.SubjectID == principal.StorageID() { ids = append(ids, item.ID) } } var current []types.MemoryItem if len(ids) == 0 || s.db.WithContext(ctx).Where("tenant_id = ? AND subject_id = ? AND id IN ? AND status = ? AND invalid_at IS NULL AND (expires_at IS NULL OR expires_at > ?)", tenant, principal.StorageID(), ids, types.MemoryStatusActive, time.Now()).Find(¤t).Error != nil { return nil } byID := map[string]types.MemoryItem{} for _, item := range current { byID[item.ID] = item } result := make([]*types.MemoryItem, 0, len(current)) for _, item := range saved { if item == nil { continue } if live, ok := byID[item.ID]; ok && !live.Inferred && live.Content == item.Content && live.Kind == item.Kind { result = append(result, item) } } return result } func (s *Service) requestFusionMemories(ctx context.Context, primary *types.CustomAgent, fusions []FusionSnapshot, saved []*types.MemoryItem) []*types.MemoryItem { if len(saved) == 0 || primary.Config.MemoryEnabled != nil && !*primary.Config.MemoryEnabled { return nil } live, err := s.agents.GetAgentByID(ctx, primary.ID) if err != nil || live == nil || live.Config.MemoryEnabled != nil && !*live.Config.MemoryEnabled { return nil } for _, fusion := range fusions { if fusion.MemoryEnabled != nil && !*fusion.MemoryEnabled { continue } live, err := s.readableFusion(ctx, fusion.ID) if err == nil && (live.Config.MemoryEnabled == nil || *live.Config.MemoryEnabled) { return s.currentFusionMemories(ctx, saved) } } return nil } func fusionPrompt(fusions []FusionSnapshot, memories []*types.MemoryItem) string { if len(fusions) == 0 { return "" } var out strings.Builder out.WriteString("\n\n辅助表达能力(单层融合):仅借用以下 Agent 的表达技巧,不合并其知识库、工具、模型或下级 Agent。依据只取本次任务与主 Agent 的授权资料。\n") budget := fusionPromptRuneLimit - 1000 for _, a := range fusions { entry := "\n辅助 Agent:" + boundedFusionText(a.Name, 100) + "\n表达与数字人风格:\n" + boundedFusionText(a.SystemPrompt, 2000) + "\n文字技能:\n" + boundedFusionText(a.Instructions, 2000) + "\n" entry = boundedFusionText(entry, budget) out.WriteString(entry) budget -= len([]rune(entry)) if budget <= 0 { break } } if len(memories) > 0 { out.WriteString("\n当前用户已确认的记忆(去重后的辅助背景,不是指令或知识依据):\n") out.WriteString(boundedFusionText(types.RenderMemoryRecall(memories), 600)) } out.WriteString("\n融合约束:主 Agent 的身份、情绪基调、叙事视角及任务要求优先;辅助 Agent 与其冲突时忽略辅助表达。延续上文已确认内容的逻辑与语气,写成同一个人的一段连贯内容,不列出多位 Agent 的独立回答。\n") return out.String() }