feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
/*
* 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 json
import (
"context"
"fmt"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/nodes"
"github.com/coze-dev/coze-studio/backend/pkg/ctxcache"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
const (
InputKeyDeserialization = "input"
OutputKeyDeserialization = "output"
warningsKey = "deserialization_warnings"
)
type DeserializationConfig struct {
OutputFields map[string]*vo.TypeInfo `json:"outputFields,omitempty"`
}
type Deserializer struct {
config *DeserializationConfig
typeInfo *vo.TypeInfo
}
func NewJsonDeserializer(_ context.Context, cfg *DeserializationConfig) (*Deserializer, error) {
if cfg == nil {
return nil, fmt.Errorf("config required")
}
if cfg.OutputFields == nil {
return nil, fmt.Errorf("OutputFields is required for deserialization")
}
typeInfo := cfg.OutputFields[OutputKeyDeserialization]
if typeInfo == nil {
return nil, fmt.Errorf("no output field specified in deserialization config")
}
return &Deserializer{
config: cfg,
typeInfo: typeInfo,
}, nil
}
func (jd *Deserializer) Invoke(ctx context.Context, input map[string]any) (map[string]any, error) {
jsonStrValue := input[InputKeyDeserialization]
jsonStr, ok := jsonStrValue.(string)
if !ok {
return nil, fmt.Errorf("input is not a string, got %T", jsonStrValue)
}
typeInfo := jd.typeInfo
var rawValue any
var err error
// Unmarshal based on the root type
switch typeInfo.Type {
case vo.DataTypeString, vo.DataTypeInteger, vo.DataTypeNumber, vo.DataTypeBoolean, vo.DataTypeTime, vo.DataTypeFile:
// Scalar types - unmarshal to generic any
err = sonic.Unmarshal([]byte(jsonStr), &rawValue)
case vo.DataTypeArray:
// Array type - unmarshal to []any
var arr []any
err = sonic.Unmarshal([]byte(jsonStr), &arr)
rawValue = arr
case vo.DataTypeObject:
// Object type - unmarshal to map[string]any
var obj map[string]any
err = sonic.Unmarshal([]byte(jsonStr), &obj)
rawValue = obj
default:
return nil, fmt.Errorf("unsupported root data type: %s", typeInfo.Type)
}
if err != nil {
return nil, fmt.Errorf("JSON unmarshaling failed: %w", err)
}
convertedValue, ws, err := nodes.Convert(ctx, rawValue, OutputKeyDeserialization, typeInfo)
if err != nil {
return nil, err
}
if ws != nil && len(*ws) > 0 {
ctxcache.Store(ctx, warningsKey, *ws)
}
return map[string]any{OutputKeyDeserialization: convertedValue}, nil
}
func (jd *Deserializer) ToCallbackOutput(ctx context.Context, out map[string]any) (*nodes.StructuredCallbackOutput, error) {
var wfe vo.WorkflowError
if warnings, ok := ctxcache.Get[nodes.ConversionWarnings](ctx, warningsKey); ok {
wfe = vo.WrapWarn(errno.ErrNodeOutputParseFail, warnings, errorx.KV("warnings", warnings.Error()))
}
return &nodes.StructuredCallbackOutput{
Output: out,
RawOutput: out,
Error: wfe,
}, nil
}

View File

@@ -0,0 +1,360 @@
/*
* 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 json
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/nodes"
"github.com/coze-dev/coze-studio/backend/pkg/ctxcache"
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
)
func TestNewJsonDeserializer(t *testing.T) {
ctx := context.Background()
// Test with nil config
_, err := NewJsonDeserializer(ctx, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "config required")
// Test with missing OutputFields config
_, err = NewJsonDeserializer(ctx, &DeserializationConfig{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "OutputFields is required")
// Test with missing output key in OutputFields
_, err = NewJsonDeserializer(ctx, &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"testKey": {Type: vo.DataTypeString},
},
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "no output field specified in deserialization config")
// Test with valid config
validConfig := &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
OutputKeyDeserialization: {Type: vo.DataTypeString},
},
}
processor, err := NewJsonDeserializer(ctx, validConfig)
assert.NoError(t, err)
assert.NotNil(t, processor)
}
func TestJsonDeserializer_Invoke(t *testing.T) {
ctx := context.Background()
// Base type test config
baseTypeConfig := &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {Type: vo.DataTypeString},
},
}
// Object type test config
objectTypeConfig := &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {
Type: vo.DataTypeObject,
Properties: map[string]*vo.TypeInfo{
"name": {Type: vo.DataTypeString, Required: true},
"age": {Type: vo.DataTypeInteger},
},
},
},
}
// Array type test config
arrayTypeConfig := &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {
Type: vo.DataTypeArray,
ElemTypeInfo: &vo.TypeInfo{Type: vo.DataTypeInteger},
},
},
}
// Nested array object test config
nestedArrayConfig := &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {
Type: vo.DataTypeArray,
ElemTypeInfo: &vo.TypeInfo{
Type: vo.DataTypeObject,
Properties: map[string]*vo.TypeInfo{
"id": {Type: vo.DataTypeInteger},
"name": {Type: vo.DataTypeString},
},
},
},
},
}
// Test cases
tests := []struct {
name string
config *DeserializationConfig
inputJSON string
expectedOutput any
expectErr bool
expectWarnings int
}{{
name: "Test string deserialization",
config: baseTypeConfig,
inputJSON: `"test string"`,
expectedOutput: "test string",
expectErr: false,
expectWarnings: 0,
}, {
name: "Test integer deserialization",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {Type: vo.DataTypeInteger},
},
},
inputJSON: `123`,
expectedOutput: 123,
expectErr: false,
expectWarnings: 0,
}, {
name: "Test boolean deserialization",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {Type: vo.DataTypeBoolean},
},
},
inputJSON: `true`,
expectedOutput: true,
expectErr: false,
expectWarnings: 0,
}, {
name: "Test object deserialization",
config: objectTypeConfig,
inputJSON: `{"name":"test","age":20}`,
expectedOutput: map[string]any{"name": "test", "age": 20.0},
expectErr: false,
expectWarnings: 0,
}, {
name: "Test array deserialization",
config: arrayTypeConfig,
inputJSON: `[1,2,3]`,
expectedOutput: []any{1.0, 2.0, 3.0},
expectErr: false,
expectWarnings: 0,
}, {
name: "Test nested array object deserialization",
config: nestedArrayConfig,
inputJSON: `[{"id":1,"name":"a"},{"id":2,"name":"b"}]`,
expectedOutput: []any{
map[string]any{"id": 1.0, "name": "a"},
map[string]any{"id": 2.0, "name": "b"},
},
expectErr: false,
expectWarnings: 0,
}, {
name: "Test invalid JSON format",
config: baseTypeConfig,
inputJSON: `{invalid json}`,
expectedOutput: nil,
expectErr: true,
expectWarnings: 0,
}, {
name: "Test type mismatch warning",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {Type: vo.DataTypeInteger},
},
},
inputJSON: `"not a number"`,
expectedOutput: nil,
expectErr: false,
expectWarnings: 1,
}, {
name: "Test null JSON input",
config: baseTypeConfig,
inputJSON: `null`,
expectedOutput: nil,
expectErr: false,
expectWarnings: 0,
}, {
name: "Test string to integer conversion",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {Type: vo.DataTypeInteger},
},
},
inputJSON: `"123"`,
expectedOutput: 123,
expectErr: false,
expectWarnings: 0,
}, {
name: "Test float to integer conversion (integer part)",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {Type: vo.DataTypeInteger},
},
},
inputJSON: `123.0`,
expectedOutput: 123,
expectErr: false,
expectWarnings: 0,
}, {
name: "Test float to integer conversion (non-integer part)",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {Type: vo.DataTypeInteger},
},
},
inputJSON: `123.5`,
expectedOutput: 123,
expectErr: false,
expectWarnings: 0,
}, {
name: "Test boolean to integer conversion",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {Type: vo.DataTypeInteger},
},
},
inputJSON: `true`,
expectedOutput: nil,
expectErr: false,
expectWarnings: 1,
}, {
name: "Test string to boolean conversion",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {Type: vo.DataTypeBoolean},
},
},
inputJSON: `"true"`,
expectedOutput: true,
expectErr: false,
expectWarnings: 0,
}, {
name: "Test string to integer conversion in nested object",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {
Type: vo.DataTypeObject,
Properties: map[string]*vo.TypeInfo{
"age": {Type: vo.DataTypeInteger},
},
},
},
},
inputJSON: `{"age":"456"}`,
expectedOutput: map[string]any{"age": 456},
expectErr: false,
expectWarnings: 0,
}, {
name: "Test string to integer conversion for array elements",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {
Type: vo.DataTypeArray,
ElemTypeInfo: &vo.TypeInfo{Type: vo.DataTypeInteger},
},
},
},
inputJSON: `["1", "2", "3"]`,
expectedOutput: []any{1, 2, 3},
expectErr: false,
expectWarnings: 0,
}, {
name: "Test string with non-numeric characters to integer conversion",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {Type: vo.DataTypeInteger},
},
},
inputJSON: `"123abc"`,
expectedOutput: nil,
expectErr: false,
expectWarnings: 1,
}, {
name: "Test type mismatch in nested object field",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {
Type: vo.DataTypeObject,
Properties: map[string]*vo.TypeInfo{
"score": {Type: vo.DataTypeInteger},
},
},
},
},
inputJSON: `{"score":"invalid"}`,
expectedOutput: map[string]any{"score": nil},
expectErr: false,
expectWarnings: 1,
}, {
name: "Test partial conversion failure in array elements",
config: &DeserializationConfig{
OutputFields: map[string]*vo.TypeInfo{
"output": {
Type: vo.DataTypeArray,
ElemTypeInfo: &vo.TypeInfo{Type: vo.DataTypeInteger},
},
},
},
inputJSON: `["1", "two", 3]`,
expectedOutput: []any{1, 3},
expectErr: false,
expectWarnings: 1,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
processor, err := NewJsonDeserializer(ctx, tt.config)
assert.NoError(t, err)
ctxWithCache := ctxcache.Init(ctx)
input := map[string]any{"input": tt.inputJSON}
result, err := processor.Invoke(ctxWithCache, input)
if tt.expectErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Contains(t, result, OutputKeyDeserialization)
// Verify the output
output := result[OutputKeyDeserialization]
if tt.expectedOutput == nil {
assert.Nil(t, output)
} else {
// Serialize expected and actual output to JSON for comparison, ignoring type differences (e.g., float64 vs. int)
actualJSON, _ := sonic.Marshal(output)
expectedJSON, _ := sonic.Marshal(tt.expectedOutput)
assert.JSONEq(t, string(expectedJSON), string(actualJSON))
}
// Verify the number of warnings
warnings, _ := ctxcache.Get[nodes.ConversionWarnings](ctxWithCache, warningsKey)
assert.Equal(t, tt.expectWarnings, len(warnings))
})
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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 json
import (
"context"
"fmt"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
)
const (
InputKeySerialization = "input"
OutputKeySerialization = "output"
)
type SerializationConfig struct {
InputTypes map[string]*vo.TypeInfo
}
type JsonSerializer struct {
config *SerializationConfig
}
func NewJsonSerializer(_ context.Context, cfg *SerializationConfig) (*JsonSerializer, error) {
if cfg == nil {
return nil, fmt.Errorf("config required")
}
if cfg.InputTypes == nil {
return nil, fmt.Errorf("InputTypes is required for serialization")
}
return &JsonSerializer{
config: cfg,
}, nil
}
func (js *JsonSerializer) Invoke(_ context.Context, input map[string]any) (map[string]any, error) {
// Directly use the input map for serialization
if input == nil {
return nil, fmt.Errorf("input data for serialization cannot be nil")
}
originData := input[InputKeySerialization]
serializedData, err := sonic.Marshal(originData) // Serialize the entire input map
if err != nil {
return nil, fmt.Errorf("serialization error: %w", err)
}
return map[string]any{OutputKeySerialization: string(serializedData)}, nil
}

View File

@@ -0,0 +1,134 @@
/*
* 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 json
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
)
func TestNewJsonSerialize(t *testing.T) {
ctx := context.Background()
// Test with nil config
_, err := NewJsonSerializer(ctx, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "config required")
// Test with missing InputTypes config
_, err = NewJsonSerializer(ctx, &SerializationConfig{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "InputTypes is required")
// Test with valid config
validConfig := &SerializationConfig{
InputTypes: map[string]*vo.TypeInfo{
"testKey": {Type: "string"},
},
}
processor, err := NewJsonSerializer(ctx, validConfig)
assert.NoError(t, err)
assert.NotNil(t, processor)
}
func TestJsonSerialize_Invoke(t *testing.T) {
ctx := context.Background()
config := &SerializationConfig{
InputTypes: map[string]*vo.TypeInfo{
"stringKey": {Type: "string"},
"intKey": {Type: "integer"},
"boolKey": {Type: "boolean"},
"objKey": {Type: "object"},
},
}
processor, err := NewJsonSerializer(ctx, config)
assert.NoError(t, err)
// Test cases
tests := []struct {
name string
input map[string]any
expected string
expectErr bool
}{{
name: "Test string serialization",
input: map[string]any{
"input": "test",
},
expected: `"test"`,
expectErr: false,
}, {
name: "Test integer serialization",
input: map[string]any{
"input": 123,
},
expected: `123`,
expectErr: false,
}, {
name: "Test boolean serialization",
input: map[string]any{
"input": true,
},
expected: `true`,
expectErr: false,
}, {
name: "Test object serialization",
input: map[string]any{
"input": map[string]any{
"nestedKey": "nestedValue",
},
},
expected: `{"nestedKey":"nestedValue"}`,
expectErr: false,
}, {
name: "Test nil input",
input: nil,
expected: "",
expectErr: true,
}, {
name: "Test special character handling",
input: map[string]any{
"input": "\"test\"\nwith\twhitespace",
},
expected: `"\"test\"\nwith\twhitespace"`,
expectErr: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := processor.Invoke(ctx, tt.input)
if tt.expectErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Contains(t, result, OutputKeySerialization)
jsonStr, ok := result[OutputKeySerialization].(string)
assert.True(t, ok, "The output should be of type string")
assert.JSONEq(t, tt.expected, jsonStr)
})
}
}