refactor: how to add a node type in workflow (#558)

This commit is contained in:
shentongmartin
2025-08-05 14:02:33 +08:00
committed by GitHub
parent 5dafd81a3f
commit bb6ff0026b
96 changed files with 8305 additions and 8717 deletions

View File

@@ -58,6 +58,11 @@ import (
"github.com/coze-dev/coze-studio/backend/types/consts"
)
func TestMain(m *testing.M) {
RegisterAllNodeAdaptors()
m.Run()
}
func TestIntentDetectorAndDatabase(t *testing.T) {
mockey.PatchConvey("intent detector & database custom sql", t, func() {
data, err := os.ReadFile("../examples/intent_detector_database_custom_sql.json")

View File

@@ -26,10 +26,11 @@ import (
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/compose"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/schema"
)
func WorkflowSchemaFromNode(ctx context.Context, c *vo.Canvas, nodeID string) (
*compose.WorkflowSchema, error) {
*schema.WorkflowSchema, error) {
var (
n *vo.Node
nodeFinder func(nodes []*vo.Node) *vo.Node
@@ -62,35 +63,27 @@ func WorkflowSchemaFromNode(ctx context.Context, c *vo.Canvas, nodeID string) (
n = batchN
}
implicitDependencies, err := extractImplicitDependency(n, c.Nodes)
if err != nil {
return nil, err
}
opts := make([]OptionFn, 0, 1)
if len(implicitDependencies) > 0 {
opts = append(opts, WithImplicitNodeDependencies(implicitDependencies))
}
nsList, hierarchy, err := NodeToNodeSchema(ctx, n, opts...)
nsList, hierarchy, err := NodeToNodeSchema(ctx, n, c)
if err != nil {
return nil, err
}
var (
ns *compose.NodeSchema
innerNodes map[vo.NodeKey]*compose.NodeSchema // inner nodes of the composite node if nodeKey is composite
connections []*compose.Connection
ns *schema.NodeSchema
innerNodes map[vo.NodeKey]*schema.NodeSchema // inner nodes of the composite node if nodeKey is composite
connections []*schema.Connection
)
if len(nsList) == 1 {
ns = nsList[0]
} else {
innerNodes = make(map[vo.NodeKey]*compose.NodeSchema)
innerNodes = make(map[vo.NodeKey]*schema.NodeSchema)
for i := range nsList {
one := nsList[i]
if _, ok := hierarchy[one.Key]; ok {
innerNodes[one.Key] = one
if one.Type == entity.NodeTypeContinue || one.Type == entity.NodeTypeBreak {
connections = append(connections, &compose.Connection{
connections = append(connections, &schema.Connection{
FromNode: one.Key,
ToNode: vo.NodeKey(nodeID),
})
@@ -106,13 +99,13 @@ func WorkflowSchemaFromNode(ctx context.Context, c *vo.Canvas, nodeID string) (
}
const inputFillerKey = "input_filler"
connections = append(connections, &compose.Connection{
connections = append(connections, &schema.Connection{
FromNode: einoCompose.START,
ToNode: inputFillerKey,
}, &compose.Connection{
}, &schema.Connection{
FromNode: inputFillerKey,
ToNode: ns.Key,
}, &compose.Connection{
}, &schema.Connection{
FromNode: ns.Key,
ToNode: einoCompose.END,
})
@@ -209,7 +202,7 @@ func WorkflowSchemaFromNode(ctx context.Context, c *vo.Canvas, nodeID string) (
return newOutput, nil
}
inputFiller := &compose.NodeSchema{
inputFiller := &schema.NodeSchema{
Key: inputFillerKey,
Type: entity.NodeTypeLambda,
Lambda: einoCompose.InvokableLambda(i),
@@ -227,10 +220,16 @@ func WorkflowSchemaFromNode(ctx context.Context, c *vo.Canvas, nodeID string) (
OutputTypes: startOutputTypes,
}
trimmedSC := &compose.WorkflowSchema{
Nodes: append([]*compose.NodeSchema{ns, inputFiller}, maps.Values(innerNodes)...),
branches, err := schema.BuildBranches(connections)
if err != nil {
return nil, err
}
trimmedSC := &schema.WorkflowSchema{
Nodes: append([]*schema.NodeSchema{ns, inputFiller}, maps.Values(innerNodes)...),
Connections: connections,
Hierarchy: hierarchy,
Branches: branches,
}
if enabled {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,663 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package convert
import (
"fmt"
"strconv"
"strings"
einoCompose "github.com/cloudwego/eino/compose"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/schema"
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
func CanvasVariableToTypeInfo(v *vo.Variable) (*vo.TypeInfo, error) {
tInfo := &vo.TypeInfo{
Required: v.Required,
Desc: v.Description,
}
switch v.Type {
case vo.VariableTypeString:
switch v.AssistType {
case vo.AssistTypeTime:
tInfo.Type = vo.DataTypeTime
case vo.AssistTypeNotSet:
tInfo.Type = vo.DataTypeString
default:
fileType, ok := assistTypeToFileType(v.AssistType)
if ok {
tInfo.Type = vo.DataTypeFile
tInfo.FileType = &fileType
} else {
return nil, fmt.Errorf("unsupported assist type: %v", v.AssistType)
}
}
case vo.VariableTypeInteger:
tInfo.Type = vo.DataTypeInteger
case vo.VariableTypeFloat:
tInfo.Type = vo.DataTypeNumber
case vo.VariableTypeBoolean:
tInfo.Type = vo.DataTypeBoolean
case vo.VariableTypeObject:
tInfo.Type = vo.DataTypeObject
tInfo.Properties = make(map[string]*vo.TypeInfo)
if v.Schema != nil {
for _, subVAny := range v.Schema.([]any) {
subV, err := vo.ParseVariable(subVAny)
if err != nil {
return nil, err
}
subTInfo, err := CanvasVariableToTypeInfo(subV)
if err != nil {
return nil, err
}
tInfo.Properties[subV.Name] = subTInfo
}
}
case vo.VariableTypeList:
tInfo.Type = vo.DataTypeArray
subVAny := v.Schema
subV, err := vo.ParseVariable(subVAny)
if err != nil {
return nil, err
}
subTInfo, err := CanvasVariableToTypeInfo(subV)
if err != nil {
return nil, err
}
tInfo.ElemTypeInfo = subTInfo
default:
return nil, fmt.Errorf("unsupported variable type: %s", v.Type)
}
return tInfo, nil
}
func CanvasBlockInputToTypeInfo(b *vo.BlockInput) (tInfo *vo.TypeInfo, err error) {
defer func() {
if err != nil {
err = vo.WrapIfNeeded(errno.ErrSchemaConversionFail, err)
}
}()
tInfo = &vo.TypeInfo{}
if b == nil {
return tInfo, nil
}
switch b.Type {
case vo.VariableTypeString:
switch b.AssistType {
case vo.AssistTypeTime:
tInfo.Type = vo.DataTypeTime
case vo.AssistTypeNotSet:
tInfo.Type = vo.DataTypeString
default:
fileType, ok := assistTypeToFileType(b.AssistType)
if ok {
tInfo.Type = vo.DataTypeFile
tInfo.FileType = &fileType
} else {
return nil, fmt.Errorf("unsupported assist type: %v", b.AssistType)
}
}
case vo.VariableTypeInteger:
tInfo.Type = vo.DataTypeInteger
case vo.VariableTypeFloat:
tInfo.Type = vo.DataTypeNumber
case vo.VariableTypeBoolean:
tInfo.Type = vo.DataTypeBoolean
case vo.VariableTypeObject:
tInfo.Type = vo.DataTypeObject
tInfo.Properties = make(map[string]*vo.TypeInfo)
if b.Schema != nil {
for _, subVAny := range b.Schema.([]any) {
if b.Value.Type == vo.BlockInputValueTypeRef {
subV, err := vo.ParseVariable(subVAny)
if err != nil {
return nil, err
}
subTInfo, err := CanvasVariableToTypeInfo(subV)
if err != nil {
return nil, err
}
tInfo.Properties[subV.Name] = subTInfo
} else if b.Value.Type == vo.BlockInputValueTypeObjectRef {
subV, err := parseParam(subVAny)
if err != nil {
return nil, err
}
subTInfo, err := CanvasBlockInputToTypeInfo(subV.Input)
if err != nil {
return nil, err
}
tInfo.Properties[subV.Name] = subTInfo
}
}
}
case vo.VariableTypeList:
tInfo.Type = vo.DataTypeArray
subVAny := b.Schema
subV, err := vo.ParseVariable(subVAny)
if err != nil {
return nil, err
}
subTInfo, err := CanvasVariableToTypeInfo(subV)
if err != nil {
return nil, err
}
tInfo.ElemTypeInfo = subTInfo
default:
return nil, fmt.Errorf("unsupported variable type: %s", b.Type)
}
return tInfo, nil
}
func CanvasBlockInputToFieldInfo(b *vo.BlockInput, path einoCompose.FieldPath, parentNode *vo.Node) (sources []*vo.FieldInfo, err error) {
value := b.Value
if value == nil {
return nil, fmt.Errorf("input %v has no value, type= %s", path, b.Type)
}
switch value.Type {
case vo.BlockInputValueTypeObjectRef:
sc := b.Schema
if sc == nil {
return nil, fmt.Errorf("input %v has no schema, type= %s", path, b.Type)
}
paramList, ok := sc.([]any)
if !ok {
return nil, fmt.Errorf("input %v schema not []any, type= %T", path, sc)
}
for i := range paramList {
paramAny := paramList[i]
param, err := parseParam(paramAny)
if err != nil {
return nil, err
}
copied := make([]string, len(path))
copy(copied, path)
subFieldInfo, err := CanvasBlockInputToFieldInfo(param.Input, append(copied, param.Name), parentNode)
if err != nil {
return nil, err
}
sources = append(sources, subFieldInfo...)
}
return sources, nil
case vo.BlockInputValueTypeLiteral:
content := value.Content
if content == nil {
return nil, fmt.Errorf("input %v is literal but has no value, type= %s", path, b.Type)
}
switch b.Type {
case vo.VariableTypeObject:
m := make(map[string]any)
if err = sonic.UnmarshalString(content.(string), &m); err != nil {
return nil, err
}
content = m
case vo.VariableTypeList:
l := make([]any, 0)
if err = sonic.UnmarshalString(content.(string), &l); err != nil {
return nil, err
}
content = l
case vo.VariableTypeInteger:
switch content.(type) {
case string:
content, err = strconv.ParseInt(content.(string), 10, 64)
if err != nil {
return nil, err
}
case int64:
content = content.(int64)
case float64:
content = int64(content.(float64))
default:
return nil, fmt.Errorf("unsupported variable type fot integer: %s", b.Type)
}
case vo.VariableTypeFloat:
switch content.(type) {
case string:
content, err = strconv.ParseFloat(content.(string), 64)
if err != nil {
return nil, err
}
case int64:
content = float64(content.(int64))
case float64:
content = content.(float64)
default:
return nil, fmt.Errorf("unsupported variable type for float: %s", b.Type)
}
case vo.VariableTypeBoolean:
switch content.(type) {
case string:
content, err = strconv.ParseBool(content.(string))
if err != nil {
return nil, err
}
case bool:
content = content.(bool)
default:
return nil, fmt.Errorf("unsupported variable type for boolean: %s", b.Type)
}
default:
}
return []*vo.FieldInfo{
{
Path: path,
Source: vo.FieldSource{
Val: content,
},
},
}, nil
case vo.BlockInputValueTypeRef:
content := value.Content
if content == nil {
return nil, fmt.Errorf("input %v is literal but has no value, type= %s", path, b.Type)
}
ref, err := parseBlockInputRef(content)
if err != nil {
return nil, err
}
fieldSource, err := CanvasBlockInputRefToFieldSource(ref)
if err != nil {
return nil, err
}
if parentNode != nil {
if fieldSource.Ref != nil && len(fieldSource.Ref.FromNodeKey) > 0 && fieldSource.Ref.FromNodeKey == vo.NodeKey(parentNode.ID) {
varRoot := fieldSource.Ref.FromPath[0]
if parentNode.Data.Inputs.Loop != nil {
for _, p := range parentNode.Data.Inputs.VariableParameters {
if p.Name == varRoot {
fieldSource.Ref.FromNodeKey = ""
pi := vo.ParentIntermediate
fieldSource.Ref.VariableType = &pi
}
}
}
}
}
return []*vo.FieldInfo{
{
Path: path,
Source: *fieldSource,
},
}, nil
default:
return nil, fmt.Errorf("unsupported value type: %s for blockInput type= %s", value.Type, b.Type)
}
}
func parseBlockInputRef(content any) (*vo.BlockInputReference, error) {
if bi, ok := content.(*vo.BlockInputReference); ok {
return bi, nil
}
m, ok := content.(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid content type: %T when parse BlockInputRef", content)
}
marshaled, err := sonic.Marshal(m)
if err != nil {
return nil, err
}
p := &vo.BlockInputReference{}
if err := sonic.Unmarshal(marshaled, p); err != nil {
return nil, err
}
return p, nil
}
func parseParam(v any) (*vo.Param, error) {
if pa, ok := v.(*vo.Param); ok {
return pa, nil
}
m, ok := v.(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid content type: %T when parse Param", v)
}
marshaled, err := sonic.Marshal(m)
if err != nil {
return nil, err
}
p := &vo.Param{}
if err := sonic.Unmarshal(marshaled, p); err != nil {
return nil, err
}
return p, nil
}
func CanvasBlockInputRefToFieldSource(r *vo.BlockInputReference) (*vo.FieldSource, error) {
switch r.Source {
case vo.RefSourceTypeBlockOutput:
if len(r.BlockID) == 0 {
return nil, fmt.Errorf("invalid BlockInputReference = %+v, BlockID is empty when source is block output", r)
}
parts := strings.Split(r.Name, ".") // an empty r.Name signals an all-to-all mapping
return &vo.FieldSource{
Ref: &vo.Reference{
FromNodeKey: vo.NodeKey(r.BlockID),
FromPath: parts,
},
}, nil
case vo.RefSourceTypeGlobalApp, vo.RefSourceTypeGlobalSystem, vo.RefSourceTypeGlobalUser:
if len(r.Path) == 0 {
return nil, fmt.Errorf("invalid BlockInputReference = %+v, Path is empty when source is variables", r)
}
var varType vo.GlobalVarType
switch r.Source {
case vo.RefSourceTypeGlobalApp:
varType = vo.GlobalAPP
case vo.RefSourceTypeGlobalSystem:
varType = vo.GlobalSystem
case vo.RefSourceTypeGlobalUser:
varType = vo.GlobalUser
default:
return nil, fmt.Errorf("invalid BlockInputReference = %+v, Source is invalid", r)
}
return &vo.FieldSource{
Ref: &vo.Reference{
VariableType: &varType,
FromPath: r.Path,
},
}, nil
default:
return nil, fmt.Errorf("unsupported ref source type: %s", r.Source)
}
}
func assistTypeToFileType(a vo.AssistType) (vo.FileSubType, bool) {
switch a {
case vo.AssistTypeNotSet:
return "", false
case vo.AssistTypeTime:
return "", false
case vo.AssistTypeImage:
return vo.FileTypeImage, true
case vo.AssistTypeAudio:
return vo.FileTypeAudio, true
case vo.AssistTypeVideo:
return vo.FileTypeVideo, true
case vo.AssistTypeDefault:
return vo.FileTypeDefault, true
case vo.AssistTypeDoc:
return vo.FileTypeDocument, true
case vo.AssistTypeExcel:
return vo.FileTypeExcel, true
case vo.AssistTypeCode:
return vo.FileTypeCode, true
case vo.AssistTypePPT:
return vo.FileTypePPT, true
case vo.AssistTypeTXT:
return vo.FileTypeTxt, true
case vo.AssistTypeSvg:
return vo.FileTypeSVG, true
case vo.AssistTypeVoice:
return vo.FileTypeVoice, true
case vo.AssistTypeZip:
return vo.FileTypeZip, true
default:
panic("impossible")
}
}
func SetInputsForNodeSchema(n *vo.Node, ns *schema.NodeSchema) error {
if n.Data.Inputs == nil {
return nil
}
inputParams := n.Data.Inputs.InputParameters
if len(inputParams) == 0 {
return nil
}
for _, param := range inputParams {
name := param.Name
tInfo, err := CanvasBlockInputToTypeInfo(param.Input)
if err != nil {
return err
}
ns.SetInputType(name, tInfo)
sources, err := CanvasBlockInputToFieldInfo(param.Input, einoCompose.FieldPath{name}, n.Parent())
if err != nil {
return err
}
ns.AddInputSource(sources...)
}
return nil
}
func SetOutputTypesForNodeSchema(n *vo.Node, ns *schema.NodeSchema) error {
for _, vAny := range n.Data.Outputs {
v, err := vo.ParseVariable(vAny)
if err != nil {
return err
}
tInfo, err := CanvasVariableToTypeInfo(v)
if err != nil {
return err
}
if v.ReadOnly {
if v.Name == "errorBody" { // reserved output fields when exception happens
continue
}
}
ns.SetOutputType(v.Name, tInfo)
}
return nil
}
func SetOutputsForNodeSchema(n *vo.Node, ns *schema.NodeSchema) error {
for _, vAny := range n.Data.Outputs {
param, err := parseParam(vAny)
if err != nil {
return err
}
name := param.Name
tInfo, err := CanvasBlockInputToTypeInfo(param.Input)
if err != nil {
return err
}
ns.SetOutputType(name, tInfo)
sources, err := CanvasBlockInputToFieldInfo(param.Input, einoCompose.FieldPath{name}, n.Parent())
if err != nil {
return err
}
ns.AddOutputSource(sources...)
}
return nil
}
func BlockInputToNamedTypeInfo(name string, b *vo.BlockInput) (*vo.NamedTypeInfo, error) {
tInfo := &vo.NamedTypeInfo{
Name: name,
}
if b == nil {
return tInfo, nil
}
switch b.Type {
case vo.VariableTypeString:
switch b.AssistType {
case vo.AssistTypeTime:
tInfo.Type = vo.DataTypeTime
case vo.AssistTypeNotSet:
tInfo.Type = vo.DataTypeString
default:
fileType, ok := assistTypeToFileType(b.AssistType)
if ok {
tInfo.Type = vo.DataTypeFile
tInfo.FileType = &fileType
} else {
return nil, fmt.Errorf("unsupported assist type: %v", b.AssistType)
}
}
case vo.VariableTypeInteger:
tInfo.Type = vo.DataTypeInteger
case vo.VariableTypeFloat:
tInfo.Type = vo.DataTypeNumber
case vo.VariableTypeBoolean:
tInfo.Type = vo.DataTypeBoolean
case vo.VariableTypeObject:
tInfo.Type = vo.DataTypeObject
if b.Schema != nil {
tInfo.Properties = make([]*vo.NamedTypeInfo, 0, len(b.Schema.([]any)))
for _, subVAny := range b.Schema.([]any) {
if b.Value.Type == vo.BlockInputValueTypeRef {
subV, err := vo.ParseVariable(subVAny)
if err != nil {
return nil, err
}
subNInfo, err := VariableToNamedTypeInfo(subV)
if err != nil {
return nil, err
}
tInfo.Properties = append(tInfo.Properties, subNInfo)
} else if b.Value.Type == vo.BlockInputValueTypeObjectRef {
subV, err := parseParam(subVAny)
if err != nil {
return nil, err
}
subNInfo, err := BlockInputToNamedTypeInfo(subV.Name, subV.Input)
if err != nil {
return nil, err
}
tInfo.Properties = append(tInfo.Properties, subNInfo)
}
}
}
case vo.VariableTypeList:
tInfo.Type = vo.DataTypeArray
subVAny := b.Schema
subV, err := vo.ParseVariable(subVAny)
if err != nil {
return nil, err
}
subNInfo, err := VariableToNamedTypeInfo(subV)
if err != nil {
return nil, err
}
tInfo.ElemTypeInfo = subNInfo
default:
return nil, fmt.Errorf("unsupported variable type: %s", b.Type)
}
return tInfo, nil
}
func VariableToNamedTypeInfo(v *vo.Variable) (*vo.NamedTypeInfo, error) {
nInfo := &vo.NamedTypeInfo{
Required: v.Required,
Name: v.Name,
Desc: v.Description,
}
switch v.Type {
case vo.VariableTypeString:
switch v.AssistType {
case vo.AssistTypeTime:
nInfo.Type = vo.DataTypeTime
case vo.AssistTypeNotSet:
nInfo.Type = vo.DataTypeString
default:
fileType, ok := assistTypeToFileType(v.AssistType)
if ok {
nInfo.Type = vo.DataTypeFile
nInfo.FileType = &fileType
} else {
return nil, fmt.Errorf("unsupported assist type: %v", v.AssistType)
}
}
case vo.VariableTypeInteger:
nInfo.Type = vo.DataTypeInteger
case vo.VariableTypeFloat:
nInfo.Type = vo.DataTypeNumber
case vo.VariableTypeBoolean:
nInfo.Type = vo.DataTypeBoolean
case vo.VariableTypeObject:
nInfo.Type = vo.DataTypeObject
if v.Schema != nil {
nInfo.Properties = make([]*vo.NamedTypeInfo, 0)
for _, subVAny := range v.Schema.([]any) {
subV, err := vo.ParseVariable(subVAny)
if err != nil {
return nil, err
}
subTInfo, err := VariableToNamedTypeInfo(subV)
if err != nil {
return nil, err
}
nInfo.Properties = append(nInfo.Properties, subTInfo)
}
}
case vo.VariableTypeList:
nInfo.Type = vo.DataTypeArray
subVAny := v.Schema
subV, err := vo.ParseVariable(subVAny)
if err != nil {
return nil, err
}
subTInfo, err := VariableToNamedTypeInfo(subV)
if err != nil {
return nil, err
}
nInfo.ElemTypeInfo = subTInfo
default:
return nil, fmt.Errorf("unsupported variable type: %s", v.Type)
}
return nInfo, nil
}

View File

@@ -24,8 +24,11 @@ import (
"github.com/coze-dev/coze-studio/backend/domain/workflow"
"github.com/coze-dev/coze-studio/backend/domain/workflow/crossdomain/variable"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/canvas/adaptor"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/canvas/convert"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/nodes"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/schema"
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
@@ -123,7 +126,7 @@ func (cv *CanvasValidator) ValidateConnections(ctx context.Context) (issues []*I
return issues, nil
}
func (cv *CanvasValidator) CheckRefVariable(ctx context.Context) (issues []*Issue, err error) {
func (cv *CanvasValidator) CheckRefVariable(_ context.Context) (issues []*Issue, err error) {
issues = make([]*Issue, 0)
var checkRefVariable func(reachability *reachability, reachableNodes map[string]bool) error
checkRefVariable = func(reachability *reachability, parentReachableNodes map[string]bool) error {
@@ -237,7 +240,7 @@ func (cv *CanvasValidator) CheckRefVariable(ctx context.Context) (issues []*Issu
return issues, nil
}
func (cv *CanvasValidator) ValidateNestedFlows(ctx context.Context) (issues []*Issue, err error) {
func (cv *CanvasValidator) ValidateNestedFlows(_ context.Context) (issues []*Issue, err error) {
issues = make([]*Issue, 0)
for nodeID, node := range cv.reachability.reachableNodes {
if nestedReachableNodes, ok := cv.reachability.nestedReachability[nodeID]; ok && len(nestedReachableNodes.nestedReachability) > 0 {
@@ -265,13 +268,13 @@ func (cv *CanvasValidator) CheckGlobalVariables(ctx context.Context) (issues []*
nVars := make([]*nodeVars, 0)
for _, node := range cv.cfg.Canvas.Nodes {
if node.Type == vo.BlockTypeBotComment {
if node.Type == entity.NodeTypeComment.IDStr() {
continue
}
if node.Type == vo.BlockTypeBotAssignVariable {
if node.Type == entity.NodeTypeVariableAssigner.IDStr() {
v := &nodeVars{node: node, vars: make(map[string]*vo.TypeInfo)}
for _, p := range node.Data.Inputs.InputParameters {
v.vars[p.Name], err = adaptor.CanvasBlockInputToTypeInfo(p.Left)
v.vars[p.Name], err = convert.CanvasBlockInputToTypeInfo(p.Left)
if err != nil {
return nil, err
}
@@ -338,7 +341,7 @@ func (cv *CanvasValidator) CheckSubWorkFlowTerminatePlanType(ctx context.Context
var collectSubWorkFlowNodes func(nodes []*vo.Node)
collectSubWorkFlowNodes = func(nodes []*vo.Node) {
for _, n := range nodes {
if n.Type == vo.BlockTypeBotSubWorkflow {
if n.Type == entity.NodeTypeSubWorkflow.IDStr() {
subWfMap = append(subWfMap, n)
wID, err := strconv.ParseInt(n.Data.Inputs.WorkflowID, 10, 64)
if err != nil {
@@ -465,62 +468,28 @@ func validateConnections(ctx context.Context, c *vo.Canvas) (issues []*Issue, er
selectorPorts := make(map[string]map[string]bool)
for nodeID, node := range nodeMap {
switch node.Type {
case vo.BlockTypeCondition:
branches := node.Data.Inputs.Branches
if node.Data.Inputs != nil && node.Data.Inputs.SettingOnError != nil &&
node.Data.Inputs.SettingOnError.ProcessType != nil &&
*node.Data.Inputs.SettingOnError.ProcessType == vo.ErrorProcessTypeExceptionBranch {
if _, exists := selectorPorts[nodeID]; !exists {
selectorPorts[nodeID] = make(map[string]bool)
}
selectorPorts[nodeID]["false"] = true
for index := range branches {
if index == 0 {
selectorPorts[nodeID]["true"] = true
} else {
selectorPorts[nodeID][fmt.Sprintf("true_%v", index)] = true
}
}
case vo.BlockTypeBotIntent:
intents := node.Data.Inputs.Intents
if _, exists := selectorPorts[nodeID]; !exists {
selectorPorts[nodeID] = make(map[string]bool)
}
for index := range intents {
selectorPorts[nodeID][fmt.Sprintf("branch_%v", index)] = true
}
selectorPorts[nodeID]["default"] = true
if node.Data.Inputs.SettingOnError != nil && node.Data.Inputs.SettingOnError.ProcessType != nil &&
*node.Data.Inputs.SettingOnError.ProcessType == vo.ErrorProcessTypeExceptionBranch {
selectorPorts[nodeID]["branch_error"] = true
}
case vo.BlockTypeQuestion:
if node.Data.Inputs.QA.AnswerType == vo.QAAnswerTypeOption {
if _, exists := selectorPorts[nodeID]; !exists {
selectorPorts[nodeID] = make(map[string]bool)
}
if node.Data.Inputs.QA.OptionType == vo.QAOptionTypeStatic {
for index := range node.Data.Inputs.QA.Options {
selectorPorts[nodeID][fmt.Sprintf("branch_%v", index)] = true
}
}
if node.Data.Inputs.QA.OptionType == vo.QAOptionTypeDynamic {
selectorPorts[nodeID][fmt.Sprintf("branch_%v", 0)] = true
}
}
default:
if node.Data.Inputs != nil && node.Data.Inputs.SettingOnError != nil &&
node.Data.Inputs.SettingOnError.ProcessType != nil &&
*node.Data.Inputs.SettingOnError.ProcessType == vo.ErrorProcessTypeExceptionBranch {
if _, exists := selectorPorts[nodeID]; !exists {
selectorPorts[nodeID] = make(map[string]bool)
}
selectorPorts[nodeID]["branch_error"] = true
selectorPorts[nodeID]["default"] = true
} else {
outDegree[node.ID] = 0
}
selectorPorts[nodeID][schema.PortBranchError] = true
selectorPorts[nodeID][schema.PortDefault] = true
}
ba, ok := nodes.GetBranchAdaptor(entity.IDStrToNodeType(node.Type))
if ok {
expects := ba.ExpectPorts(ctx, node)
if len(expects) > 0 {
if _, exists := selectorPorts[nodeID]; !exists {
selectorPorts[nodeID] = make(map[string]bool)
}
for _, e := range expects {
selectorPorts[nodeID][e] = true
}
}
}
}
for _, edge := range c.Edges {
@@ -544,8 +513,8 @@ func validateConnections(ctx context.Context, c *vo.Canvas) (issues []*Issue, er
for nodeID, node := range nodeMap {
nodeName := node.Data.Meta.Title
switch node.Type {
case vo.BlockTypeBotStart:
switch et := entity.IDStrToNodeType(node.Type); et {
case entity.NodeTypeEntry:
if outDegree[nodeID] == 0 {
issues = append(issues, &Issue{
NodeErr: &NodeErr{
@@ -555,13 +524,9 @@ func validateConnections(ctx context.Context, c *vo.Canvas) (issues []*Issue, er
Message: `node "start" not connected`,
})
}
case vo.BlockTypeBotEnd:
case entity.NodeTypeExit:
default:
if ports, isSelector := selectorPorts[nodeID]; isSelector {
selectorIssues := &Issue{NodeErr: &NodeErr{
NodeID: node.ID,
NodeName: nodeName,
}}
message := ""
for port := range ports {
if portOutDegree[nodeID][port] == 0 {
@@ -569,12 +534,15 @@ func validateConnections(ctx context.Context, c *vo.Canvas) (issues []*Issue, er
}
}
if len(message) > 0 {
selectorIssues.Message = message
selectorIssues := &Issue{NodeErr: &NodeErr{
NodeID: node.ID,
NodeName: nodeName,
}, Message: message}
issues = append(issues, selectorIssues)
}
} else {
// Break, continue without checking out degrees
if node.Type == vo.BlockTypeBotBreak || node.Type == vo.BlockTypeBotContinue {
if et == entity.NodeTypeBreak || et == entity.NodeTypeContinue {
continue
}
if outDegree[nodeID] == 0 {
@@ -585,7 +553,6 @@ func validateConnections(ctx context.Context, c *vo.Canvas) (issues []*Issue, er
},
Message: fmt.Sprintf(`node "%v" not connected`, nodeName),
})
}
}
}
@@ -602,7 +569,7 @@ func analyzeCanvasReachability(c *vo.Canvas) (*reachability, error) {
return nil, err
}
startNode, endNode, err := findStartAndEndNodes(c.Nodes)
startNode, _, err := findStartAndEndNodes(c.Nodes)
if err != nil {
return nil, err
}
@@ -612,7 +579,7 @@ func analyzeCanvasReachability(c *vo.Canvas) (*reachability, error) {
edgeMap[edge.SourceNodeID] = append(edgeMap[edge.SourceNodeID], edge.TargetNodeID)
}
reachable.reachableNodes, err = performReachabilityAnalysis(nodeMap, edgeMap, startNode, endNode)
reachable.reachableNodes, err = performReachabilityAnalysis(nodeMap, edgeMap, startNode)
if err != nil {
return nil, err
}
@@ -635,12 +602,12 @@ func processNestedReachability(c *vo.Canvas, r *reachability) error {
Nodes: append([]*vo.Node{
{
ID: node.ID,
Type: vo.BlockTypeBotStart,
Type: entity.NodeTypeEntry.IDStr(),
Data: node.Data,
},
{
ID: node.ID,
Type: vo.BlockTypeBotEnd,
Type: entity.NodeTypeExit.IDStr(),
},
}, node.Blocks...),
Edges: node.Edges,
@@ -663,9 +630,9 @@ func findStartAndEndNodes(nodes []*vo.Node) (*vo.Node, *vo.Node, error) {
for _, node := range nodes {
switch node.Type {
case vo.BlockTypeBotStart:
case entity.NodeTypeEntry.IDStr():
startNode = node
case vo.BlockTypeBotEnd:
case entity.NodeTypeExit.IDStr():
endNode = node
}
}
@@ -680,7 +647,7 @@ func findStartAndEndNodes(nodes []*vo.Node) (*vo.Node, *vo.Node, error) {
return startNode, endNode, nil
}
func performReachabilityAnalysis(nodeMap map[string]*vo.Node, edgeMap map[string][]string, startNode *vo.Node, endNode *vo.Node) (map[string]*vo.Node, error) {
func performReachabilityAnalysis(nodeMap map[string]*vo.Node, edgeMap map[string][]string, startNode *vo.Node) (map[string]*vo.Node, error) {
result := make(map[string]*vo.Node)
result[startNode.ID] = startNode