feat: add canvas compatibility checking for connection validation
ci / test (push) Failing after 2m46s
ci / test (push) Failing after 2m46s
This commit is contained in:
+85
-17
@@ -21,10 +21,13 @@ type CanvasReasonerInput struct {
|
||||
|
||||
// CanvasReasonerOutput returns suggested edges and reasoning
|
||||
type CanvasReasonerOutput struct {
|
||||
SuggestedEdges []db.WorkflowEdge `json:"suggested_edges"` // New edges to add
|
||||
RemovedEdges []db.WorkflowEdge `json:"removed_edges,omitempty"` // Edges to remove (if redesign)
|
||||
Reasoning string `json:"reasoning"` // LLM explanation
|
||||
Confidence float64 `json:"confidence"` // 0.0-1.0
|
||||
SuggestedEdges []db.WorkflowEdge `json:"suggested_edges"` // New edges to add
|
||||
RemovedEdges []db.WorkflowEdge `json:"removed_edges,omitempty"` // Edges to remove (if redesign)
|
||||
Reasoning string `json:"reasoning"` // LLM explanation
|
||||
Confidence float64 `json:"confidence"` // 0.0-1.0
|
||||
IncompatibleEdges []IncompatibilityWarning `json:"incompatible_edges,omitempty"` // Edges that can't be created
|
||||
DisconnectedNodes []string `json:"disconnected_nodes,omitempty"` // Nodes with no connections
|
||||
UserAlerts []string `json:"user_alerts,omitempty"` // Human-readable warnings
|
||||
}
|
||||
|
||||
// CanvasReasonerActivity uses LLM to infer connections between workflow activities
|
||||
@@ -45,23 +48,29 @@ func CanvasReasonerActivity(ctx context.Context, in CanvasReasonerInput) (Canvas
|
||||
nodeDesc := buildNodeDescriptions(in.Nodes)
|
||||
edgeDesc := buildEdgeDescriptions(in.Edges)
|
||||
|
||||
// Create prompt for LLM reasoning
|
||||
// Create prompt for LLM reasoning with compatibility guidance
|
||||
systemPrompt := `You are a workflow automation expert. Analyze the following activities and suggest logical connections (edges) between them based on:
|
||||
1. Activity input/output compatibility
|
||||
2. Logical execution order
|
||||
3. Data flow requirements
|
||||
1. Activity input/output compatibility (CRITICAL - only connect if outputs match inputs)
|
||||
2. Logical execution order and data flow
|
||||
3. Required dependencies
|
||||
4. Common workflow patterns
|
||||
|
||||
IMPORTANT: Only suggest edges where:
|
||||
- Source activity has outputs (check "outputs" fields)
|
||||
- Target activity has inputs (check "inputs" fields)
|
||||
- Data types are compatible (string→string, object→object, etc)
|
||||
- Connection makes semantic sense (don't connect a notifier to an analyzer)
|
||||
|
||||
Respond with JSON containing:
|
||||
{
|
||||
"edges": [{"source": "node-1", "target": "node-2"}, ...],
|
||||
"reasoning": "explanation of why these connections make sense",
|
||||
"reasoning": "explanation of why these connections make sense and any type mismatches noted",
|
||||
"confidence": 0.85
|
||||
}`
|
||||
|
||||
userPrompt := fmt.Sprintf(`Canvas Analysis:
|
||||
|
||||
Nodes:
|
||||
Nodes (including inputs/outputs):
|
||||
%s
|
||||
|
||||
Current Edges:
|
||||
@@ -69,8 +78,12 @@ Current Edges:
|
||||
|
||||
Task: %s
|
||||
|
||||
Preserve existing edges and suggest only NEW edges to add.
|
||||
If any existing edges don't make sense, note them but keep them unless explicitly wrong.
|
||||
KEY RULES:
|
||||
- Preserve existing edges and suggest only NEW edges to add
|
||||
- SKIP any connections where input/output types don't match
|
||||
- If an activity has no outputs, it cannot be a source
|
||||
- If an activity has no inputs, it cannot be a target
|
||||
- Note any activities that are hard to connect (terminal activities, generators, etc)
|
||||
|
||||
Return ONLY valid JSON, no markdown code blocks.`, nodeDesc, edgeDesc, getReasoningTask(in.PreserveExisting))
|
||||
|
||||
@@ -143,20 +156,75 @@ Return ONLY valid JSON, no markdown code blocks.`, nodeDesc, edgeDesc, getReason
|
||||
output.Reasoning = reasonerResp.Reasoning
|
||||
output.Confidence = reasonerResp.Confidence
|
||||
|
||||
logger.logf("info", "LLM suggested %d edges with confidence %.2f", len(validEdges), output.Confidence)
|
||||
// Check compatibility of suggested edges
|
||||
incompatibilities := CheckCanvasConnectivity(in.Nodes, validEdges)
|
||||
if len(incompatibilities) > 0 {
|
||||
output.IncompatibleEdges = incompatibilities
|
||||
logger.logf("warn", "Found %d incompatible edge connections", len(incompatibilities))
|
||||
|
||||
// Generate user-friendly alerts
|
||||
for i, incompat := range incompatibilities {
|
||||
if i < 5 { // Limit to 5 alerts to avoid spam
|
||||
alert := fmt.Sprintf(
|
||||
"⚠️ %s → %s: %s. %s",
|
||||
incompat.Source, incompat.Target, incompat.Reason, incompat.Suggestion,
|
||||
)
|
||||
output.UserAlerts = append(output.UserAlerts, alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Identify disconnected nodes
|
||||
disconnected := IdentifyDisconnectedNodes(in.Nodes, validEdges)
|
||||
if len(disconnected) > 0 {
|
||||
output.DisconnectedNodes = disconnected
|
||||
logger.logf("warn", "Found %d disconnected nodes", len(disconnected))
|
||||
|
||||
for _, nodeID := range disconnected {
|
||||
var label string
|
||||
for _, node := range in.Nodes {
|
||||
if node.ID == nodeID {
|
||||
label = node.Label
|
||||
break
|
||||
}
|
||||
}
|
||||
alert := fmt.Sprintf(
|
||||
"🔌 Node '%s' has no connections. Consider adding edges or removing it.",
|
||||
label,
|
||||
)
|
||||
output.UserAlerts = append(output.UserAlerts, alert)
|
||||
}
|
||||
}
|
||||
|
||||
logger.logf("info", "LLM suggested %d edges with confidence %.2f | %d incompatibilities | %d disconnected",
|
||||
len(validEdges), output.Confidence, len(incompatibilities), len(disconnected))
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// buildNodeDescriptions creates readable node descriptions for LLM
|
||||
// buildNodeDescriptions creates readable node descriptions for LLM (including schemas)
|
||||
func buildNodeDescriptions(nodes []db.WorkflowNode) string {
|
||||
var desc string
|
||||
for i, node := range nodes {
|
||||
desc += fmt.Sprintf("%d. %s (type: %s)\n", i+1, node.ID, node.Type)
|
||||
desc += fmt.Sprintf(" Label: %s\n", node.Label)
|
||||
desc += fmt.Sprintf("%d. [%s] %s (type: %s)\n", i+1, node.ID, node.Label, node.Type)
|
||||
|
||||
// Add input/output schema info
|
||||
if schema, err := getActivitySchema(node.Type); err == nil {
|
||||
if len(schema.Inputs) > 0 {
|
||||
desc += fmt.Sprintf(" INPUTS: %v\n", getInputNames(schema.Inputs))
|
||||
} else {
|
||||
desc += fmt.Sprintf(" INPUTS: none (generator/trigger)\n")
|
||||
}
|
||||
if len(schema.Outputs) > 0 {
|
||||
desc += fmt.Sprintf(" OUTPUTS: %v\n", getOutputNames(schema.Outputs))
|
||||
} else {
|
||||
desc += fmt.Sprintf(" OUTPUTS: none (terminal/sink)\n")
|
||||
}
|
||||
}
|
||||
|
||||
if node.Data != nil {
|
||||
if b, err := json.MarshalIndent(node.Data, " ", " "); err == nil {
|
||||
desc += fmt.Sprintf(" Config: %s\n", string(b))
|
||||
desc += fmt.Sprintf(" CONFIG: %s\n", string(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user