From 6ea93e5e82c8016bf137cc7e9ef9c58ed34346e5 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 10:23:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20=E5=AE=9E=E7=8E=B0=E6=B7=B7?= =?UTF-8?q?=E5=90=88=E4=B8=8A=E4=B8=8B=E6=96=87=E8=A3=85=E9=85=8D=E5=BC=95?= =?UTF-8?q?=E6=93=8E(Context=20Blender)=E4=B8=8E=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E4=BD=93=E5=B0=8F=E9=98=9F=E6=B2=99=E7=AE=B1=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- internal/bizdev/blended_context_test.go | 106 ++++++++++++++++++++++ internal/bizdev/service.go | 111 +++++++++++++++++++----- internal/bizdev/types.go | 10 ++- internal/chat/session.go | 10 +++ internal/server/server.go | 3 + internal/squad/service.go | 4 + 6 files changed, 220 insertions(+), 24 deletions(-) create mode 100644 internal/bizdev/blended_context_test.go diff --git a/internal/bizdev/blended_context_test.go b/internal/bizdev/blended_context_test.go new file mode 100644 index 00000000..fa0adb12 --- /dev/null +++ b/internal/bizdev/blended_context_test.go @@ -0,0 +1,106 @@ +package bizdev + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "chat-agent-system/internal/config" + "chat-agent-system/internal/ctxmgr" +) + +func TestService_BlendedContextInjection(t *testing.T) { + tempDir, err := os.MkdirTemp("", "blended_ctx_test_*") + if err != nil { + t.Fatalf("mkdir temp failed: %v", err) + } + defer os.RemoveAll(tempDir) + + cfg := &config.Config{ + Env: "test", + DataDir: tempDir, + AgentBackend: "stub", + } + + svc := NewService(cfg) + ctx := context.Background() + + // 1. 设置用户个性化偏好 + userID := int64(108) + projectID := int64(9001) + + pref := &ctxmgr.UserPreference{ + UserID: userID, + AccountID: "user_blended_test", + Name: "项目总监", + Role: "director", + CommunicationStyle: ctxmgr.StyleExecutiveSummary, + FocusAreas: []string{"cost_budget", "timeline"}, + OutputFormatPref: "ppt_bullets", + } + _ = svc.ContextManager().UpdateUserPreference(ctx, pref) + + // 2. 记录先验里程碑决策 + _, _ = svc.ContextManager().RecordActivity(ctx, projectID, userID, "项目总监", "decision", "首期控制在50万预算内并优先过型检", nil, 1.0, true) + + // 3. 执行任务 (使用 stub 后端进行快速逻辑验证) + res, err := svc.ExecuteTask(ctx, TaskRunRequest{ + TaskKey: "intake", + ProjectName: "微型超声内窥镜系统", + CustomerName: "北京水木东方", + DeviceType: "超声内窥镜", + TargetMarket: "国内", + Backend: "stub", + ProjectID: projectID, + UserID: userID, + }) + if err != nil { + t.Fatalf("execute task failed: %v", err) + } + if !res.Success { + t.Fatalf("expected task success") + } + + // 4. 验证工作区内的 context/user.md 与 context/memory/ 是否注入了混合上下文 + userMdPath := filepath.Join(res.WorkDir, "context", "user.md") + userMdData, err := os.ReadFile(userMdPath) + if err != nil { + t.Fatalf("read user.md failed: %v", err) + } + userMdStr := string(userMdData) + + if !strings.Contains(userMdStr, "高管决策摘要") { + t.Errorf("user.md missing executive style guidance: %s", userMdStr) + } + if !strings.Contains(userMdStr, "CDO工时核算、BOM成本与预算边界") { + t.Errorf("user.md missing cost focus: %s", userMdStr) + } + + // 5. 验证 context/memory/team_decisions.md 注入 + teamDecPath := filepath.Join(res.WorkDir, "context", "memory", "team_decisions.md") + teamDecData, err := os.ReadFile(teamDecPath) + if err != nil { + t.Fatalf("read team_decisions.md failed: %v", err) + } + if !strings.Contains(string(teamDecData), "首期控制在50万预算内") { + t.Errorf("team_decisions.md missing milestone decision: %s", string(teamDecData)) + } + + // 6. 验证任务完成后自动记录了里程碑活动 + pCtx, err := svc.ContextManager().GetProjectContext(ctx, projectID) + if err != nil { + t.Fatalf("get project context failed: %v", err) + } + foundDeliveryActivity := false + for _, act := range pCtx.AllActivities { + if strings.Contains(act.Title, "完成交付物") { + foundDeliveryActivity = true + break + } + } + if !foundDeliveryActivity { + t.Errorf("expected delivery activity recorded in project context") + } +} diff --git a/internal/bizdev/service.go b/internal/bizdev/service.go index 1feb5e13..03b7561e 100644 --- a/internal/bizdev/service.go +++ b/internal/bizdev/service.go @@ -10,23 +10,57 @@ import ( "time" "chat-agent-system/internal/config" + "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/kb" "chat-agent-system/internal/squad" + "chat-agent-system/internal/storage" "chat-agent-system/internal/stream" + + "github.com/jackc/pgx/v5/pgxpool" ) type Service struct { - cfg *config.Config - kb *kb.Client - squad *squad.Service + cfg *config.Config + kb *kb.Client + squad *squad.Service + ctxMgr *ctxmgr.ContextManager + cas *storage.CAS } func NewService(cfg *config.Config) *Service { - return &Service{ - cfg: cfg, - kb: kb.NewClient(cfg), - squad: squad.NewService(cfg), + if cfg == nil { + cfg = &config.Config{ + DataDir: "./data/dev", + } } + var pool *pgxpool.Pool + if cfg.ContextDbHost != "" { + poolCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if p, err := pgxpool.New(poolCtx, cfg.ContextDSN()); err == nil { + if p.Ping(poolCtx) == nil { + pool = p + } + } + } + casStore, _ := storage.NewCAS(filepath.Join(cfg.DataDir, "storage"), pool, nil) + ctxMgr := ctxmgr.NewContextManager(pool, casStore, ctxmgr.DefaultTimeDecayConfig()) + + return &Service{ + cfg: cfg, + kb: kb.NewClient(cfg), + squad: squad.NewService(cfg), + ctxMgr: ctxMgr, + cas: casStore, + } +} + +func (s *Service) ContextManager() *ctxmgr.ContextManager { + return s.ctxMgr +} + +func (s *Service) CAS() *storage.CAS { + return s.cas } var SupportedTasks = squad.SupportedTasks @@ -111,22 +145,46 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun } } + // 混合上下文装配 (Context Blender) + userCtxStr := req.UserContext + memBank := req.MemoryBankFiles + if memBank == nil { + memBank = make(map[string]string) + } + + if s.ctxMgr != nil { + blended, err := s.ctxMgr.BuildBlendedSquadContext(ctx, req.ProjectID, req.UserID, req.TaskKey, "squad") + if err == nil && blended != nil { + if userCtxStr == "" && blended.PersonalizedGuidance != "" { + userCtxStr = blended.PersonalizedGuidance + } + if blended.PersonalizedGuidance != "" { + memBank["user_preferences.md"] = blended.PersonalizedGuidance + } + if len(blended.KeyDecisions) > 0 { + memBank["team_decisions.md"] = "# 项目团队关键决策与里程碑记录\n\n" + strings.Join(blended.KeyDecisions, "\n") + } + } + } + squadReq := squad.SquadRunRequest{ - TaskKey: req.TaskKey, - ProjectName: req.ProjectName, - CustomerName: req.CustomerName, - DeviceType: req.DeviceType, - IntendedUse: req.IntendedUse, - TargetMarket: req.TargetMarket, - Fields: req.Fields, - Materials: materials, - Backend: req.Backend, - Timeout: req.Timeout, - WorkDir: req.WorkDir, - CustomSteps: req.CustomSteps, - AutoRepair: req.AutoRepair, - KnowledgeHits: hits, - PriorDocs: req.PriorDocs, + TaskKey: req.TaskKey, + ProjectName: req.ProjectName, + CustomerName: req.CustomerName, + DeviceType: req.DeviceType, + IntendedUse: req.IntendedUse, + TargetMarket: req.TargetMarket, + Fields: req.Fields, + Materials: materials, + Backend: req.Backend, + Timeout: req.Timeout, + WorkDir: req.WorkDir, + CustomSteps: req.CustomSteps, + AutoRepair: req.AutoRepair, + KnowledgeHits: hits, + PriorDocs: req.PriorDocs, + UserContext: userCtxStr, + MemoryBankFiles: memBank, } res, err := s.squad.ExecuteTask(ctx, squadReq) @@ -146,6 +204,13 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun return nil, err } + if res.Success && s.ctxMgr != nil && req.ProjectID > 0 { + _, _ = s.ctxMgr.RecordActivity(ctx, req.ProjectID, req.UserID, "系统智能体小队", "operation", + fmt.Sprintf("完成交付物【%s】", res.ArtifactTitle), + map[string]any{"task_key": res.TaskKey, "work_dir": res.WorkDir}, + 1.0, true) + } + return &TaskRunResult{ TaskKey: res.TaskKey, ArtifactTitle: res.ArtifactTitle, @@ -245,6 +310,8 @@ func (s *Service) executeStageInternal(ctx context.Context, req StageRunRequest) AutoRepair: req.AutoRepair, KnowledgeHits: req.KnowledgeHits, PriorDocs: accumulatedPriorDocs, + ProjectID: req.ProjectID, + UserID: req.UserID, } res, err := s.ExecuteTask(ctx, tReq) diff --git a/internal/bizdev/types.go b/internal/bizdev/types.go index 400e18f0..c32f6bc4 100644 --- a/internal/bizdev/types.go +++ b/internal/bizdev/types.go @@ -48,8 +48,12 @@ type TaskRunRequest struct { WorkDir string `json:"work_dir"` CustomSteps []squad.StepSpec `json:"custom_steps,omitempty"` AutoRepair bool `json:"auto_repair,omitempty"` - KnowledgeHits []squad.KnowledgeHit `json:"knowledge_hits,omitempty"` - PriorDocs []squad.PriorDocItem `json:"prior_docs,omitempty"` + KnowledgeHits []squad.KnowledgeHit `json:"knowledge_hits,omitempty"` + PriorDocs []squad.PriorDocItem `json:"prior_docs,omitempty"` + UserID int64 `json:"user_id,omitempty"` + ProjectID int64 `json:"project_id,omitempty"` + UserContext string `json:"user_context,omitempty"` + MemoryBankFiles map[string]string `json:"memory_bank_files,omitempty"` } type TaskRunResult struct { @@ -84,6 +88,8 @@ type StageRunRequest struct { AutoRepair bool `json:"auto_repair,omitempty"` PriorDocs []squad.PriorDocItem `json:"prior_docs,omitempty"` KnowledgeHits []squad.KnowledgeHit `json:"knowledge_hits,omitempty"` + UserID int64 `json:"user_id,omitempty"` + ProjectID int64 `json:"project_id,omitempty"` } // StageRunResult 成熟度阶段执行结果(双轨交付:A轨合规文档 + B轨业务参谋锦囊) diff --git a/internal/chat/session.go b/internal/chat/session.go index 93e227ea..3dd1b057 100644 --- a/internal/chat/session.go +++ b/internal/chat/session.go @@ -6,6 +6,7 @@ import ( "time" "chat-agent-system/internal/coordinator" + "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/intent" "chat-agent-system/internal/squad" "chat-agent-system/internal/stream" @@ -38,6 +39,7 @@ type Session struct { UserCtx *coordinator.UserProjectContext `json:"user_ctx"` Recognizer intent.Recognizer `json:"-"` Coordinator *coordinator.CoordinatorEngine `json:"-"` + CtxMgr *ctxmgr.ContextManager `json:"-"` Messages []Message `json:"messages"` CreatedAt time.Time `json:"created_at"` } @@ -91,6 +93,10 @@ func (s *Session) sendMessageInternal(ctx context.Context, userInput string, str } s.Messages = append(s.Messages, userMsg) + if s.CtxMgr != nil { + _ = s.CtxMgr.RecordUserMessage(ctx, s.ID, 1, "user", userInput, "", "", nil) + } + // 1. 意图识别 (L1 对话层,带非阻塞桌角备忘快照) var snap *intent.ContextSnapshot if s.UserCtx != nil { @@ -148,6 +154,10 @@ func (s *Session) sendMessageInternal(ctx context.Context, userInput string, str } s.Messages = append(s.Messages, asstMsg) + if s.CtxMgr != nil { + _ = s.CtxMgr.RecordUserMessage(ctx, s.ID, 1, "assistant", coordRes.NaturalReply, string(coordRes.Intent), string(recRes.Branch), nil) + } + // 4. 流式推送参谋自然语言回复与决策锦囊 if streamer != nil { if coordRes.NaturalReply != "" { diff --git a/internal/server/server.go b/internal/server/server.go index 375b50ef..02fda00c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -176,6 +176,9 @@ func (s *Server) getOrCreateSession( } sess := chat.NewSession(sessionID, userCtx, s.rec, s.engine) + if s.svc != nil { + sess.CtxMgr = s.svc.ContextManager() + } s.sessions[sessionID] = sess return sess } diff --git a/internal/squad/service.go b/internal/squad/service.go index 612a1c9d..3e047a53 100644 --- a/internal/squad/service.go +++ b/internal/squad/service.go @@ -47,6 +47,8 @@ type SquadRunRequest struct { AutoRepair bool `json:"auto_repair"` KnowledgeHits []KnowledgeHit `json:"knowledge_hits"` PriorDocs []PriorDocItem `json:"prior_docs"` + UserContext string `json:"user_context,omitempty"` + MemoryBankFiles map[string]string `json:"memory_bank_files,omitempty"` StepNotify func(StepEvent) `json:"-"` } @@ -194,6 +196,8 @@ func (s *Service) ExecuteTask(ctx context.Context, req SquadRunRequest) (*SquadR PriorDocs: req.PriorDocs, WorkflowSteps: req.CustomSteps, InitialFields: req.Fields, + UserContext: req.UserContext, + MemoryBankFiles: req.MemoryBankFiles, } _, err := s.engine.PrepareWorkspace(wsReq)