Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 120 additions & 9 deletions backend/domain/workflow/internal/canvas/convert/type_convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package convert

import (
"fmt"
"sort"

"strconv"
"strings"
Expand Down Expand Up @@ -63,7 +64,11 @@ func CanvasVariableToTypeInfo(v *vo.Variable) (*vo.TypeInfo, error) {
tInfo.Type = vo.DataTypeObject
tInfo.Properties = make(map[string]*vo.TypeInfo)
if v.Schema != nil {
for _, subVAny := range v.Schema.([]any) {
subVariables, err := normalizeObjectSchemaItems(v.Schema)
if err != nil {
return nil, err
}
for _, subVAny := range subVariables {
subV, err := vo.ParseVariable(subVAny)
if err != nil {
return nil, err
Expand Down Expand Up @@ -134,7 +139,14 @@ func CanvasBlockInputToTypeInfo(b *vo.BlockInput) (tInfo *vo.TypeInfo, err error
tInfo.Type = vo.DataTypeObject
tInfo.Properties = make(map[string]*vo.TypeInfo)
if b.Schema != nil {
for _, subVAny := range b.Schema.([]any) {
subItems, err := normalizeObjectSchemaItems(b.Schema)
if err != nil {
return nil, err
}
if b.Value == nil {
break
}
for _, subVAny := range subItems {
if b.Value.Type == vo.BlockInputValueTypeRef {
subV, err := vo.ParseVariable(subVAny)
if err != nil {
Expand Down Expand Up @@ -193,9 +205,9 @@ func CanvasBlockInputToFieldInfo(b *vo.BlockInput, path einoCompose.FieldPath, p
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)
paramList, err := normalizeObjectSchemaItems(sc)
if err != nil {
return nil, fmt.Errorf("input %v schema invalid, err=%w", path, err)
}

for i := range paramList {
Expand Down Expand Up @@ -259,7 +271,11 @@ func CanvasBlockInputToFieldInfo(b *vo.BlockInput, path einoCompose.FieldPath, p
FileNames: make([]string, 0, len(filenames)),
}
for _, filename := range filenames {
fileExtra.FileNames = append(fileExtra.FileNames, filename.(string))
filenameStr, ok := filename.(string)
if !ok {
return nil, fmt.Errorf("invalid filename type: %T", filename)
}
fileExtra.FileNames = append(fileExtra.FileNames, filenameStr)
}
}

Expand Down Expand Up @@ -416,6 +432,90 @@ func ParseParam(v any) (*vo.Param, error) {
return p, nil
}

func normalizeObjectSchemaItems(sc any) ([]any, error) {
switch v := sc.(type) {
case nil:
return nil, nil
case []any:
return v, nil
case []*vo.Variable:
items := make([]any, 0, len(v))
for _, item := range v {
items = append(items, item)
}
return items, nil
case []*vo.Param:
items := make([]any, 0, len(v))
for _, item := range v {
items = append(items, item)
}
return items, nil
case map[string]any:
if nested, ok := v["schema"]; ok {
if typeStr, ok := asVariableTypeString(v["type"]); ok && typeStr == string(vo.VariableTypeObject) {
return normalizeObjectSchemaItems(nested)
}
}

keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)

items := make([]any, 0, len(keys))
for _, key := range keys {
items = append(items, withSchemaItemName(v[key], key))
}
return items, nil
default:
return nil, fmt.Errorf("unsupported object schema type: %T", sc)
}
}

func withSchemaItemName(v any, name string) any {
switch item := v.(type) {
case map[string]any:
if _, ok := item["name"]; ok {
return item
}

copied := make(map[string]any, len(item)+1)
for k, val := range item {
copied[k] = val
}
copied["name"] = name
return copied
case *vo.Variable:
if item == nil || len(item.Name) > 0 {
return item
}
cloned := *item
cloned.Name = name
return &cloned
case *vo.Param:
if item == nil || len(item.Name) > 0 {
return item
}
cloned := *item
cloned.Name = name
return &cloned
default:
return v
}
}

func asVariableTypeString(v any) (string, bool) {
switch t := v.(type) {
case string:
return t, true
case vo.VariableType:
return string(t), true
default:
return "", false
}
}

func CanvasBlockInputRefToFieldSource(r *vo.BlockInputReference) (*vo.FieldSource, error) {
switch r.Source {
case vo.RefSourceTypeBlockOutput:
Expand Down Expand Up @@ -602,8 +702,15 @@ func BlockInputToNamedTypeInfo(name string, b *vo.BlockInput) (*vo.NamedTypeInfo
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) {
subItems, err := normalizeObjectSchemaItems(b.Schema)
if err != nil {
return nil, err
}
tInfo.Properties = make([]*vo.NamedTypeInfo, 0, len(subItems))
if b.Value == nil {
break
}
for _, subVAny := range subItems {
if b.Value.Type == vo.BlockInputValueTypeRef {
subV, err := vo.ParseVariable(subVAny)
if err != nil {
Expand Down Expand Up @@ -678,8 +785,12 @@ func VariableToNamedTypeInfo(v *vo.Variable) (*vo.NamedTypeInfo, error) {
case vo.VariableTypeObject:
nInfo.Type = vo.DataTypeObject
if v.Schema != nil {
subVariables, err := normalizeObjectSchemaItems(v.Schema)
if err != nil {
return nil, err
}
nInfo.Properties = make([]*vo.NamedTypeInfo, 0)
for _, subVAny := range v.Schema.([]any) {
for _, subVAny := range subVariables {
subV, err := vo.ParseVariable(subVAny)
if err != nil {
return nil, err
Expand Down
147 changes: 147 additions & 0 deletions backend/domain/workflow/internal/canvas/convert/type_convert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* 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 (
"testing"

einoCompose "github.com/cloudwego/eino/compose"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
)

func TestCanvasBlockInputToTypeInfo_ObjectSchemaMap(t *testing.T) {
b := &vo.BlockInput{
Type: vo.VariableTypeObject,
Schema: map[string]any{
"foo": map[string]any{
"type": vo.VariableTypeString,
},
"bar": map[string]any{
"type": vo.VariableTypeInteger,
},
},
Value: &vo.BlockInputValue{
Type: vo.BlockInputValueTypeRef,
},
}

tInfo, err := CanvasBlockInputToTypeInfo(b)
require.NoError(t, err)
require.NotNil(t, tInfo)

assert.Equal(t, vo.DataTypeObject, tInfo.Type)
require.Len(t, tInfo.Properties, 2)
assert.Equal(t, vo.DataTypeString, tInfo.Properties["foo"].Type)
assert.Equal(t, vo.DataTypeInteger, tInfo.Properties["bar"].Type)
}

func TestCanvasBlockInputToTypeInfo_ObjectSchemaWrappedMap(t *testing.T) {
b := &vo.BlockInput{
Type: vo.VariableTypeObject,
Schema: map[string]any{
"type": vo.VariableTypeObject,
"schema": []any{
map[string]any{
"name": "foo",
"type": vo.VariableTypeString,
},
},
},
Value: &vo.BlockInputValue{
Type: vo.BlockInputValueTypeRef,
},
}

tInfo, err := CanvasBlockInputToTypeInfo(b)
require.NoError(t, err)
require.NotNil(t, tInfo)

assert.Equal(t, vo.DataTypeObject, tInfo.Type)
require.Len(t, tInfo.Properties, 1)
assert.Equal(t, vo.DataTypeString, tInfo.Properties["foo"].Type)
}

func TestCanvasBlockInputToFieldInfo_ObjectRefWithMapSchema(t *testing.T) {
b := &vo.BlockInput{
Type: vo.VariableTypeObject,
Schema: map[string]any{
"foo": map[string]any{
"input": map[string]any{
"type": vo.VariableTypeString,
"value": map[string]any{
"type": vo.BlockInputValueTypeLiteral,
"content": "abc",
},
},
},
},
Value: &vo.BlockInputValue{
Type: vo.BlockInputValueTypeObjectRef,
},
}

sources, err := CanvasBlockInputToFieldInfo(b, einoCompose.FieldPath{"root"}, nil)
require.NoError(t, err)
require.Len(t, sources, 1)

assert.Equal(t, einoCompose.FieldPath{"root", "foo"}, sources[0].Path)
assert.Equal(t, "abc", sources[0].Source.Val)
}

func TestVariableToNamedTypeInfo_ObjectSchemaMap(t *testing.T) {
v := &vo.Variable{
Name: "obj",
Type: vo.VariableTypeObject,
Schema: map[string]any{
"foo": map[string]any{
"type": vo.VariableTypeString,
},
},
}

nInfo, err := VariableToNamedTypeInfo(v)
require.NoError(t, err)
require.NotNil(t, nInfo)
require.Len(t, nInfo.Properties, 1)

assert.Equal(t, "foo", nInfo.Properties[0].Name)
assert.Equal(t, vo.DataTypeString, nInfo.Properties[0].Type)
}

func TestCanvasBlockInputToFieldInfo_ListFileMetaInvalidFilenameType(t *testing.T) {
b := &vo.BlockInput{
Type: vo.VariableTypeList,
Schema: map[string]any{
"type": vo.VariableTypeString,
"assistType": vo.AssistTypeImage,
},
Value: &vo.BlockInputValue{
Type: vo.BlockInputValueTypeLiteral,
Content: []any{"a"},
RawMeta: map[string]any{
"fileName": []any{1},
},
},
}

_, err := CanvasBlockInputToFieldInfo(b, einoCompose.FieldPath{"files"}, nil)
require.Error(t, err)
require.ErrorContains(t, err, "invalid filename type")
}