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

319 lines
10 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"
"fmt"
"strings"
"github.com/bytedance/sonic"
"github.com/eino-contrib/jsonschema"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/internal/generic"
"github.com/cloudwego/eino/schema"
)
// InvokeFunc is the function type for the tool.
type InvokeFunc[T, D any] func(ctx context.Context, input T) (output D, err error)
// OptionableInvokeFunc is the function type for the tool with tool option.
type OptionableInvokeFunc[T, D any] func(ctx context.Context, input T, opts ...tool.Option) (output D, err error)
// InferTool creates an [tool.InvokableTool] by inferring the parameter JSON
// schema from the fields and tags of the input type T.
//
// The tool automatically JSON-decodes the model's argument string into T before
// calling fn, and JSON-encodes the D return value into the result string.
//
// Use [WithSchemaModifier] in opts to customise how struct tags are mapped to
// JSON schema fields.
func InferTool[T, D any](toolName, toolDesc string, i InvokeFunc[T, D], opts ...Option) (tool.InvokableTool, error) {
ti, err := goStruct2ToolInfo[T](toolName, toolDesc, opts...)
if err != nil {
return nil, err
}
return NewTool(ti, i, opts...), nil
}
// InferOptionableTool is like [InferTool] but the function also receives
// [tool.Option] values passed by ToolsNode at call time.
func InferOptionableTool[T, D any](toolName, toolDesc string, i OptionableInvokeFunc[T, D], opts ...Option) (tool.InvokableTool, error) {
ti, err := goStruct2ToolInfo[T](toolName, toolDesc, opts...)
if err != nil {
return nil, err
}
return newOptionableTool(ti, i, opts...), nil
}
// EnhancedInvokeFunc is the function type for the enhanced tool.
type EnhancedInvokeFunc[T any] func(ctx context.Context, input T) (output *schema.ToolResult, err error)
// OptionableEnhancedInvokeFunc is the function type for the enhanced tool with tool option.
type OptionableEnhancedInvokeFunc[T any] func(ctx context.Context, input T, opts ...tool.Option) (output *schema.ToolResult, err error)
// InferEnhancedTool creates an [tool.EnhancedInvokableTool] by inferring the
// parameter JSON schema from type T. The function returns a [schema.ToolResult]
// for multimodal output (text, images, audio, video, files).
func InferEnhancedTool[T any](toolName, toolDesc string, i EnhancedInvokeFunc[T], opts ...Option) (tool.EnhancedInvokableTool, error) {
ti, err := goStruct2ToolInfo[T](toolName, toolDesc, opts...)
if err != nil {
return nil, err
}
return NewEnhancedTool(ti, i, opts...), nil
}
// InferOptionableEnhancedTool creates an EnhancedInvokableTool from a given function by inferring the ToolInfo from the function's request parameters, with tool option.
func InferOptionableEnhancedTool[T any](toolName, toolDesc string, i OptionableEnhancedInvokeFunc[T], opts ...Option) (tool.EnhancedInvokableTool, error) {
ti, err := goStruct2ToolInfo[T](toolName, toolDesc, opts...)
if err != nil {
return nil, err
}
return newOptionableEnhancedTool(ti, i, opts...), nil
}
// GoStruct2ParamsOneOf converts a Go struct's fields and tags into a
// [schema.ParamsOneOf] (JSON Schema 2020-12). Useful for ChatModel structured
// output via ResponseFormat without creating a full tool.
func GoStruct2ParamsOneOf[T any](opts ...Option) (*schema.ParamsOneOf, error) {
return goStruct2ParamsOneOf[T](opts...)
}
// GoStruct2ToolInfo converts a Go struct into a [schema.ToolInfo]. Useful for
// binding a typed schema to a ChatModel via BindTools for structured output,
// when you do not need a full executable tool.
func GoStruct2ToolInfo[T any](toolName, toolDesc string, opts ...Option) (*schema.ToolInfo, error) {
return goStruct2ToolInfo[T](toolName, toolDesc, opts...)
}
func goStruct2ToolInfo[T any](toolName, toolDesc string, opts ...Option) (*schema.ToolInfo, error) {
paramsOneOf, err := goStruct2ParamsOneOf[T](opts...)
if err != nil {
return nil, err
}
return &schema.ToolInfo{
Name: toolName,
Desc: toolDesc,
ParamsOneOf: paramsOneOf,
}, nil
}
func goStruct2ParamsOneOf[T any](opts ...Option) (*schema.ParamsOneOf, error) {
options := getToolOptions(opts...)
r := &jsonschema.Reflector{
Anonymous: true,
DoNotReference: true,
SchemaModifier: jsonschema.SchemaModifierFn(options.scModifier),
}
js := r.Reflect(generic.NewInstance[T]())
js.Version = ""
paramsOneOf := schema.NewParamsOneOfByJSONSchema(js)
return paramsOneOf, nil
}
// NewTool creates an [tool.InvokableTool] from an explicit [schema.ToolInfo]
// and a typed function. Use this when the schema cannot be inferred from struct
// tags (e.g. dynamic or complex parameter schemas).
//
// Note: you are responsible for keeping desc.ParamsOneOf consistent with the
// actual fields of T — there is no compile-time check.
func NewTool[T, D any](desc *schema.ToolInfo, i InvokeFunc[T, D], opts ...Option) tool.InvokableTool {
return newOptionableTool(desc, func(ctx context.Context, input T, _ ...tool.Option) (D, error) {
return i(ctx, input)
}, opts...)
}
func newOptionableTool[T, D any](desc *schema.ToolInfo, i OptionableInvokeFunc[T, D], opts ...Option) tool.InvokableTool {
to := getToolOptions(opts...)
return &invokableTool[T, D]{
info: desc,
um: to.um,
m: to.m,
Fn: i,
}
}
type invokableTool[T, D any] struct {
info *schema.ToolInfo
um UnmarshalArguments
m MarshalOutput
Fn OptionableInvokeFunc[T, D]
}
func (i *invokableTool[T, D]) Info(ctx context.Context) (*schema.ToolInfo, error) {
return i.info, nil
}
// InvokableRun invokes the tool with the given arguments.
func (i *invokableTool[T, D]) InvokableRun(ctx context.Context, arguments string, opts ...tool.Option) (output string, err error) {
var inst T
if i.um != nil {
var val any
val, err = i.um(ctx, arguments)
if err != nil {
return "", fmt.Errorf("[LocalFunc] failed to unmarshal arguments, toolName=%s, err=%w", i.getToolName(), err)
}
gt, ok := val.(T)
if !ok {
return "", fmt.Errorf("[LocalFunc] invalid type, toolName=%s, expected=%T, given=%T", i.getToolName(), inst, val)
}
inst = gt
} else {
inst = generic.NewInstance[T]()
err = sonic.UnmarshalString(arguments, &inst)
if err != nil {
return "", fmt.Errorf("[LocalFunc] failed to unmarshal arguments in json, toolName=%s, err=%w", i.getToolName(), err)
}
}
resp, err := i.Fn(ctx, inst, opts...)
if err != nil {
return "", fmt.Errorf("[LocalFunc] failed to invoke tool, toolName=%s, err=%w", i.getToolName(), err)
}
if i.m != nil {
output, err = i.m(ctx, resp)
if err != nil {
return "", fmt.Errorf("[LocalFunc] failed to marshal output, toolName=%s, err=%w", i.getToolName(), err)
}
} else {
output, err = marshalString(resp)
if err != nil {
return "", fmt.Errorf("[LocalFunc] failed to marshal output in json, toolName=%s, err=%w", i.getToolName(), err)
}
}
return output, nil
}
func (i *invokableTool[T, D]) GetType() string {
return snakeToCamel(i.getToolName())
}
func (i *invokableTool[T, D]) getToolName() string {
if i.info == nil {
return ""
}
return i.info.Name
}
// snakeToCamel converts a snake_case string to CamelCase.
func snakeToCamel(s string) string {
if s == "" {
return ""
}
parts := strings.Split(s, "_")
for i := 0; i < len(parts); i++ {
if len(parts[i]) > 0 {
parts[i] = strings.ToUpper(string(parts[i][0])) + strings.ToLower(parts[i][1:])
}
}
return strings.Join(parts, "")
}
// NewEnhancedTool creates an [tool.EnhancedInvokableTool] from an explicit
// [schema.ToolInfo] and a function that returns [schema.ToolResult].
func NewEnhancedTool[T any](desc *schema.ToolInfo, i EnhancedInvokeFunc[T], opts ...Option) tool.EnhancedInvokableTool {
return newOptionableEnhancedTool(desc, func(ctx context.Context, input T, _ ...tool.Option) (*schema.ToolResult, error) {
return i(ctx, input)
}, opts...)
}
func newOptionableEnhancedTool[T any](desc *schema.ToolInfo, i OptionableEnhancedInvokeFunc[T], opts ...Option) tool.EnhancedInvokableTool {
to := getToolOptions(opts...)
return &enhancedInvokableTool[T]{
info: desc,
um: to.um,
Fn: i,
}
}
type enhancedInvokableTool[T any] struct {
info *schema.ToolInfo
um UnmarshalArguments
Fn OptionableEnhancedInvokeFunc[T]
}
func (e *enhancedInvokableTool[T]) Info(ctx context.Context) (*schema.ToolInfo, error) {
return e.info, nil
}
func (e *enhancedInvokableTool[T]) InvokableRun(ctx context.Context, toolArgument *schema.ToolArgument, opts ...tool.Option) (*schema.ToolResult, error) {
var inst T
var err error
if e.um != nil {
var val any
val, err = e.um(ctx, toolArgument.Text)
if err != nil {
return nil, fmt.Errorf("[EnhancedLocalFunc] failed to unmarshal arguments, toolName=%s, err=%w", e.getToolName(), err)
}
gt, ok := val.(T)
if !ok {
return nil, fmt.Errorf("[EnhancedLocalFunc] invalid type, toolName=%s, expected=%T, given=%T", e.getToolName(), inst, val)
}
inst = gt
} else {
inst = generic.NewInstance[T]()
err = sonic.UnmarshalString(toolArgument.Text, &inst)
if err != nil {
return nil, fmt.Errorf("[EnhancedLocalFunc] failed to unmarshal arguments in json, toolName=%s, err=%w", e.getToolName(), err)
}
}
resp, err := e.Fn(ctx, inst, opts...)
if err != nil {
return nil, fmt.Errorf("[EnhancedLocalFunc] failed to invoke tool, toolName=%s, err=%w", e.getToolName(), err)
}
return resp, nil
}
func (e *enhancedInvokableTool[T]) GetType() string {
return snakeToCamel(e.getToolName())
}
func (e *enhancedInvokableTool[T]) getToolName() string {
if e.info == nil {
return ""
}
return e.info.Name
}