Files
yawan f7fc992f57
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
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

198 lines
7.5 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 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, &copy); 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, &copy)
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(&current).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()
}