319 lines
10 KiB
Go
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
|
|
}
|