179 lines
4.9 KiB
Go
179 lines
4.9 KiB
Go
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",
|
||
|
|
}
|
||
|
|
|
||
|
|
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
|
||
|
|
}
|