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 var userCtx *coordinator.UserProjectContext if a.Inner != nil && strings.TrimSpace(req.WorkDir) != "" { userCtx, _ = a.Inner.AssembleFromWorkspace(req.WorkDir, req.User) } if userCtx == nil { userCtx = &coordinator.UserProjectContext{ SnapshotID: fmt.Sprintf("ctx_%d", time.Now().UnixNano()), AssembledAt: time.Now(), User: req.User, WorkDir: req.WorkDir, Project: req.Fallback, BlackboardFacts: make(map[string]string), AvailableActions: coordinator.ComputeAvailableActions(maturity.StageV01_Triage, req.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 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 }