Files
poimen-workflows/internal/routing/canvas_converter.go
T

180 lines
5.0 KiB
Go
Raw Normal View History

package routing
import (
"fmt"
"github.com/rockliang/poimen/workflows/pkg/db"
)
// CanvasConverter converts visual canvas to executable WorkflowSpec
type CanvasConverter struct {
validator *CanvasValidator
}
// NewCanvasConverter creates a converter
func NewCanvasConverter() *CanvasConverter {
return &CanvasConverter{
validator: NewCanvasValidator(),
}
}
// CanvasToWorkflowSpec converts canvas to WorkflowSpec
func (cc *CanvasConverter) CanvasToWorkflowSpec(canvas *db.Canvas) (*WorkflowSpec, error) {
// Validate first
if err := cc.validator.ValidateCanvas(canvas); err != nil {
return nil, fmt.Errorf("canvas validation failed: %w", err)
}
// Get topological order
sortedNodes, err := cc.validator.TopoSort(canvas.Nodes, canvas.Edges)
if err != nil {
return nil, fmt.Errorf("topological sort failed: %w", err)
}
// Build states from sorted nodes
states := []State{}
nodeToState := make(map[string]int) // node ID to state index
for i, node := range sortedNodes {
state := cc.nodeToState(node, canvas.Edges)
states = append(states, state)
nodeToState[node.ID] = i
}
// Wire up transitions
for i, node := range sortedNodes {
outgoing := cc.getOutgoingEdges(node.ID, canvas.Edges)
if len(outgoing) == 0 {
// Last state - no transitions
continue
}
if len(outgoing) == 1 {
// Single outgoing edge
targetNode := outgoing[0]
targetIdx := nodeToState[targetNode]
if targetIdx > i {
states[i].Next = states[targetIdx].Name
}
} else {
// Multiple outgoing edges - parallel
states[i].Type = "Parallel"
branches := []interface{}{}
for _, targetNode := range outgoing {
branches = append(branches, map[string]string{
"state": states[nodeToState[targetNode]].Name,
})
}
if states[i].Branches == nil {
states[i].Branches = branches
}
}
}
spec := &WorkflowSpec{
Name: canvas.Name,
Input: map[string]interface{}{},
States: states,
}
return spec, nil
}
// nodeToState converts a canvas node to a workflow state
func (cc *CanvasConverter) nodeToState(node db.WorkflowNode, edges []db.WorkflowEdge) State {
// Map node type to activity name
activityName := cc.mapActivityType(node.Type)
state := State{
Name: node.ID,
Type: TaskActivity,
Activity: activityName,
Retry: &RetryPolicy{MaxAttempts: 3, BackoffSeconds: 2},
Timeout: "300s",
Parameters: node.Data,
}
return state
}
// mapActivityType maps canvas activity type to Poimen activity
func (cc *CanvasConverter) mapActivityType(canvasType string) string {
typeMap := map[string]string{
"clone-repo": "CloneRepoActivity",
"analyze-code": "AnalyzeCodeActivity",
"security-scan": "SecurityScanActivity",
"generate-report": "GenerateReportActivity",
"deployment-precheck": "DeploymentPreCheckActivity",
"notify-status": "NotifyStatusActivity",
"approve-workflow": "ApproveWorkflowActivity",
"archive-results": "ArchiveResultsActivity",
"retrieve-memory": "RetrieveMemoryActivity",
"assume-role": "AssumeRoleActivity",
"llm-inference": "LLMInferenceActivity",
"llm-batch-inference": "LLMBatchInferenceActivity",
"canvas-reasoner": "CanvasReasonerActivity",
}
if mapped, ok := typeMap[canvasType]; ok {
return mapped
}
return canvasType // fallback to type as-is
}
// getOutgoingEdges returns target node IDs for a given source node
func (cc *CanvasConverter) getOutgoingEdges(nodeID string, edges []db.WorkflowEdge) []string {
targets := []string{}
seen := make(map[string]bool)
for _, edge := range edges {
if edge.Source == nodeID && !seen[edge.Target] {
targets = append(targets, edge.Target)
seen[edge.Target] = true
}
}
return targets
}
// CanvasToExecutionPlan converts canvas to sequential activity list
func (cc *CanvasConverter) CanvasToExecutionPlan(canvas *db.Canvas) ([]ExecutionStep, error) {
// Validate first
if err := cc.validator.ValidateCanvas(canvas); err != nil {
return nil, fmt.Errorf("canvas validation failed: %w", err)
}
// Get topological order
sortedNodes, err := cc.validator.TopoSort(canvas.Nodes, canvas.Edges)
if err != nil {
return nil, fmt.Errorf("topological sort failed: %w", err)
}
steps := []ExecutionStep{}
for i, node := range sortedNodes {
step := ExecutionStep{
Index: i,
NodeID: node.ID,
ActivityName: cc.mapActivityType(node.Type),
Label: node.Label,
Parameters: node.Data,
Timeout: "300s",
}
steps = append(steps, step)
}
return steps, nil
}
// ExecutionStep represents one activity in execution plan
type ExecutionStep struct {
Index int `json:"index"`
NodeID string `json:"node_id"`
ActivityName string `json:"activity_name"`
Label string `json:"label"`
Parameters map[string]interface{} `json:"parameters"`
Timeout string `json:"timeout"`
DependsOn []int `json:"depends_on,omitempty"` // Indices of predecessor steps
}