fix(plugin): default value for tool execution response parameter type (#683)
Co-authored-by: mrh <mrh997>
This commit is contained in:
parent
6501e9aef6
commit
ad18b9cc42
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(¶mValMap)
|
||||
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(¶mValMap)
|
||||
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(¶mValMap)
|
||||
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(¶mValMap)
|
||||
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)
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue