feat: implement tool call
This commit is contained in:
@@ -82,6 +82,12 @@ public class AnthropicApiClient : IAnthropicApiClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
var chatResponse = Deserialize<ChatResponse>(responseContent) ?? new ChatResponse();
|
var chatResponse = Deserialize<ChatResponse>(responseContent) ?? new ChatResponse();
|
||||||
|
|
||||||
|
if (request.Tools is not null && request.Tools.Count > 0)
|
||||||
|
{
|
||||||
|
chatResponse.ToolCall = GetToolCall(chatResponse, request.Tools);
|
||||||
|
}
|
||||||
|
|
||||||
return AnthropicResult<ChatResponse>.Success(chatResponse, anthropicHeaders);
|
return AnthropicResult<ChatResponse>.Success(chatResponse, anthropicHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +196,11 @@ public class AnthropicApiClient : IAnthropicApiClient
|
|||||||
Usage = newUsage,
|
Usage = newUsage,
|
||||||
Content = chatResponse.Content,
|
Content = chatResponse.Content,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (request.Tools is not null && request.Tools.Count > 0)
|
||||||
|
{
|
||||||
|
chatResponse.ToolCall = GetToolCall(chatResponse, request.Tools);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// yield chat response on message stop
|
// yield chat response on message stop
|
||||||
@@ -228,6 +239,25 @@ public class AnthropicApiClient : IAnthropicApiClient
|
|||||||
} while (true);
|
} while (true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ToolCall? GetToolCall(ChatResponse response, List<Tool> tools)
|
||||||
|
{
|
||||||
|
var toolUse = response.Content.OfType<ToolUseContent>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (toolUse is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tool = tools.FirstOrDefault(t => t.Name == toolUse.Name);
|
||||||
|
|
||||||
|
if (tool is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ToolCall(tool, toolUse);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> SendRequestAsync(MessageRequest request)
|
private async Task<HttpResponseMessage> SendRequestAsync(MessageRequest request)
|
||||||
{
|
{
|
||||||
var requestContent = new StringContent(Serialize(request), Encoding.UTF8, JsonContentType);
|
var requestContent = new StringContent(Serialize(request), Encoding.UTF8, JsonContentType);
|
||||||
|
|||||||
@@ -48,4 +48,10 @@ public class ChatResponse
|
|||||||
/// Gets the contents of the chat response.
|
/// Gets the contents of the chat response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<Content> Content { get; init; } = [];
|
public List<Content> Content { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the tool call of the chat response. If the chat response does not contain a tool call, this property is null.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public ToolCall? ToolCall { get; set; } = null;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using AnthropicClient.Json;
|
||||||
|
|
||||||
namespace AnthropicClient.Models;
|
namespace AnthropicClient.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -5,13 +10,112 @@ namespace AnthropicClient.Models;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ToolCall
|
public class ToolCall
|
||||||
{
|
{
|
||||||
// TODO: Implementation
|
/// <summary>
|
||||||
// - each tool call needs parameters
|
/// Gets the tool of the tool call.
|
||||||
// - each tool call needs a tool name
|
/// </summary>
|
||||||
// - each tool needs function to call
|
public Tool Tool { get; }
|
||||||
// - tool may or may not need an instance
|
|
||||||
// - tool call will be given input from tool use
|
/// <summary>
|
||||||
// - tool call should have list of known functions
|
/// Gets the tool use of the tool call.
|
||||||
// - tool call should be able to find function by name
|
/// </summary>
|
||||||
// - tool call should be able to translate input to function parameters
|
public ToolUseContent ToolUse { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ToolCall"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tool">The tool of the tool call.</param>
|
||||||
|
/// <param name="toolUse">The tool use of the tool call.</param>
|
||||||
|
/// <returns>A new instance of the <see cref="ToolCall"/> class.</returns>
|
||||||
|
public ToolCall(Tool tool, ToolUseContent toolUse)
|
||||||
|
{
|
||||||
|
Tool = tool;
|
||||||
|
ToolUse = toolUse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the tool call.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The result of the tool call as an instance of <see cref="ToolCallResult{T}"/>.</returns>
|
||||||
|
public async Task<ToolCallResult<object>> InvokeAsync()
|
||||||
|
{
|
||||||
|
return await InvokeAsync<object>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the tool call.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the result value.</typeparam>
|
||||||
|
/// <returns>The result of the tool call as an instance of <see cref="ToolCallResult{T}"/>.</returns>
|
||||||
|
/// <exception cref="ArgumentException">Thrown when a parameter name is not found.</exception>
|
||||||
|
/// <exception cref="ArgumentException">Thrown when an argument is missing.</exception>
|
||||||
|
public async Task<ToolCallResult<T>> InvokeAsync<T>()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
T? result = default;
|
||||||
|
|
||||||
|
var arguments = GetArguments();
|
||||||
|
var isAwaitable = Tool.Function.Method.ReturnType.GetMethod(nameof(Task.GetAwaiter)) is not null;
|
||||||
|
|
||||||
|
if (isAwaitable)
|
||||||
|
{
|
||||||
|
var task = (Task)Tool.Function.Method.Invoke(Tool.Function.Instance, arguments);
|
||||||
|
|
||||||
|
await task;
|
||||||
|
|
||||||
|
const string resultPropertyName = "Result";
|
||||||
|
var resultProperty = task.GetType().GetProperty(resultPropertyName);
|
||||||
|
result = resultProperty is not null ? (T)resultProperty.GetValue(task) : default;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = (T)Tool.Function.Method.Invoke(Tool.Function.Instance, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToolCallResult<T>.Success(result);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return ToolCallResult<T>.Failure(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object?[] GetArguments()
|
||||||
|
{
|
||||||
|
var parameters = Tool.Function.Method.GetParameters();
|
||||||
|
var arguments = new object?[parameters.Length];
|
||||||
|
|
||||||
|
for (var i = 0; i < parameters.Length; i++)
|
||||||
|
{
|
||||||
|
var parameter = parameters[i];
|
||||||
|
var attribute = parameter.GetCustomAttribute<FunctionParameterAttribute>();
|
||||||
|
var parameterName = attribute is not null
|
||||||
|
? string.IsNullOrWhiteSpace(attribute.Name)
|
||||||
|
? parameter.Name
|
||||||
|
: attribute.Name
|
||||||
|
: parameter.Name;
|
||||||
|
|
||||||
|
if (parameterName == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Failed to find a valid parameter name for {Tool.Function.Method.DeclaringType}.{Tool.Function.Method.Name}()");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ToolUse.Input.TryGetValue(parameterName, out var value))
|
||||||
|
{
|
||||||
|
arguments[i] = value is string s && parameter.ParameterType.IsEnum
|
||||||
|
? Enum.Parse(parameter.ParameterType, s, true)
|
||||||
|
: value is JsonElement element
|
||||||
|
? JsonSerializer.Deserialize(element.GetRawText(), parameter.ParameterType, JsonSerializationOptions.DefaultOptions)
|
||||||
|
: value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
arguments[i] = parameter.HasDefaultValue
|
||||||
|
? parameter.DefaultValue
|
||||||
|
: throw new ArgumentException($"Missing argument for parameter '{parameter.Name}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arguments;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
namespace AnthropicClient.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a tool call result.
|
||||||
|
/// </summary>
|
||||||
|
public class ToolCallResult<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The value of the tool call result.
|
||||||
|
/// </summary>
|
||||||
|
public T? Value { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The error of the tool call result.
|
||||||
|
/// </summary>
|
||||||
|
public Exception Error { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether the tool call was successful.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSuccess { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ToolCallResult{T}"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value of the tool call result.</param>
|
||||||
|
/// <param name="error">The error of the tool call result.</param>
|
||||||
|
/// <param name="isSuccess">Indicates whether the tool call was successful.</param>
|
||||||
|
/// <returns>A new instance of the <see cref="ToolCallResult{T}"/> class.</returns>
|
||||||
|
protected ToolCallResult(T? value, Exception error, bool isSuccess)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
Error = error;
|
||||||
|
IsSuccess = isSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a successful tool call result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value of the tool call result.</param>
|
||||||
|
/// <returns>A new instance of the <see cref="ToolCallResult{T}"/> class.</returns>
|
||||||
|
public static ToolCallResult<T> Success(T? value)
|
||||||
|
{
|
||||||
|
return new ToolCallResult<T>(value, null!, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a failed ool call result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="error">The error of the tool call result.</param>
|
||||||
|
/// <returns>A new instance of the <see cref="ToolCallResult{T}"/> class.</returns>
|
||||||
|
public static ToolCallResult<T> Failure(Exception error)
|
||||||
|
{
|
||||||
|
return new ToolCallResult<T>(default!, error, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
namespace AnthropicClient.Tests.Unit.Models;
|
||||||
|
|
||||||
|
public class ToolCallTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_WhenCalledToolHasNoParamsIsNotAwaitableAndToolCallIsSuccessful_ItShouldReturnSuccessResult()
|
||||||
|
{
|
||||||
|
var func = () => "Hello, World!";
|
||||||
|
var anthropicFunction = new AnthropicFunction(func.Method, func.Target);
|
||||||
|
var tool = new Tool("tool", "description", anthropicFunction);
|
||||||
|
var toolCall = new ToolCall(tool, new ToolUseContent());
|
||||||
|
|
||||||
|
var result = await toolCall.InvokeAsync();
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().Be("Hello, World!");
|
||||||
|
result.Error.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_WhenCalledToolHasNoParamsIsNotAwaitableAndToolCallFails_ItShouldReturnFailureResult()
|
||||||
|
{
|
||||||
|
var func = new Func<string>(() => throw new Exception("Error"));
|
||||||
|
var anthropicFunction = new AnthropicFunction(func.Method, func.Target);
|
||||||
|
var tool = new Tool("tool", "description", anthropicFunction);
|
||||||
|
var toolCall = new ToolCall(tool, new ToolUseContent());
|
||||||
|
|
||||||
|
var result = await toolCall.InvokeAsync();
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeFalse();
|
||||||
|
result.Value.Should().BeNull();
|
||||||
|
result.Error.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_WhenCalledToolHasNoParamsIsAwaitableAndToolCallIsSuccessful_ItShouldReturnSuccessResult()
|
||||||
|
{
|
||||||
|
var func = async () => await Task.FromResult("Hello, World!");
|
||||||
|
var anthropicFunction = new AnthropicFunction(func.Method, func.Target);
|
||||||
|
var tool = new Tool("tool", "description", anthropicFunction);
|
||||||
|
var toolCall = new ToolCall(tool, new ToolUseContent());
|
||||||
|
|
||||||
|
var result = await toolCall.InvokeAsync();
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().Be("Hello, World!");
|
||||||
|
result.Error.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_WhenCalledToolHasNoParamsIsAwaitableAndToolCallFails_ItShouldReturnFailureResult()
|
||||||
|
{
|
||||||
|
var func = new Func<Task<string>>(() => throw new Exception("Error"));
|
||||||
|
var anthropicFunction = new AnthropicFunction(func.Method, func.Target);
|
||||||
|
var tool = new Tool("tool", "description", anthropicFunction);
|
||||||
|
var toolCall = new ToolCall(tool, new ToolUseContent());
|
||||||
|
|
||||||
|
var result = await toolCall.InvokeAsync();
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeFalse();
|
||||||
|
result.Value.Should().BeNull();
|
||||||
|
result.Error.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_WhenCalledToolHasParamsIsNotAwaitableAndToolCallIsSuccessful_ItShouldReturnSuccessResult()
|
||||||
|
{
|
||||||
|
var func = (int i) => i.ToString();
|
||||||
|
var anthropicFunction = new AnthropicFunction(func.Method, func.Target);
|
||||||
|
var tool = new Tool("tool", "description", anthropicFunction);
|
||||||
|
|
||||||
|
var input = new Dictionary<string, object?> { { "i", 42 } };
|
||||||
|
var toolCall = new ToolCall(tool, new ToolUseContent { Input = input });
|
||||||
|
|
||||||
|
var result = await toolCall.InvokeAsync();
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().Be("42");
|
||||||
|
result.Error.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_WhenCalledToolHasParamsIsNotAwaitableAndToolCallFails_ItShouldReturnFailureResult()
|
||||||
|
{
|
||||||
|
var func = new Func<int, string>(i => throw new Exception("Error"));
|
||||||
|
var anthropicFunction = new AnthropicFunction(func.Method, func.Target);
|
||||||
|
var tool = new Tool("tool", "description", anthropicFunction);
|
||||||
|
|
||||||
|
var input = new Dictionary<string, object?> { { "i", 42 } };
|
||||||
|
var toolCall = new ToolCall(tool, new ToolUseContent { Input = input });
|
||||||
|
|
||||||
|
var result = await toolCall.InvokeAsync();
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeFalse();
|
||||||
|
result.Value.Should().BeNull();
|
||||||
|
result.Error.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_WhenCalledToolHasParamsIsAwaitableAndToolCallIsSuccessful_ItShouldReturnSuccessResult()
|
||||||
|
{
|
||||||
|
var func = async (int i) => await Task.FromResult(i.ToString());
|
||||||
|
var anthropicFunction = new AnthropicFunction(func.Method, func.Target);
|
||||||
|
var tool = new Tool("tool", "description", anthropicFunction);
|
||||||
|
|
||||||
|
var input = new Dictionary<string, object?> { { "i", 42 } };
|
||||||
|
var toolCall = new ToolCall(tool, new ToolUseContent { Input = input });
|
||||||
|
|
||||||
|
var result = await toolCall.InvokeAsync();
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().Be("42");
|
||||||
|
result.Error.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_WhenCalledToolHasParamsIsAwaitableAndToolCallFails_ItShouldReturnFailureResult()
|
||||||
|
{
|
||||||
|
var func = new Func<int, Task<string>>(i => throw new Exception("Error"));
|
||||||
|
var anthropicFunction = new AnthropicFunction(func.Method, func.Target);
|
||||||
|
var tool = new Tool("tool", "description", anthropicFunction);
|
||||||
|
|
||||||
|
var input = new Dictionary<string, object?> { { "i", 42 } };
|
||||||
|
var toolCall = new ToolCall(tool, new ToolUseContent { Input = input });
|
||||||
|
|
||||||
|
var result = await toolCall.InvokeAsync();
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeFalse();
|
||||||
|
result.Value.Should().BeNull();
|
||||||
|
result.Error.Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_WhenCalledAndToolHasCustomParameterName_ItShouldReturnSuccessResult()
|
||||||
|
{
|
||||||
|
var func = ([FunctionParameter("Person's Age", "Age")]int i) => i.ToString();
|
||||||
|
var anthropicFunction = new AnthropicFunction(func.Method, func.Target);
|
||||||
|
var tool = new Tool("tool", "description", anthropicFunction);
|
||||||
|
|
||||||
|
var input = new Dictionary<string, object?> { { "Age", 42 } };
|
||||||
|
var toolCall = new ToolCall(tool, new ToolUseContent { Input = input });
|
||||||
|
|
||||||
|
var result = await toolCall.InvokeAsync();
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().Be("42");
|
||||||
|
result.Error.Should().BeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user