refactor: how to add a node type in workflow (#558)
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
663
backend/domain/workflow/internal/canvas/convert/type_convert.go
Normal file
663
backend/domain/workflow/internal/canvas/convert/type_convert.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user