355 lines
10 KiB
Go
355 lines
10 KiB
Go
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
|
|
}
|