Files
poimen-workflows/action/canvas_compatibility.go
T

296 lines
8.6 KiB
Go
Raw Normal View History

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 ""
}