diff --git a/src/AnthropicClient/Models/Tool.cs b/src/AnthropicClient/Models/Tool.cs index a459c51..5476436 100644 --- a/src/AnthropicClient/Models/Tool.cs +++ b/src/AnthropicClient/Models/Tool.cs @@ -7,6 +7,27 @@ using AnthropicClient.Utils; namespace AnthropicClient.Models; +/// +/// Interface that a class can implement to be used to create a tool. +/// +public interface ITool +{ + /// + /// Gets the name of the tool. Should not be null or empty. + /// + public string Name { get; } + + /// + /// Gets the description of the tool. Should not be null or empty. + /// + public string Description { get; } + + /// + /// Gets the input schema of the tool. Should not be null. + /// + public MethodInfo Function { get; } +} + /// /// Represents a tool that can be used in the chat. /// @@ -40,6 +61,18 @@ public class Tool [JsonIgnore] public string DisplayName { get; } + [JsonConstructor] + internal Tool() + { + var func = () => {}; + + Name = string.Empty; + Description = string.Empty; + InputSchema = []; + Function = new AnthropicFunction(func.Method); + DisplayName = string.Empty; + } + internal Tool(string name, string description, AnthropicFunction function) { ArgumentValidator.ThrowIfNullOrWhitespace(name, nameof(name)); @@ -58,6 +91,25 @@ public class Tool InputSchema = JsonSchemaGenerator.GenerateInputSchema(function); } + /// + /// Creates a tool from a type that implements . + /// + /// The type that implements . + /// Thrown when the name or description of the tool is null or empty. + /// Thrown when the function of the tool is null. + /// The created tool as instance of . + /// The implementation of must have a parameterless constructor. + public static Tool CreateFromClass() where T : ITool, new() + { + var tool = new T(); + + ArgumentValidator.ThrowIfNullOrWhitespace(tool.Name, nameof(tool.Name)); + ArgumentValidator.ThrowIfNullOrWhitespace(tool.Description, nameof(tool.Description)); + ArgumentValidator.ThrowIfNull(tool.Function, nameof(tool.Function)); + + return new Tool(tool.Name, tool.Description, new AnthropicFunction(tool.Function)); + } + /// /// Creates a tool from a static method. /// diff --git a/tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs index b84a11e..9e4b48d 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs @@ -246,6 +246,67 @@ public class ToolTests : SerializationTest t => t.IgnoringCyclicReferences() ); } + + [Fact] + public void CreateFromClass_WhenCalledWithToolWhoseNameIsNull_ItShouldThrowException() + { + var action = () => Tool.CreateFromClass(); + + action.Should().Throw(); + } + + [Fact] + public void CreateFromClass_WhenCalledWithToolWhoseNameIsEmpty_ItShouldThrowException() + { + var action = () => Tool.CreateFromClass(); + + action.Should().Throw(); + } + + [Fact] + public void CreateFromClass_WhenCalledWithToolWhoseDescriptionIsNull_ItShouldThrowException() + { + var action = () => Tool.CreateFromClass(); + + action.Should().Throw(); + } + + [Fact] + public void CreateFromClass_WhenCalledWithToolWhoseDescriptionIsEmpty_ItShouldThrowException() + { + var action = () => Tool.CreateFromClass(); + + action.Should().Throw(); + } + + [Fact] + public void CreateFromClass_WhenCalledWithToolWhoseFunctionIsNull_ItShouldThrowException() + { + var action = () => Tool.CreateFromClass(); + + action.Should().Throw(); + } + + [Fact] + public void CreateFromClass_WhenCalledWithProperTool_ItShouldReturnTool() + { + var tool = Tool.CreateFromClass(); + + var expectedSchema = new JsonObject() + { + ["type"] = "object", + }; + + tool.Name.Should().Be("Name"); + tool.DisplayName.Should().Be("Name"); + tool.Description.Should().Be("Description"); + tool.Function.Method.Name.Should().Be(nameof(ProperTool.GetWeather)); + tool.Function.Instance.Should().BeNull(); + tool.InputSchema.Should().BeEquivalentTo( + expectedSchema, + t => t.IgnoringCyclicReferences() + ); + } } class TestClass @@ -254,4 +315,83 @@ class TestClass public static bool TestStaticMethod() => true; public bool TestInstanceMethod() => _result; +} + +class ProperTool : ITool +{ + public string Name => "Name"; + + public string Description { get; } = "Description"; + + public MethodInfo Function => typeof(ProperTool).GetMethod(nameof(GetWeather))!; + + public static string GetWeather() + { + return "Sunny"; + } +} + +class ToolWithNullName : ITool +{ + public string Name => null!; + + public string Description { get; } = "Description"; + + public MethodInfo Function => typeof(ToolWithNullName).GetMethod(nameof(Tool))!; + + public static string GetWeather() + { + return "Sunny"; + } +} + +class ToolWithEmptyName : ITool +{ + public string Name => string.Empty; + + public string Description { get; } = "Description"; + + public MethodInfo Function => typeof(ToolWithEmptyName).GetMethod(nameof(Tool))!; + + public static string GetWeather() + { + return "Sunny"; + } +} + +class ToolWithNullDescription : ITool +{ + public string Name => "Name"; + + public string Description => null!; + + public MethodInfo Function => typeof(ToolWithNullDescription).GetMethod(nameof(Tool))!; + + public static string GetWeather() + { + return "Sunny"; + } +} + +class ToolWithEmptyDescription : ITool +{ + public string Name => "Name"; + + public string Description => string.Empty; + + public MethodInfo Function => typeof(ToolWithEmptyDescription).GetMethod(nameof(Tool))!; + + public static string GetWeather() + { + return "Sunny"; + } +} + +class ToolWithNullFunction : ITool +{ + public string Name => "Name"; + + public string Description => "Description"; + + public MethodInfo Function => null!; } \ No newline at end of file