Files
2026-07-13 12:49:10 +08:00

406 lines
12 KiB
Go

/*
* Copyright 2024 CloudWeGo 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 utils
import (
"context"
"encoding/json"
"fmt"
"testing"
"github.com/eino-contrib/jsonschema"
"github.com/stretchr/testify/assert"
orderedmap "github.com/wk8/go-ordered-map/v2"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
)
type Job struct {
Company string `json:"company" jsonschema:"description=the company where the user works"`
Position string `json:"position,omitempty" jsonschema:"description=the position of the user's job"`
ServiceLength float32 `json:"service_length,omitempty" jsonschema:"description=the year of user's service"` // 司龄,年
}
type Income struct {
Source string `json:"source" jsonschema:"description=the source of income"`
Amount int `json:"amount" jsonschema:"description=the amount of income"`
HasPayTax bool `json:"has_pay_tax" jsonschema:"description=whether the user has paid tax"`
Job *Job `json:"job,omitempty" jsonschema:"description=the job of the user when earning this income"`
}
type User struct {
Name string `json:"name" jsonschema:"required,description=the name of the user"`
Age int `json:"age" jsonschema:"required,description=the age of the user"`
Job *Job `json:"job,omitempty" jsonschema:"description=the job of the user"`
Incomes []*Income `json:"incomes" jsonschema:"description=the incomes of the user"`
}
type UserResult struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
var toolInfo = &schema.ToolInfo{
Name: "update_user_info",
Desc: "full update user info",
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(
&jsonschema.Schema{
Type: "object",
Required: []string{"name", "age", "incomes"},
AdditionalProperties: jsonschema.FalseSchema,
Properties: orderedmap.New[string, *jsonschema.Schema](
orderedmap.WithInitialData(
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "name",
Value: &jsonschema.Schema{
Type: "string",
Description: "the name of the user",
},
},
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "age",
Value: &jsonschema.Schema{
Type: "integer",
Description: "the age of the user",
},
},
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "job",
Value: &jsonschema.Schema{
Type: "object",
Required: []string{"company"},
AdditionalProperties: jsonschema.FalseSchema,
Description: "the job of the user",
Properties: orderedmap.New[string, *jsonschema.Schema](
orderedmap.WithInitialData(
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "company",
Value: &jsonschema.Schema{
Type: "string",
Description: "the company where the user works",
},
},
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "position",
Value: &jsonschema.Schema{
Type: "string",
Description: "the position of the user's job",
},
},
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "service_length",
Value: &jsonschema.Schema{
Type: "number",
Description: "the year of user's service",
},
},
),
),
},
},
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "incomes",
Value: &jsonschema.Schema{
Type: "array",
Description: "the incomes of the user",
Items: &jsonschema.Schema{
Type: "object",
AdditionalProperties: jsonschema.FalseSchema,
Required: []string{"source", "amount", "has_pay_tax"},
Properties: orderedmap.New[string, *jsonschema.Schema](
orderedmap.WithInitialData(
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "source",
Value: &jsonschema.Schema{
Type: "string",
Description: "the source of income",
},
},
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "amount",
Value: &jsonschema.Schema{
Type: "integer",
Description: "the amount of income",
},
},
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "has_pay_tax",
Value: &jsonschema.Schema{
Type: "boolean",
Description: "whether the user has paid tax",
},
},
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "job",
Value: &jsonschema.Schema{
Type: "object",
AdditionalProperties: jsonschema.FalseSchema,
Required: []string{"company"},
Description: "the job of the user when earning this income",
Properties: orderedmap.New[string, *jsonschema.Schema](
orderedmap.WithInitialData(
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "company",
Value: &jsonschema.Schema{
Type: "string",
Description: "the company where the user works",
},
},
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "position",
Value: &jsonschema.Schema{
Type: "string",
Description: "the position of the user's job",
},
},
orderedmap.Pair[string, *jsonschema.Schema]{
Key: "service_length",
Value: &jsonschema.Schema{
Type: "number",
Description: "the year of user's service",
},
},
),
),
},
},
),
),
},
},
},
),
),
}),
}
func updateUserInfo(ctx context.Context, input *User) (output *UserResult, err error) {
return &UserResult{
Code: 200,
Msg: fmt.Sprintf("update %v success", input.Name),
}, nil
}
type UserInfoOption struct {
Field1 string
}
func WithUserInfoOption(s string) tool.Option {
return tool.WrapImplSpecificOptFn(func(t *UserInfoOption) {
t.Field1 = s
})
}
func updateUserInfoWithOption(_ context.Context, input *User, opts ...tool.Option) (output *UserResult, err error) {
baseOption := &UserInfoOption{
Field1: "test_origin",
}
option := tool.GetImplSpecificOptions(baseOption, opts...)
return &UserResult{
Code: 200,
Msg: option.Field1,
}, nil
}
func TestInferTool(t *testing.T) {
t.Run("invoke_infer_tool", func(t *testing.T) {
ctx := context.Background()
tl, err := InferTool("update_user_info", "full update user info", updateUserInfo)
assert.NoError(t, err)
info, err := tl.Info(context.Background())
assert.NoError(t, err)
actual, err := info.ToJSONSchema()
assert.NoError(t, err)
actualStr, err := json.Marshal(actual)
assert.NoError(t, err)
expect, err := toolInfo.ToJSONSchema()
assert.NoError(t, err)
expectStr, err := json.Marshal(expect)
assert.NoError(t, err)
assert.Equal(t, string(expectStr), string(actualStr))
content, err := tl.InvokableRun(ctx, `{"name": "bruce lee"}`)
assert.NoError(t, err)
assert.JSONEq(t, `{"code":200,"msg":"update bruce lee success"}`, content)
})
}
func TestInferOptionableTool(t *testing.T) {
ctx := context.Background()
t.Run("invoke_infer_optionable_tool", func(t *testing.T) {
tl, err := InferOptionableTool("invoke_infer_optionable_tool", "full update user info", updateUserInfoWithOption)
assert.NoError(t, err)
content, err := tl.InvokableRun(ctx, `{"name": "bruce lee"}`, WithUserInfoOption("hello world"))
assert.NoError(t, err)
assert.JSONEq(t, `{"code":200,"msg":"hello world"}`, content)
})
}
func TestNewTool(t *testing.T) {
ctx := context.Background()
type Input struct {
Name string `json:"name"`
}
type Output struct {
Name string `json:"name"`
}
t.Run("struct_input_struct_output", func(t *testing.T) {
tl := NewTool[Input, Output](nil, func(ctx context.Context, input Input) (output Output, err error) {
return Output{
Name: input.Name,
}, nil
})
_, err := tl.InvokableRun(ctx, `{"name":"test"}`)
assert.Nil(t, err)
})
t.Run("pointer_input_pointer_output", func(t *testing.T) {
tl := NewTool[*Input, *Output](nil, func(ctx context.Context, input *Input) (output *Output, err error) {
return &Output{
Name: input.Name,
}, nil
})
content, err := tl.InvokableRun(ctx, `{"name":"test"}`)
assert.NoError(t, err)
assert.Equal(t, `{"name":"test"}`, content)
})
t.Run("string_input_int64_output", func(t *testing.T) {
tl := NewTool(nil, func(ctx context.Context, input string) (output int64, err error) {
return 10, nil
})
content, err := tl.InvokableRun(ctx, `100`) // json unmarshal must contains double quote if is not json string.
assert.Error(t, err)
assert.Equal(t, "", content)
})
t.Run("string_pointer_input_int64_pointer_output", func(t *testing.T) {
tl := NewTool[*string, *int64](nil, func(ctx context.Context, input *string) (output *int64, err error) {
n := int64(10)
return &n, nil
})
content, err := tl.InvokableRun(ctx, `"100"`)
assert.NoError(t, err)
assert.Equal(t, `10`, content)
})
}
func TestSnakeToCamel(t *testing.T) {
t.Run("normal_case", func(t *testing.T) {
assert.Equal(t, "GoogleSearch3", snakeToCamel("google_search_3"))
})
t.Run("empty_case", func(t *testing.T) {
assert.Equal(t, "", snakeToCamel(""))
})
t.Run("single_word_case", func(t *testing.T) {
assert.Equal(t, "Google", snakeToCamel("google"))
})
t.Run("upper_case", func(t *testing.T) {
assert.Equal(t, "HttpHost", snakeToCamel("_HTTP_HOST_"))
})
t.Run("underscore_case", func(t *testing.T) {
assert.Equal(t, "", snakeToCamel("_"))
})
}
type stringAlias string
type integerAlias uint32
type floatAlias float64
type boolAlias bool
type testEnumStruct struct {
Field1 string `json:"field1" jsonschema:"enum=a,enum=b"`
Field2 int `json:"field2" jsonschema:"enum=1,enum=2"`
Field3 float32 `json:"field3" jsonschema:"enum=1.1,enum=2.2"`
Field4 bool `json:"field4" jsonschema:"default=true"`
Field5 stringAlias `json:"field5" jsonschema:"enum=a,enum=c"`
Field6 integerAlias `json:"field6" jsonschema:"enum=3,enum=4"`
Field7 floatAlias `json:"field7" jsonschema:"enum=3.3,enum=4.4"`
Field8 boolAlias `json:"field8" jsonschema:"enum=false"`
}
type testEnumStruct2 struct {
Field1 int8 `json:"field1" jsonschema:"enum=1.1"`
}
type testEnumStruct3 struct {
Field1 float64 `json:"field1" jsonschema:"enum=a"`
}
func TestEnumTag(t *testing.T) {
info, err := goStruct2ParamsOneOf[testEnumStruct]()
assert.NoError(t, err)
s, err := info.ToJSONSchema()
assert.NoError(t, err)
enum, ok := s.Properties.Get("field1")
assert.True(t, ok)
assert.Equal(t, []any{"a", "b"}, enum.Enum)
enum, ok = s.Properties.Get("field2")
assert.True(t, ok)
assert.Equal(t, []any{json.Number("1"), json.Number("2")}, enum.Enum)
enum, ok = s.Properties.Get("field3")
assert.True(t, ok)
assert.Equal(t, []any{json.Number("1.1"), json.Number("2.2")}, enum.Enum)
enum, ok = s.Properties.Get("field4")
assert.True(t, ok)
assert.Equal(t, true, enum.Default)
enum, ok = s.Properties.Get("field5")
assert.True(t, ok)
assert.Equal(t, []any{"a", "c"}, enum.Enum)
enum, ok = s.Properties.Get("field6")
assert.True(t, ok)
assert.Equal(t, []any{json.Number("3"), json.Number("4")}, enum.Enum)
enum, ok = s.Properties.Get("field7")
assert.True(t, ok)
assert.Equal(t, []any{json.Number("3.3"), json.Number("4.4")}, enum.Enum)
_, err = goStruct2ParamsOneOf[testEnumStruct2]()
assert.NoError(t, err)
_, err = goStruct2ParamsOneOf[testEnumStruct3]()
assert.NoError(t, err)
}