fix(plugin): default value for tool execution response parameter type (#683)

Co-authored-by: mrh <mrh997>
This commit is contained in:
mrh997 2025-08-11 14:09:08 +08:00 committed by GitHub
parent 6501e9aef6
commit ad18b9cc42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 394 additions and 13 deletions

View File

@ -77,6 +77,7 @@ type InvalidResponseProcessStrategy int8
const (
InvalidResponseProcessStrategyOfReturnRaw InvalidResponseProcessStrategy = 0 // If the value of a field is invalid, the raw response value of the field is returned.
InvalidResponseProcessStrategyOfReturnDefault InvalidResponseProcessStrategy = 1 // If the value of a field is invalid, the default value of the field is returned.
InvalidResponseProcessStrategyOfReturnErr InvalidResponseProcessStrategy = 2 // If the value of a field is invalid, error is returned.
)
const (

View File

@ -181,6 +181,9 @@ func (op *Openapi3Operation) ToEinoSchemaParameterInfo(ctx context.Context) (map
if err != nil {
return nil, err
}
if subParam == nil {
continue
}
subParams[paramName] = subParam
}

View File

@ -1152,6 +1152,16 @@ func (p *PluginApplicationService) DebugAPI(ctx context.Context, req *pluginAPI.
Resp: "{}",
}
opts := []model.ExecuteToolOpt{}
switch req.Operation {
case common.DebugOperation_Debug:
opts = append(opts, model.WithInvalidRespProcessStrategy(model.InvalidResponseProcessStrategyOfReturnErr))
case common.DebugOperation_Parse:
opts = append(opts, model.WithAutoGenRespSchema(),
model.WithInvalidRespProcessStrategy(model.InvalidResponseProcessStrategyOfReturnRaw),
)
}
res, err := p.DomainSVC.ExecuteTool(ctx, &service.ExecuteToolRequest{
UserID: conv.Int64ToStr(*userID),
PluginID: req.PluginID,
@ -1159,7 +1169,7 @@ func (p *PluginApplicationService) DebugAPI(ctx context.Context, req *pluginAPI.
ExecScene: model.ExecSceneOfToolDebug,
ExecDraftTool: true,
ArgumentsInJson: req.Parameters,
}, model.WithAutoGenRespSchema())
}, opts...)
if err != nil {
var e errorx.StatusError
if errors.As(err, &e) {

View File

@ -121,6 +121,7 @@ func (p *pluginInvokableTool) InvokableRun(ctx context.Context, argumentsInJSON
}
opts := []pluginEntity.ExecuteToolOpt{
plugin.WithInvalidRespProcessStrategy(plugin.InvalidResponseProcessStrategyOfReturnDefault),
plugin.WithToolVersion(p.toolInfo.GetVersion()),
plugin.WithProjectInfo(p.projectInfo),
}

View File

@ -69,6 +69,7 @@ func (pr *toolPreCallConf) toolPreRetrieve(ctx context.Context, ar *AgentRequest
}
opts := []pluginEntity.ExecuteToolOpt{
plugin.WithInvalidRespProcessStrategy(plugin.InvalidResponseProcessStrategyOfReturnDefault),
plugin.WithProjectInfo(&plugin.ProjectInfo{
ProjectID: ar.Identity.AgentID,
ProjectType: plugin.ProjectTypeOfAgent,

View File

@ -1037,33 +1037,33 @@ func (t *toolExecutor) processResponse(ctx context.Context, rawResp string) (tri
return "", nil
}
// FIXME: trimming is a weak dependency function and does not affect the response
var trimmedRespMap map[string]any
switch t.invalidRespProcessStrategy {
case model.InvalidResponseProcessStrategyOfReturnRaw:
trimmedRespMap, err = t.processWithInvalidRespProcessStrategyOfReturnRaw(ctx, respMap, schemaVal)
if err != nil {
logs.CtxErrorf(ctx, "processWithInvalidRespProcessStrategyOfReturnRaw failed, err=%v", err)
return rawResp, nil
return "", err
}
case model.InvalidResponseProcessStrategyOfReturnDefault:
trimmedRespMap, err = t.processWithInvalidRespProcessStrategyOfReturnDefault(ctx, respMap, schemaVal)
if err != nil {
logs.CtxErrorf(ctx, "processWithInvalidRespProcessStrategyOfReturnDefault failed, err=%v", err)
return rawResp, nil
return "", err
}
case model.InvalidResponseProcessStrategyOfReturnErr:
trimmedRespMap, err = t.processWithInvalidRespProcessStrategyOfReturnErr(ctx, respMap, schemaVal)
if err != nil {
return "", err
}
default:
logs.CtxErrorf(ctx, "invalid response process strategy '%d'", t.invalidRespProcessStrategy)
return rawResp, nil
return rawResp, fmt.Errorf("invalid response process strategy '%d'", t.invalidRespProcessStrategy)
}
trimmedResp, err = sonic.MarshalString(trimmedRespMap)
if err != nil {
logs.CtxErrorf(ctx, "marshal trimmed response failed, err=%v", err)
return rawResp, nil
return "", errorx.Wrapf(err, "marshal trimmed response failed")
}
return trimmedResp, nil
@ -1095,6 +1095,115 @@ func (t *toolExecutor) processWithInvalidRespProcessStrategyOfReturnRaw(ctx cont
return paramVals, nil
}
func (t *toolExecutor) processWithInvalidRespProcessStrategyOfReturnErr(_ context.Context, paramVals map[string]any, paramSchema *openapi3.Schema) (map[string]any, error) {
var processor func(paramName string, paramVal any, schemaVal *openapi3.Schema) (any, error)
processor = func(paramName string, paramVal any, schemaVal *openapi3.Schema) (any, error) {
switch schemaVal.Type {
case openapi3.TypeObject:
newParamValMap := map[string]any{}
paramValMap, ok := paramVal.(map[string]any)
if !ok {
return nil, errorx.New(errno.ErrPluginExecuteToolFailed, errorx.KVf(errno.PluginMsgKey,
"expected '%s' to be of type 'object', but got '%T'", paramName, paramVal))
}
for paramName_, paramVal_ := range paramValMap {
paramSchema_, ok := schemaVal.Properties[paramName]
if !ok || t.disabledParam(paramSchema_.Value) { // Only the object field can be disabled, and the top level of request and response must be the object structure
continue
}
newParamVal, err := processor(paramName_, paramVal_, paramSchema_.Value)
if err != nil {
return nil, err
}
newParamValMap[paramName_] = newParamVal
}
return newParamValMap, nil
case openapi3.TypeArray:
newParamValSlice := []any{}
paramValSlice, ok := paramVal.([]any)
if !ok {
return nil, errorx.New(errno.ErrPluginExecuteToolFailed, errorx.KVf(errno.PluginMsgKey,
"expected '%s' to be of type 'array', but got '%T'", paramName, paramVal))
}
for _, paramVal_ := range paramValSlice {
newParamVal, err := processor(paramName, paramVal_, schemaVal.Items.Value)
if err != nil {
return nil, err
}
if newParamVal != nil {
newParamValSlice = append(newParamValSlice, newParamVal)
}
}
return newParamValSlice, nil
case openapi3.TypeString:
paramValStr, ok := paramVal.(string)
if !ok {
return nil, errorx.New(errno.ErrPluginExecuteToolFailed, errorx.KVf(errno.PluginMsgKey,
"expected '%s' to be of type 'string', but got '%T'", paramName, paramVal))
}
return paramValStr, nil
case openapi3.TypeBoolean:
paramValBool, ok := paramVal.(bool)
if !ok {
return false, fmt.Errorf("expected '%s' to be of type 'boolean', but got '%T'", paramName, paramVal)
}
return paramValBool, nil
case openapi3.TypeInteger:
paramValNum, ok := paramVal.(json.Number)
if !ok {
return nil, errorx.New(errno.ErrPluginExecuteToolFailed, errorx.KVf(errno.PluginMsgKey,
"expected '%s' to be of type 'integer', but got '%T'", paramName, paramVal))
}
paramValInt, err := paramValNum.Int64()
if err != nil {
return nil, errorx.New(errno.ErrPluginExecuteToolFailed, errorx.KVf(errno.PluginMsgKey,
"expected '%s' to be of type 'integer', but got '%T'", paramName, paramVal))
}
return paramValInt, nil
case openapi3.TypeNumber:
paramValNum, ok := paramVal.(json.Number)
if !ok {
return nil, errorx.New(errno.ErrPluginExecuteToolFailed, errorx.KVf(errno.PluginMsgKey,
"expected '%s' to be of type 'number', but got '%T'", paramName, paramVal))
}
return paramValNum, nil
default:
return nil, fmt.Errorf("unsupported type '%s'", schemaVal.Type)
}
}
newParamVals := make(map[string]any, len(paramVals))
for paramName, paramVal_ := range paramVals {
paramSchema_, ok := paramSchema.Properties[paramName]
if !ok || t.disabledParam(paramSchema_.Value) {
continue
}
newParamVal, err := processor(paramName, paramVal_, paramSchema_.Value)
if err != nil {
return nil, err
}
newParamVals[paramName] = newParamVal
}
return newParamVals, nil
}
func (t *toolExecutor) processWithInvalidRespProcessStrategyOfReturnDefault(_ context.Context, paramVals map[string]any, paramSchema *openapi3.Schema) (map[string]any, error) {
var processor func(paramVal any, schemaVal *openapi3.Schema) (any, error)
processor = func(paramVal any, schemaVal *openapi3.Schema) (any, error) {
@ -1156,9 +1265,13 @@ func (t *toolExecutor) processWithInvalidRespProcessStrategyOfReturnDefault(_ co
return paramValBool, nil
case openapi3.TypeInteger:
paramValInt, ok := paramVal.(float64)
paramValNum, ok := paramVal.(json.Number)
if !ok {
return float64(0), nil
return int64(0), nil
}
paramValInt, err := paramValNum.Int64()
if err != nil {
return int64(0), nil
}
return paramValInt, nil

View File

@ -0,0 +1,252 @@
/*
* 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 service
import (
"bytes"
"encoding/json"
"errors"
"testing"
"github.com/bytedance/mockey"
"github.com/bytedance/sonic"
"github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/assert"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin"
)
func TestToolExecutorProcessWithInvalidRespProcessStrategyOfReturnDefault(t *testing.T) {
executor := &toolExecutor{
invalidRespProcessStrategy: model.InvalidResponseProcessStrategyOfReturnDefault,
}
paramVal := `
{
"a1": 1,
"b1": {
"a2": 2.1
},
"c1": [
{
"a2": 3.1
}
],
"d1": "hello",
"f1": true
}
`
decoder := sonic.ConfigDefault.NewDecoder(bytes.NewBufferString(paramVal))
decoder.UseNumber()
paramValMap := map[string]any{}
err := decoder.Decode(&paramValMap)
assert.NoError(t, err)
paramSchema := &openapi3.Schema{
Type: openapi3.TypeObject,
Properties: map[string]*openapi3.SchemaRef{
"a1": {
Value: &openapi3.Schema{
Type: openapi3.TypeInteger,
},
},
"b1": {
Value: &openapi3.Schema{
Type: openapi3.TypeObject,
Properties: map[string]*openapi3.SchemaRef{
"a2": {
Value: &openapi3.Schema{
Type: openapi3.TypeNumber,
},
},
},
},
},
"c1": {
Value: &openapi3.Schema{
Type: openapi3.TypeArray,
Items: &openapi3.SchemaRef{
Value: &openapi3.Schema{
Type: openapi3.TypeObject,
Properties: map[string]*openapi3.SchemaRef{
"a2": {
Value: &openapi3.Schema{
Type: openapi3.TypeNumber,
},
},
},
},
},
},
},
"d1": {
Value: &openapi3.Schema{
Type: openapi3.TypeString,
},
},
"f1": {
Value: &openapi3.Schema{
Type: openapi3.TypeBoolean,
},
},
},
}
processedParamValMap, err := executor.processWithInvalidRespProcessStrategyOfReturnDefault(nil, paramValMap, paramSchema)
assert.NoError(t, err)
assert.NotNil(t, processedParamValMap)
assert.Equal(t, int64(1), processedParamValMap["a1"])
assert.Equal(t, json.Number("2.1"), processedParamValMap["b1"].(map[string]any)["a2"])
assert.Equal(t, json.Number("3.1"), processedParamValMap["c1"].([]any)[0].(map[string]any)["a2"])
assert.Equal(t, "hello", processedParamValMap["d1"])
assert.Equal(t, true, processedParamValMap["f1"])
}
func TestToolExecutorProcessWithInvalidRespProcessStrategyOfReturnErr(t *testing.T) {
executor := &toolExecutor{
invalidRespProcessStrategy: model.InvalidResponseProcessStrategyOfReturnErr,
}
mockey.PatchConvey("integer", t, func() {
paramVal := `
{
"a": 1
}
`
decoder := sonic.ConfigDefault.NewDecoder(bytes.NewBufferString(paramVal))
decoder.UseNumber()
paramValMap := map[string]any{}
err := decoder.Decode(&paramValMap)
assert.NoError(t, err)
paramSchema := &openapi3.Schema{
Type: openapi3.TypeObject,
Properties: map[string]*openapi3.SchemaRef{
"a": {
Value: &openapi3.Schema{
Type: openapi3.TypeString,
},
},
},
}
_, err = executor.processWithInvalidRespProcessStrategyOfReturnErr(nil, paramValMap, paramSchema)
var customErr errorx.StatusError
assert.True(t, errors.As(err, &customErr))
assert.Equal(t, "execute tool failed : expected 'a' to be of type 'string', but got 'json.Number'", customErr.Msg())
paramSchema = &openapi3.Schema{
Type: openapi3.TypeObject,
Properties: map[string]*openapi3.SchemaRef{
"a1": {
Value: &openapi3.Schema{
Type: openapi3.TypeInteger,
},
},
},
}
_, err = executor.processWithInvalidRespProcessStrategyOfReturnErr(nil, paramValMap, paramSchema)
assert.NoError(t, err)
})
mockey.PatchConvey("string", t, func() {
paramVal := `
{
"a": "1"
}
`
decoder := sonic.ConfigDefault.NewDecoder(bytes.NewBufferString(paramVal))
decoder.UseNumber()
paramValMap := map[string]any{}
err := decoder.Decode(&paramValMap)
assert.NoError(t, err)
paramSchema := &openapi3.Schema{
Type: openapi3.TypeObject,
Properties: map[string]*openapi3.SchemaRef{
"a": {
Value: &openapi3.Schema{
Type: openapi3.TypeInteger,
},
},
},
}
_, err = executor.processWithInvalidRespProcessStrategyOfReturnErr(nil, paramValMap, paramSchema)
var customErr errorx.StatusError
assert.True(t, errors.As(err, &customErr))
assert.Equal(t, "execute tool failed : expected 'a' to be of type 'integer', but got 'string'", customErr.Msg())
paramSchema = &openapi3.Schema{
Type: openapi3.TypeObject,
Properties: map[string]*openapi3.SchemaRef{
"a": {
Value: &openapi3.Schema{
Type: openapi3.TypeString,
},
},
},
}
_, err = executor.processWithInvalidRespProcessStrategyOfReturnErr(nil, paramValMap, paramSchema)
assert.NoError(t, err)
})
mockey.PatchConvey("boolean", t, func() {
paramVal := `
{
"a": false
}
`
decoder := sonic.ConfigDefault.NewDecoder(bytes.NewBufferString(paramVal))
decoder.UseNumber()
paramValMap := map[string]any{}
err := decoder.Decode(&paramValMap)
assert.NoError(t, err)
paramSchema := &openapi3.Schema{
Type: openapi3.TypeObject,
Properties: map[string]*openapi3.SchemaRef{
"a": {
Value: &openapi3.Schema{
Type: openapi3.TypeString,
},
},
},
}
_, err = executor.processWithInvalidRespProcessStrategyOfReturnErr(nil, paramValMap, paramSchema)
var customErr errorx.StatusError
assert.True(t, errors.As(err, &customErr))
assert.Equal(t, "execute tool failed : expected 'a' to be of type 'string', but got 'bool'", customErr.Msg())
paramSchema = &openapi3.Schema{
Type: openapi3.TypeObject,
Properties: map[string]*openapi3.SchemaRef{
"a": {
Value: &openapi3.Schema{
Type: openapi3.TypeBoolean,
},
},
},
}
_, err = executor.processWithInvalidRespProcessStrategyOfReturnErr(nil, paramValMap, paramSchema)
assert.NoError(t, err)
})
}