package chatflow import ( "context" "fmt" "strings" "time" "chat-agent-system/internal/coordinator" "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/maturity" ) // Assembler adapts coordinator.ContextAssembler and ChatLog so prompt context // only contains the authorized message set. type Assembler struct { Inner *coordinator.ContextAssembler Log ChatLog } func NewAssembler(inner *coordinator.ContextAssembler, log ChatLog) *Assembler { if inner == nil { inner = coordinator.NewContextAssembler() } return &Assembler{Inner: inner, Log: log} } func (a *Assembler) Assemble(ctx context.Context, p Principal, req AssembleRequest) (*coordinator.UserProjectContext, error) { if p.UserID <= 0 { return nil, ErrUnauthenticated } p.Action = ActionAssemble user := toCoordinatorUser(req.User) fallback := toCoordinatorProject(req.Fallback) var userCtx *coordinator.UserProjectContext if a.Inner != nil && strings.TrimSpace(req.WorkDir) != "" { userCtx, _ = a.Inner.AssembleFromWorkspace(req.WorkDir, user) } if userCtx == nil { userCtx = &coordinator.UserProjectContext{ SnapshotID: fmt.Sprintf("ctx_%d", time.Now().UnixNano()), AssembledAt: time.Now(), User: user, WorkDir: req.WorkDir, Project: fallback, BlackboardFacts: make(map[string]string), AvailableActions: coordinator.ComputeAvailableActions(maturity.StageV01_Triage, user.ProjectRole), } if userCtx.Project.CurrentStage == "" { userCtx.Project.CurrentStage = maturity.StageV01_Triage } if userCtx.Project.CurrentWorkflow == "" { userCtx.Project.CurrentWorkflow = "intake" } } if a.Log == nil || strings.TrimSpace(req.SessionID) == "" { return userCtx, nil } restored, err := a.Log.Restore(ctx, p, req.SessionID) if err != nil { if err == ErrForbidden || err == ErrNotFound || err == ErrUnauthenticated { userCtx.RecentDialogue = nil userCtx.DialogueSummary = "" userCtx.RecalledMemories = nil userCtx.RecalledMemorySnippet = "" return userCtx, nil } return nil, err } if restored == nil { return userCtx, nil } upd := coordinator.ContextUpdate{ ExpectedVersion: userCtx.Version, BlackboardFill: restored.Facts, SetRecentDialogue: true, RecentDialogue: authorizedTurns(restored), } if restored.ProjectID > 0 { upd.ProjectIDIfEmpty = fmt.Sprintf("%d", restored.ProjectID) } _ = coordinator.ApplyContextUpdate(userCtx, upd) return userCtx, nil } func toCoordinatorUser(u AssembleUser) coordinator.UserInfo { return coordinator.UserInfo{ ID: u.ID, Name: u.Name, Role: u.Role, ProjectRole: u.ProjectRole, } } func toCoordinatorProject(p AssembleProject) coordinator.ProjectInfo { stage := maturity.MaturityStage(strings.TrimSpace(p.CurrentStage)) return coordinator.ProjectInfo{ ID: p.ID, Name: p.Name, CustomerName: p.CustomerName, DeviceType: p.DeviceType, IntendedUse: p.IntendedUse, TargetMarket: p.TargetMarket, CreatorUserID: p.CreatorUserID, ActiveWorkflowKey: p.ActiveWorkflowKey, WorkflowSelectionVersion: p.WorkflowSelectionVersion, CurrentWorkflow: p.CurrentWorkflow, CurrentStage: stage, StageRules: append([]string(nil), p.StageRules...), } } func authorizedTurns(restored *ctxmgr.RestoredSessionContext) []coordinator.DialogueTurn { if restored == nil { return nil } turns := make([]coordinator.DialogueTurn, 0, len(restored.Messages)) records := make([]ctxmgr.ChatMessageRecord, 0, len(restored.Messages)) for _, m := range restored.Messages { if strings.TrimSpace(m.Content) == "" { continue } turns = append(turns, coordinator.DialogueTurn{Role: m.Role, Content: m.Content}) records = append(records, m) } cfg := ctxmgr.DefaultWindowConfig() window, _, _ := ctxmgr.AssembleWindow(records, cfg) if len(window) > 0 { turns = turns[:0] for _, m := range window { if strings.TrimSpace(m.Content) == "" { continue } turns = append(turns, coordinator.DialogueTurn{Role: m.Role, Content: m.Content}) } } return turns }