package action import ( "encoding/json" "fmt" "regexp" "strings" "github.com/rockliang/poimen/workflows/pkg/db" ) // IncompatibilityWarning explains why two activities can't be connected type IncompatibilityWarning struct { Source string `json:"source"` // Source node ID Target string `json:"target"` // Target node ID Reason string `json:"reason"` // Why they can't connect SourceNeeds string `json:"source_needs"` // What source would need to output TargetNeeds string `json:"target_needs"` // What target requires as input Suggestion string `json:"suggestion"` // Suggestion to make it work } // ActivitySchema describes what an activity needs/provides type ActivitySchema struct { ActivityType string `json:"activity_type"` Inputs map[string]InputField `json:"inputs"` Outputs map[string]OutputField `json:"outputs"` } type InputField struct { Type string `json:"type"` Description string `json:"description"` Required bool `json:"required"` Enum []string `json:"enum,omitempty"` } type OutputField struct { Type string `json:"type"` Description string `json:"description"` } // getActivitySchema returns schema from knowledge base func getActivitySchema(activityType string) (*ActivitySchema, error) { kb := knowledgeBaseData() if kb == nil { return nil, fmt.Errorf("knowledge base not loaded") } var activities []map[string]interface{} if err := json.Unmarshal([]byte(kb), &activities); err != nil { // Try to extract activities from full KB structure var fullKB map[string]interface{} if err := json.Unmarshal([]byte(kb), &fullKB); err != nil { return nil, fmt.Errorf("failed to parse knowledge base") } if activitiesRaw, ok := fullKB["activities"]; ok { if b, err := json.Marshal(activitiesRaw); err == nil { if err := json.Unmarshal(b, &activities); err != nil { return nil, fmt.Errorf("failed to extract activities from KB") } } } } // Find matching activity for _, act := range activities { if name, ok := act["name"].(string); ok { if toActivityName(activityType) == name { // Convert to ActivitySchema schema := &ActivitySchema{ ActivityType: activityType, Inputs: make(map[string]InputField), Outputs: make(map[string]OutputField), } if inputs, ok := act["inputs"].(map[string]interface{}); ok { for key, val := range inputs { if field, ok := val.(map[string]interface{}); ok { schema.Inputs[key] = parseInputField(field) } } } if outputs, ok := act["outputs"].(map[string]interface{}); ok { for key, val := range outputs { if field, ok := val.(map[string]interface{}); ok { schema.Outputs[key] = parseOutputField(field) } } } return schema, nil } } } return nil, fmt.Errorf("activity %s not found in knowledge base", activityType) } func parseInputField(data map[string]interface{}) InputField { field := InputField{} if t, ok := data["type"].(string); ok { field.Type = t } if d, ok := data["description"].(string); ok { field.Description = d } if r, ok := data["required"].(bool); ok { field.Required = r } return field } func parseOutputField(data map[string]interface{}) OutputField { field := OutputField{} if t, ok := data["type"].(string); ok { field.Type = t } if d, ok := data["description"].(string); ok { field.Description = d } return field } // CheckConnectionCompatibility validates if source can connect to target func CheckConnectionCompatibility(sourceNode, targetNode db.WorkflowNode) []IncompatibilityWarning { warnings := []IncompatibilityWarning{} sourceSchema, err := getActivitySchema(sourceNode.Type) if err != nil { warnings = append(warnings, IncompatibilityWarning{ Source: sourceNode.ID, Target: targetNode.ID, Reason: fmt.Sprintf("Source activity schema not found: %v", err), Suggestion: "Ensure source activity type is registered in knowledge base", }) return warnings } targetSchema, err := getActivitySchema(targetNode.Type) if err != nil { warnings = append(warnings, IncompatibilityWarning{ Source: sourceNode.ID, Target: targetNode.ID, Reason: fmt.Sprintf("Target activity schema not found: %v", err), Suggestion: "Ensure target activity type is registered in knowledge base", }) return warnings } // Check if source produces outputs that target can consume if len(sourceSchema.Outputs) == 0 { warnings = append(warnings, IncompatibilityWarning{ Source: sourceNode.ID, Target: targetNode.ID, Reason: fmt.Sprintf("%s produces no outputs", sourceNode.Type), SourceNeeds: "any output", Suggestion: "Source activity must produce outputs", }) return warnings } if len(targetSchema.Inputs) == 0 { warnings = append(warnings, IncompatibilityWarning{ Source: sourceNode.ID, Target: targetNode.ID, Reason: fmt.Sprintf("%s accepts no inputs", targetNode.Type), TargetNeeds: "no input", Suggestion: "Target activity must accept inputs. Check if it's a terminal activity.", }) return warnings } // Match outputs to inputs sourceOutputs := getOutputNames(sourceSchema.Outputs) targetInputs := getInputNames(targetSchema.Inputs) if len(sourceOutputs) == 0 || len(targetInputs) == 0 { warnings = append(warnings, IncompatibilityWarning{ Source: sourceNode.ID, Target: targetNode.ID, Reason: "No compatible output/input fields found", SourceNeeds: strings.Join(sourceOutputs, ", "), TargetNeeds: strings.Join(targetInputs, ", "), Suggestion: "Use LLM transformation to map outputs to inputs", }) } return warnings } // CheckCanvasConnectivity analyzes all suggested edges for compatibility func CheckCanvasConnectivity(nodes []db.WorkflowNode, suggestedEdges []db.WorkflowEdge) []IncompatibilityWarning { warnings := []IncompatibilityWarning{} nodeMap := make(map[string]db.WorkflowNode) for _, n := range nodes { nodeMap[n.ID] = n } for _, edge := range suggestedEdges { sourceNode, ok := nodeMap[edge.Source] if !ok { continue } targetNode, ok := nodeMap[edge.Target] if !ok { continue } edgeWarnings := CheckConnectionCompatibility(sourceNode, targetNode) warnings = append(warnings, edgeWarnings...) } return warnings } // IdentifyDisconnectedNodes finds nodes that can't connect to anything func IdentifyDisconnectedNodes(nodes []db.WorkflowNode, suggestedEdges []db.WorkflowEdge) []string { edgeMap := make(map[string]bool) for _, edge := range suggestedEdges { edgeMap[edge.Source] = true edgeMap[edge.Target] = true } var disconnected []string for _, node := range nodes { if !edgeMap[node.ID] { disconnected = append(disconnected, node.ID) } } return disconnected } // toActivityName converts canvas type to activity name (e.g., "clone-repo" -> "CloneRepoActivity") func toActivityName(canvasType string) string { parts := strings.Split(canvasType, "-") var result string for _, part := range parts { if part != "" { result += strings.ToUpper(part[:1]) + strings.ToLower(part[1:]) } } return result + "Activity" } // getOutputNames extracts output field names func getOutputNames(outputs map[string]OutputField) []string { var names []string for name := range outputs { names = append(names, name) } return names } // getInputNames extracts input field names (required ones highlighted) func getInputNames(inputs map[string]InputField) []string { var names []string for name, field := range inputs { if field.Required { names = append(names, name+"*") } else { names = append(names, name) } } return names } // SuggestDataTransformation proposes how to connect incompatible activities func SuggestDataTransformation(sourceNode, targetNode db.WorkflowNode) string { sourceSchema, _ := getActivitySchema(sourceNode.Type) targetSchema, _ := getActivitySchema(targetNode.Type) if sourceSchema == nil || targetSchema == nil { return "Cannot analyze compatibility without schemas" } sourceOuts := getOutputNames(sourceSchema.Outputs) targetIns := getInputNames(targetSchema.Inputs) return fmt.Sprintf( "To connect %s → %s:\n"+ " %s outputs: %s\n"+ " %s needs: %s\n"+ " Solution: Use LLM transformation node to map outputs to inputs", sourceNode.Label, targetNode.Label, sourceNode.Type, strings.Join(sourceOuts, ", "), targetNode.Type, strings.Join(targetIns, ", "), ) } // knowledgeBaseData returns raw KB JSON (stub - implement with actual KB loading) func knowledgeBaseData() string { // This would load from activity_knowledge_base.json // For now, return empty - real implementation loads from file return "" } // CanvasCompatibilityInput for Temporal activity type CanvasCompatibilityInput struct { Nodes []db.WorkflowNode `json:"nodes"` Edges []db.WorkflowEdge `json:"edges"` } // CanvasCompatibilityOutput returns validation results type CanvasCompatibilityOutput struct { IsValid bool `json:"is_valid"` Incompatibilities []IncompatibilityWarning `json:"incompatibilities"` DisconnectedNodes []string `json:"disconnected_nodes"` Warnings []string `json:"warnings"` } // CanvasCompatibilityActivity validates workflow canvas for type mismatches and isolation func CanvasCompatibilityActivity(ctx interface{}, input CanvasCompatibilityInput) (CanvasCompatibilityOutput, error) { output := CanvasCompatibilityOutput{ IsValid: true, Incompatibilities: []IncompatibilityWarning{}, DisconnectedNodes: []string{}, Warnings: []string{}, } // Check all edges for compatibility for _, edge := range input.Edges { var sourceNode, targetNode *db.WorkflowNode for i := range input.Nodes { if input.Nodes[i].ID == edge.Source { sourceNode = &input.Nodes[i] } if input.Nodes[i].ID == edge.Target { targetNode = &input.Nodes[i] } } if sourceNode != nil && targetNode != nil { if warning, err := ValidateConnection(sourceNode, targetNode); err != nil { output.IsValid = false output.Incompatibilities = append(output.Incompatibilities, warning) } } } // Find disconnected nodes connected := make(map[string]bool) for _, edge := range input.Edges { connected[edge.Source] = true connected[edge.Target] = true } for _, node := range input.Nodes { if node.Type == "activity" && !connected[node.ID] { output.DisconnectedNodes = append(output.DisconnectedNodes, node.ID) } } return output, nil }