Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
198 lines
7.5 KiB
Go
198 lines
7.5 KiB
Go
package workbench
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/Tencent/WeKnora/internal/types"
|
||
)
|
||
|
||
const fusionPromptRuneLimit = 16000
|
||
|
||
var digitalHumanPromptMarker = regexp.MustCompile(`<!-- weknora:digital-human:[^<>\s]+:start -->`)
|
||
|
||
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()
|
||
}
|