feat: add canvas compatibility checking for connection validation
ci / test (push) Failing after 2m46s

This commit is contained in:
Test
2026-09-05 01:01:01 -07:00
parent 2a3b080e29
commit 9624f0e18d
5 changed files with 704 additions and 19 deletions
+85 -17
View File
@@ -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))
}
}
}