feat: implement tool call

This commit is contained in:
Stevan Freeborn
2024-07-01 21:00:27 -05:00
parent cccd0e6647
commit ce6b1bf8dc
5 changed files with 354 additions and 9 deletions
@@ -48,4 +48,10 @@ public class ChatResponse
/// Gets the contents of the chat response.
/// </summary>
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;
}
+113 -9
View File
@@ -1,3 +1,8 @@
using System.Reflection;
using System.Text.Json;
using AnthropicClient.Json;
namespace AnthropicClient.Models;
/// <summary>
@@ -5,13 +10,112 @@ namespace AnthropicClient.Models;
/// </summary>
public class ToolCall
{
// TODO: Implementation
// - each tool call needs parameters
// - each tool call needs a tool name
// - each tool needs function to call
// - tool may or may not need an instance
// - tool call will be given input from tool use
// - tool call should have list of known functions
// - tool call should be able to find function by name
// - tool call should be able to translate input to function parameters
/// <summary>
/// Gets the tool of the tool call.
/// </summary>
public Tool Tool { get; }
/// <summary>
/// Gets the tool use of the tool call.
/// </summary>
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);
}
}