feat: add CanvasReasonerActivity for auto-inferring workflow connections
ci / test (push) Failing after 6m2s
ci / test (push) Failing after 6m2s
This commit is contained in:
@@ -0,0 +1,184 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rockliang/poimen/workflows/action/llm"
|
||||||
|
"github.com/rockliang/poimen/workflows/pkg/db"
|
||||||
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CanvasReasonerInput infers connections between nodes using LLM reasoning
|
||||||
|
type CanvasReasonerInput struct {
|
||||||
|
Nodes []db.WorkflowNode `json:"nodes"` // Canvas nodes
|
||||||
|
Edges []db.WorkflowEdge `json:"edges"` // Existing edges
|
||||||
|
// If true, only suggest new edges; if false, redesign entire canvas
|
||||||
|
PreserveExisting bool `json:"preserve_existing,omitempty"`
|
||||||
|
AuthToken string `json:"auth_token,omitempty"` // JWT for LLM calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanvasReasonerActivity uses LLM to infer connections between workflow activities
|
||||||
|
func CanvasReasonerActivity(ctx context.Context, in CanvasReasonerInput) (CanvasReasonerOutput, error) {
|
||||||
|
logger := newActivityLogger(ctx)
|
||||||
|
|
||||||
|
output := CanvasReasonerOutput{
|
||||||
|
SuggestedEdges: []db.WorkflowEdge{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(in.Nodes) == 0 {
|
||||||
|
return output, fmt.Errorf("no nodes provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.logf("info", "Analyzing canvas with %d nodes, %d edges", len(in.Nodes), len(in.Edges))
|
||||||
|
|
||||||
|
// Build activity descriptions for LLM context
|
||||||
|
nodeDesc := buildNodeDescriptions(in.Nodes)
|
||||||
|
edgeDesc := buildEdgeDescriptions(in.Edges)
|
||||||
|
|
||||||
|
// Create prompt for LLM reasoning
|
||||||
|
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
|
||||||
|
4. Common workflow patterns
|
||||||
|
|
||||||
|
Respond with JSON containing:
|
||||||
|
{
|
||||||
|
"edges": [{"source": "node-1", "target": "node-2"}, ...],
|
||||||
|
"reasoning": "explanation of why these connections make sense",
|
||||||
|
"confidence": 0.85
|
||||||
|
}`
|
||||||
|
|
||||||
|
userPrompt := fmt.Sprintf(`Canvas Analysis:
|
||||||
|
|
||||||
|
Nodes:
|
||||||
|
%s
|
||||||
|
|
||||||
|
Current Edges:
|
||||||
|
%s
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Return ONLY valid JSON, no markdown code blocks.`, nodeDesc, edgeDesc, getReasoningTask(in.PreserveExisting))
|
||||||
|
|
||||||
|
logger.logf("info", "Calling LLM reasoning (preserve_existing=%v)", in.PreserveExisting)
|
||||||
|
|
||||||
|
// Call LLM
|
||||||
|
client, err := llm.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return output, fmt.Errorf("failed to create LLM client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.CreateMessage(ctx, llm.MessageInput{
|
||||||
|
Model: statemachine.ModelSpec{
|
||||||
|
ModelID: "reasoning", // Use reasoning model for complex analysis
|
||||||
|
},
|
||||||
|
SystemPrompt: systemPrompt,
|
||||||
|
Messages: []llm.MessageParam{
|
||||||
|
{
|
||||||
|
Role: "user",
|
||||||
|
Content: userPrompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AuthToken: in.AuthToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return output, fmt.Errorf("LLM reasoning failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse LLM response
|
||||||
|
var reasonerResp struct {
|
||||||
|
Edges []db.WorkflowEdge `json:"edges"`
|
||||||
|
Reasoning string `json:"reasoning"`
|
||||||
|
Confidence float64 `json:"confidence"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(response), &reasonerResp); err != nil {
|
||||||
|
logger.logf("warn", "Failed to parse LLM response as JSON: %v", err)
|
||||||
|
// Try to extract from response text
|
||||||
|
output.Reasoning = response
|
||||||
|
output.Confidence = 0.5
|
||||||
|
return output, fmt.Errorf("failed to parse LLM response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate suggested edges
|
||||||
|
nodeMap := make(map[string]bool)
|
||||||
|
for _, n := range in.Nodes {
|
||||||
|
nodeMap[n.ID] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
validEdges := []db.WorkflowEdge{}
|
||||||
|
for _, edge := range reasonerResp.Edges {
|
||||||
|
if !nodeMap[edge.Source] {
|
||||||
|
logger.logf("warn", "Suggested edge references unknown source: %s", edge.Source)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !nodeMap[edge.Target] {
|
||||||
|
logger.logf("warn", "Suggested edge references unknown target: %s", edge.Target)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Don't suggest self-loops
|
||||||
|
if edge.Source == edge.Target {
|
||||||
|
logger.logf("warn", "Skipping self-loop: %s", edge.Source)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
validEdges = append(validEdges, edge)
|
||||||
|
}
|
||||||
|
|
||||||
|
output.SuggestedEdges = validEdges
|
||||||
|
output.Reasoning = reasonerResp.Reasoning
|
||||||
|
output.Confidence = reasonerResp.Confidence
|
||||||
|
|
||||||
|
logger.logf("info", "LLM suggested %d edges with confidence %.2f", len(validEdges), output.Confidence)
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildNodeDescriptions creates readable node descriptions for LLM
|
||||||
|
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)
|
||||||
|
if node.Data != nil {
|
||||||
|
if b, err := json.MarshalIndent(node.Data, " ", " "); err == nil {
|
||||||
|
desc += fmt.Sprintf(" Config: %s\n", string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildEdgeDescriptions creates readable edge descriptions for LLM
|
||||||
|
func buildEdgeDescriptions(edges []db.WorkflowEdge) string {
|
||||||
|
if len(edges) == 0 {
|
||||||
|
return "None"
|
||||||
|
}
|
||||||
|
var desc string
|
||||||
|
for i, edge := range edges {
|
||||||
|
desc += fmt.Sprintf("%d. %s → %s\n", i+1, edge.Source, edge.Target)
|
||||||
|
}
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
|
||||||
|
// getReasoningTask returns task description based on preservation mode
|
||||||
|
func getReasoningTask(preserveExisting bool) string {
|
||||||
|
if preserveExisting {
|
||||||
|
return "Keep all existing edges and suggest ONLY NEW edges to improve workflow"
|
||||||
|
}
|
||||||
|
return "Design optimal workflow by suggesting all connections and noting any redundant edges"
|
||||||
|
}
|
||||||
@@ -75,6 +75,7 @@ func main() {
|
|||||||
w.RegisterActivity(action.AssumeRoleActivity)
|
w.RegisterActivity(action.AssumeRoleActivity)
|
||||||
w.RegisterActivity(action.LLMInferenceActivity)
|
w.RegisterActivity(action.LLMInferenceActivity)
|
||||||
w.RegisterActivity(action.LLMBatchInferenceActivity)
|
w.RegisterActivity(action.LLMBatchInferenceActivity)
|
||||||
|
w.RegisterActivity(action.CanvasReasonerActivity)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
errChan := make(chan error, 2)
|
errChan := make(chan error, 2)
|
||||||
|
|||||||
@@ -593,11 +593,79 @@
|
|||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"notes": "Sequential processing of multiple prompts. Use for batch analysis, summarization, etc."
|
"notes": "Sequential processing of multiple prompts. Use for batch analysis, summarization, etc."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CanvasReasonerActivity",
|
||||||
|
"description": "Use LLM reasoning to infer and suggest connections between workflow activities",
|
||||||
|
"category": "workflow",
|
||||||
|
"inputs": {
|
||||||
|
"nodes": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Canvas workflow nodes to analyze",
|
||||||
|
"required": true,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string"},
|
||||||
|
"type": {"type": "string"},
|
||||||
|
"label": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"edges": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Existing edges in the workflow",
|
||||||
|
"required": false,
|
||||||
|
"items": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserve_existing": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If true, only suggest new edges; if false, redesign entire workflow",
|
||||||
|
"required": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"auth_token": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "JWT token for authenticated LLM calls",
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"suggested_edges": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Edges suggested by LLM reasoning",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"source": {"type": "string"},
|
||||||
|
"target": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reasoning": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "LLM explanation of suggested connections"
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Confidence score (0.0-1.0) of the suggestions"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"defaultTimeout": "120s",
|
||||||
|
"isFlaky": true,
|
||||||
|
"recommendedRetries": 2,
|
||||||
|
"retryBackoff": 2.0,
|
||||||
|
"dependencies": [],
|
||||||
|
"notes": "Uses reasoning model to analyze workflow logic. Good for understanding data flow and connections between activities."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"totalActivities": 12,
|
"totalActivities": 13,
|
||||||
"lastUpdated": "2025-08-31T00:00:00Z",
|
"lastUpdated": "2025-09-05T00:00:00Z",
|
||||||
"categories": {
|
"categories": {
|
||||||
"repository": 1,
|
"repository": 1,
|
||||||
"analysis": 1,
|
"analysis": 1,
|
||||||
@@ -609,7 +677,8 @@
|
|||||||
"storage": 1,
|
"storage": 1,
|
||||||
"memory": 1,
|
"memory": 1,
|
||||||
"authentication": 1,
|
"authentication": 1,
|
||||||
"llm": 2
|
"llm": 2,
|
||||||
|
"workflow": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ func (cc *CanvasConverter) mapActivityType(canvasType string) string {
|
|||||||
"assume-role": "AssumeRoleActivity",
|
"assume-role": "AssumeRoleActivity",
|
||||||
"llm-inference": "LLMInferenceActivity",
|
"llm-inference": "LLMInferenceActivity",
|
||||||
"llm-batch-inference": "LLMBatchInferenceActivity",
|
"llm-batch-inference": "LLMBatchInferenceActivity",
|
||||||
|
"canvas-reasoner": "CanvasReasonerActivity",
|
||||||
}
|
}
|
||||||
|
|
||||||
if mapped, ok := typeMap[canvasType]; ok {
|
if mapped, ok := typeMap[canvasType]; ok {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func NewCanvasValidator() *CanvasValidator {
|
|||||||
"assume-role": true,
|
"assume-role": true,
|
||||||
"llm-inference": true,
|
"llm-inference": true,
|
||||||
"llm-batch-inference": true,
|
"llm-batch-inference": true,
|
||||||
|
"canvas-reasoner": true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -9,6 +9,6 @@ metadata:
|
|||||||
app.kubernetes.io/name: poimen
|
app.kubernetes.io/name: poimen
|
||||||
app.kubernetes.io/component: orchestrator
|
app.kubernetes.io/component: orchestrator
|
||||||
data:
|
data:
|
||||||
GIT_COMMIT: "991c1f97" # Updated automatically by CI/CD
|
GIT_COMMIT: "5342bb51" # Updated automatically by CI/CD
|
||||||
GIT_BRANCH: "main"
|
GIT_BRANCH: "main"
|
||||||
DEPLOYMENT_DATE: "2026-09-05"
|
DEPLOYMENT_DATE: "2026-09-05"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: poimen-worker
|
app: poimen-worker
|
||||||
annotations:
|
annotations:
|
||||||
git-commit: "991c1f97" # ✅ Updated on each push, triggers rolling restart
|
git-commit: "5342bb51" # ✅ Updated on each push, triggers rolling restart
|
||||||
deployment-date: "2026-09-05"
|
deployment-date: "2026-09-05"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
|
|||||||
Reference in New Issue
Block a user