github.com/anthropics/anthropic-sdk-go@v1.70.1/toolrunner/tool.go (about)

     1  package toolrunner
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  
     8  	anthropic "github.com/anthropics/anthropic-sdk-go"
     9  	"github.com/invopop/jsonschema"
    10  )
    11  
    12  // betaTool is the internal generic implementation of anthropic.BetaTool.
    13  // Users never see this type directly - they work with the BetaTool interface.
    14  // The generic type parameter T is used internally for type-safe JSON unmarshaling.
    15  type betaTool[T any] struct {
    16  	name        string
    17  	description string
    18  	schema      anthropic.BetaToolInputSchemaParam
    19  	handler     func(context.Context, T) (anthropic.BetaToolResultBlockParamContentUnion, error)
    20  }
    21  
    22  func (t *betaTool[T]) Name() string                                    { return t.name }
    23  func (t *betaTool[T]) Description() string                             { return t.description }
    24  func (t *betaTool[T]) InputSchema() anthropic.BetaToolInputSchemaParam { return t.schema }
    25  
    26  func (t *betaTool[T]) Execute(ctx context.Context, input json.RawMessage) ([]anthropic.BetaToolResultBlockParamContentUnion, error) {
    27  	parsed, err := t.parse(input)
    28  	if err != nil {
    29  		return nil, fmt.Errorf("failed to parse tool input: %w", err)
    30  	}
    31  	result, err := t.handler(ctx, parsed)
    32  	if err != nil {
    33  		return nil, err
    34  	}
    35  	return []anthropic.BetaToolResultBlockParamContentUnion{result}, nil
    36  }
    37  
    38  // parse validates and parses the input according to the tool's schema.
    39  // This method handles special cases for json.RawMessage and []byte type parameters.
    40  func (t *betaTool[T]) parse(input json.RawMessage) (T, error) {
    41  	var parsed T
    42  
    43  	switch any(parsed).(type) {
    44  	case json.RawMessage:
    45  		// If T is json.RawMessage, return the input as is
    46  		if result, ok := any(input).(T); ok {
    47  			return result, nil
    48  		}
    49  		return parsed, fmt.Errorf("type assertion failed for json.RawMessage")
    50  	case []byte:
    51  		// If T is []byte, return the raw JSON input as bytes
    52  		if result, ok := any([]byte(input)).(T); ok {
    53  			return result, nil
    54  		}
    55  		return parsed, fmt.Errorf("type assertion failed for []byte")
    56  	default:
    57  		// For all other types (structs), unmarshal the input
    58  		if err := json.Unmarshal(input, &parsed); err != nil {
    59  			return parsed, err
    60  		}
    61  		return parsed, nil
    62  	}
    63  }
    64  
    65  func parseSchemaMap(s map[string]any) (anthropic.BetaToolInputSchemaParam, error) {
    66  	bytes, err := json.Marshal(s)
    67  	if err != nil {
    68  		return anthropic.BetaToolInputSchemaParam{}, fmt.Errorf("failed to marshal schema: %w", err)
    69  	}
    70  
    71  	var schema anthropic.BetaToolInputSchemaParam
    72  	if err := json.Unmarshal(bytes, &schema); err != nil {
    73  		return anthropic.BetaToolInputSchemaParam{}, fmt.Errorf("failed to unmarshal schema: %w", err)
    74  	}
    75  
    76  	return schema, nil
    77  }
    78  
    79  // NewBetaToolFromBytes creates a BetaTool from JSON schema bytes.
    80  func NewBetaToolFromBytes[T any](
    81  	name, description string,
    82  	schemaJSON []byte,
    83  	handler func(context.Context, T) (anthropic.BetaToolResultBlockParamContentUnion, error),
    84  ) (anthropic.BetaTool, error) {
    85  	var schema anthropic.BetaToolInputSchemaParam
    86  	if err := schema.UnmarshalJSON(schemaJSON); err != nil {
    87  		return nil, fmt.Errorf("failed to unmarshal schema: %w", err)
    88  	}
    89  	return NewBetaTool(name, description, schema, handler), nil
    90  }
    91  
    92  // NewBetaToolFromJSONSchema creates a BetaTool by inferring the schema from struct type T using reflection.
    93  // The struct should use jsonschema tags to define the schema (e.g., `jsonschema:"required,description=..."`).
    94  func NewBetaToolFromJSONSchema[T any](
    95  	name, description string,
    96  	handler func(context.Context, T) (anthropic.BetaToolResultBlockParamContentUnion, error),
    97  ) (anthropic.BetaTool, error) {
    98  	var zeroValue T
    99  	reflector := jsonschema.Reflector{
   100  		AllowAdditionalProperties:  false,
   101  		RequiredFromJSONSchemaTags: true,
   102  		DoNotReference:             true,
   103  	}
   104  
   105  	schema := reflector.Reflect(zeroValue)
   106  
   107  	schemaBytes, err := json.Marshal(schema)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	var schemaMap map[string]any
   112  	if err := json.Unmarshal(schemaBytes, &schemaMap); err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	inputSchema, err := parseSchemaMap(schemaMap)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	return NewBetaTool(name, description, inputSchema, handler), nil
   122  }
   123  
   124  // NewBetaTool creates a BetaTool with a BetaToolInputSchemaParam directly.
   125  func NewBetaTool[T any](
   126  	name, description string,
   127  	schema anthropic.BetaToolInputSchemaParam,
   128  	handler func(context.Context, T) (anthropic.BetaToolResultBlockParamContentUnion, error),
   129  ) anthropic.BetaTool {
   130  	return &betaTool[T]{
   131  		name:        name,
   132  		description: description,
   133  		schema:      schema,
   134  		handler:     handler,
   135  	}
   136  }