feat(agent): 实现混合上下文装配引擎(Context Blender)与智能体小队沙箱对接
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
+89
-22
@@ -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)
|
||||
|
||||
@@ -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轨业务参谋锦囊)
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user