diff --git a/backend/application/workflow/workflow.go b/backend/application/workflow/workflow.go index 65ab9176..9b268ad0 100644 --- a/backend/application/workflow/workflow.go +++ b/backend/application/workflow/workflow.go @@ -2520,6 +2520,12 @@ func (w *ApplicationService) GetApiDetail(ctx context.Context, req *workflow.Get return nil, err } + for _, v := range outputVars { + if err := crossplugin.GetPluginService().UnwrapArrayItemFieldsInVariable(v); err != nil { + return nil, err + } + } + toolDetailInfo := &vo.ToolDetailInfo{ ApiDetailData: &workflow.ApiDetailData{ PluginID: req.GetPluginID(), @@ -3701,7 +3707,7 @@ func toVariable(p *workflow.APIParameter) (*vo.Variable, error) { case workflow.ParameterType_Array: v.Type = vo.VariableTypeList if len(p.SubParameters) > 0 { - subVs := make([]any, 0) + subVs := make([]*vo.Variable, 0) for _, ap := range p.SubParameters { av, err := toVariable(ap) if err != nil { diff --git a/backend/crossdomain/workflow/plugin/plugin.go b/backend/crossdomain/workflow/plugin/plugin.go index 685a556b..46595434 100644 --- a/backend/crossdomain/workflow/plugin/plugin.go +++ b/backend/crossdomain/workflow/plugin/plugin.go @@ -201,7 +201,7 @@ func (t *pluginService) GetPluginToolsInfo(ctx context.Context, req *crossplugin ) if toolExample != nil { requestExample = toolExample.RequestExample - responseExample = toolExample.RequestExample + responseExample = toolExample.ResponseExample } response.ToolInfoList[tf.ID] = crossplugin.ToolInfo{ @@ -220,6 +220,63 @@ func (t *pluginService) GetPluginToolsInfo(ctx context.Context, req *crossplugin return response, nil } +func (t *pluginService) UnwrapArrayItemFieldsInVariable(v *vo.Variable) error { + if v == nil { + return nil + } + + if v.Type == vo.VariableTypeObject { + subVars, ok := v.Schema.([]*vo.Variable) + if !ok { + return nil + } + + newSubVars := make([]*vo.Variable, 0, len(subVars)) + for _, subVar := range subVars { + if subVar.Name == "[Array Item]" { + if err := t.UnwrapArrayItemFieldsInVariable(subVar); err != nil { + return err + } + // If the array item is an object, append its children + if subVar.Type == vo.VariableTypeObject { + if innerSubVars, ok := subVar.Schema.([]*vo.Variable); ok { + newSubVars = append(newSubVars, innerSubVars...) + } + } else { + // If the array item is a primitive type, clear its name and append it + subVar.Name = "" + newSubVars = append(newSubVars, subVar) + } + } else { + // For other sub-variables, recursively unwrap and append + if err := t.UnwrapArrayItemFieldsInVariable(subVar); err != nil { + return err + } + newSubVars = append(newSubVars, subVar) + } + } + v.Schema = newSubVars + + } else if v.Type == vo.VariableTypeList { + if v.Schema != nil { + subVar, ok := v.Schema.(*vo.Variable) + if !ok { + return nil + } + + if err := t.UnwrapArrayItemFieldsInVariable(subVar); err != nil { + return err + } + // If the array item definition itself has "[Array Item]" name, clear it + if subVar.Name == "[Array Item]" { + subVar.Name = "" + } + v.Schema = subVar + } + } + return nil +} + func (t *pluginService) GetPluginInvokableTools(ctx context.Context, req *crossplugin.ToolsInvokableRequest) ( _ map[int64]crossplugin.InvokableTool, err error) { defer func() { @@ -327,7 +384,7 @@ func (t *pluginService) ExecutePlugin(ctx context.Context, input map[string]any, } var output map[string]any - err = sonic.UnmarshalString(r.RawResp, &output) + err = sonic.UnmarshalString(r.TrimmedResp, &output) if err != nil { return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err) } diff --git a/backend/crossdomain/workflow/plugin/plugin_test.go b/backend/crossdomain/workflow/plugin/plugin_test.go new file mode 100644 index 00000000..66c93e33 --- /dev/null +++ b/backend/crossdomain/workflow/plugin/plugin_test.go @@ -0,0 +1,217 @@ +/* + * 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 plugin + +import ( + "testing" + + "github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo" + "github.com/stretchr/testify/assert" +) + +func TestPluginService_UnwrapArrayItemFieldsInVariable(t *testing.T) { + s := &pluginService{} + t.Run("unwraps a simple array item", func(t *testing.T) { + input := &vo.Variable{ + Name: "root", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + { + Name: "[Array Item]", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + {Name: "field1", Type: vo.VariableTypeString}, + {Name: "field2", Type: vo.VariableTypeInteger}, + }, + }, + }, + } + + expected := &vo.Variable{ + Name: "root", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + {Name: "field1", Type: vo.VariableTypeString}, + {Name: "field2", Type: vo.VariableTypeInteger}, + }, + } + + err := s.UnwrapArrayItemFieldsInVariable(input) + assert.NoError(t, err) + assert.Equal(t, expected, input) + }) + + t.Run("handles nested array items", func(t *testing.T) { + input := &vo.Variable{ + Name: "root", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + { + Name: "[Array Item]", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + {Name: "field1", Type: vo.VariableTypeString}, + { + Name: "[Array Item]", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + {Name: "nestedField", Type: vo.VariableTypeBoolean}, + }, + }, + }, + }, + }, + } + + expected := &vo.Variable{ + Name: "root", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + {Name: "field1", Type: vo.VariableTypeString}, + {Name: "nestedField", Type: vo.VariableTypeBoolean}, + }, + } + + err := s.UnwrapArrayItemFieldsInVariable(input) + assert.NoError(t, err) + assert.Equal(t, expected, input) + }) + + t.Run("handles array item within a list", func(t *testing.T) { + input := &vo.Variable{ + Name: "rootList", + Type: vo.VariableTypeList, + Schema: &vo.Variable{ + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + { + Name: "[Array Item]", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + {Name: "itemField", Type: vo.VariableTypeString}, + }, + }, + }, + }, + } + + expected := &vo.Variable{ + Name: "rootList", + Type: vo.VariableTypeList, + Schema: &vo.Variable{ + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + {Name: "itemField", Type: vo.VariableTypeString}, + }, + }, + } + + err := s.UnwrapArrayItemFieldsInVariable(input) + assert.NoError(t, err) + assert.Equal(t, expected, input) + }) + + t.Run("does nothing if no array item is present", func(t *testing.T) { + input := &vo.Variable{ + Name: "root", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + {Name: "field1", Type: vo.VariableTypeString}, + {Name: "field2", Type: vo.VariableTypeInteger}, + }, + } + + // Create a copy for comparison as the input will be modified in place. + expected := &vo.Variable{ + Name: "root", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + {Name: "field1", Type: vo.VariableTypeString}, + {Name: "field2", Type: vo.VariableTypeInteger}, + }, + } + + err := s.UnwrapArrayItemFieldsInVariable(input) + assert.NoError(t, err) + assert.Equal(t, expected, input) + }) + + t.Run("handles primitive type array item in object", func(t *testing.T) { + input := &vo.Variable{ + Name: "root", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + { + Name: "[Array Item]", + Type: vo.VariableTypeString, + }, + { + Name: "anotherField", + Type: vo.VariableTypeInteger, + }, + }, + } + + expected := &vo.Variable{ + Name: "root", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + { + Name: "", + Type: vo.VariableTypeString, + }, + { + Name: "anotherField", + Type: vo.VariableTypeInteger, + }, + }, + } + + err := s.UnwrapArrayItemFieldsInVariable(input) + assert.NoError(t, err) + assert.Equal(t, expected, input) + }) + + t.Run("handles list of primitives", func(t *testing.T) { + input := &vo.Variable{ + Name: "listOfStrings", + Type: vo.VariableTypeList, + Schema: &vo.Variable{ + Name: "[Array Item]", + Type: vo.VariableTypeString, + }, + } + + expected := &vo.Variable{ + Name: "listOfStrings", + Type: vo.VariableTypeList, + Schema: &vo.Variable{ + Name: "", + Type: vo.VariableTypeString, + }, + } + + err := s.UnwrapArrayItemFieldsInVariable(input) + assert.NoError(t, err) + assert.Equal(t, expected, input) + }) + + t.Run("handles nil input", func(t *testing.T) { + err := s.UnwrapArrayItemFieldsInVariable(nil) + assert.NoError(t, err) + }) +} \ No newline at end of file diff --git a/backend/domain/workflow/crossdomain/plugin/plugin.go b/backend/domain/workflow/crossdomain/plugin/plugin.go index 66cba5c4..0801320b 100644 --- a/backend/domain/workflow/crossdomain/plugin/plugin.go +++ b/backend/domain/workflow/crossdomain/plugin/plugin.go @@ -30,6 +30,7 @@ import ( //go:generate mockgen -destination pluginmock/plugin_mock.go --package pluginmock -source plugin.go type Service interface { GetPluginToolsInfo(ctx context.Context, req *ToolsInfoRequest) (*ToolsInfoResponse, error) + UnwrapArrayItemFieldsInVariable(v *vo.Variable) error GetPluginInvokableTools(ctx context.Context, req *ToolsInvokableRequest) (map[int64]InvokableTool, error) ExecutePlugin(ctx context.Context, input map[string]any, pe *Entity, toolID int64, cfg ExecConfig) (map[string]any, error) diff --git a/backend/domain/workflow/entity/message.go b/backend/domain/workflow/entity/message.go index a7146722..12850f92 100644 --- a/backend/domain/workflow/entity/message.go +++ b/backend/domain/workflow/entity/message.go @@ -77,8 +77,8 @@ type FunctionInfo struct { type FunctionCallInfo struct { FunctionInfo - CallID string `json:"-"` - Arguments string `json:"arguments"` + CallID string `json:"-"` + Arguments map[string]any `json:"arguments"` } type ToolResponseInfo struct { diff --git a/backend/domain/workflow/internal/canvas/adaptor/to_schema.go b/backend/domain/workflow/internal/canvas/adaptor/to_schema.go index 3d69f33c..fed6cb6c 100644 --- a/backend/domain/workflow/internal/canvas/adaptor/to_schema.go +++ b/backend/domain/workflow/internal/canvas/adaptor/to_schema.go @@ -2061,7 +2061,7 @@ func buildClauseFromParams(params []*vo.Param) (*database.Clause, error) { func parseBatchMode(n *vo.Node) ( batchN *vo.Node, // the new batch node - enabled bool, // whether the node has enabled batch mode + enabled bool, // whether the node has enabled batch mode err error) { if n.Data == nil || n.Data.Inputs == nil { return nil, false, nil diff --git a/backend/domain/workflow/internal/execute/callback.go b/backend/domain/workflow/internal/execute/callback.go index 567f6873..b8674419 100644 --- a/backend/domain/workflow/internal/execute/callback.go +++ b/backend/domain/workflow/internal/execute/callback.go @@ -27,6 +27,8 @@ import ( "strings" "time" + "github.com/coze-dev/coze-studio/backend/pkg/sonic" + "github.com/cloudwego/eino/callbacks" "github.com/cloudwego/eino/components/tool" "github.com/cloudwego/eino/compose" @@ -1271,13 +1273,21 @@ func (t *ToolHandler) OnStart(ctx context.Context, info *callbacks.RunInfo, return ctx } + var args map[string]any + if input.ArgumentsInJSON != "" { + if err := sonic.UnmarshalString(input.ArgumentsInJSON, &args); err != nil { + logs.Errorf("failed to unmarshal arguments: %v", err) + return ctx + } + } + t.ch <- &Event{ Type: FunctionCall, Context: GetExeCtx(ctx), functionCall: &entity.FunctionCallInfo{ FunctionInfo: t.info, CallID: compose.GetToolCallID(ctx), - Arguments: input.ArgumentsInJSON, + Arguments: args, }, } diff --git a/backend/domain/workflow/internal/execute/event_handle.go b/backend/domain/workflow/internal/execute/event_handle.go index 2cfec2bf..a13a2266 100644 --- a/backend/domain/workflow/internal/execute/event_handle.go +++ b/backend/domain/workflow/internal/execute/event_handle.go @@ -887,7 +887,12 @@ func getFCInfos(ctx context.Context, nodeKey vo.NodeKey) map[string]*fcInfo { } func (f *fcInfo) inputString() string { + if f.input == nil { + return "" + } + m, err := sonic.MarshalString(f.input) + if err != nil { panic(err) } @@ -899,12 +904,5 @@ func (f *fcInfo) outputString() string { return "" } - m := map[string]any{ - "data": f.output.Response, // TODO: traceID, code, message? - } - b, err := sonic.MarshalString(m) - if err != nil { - panic(err) - } - return b + return f.output.Response }