Invert chat dependencies on chatflow/intentflow/stageflow through composition-root adapters; replace AssembleRequest coordinator embeds with local DTOs and tighten the module-boundaries allowlist.
142 lines
4.1 KiB
Go
142 lines
4.1 KiB
Go
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
|
|
}
|