Skip to content

Commit

Permalink
Merge pull request #10 from Cysharp/tool
Browse files Browse the repository at this point in the history
Support Tool use (function calling)
  • Loading branch information
neuecc authored Apr 5, 2024
2 parents 01241cd + 7e68967 commit f7e51d7
Show file tree
Hide file tree
Showing 11 changed files with 758 additions and 448 deletions.
102 changes: 97 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Unofficial [Anthropic Claude API](https://www.anthropic.com/api) client for .NET

We have built a C# API similar to the official [Python SDK](https://github.com/anthropics/anthropic-sdk-python) and [TypeScript SDK](https://github.com/anthropics/anthropic-sdk-typescript). It supports netstandard2.1, net6.0, and net8.0.

In addition to the pure client SDK, it also includes a C# Source Generator for performing Function Calling, similar to [anthropic-tools](https://github.com/anthropics/anthropic-tools/).
In addition to the pure client SDK, it also includes a C# Source Generator for performing Function Calling.

Installation
---
Expand Down Expand Up @@ -494,15 +494,107 @@ void Load()

Function Calling
---
Claude supports Function Calling. The [Anthropic Cookbook](https://github.com/anthropics/anthropic-cookbook) provides examples of Function Calling. To achieve this, complex XML generation and parsing processing, as well as execution based on the parsed results, are required.
Claude supports Function Calling.

With Claudia, you only need to define static methods annotated with `[ClaudiaFunction]`, and the C# Source Generator automatically generates the necessary code, including parsers and system messages.
## TOol use

[Tool use(function calling)](https://docs.anthropic.com/claude/docs/tool-use) is new style of function calling. Currently it is beta and need to add `anthropic-beta` flag in header.

```csharp
var anthropic = new Anthropic();
anthropic.HttpClient.DefaultRequestHeaders.Add("anthropic-beta", "tools-2024-04-04");
```

With Claudia, you only need to define static methods annotated with `[ClaudiaFunction]`, and the C# Source Generator automatically generates the necessary code.

```csharp
public static partial class FunctionTools
{
/// <summary>
/// Retrieve the current time of day in Hour-Minute-Second format for a specified time zone. Time zones should be written in standard formats such as UTC, US/Pacific, Europe/London.
/// </summary>
/// <param name="timeZone">The time zone to get the current time for, such as UTC, US/Pacific, Europe/London.</param>
[ClaudiaFunction]
public static string TimeOfDay(string timeZone)
{
var time = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(DateTime.UtcNow, timeZone);
return time.ToString("HH:mm:ss");
}
}
```

The `partial class` includes the generated `.AllTools`, `.Tools.[Methods]` and `.InvokeToolAsync(MessageResponse)`.

Function Calling requires two requests to Claude. The flow is as follows: "Initial request to Claude with available tools in System Prompt -> Execute functions based on the message containing the necessary tools -> Include the results in a new message and send another request to Claude."

```csharp
var anthropic = new Anthropic();
anthropic.HttpClient.DefaultRequestHeaders.Add("anthropic-beta", "tools-2024-04-04");

var input = new Message { Role = Roles.User, Content = "What time is it in Los Angeles?" };
var message = await anthropic.Messages.CreateAsync(new()
{
Model = Models.Claude3Haiku,
MaxTokens = 1024,
Tools = FunctionTools.AllTools, // use generated Tools
Messages = [input],
});

// invoke local function
var toolResult = await FunctionTools.InvokeToolAsync(message);

var response = await anthropic.Messages.CreateAsync(new()
{
Model = Models.Claude3Haiku,
MaxTokens = 1024,
Tools = [ToolUseSamples.Tools.Calculator],
Messages = [
input,
new() { Role = Roles.Assistant, Content = message.Content },
new() { Role = Roles.User, Content = toolResult! }
],
});

// The current time in Los Angeles is 10:45 AM.
Console.WriteLine(response.Content.ToString());
```

The return type of `ClaudiaFunction` can also be specified as `Task<T>` or `ValueTask<T>`. This allows you to execute a variety of tasks, such as HTTP requests or database requests. For example, a function that retrieves the content of a specified webpage can be defined as shown above.

```csharp
public static partial class FunctionTools
{
// Sample of anthropic-tools https://github.com/anthropics/anthropic-tools#basetool
// ...
/// <summary>
/// Retrieves the HTML from the specified URL.
/// </summary>
/// <param name="url">The URL to retrieve the HTML from.</param>
[ClaudiaFunction]
static async Task<string> GetHtmlFromWeb(string url)
{
// When using this in a real-world application, passing the raw HTML might consume too many tokens.
// You can parse the HTML locally using libraries like AngleSharp and convert it into a compact text structure to save tokens.
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
}
```

Note that the allowed parameter types are `bool`, `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `decimal`, `float`, `double`, `string`, `DateTime`, `DateTimeOffset`, `Guid`, `TimeSpan` and `Enum`.

The return value can be of any type, but it will be converted to a string using `ToString()`. If you want to return a custom string, make the return type `string` and format the string within the function.


## Legacy style

The [Anthropic Cookbook](https://github.com/anthropics/anthropic-cookbook) provides examples of Function Calling. To achieve this, complex XML generation and parsing processing, as well as execution based on the parsed results, are required.

With Claudia, you only need to define static methods annotated with `[ClaudiaFunction]`, and the C# Source Generator automatically generates the necessary code, including parsers and system messages.

```csharp
public static partial class FunctionTools
{
/// <summary>
/// Retrieve the current time of day in Hour-Minute-Second format for a specified time zone. Time zones should be written in standard formats such as UTC, US/Pacific, Europe/London.
/// </summary>
Expand Down Expand Up @@ -739,7 +831,7 @@ var callResult = await anthropic.Messages.CreateAsync(new()
Console.WriteLine(callResult);
```

Note that the allowed parameter types are `bool`, `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `decimal`, `float`, `double`, `string`, `DateTime`, `DateTimeOffset`, `Guid`, and `TimeSpan`.
Note that the allowed parameter types are `bool`, `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `decimal`, `float`, `double`, `string`, `DateTime`, `DateTimeOffset`, `Guid`, `TimeSpan` and `Enum`.

The return value can be of any type, but it will be converted to a string using `ToString()`. If you want to return a custom string, make the return type `string` and format the string within the function.

Expand Down
182 changes: 41 additions & 141 deletions sandbox/ConsoleApp1/GeneratedMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,176 +17,76 @@

//using Claudia;
//using System;
//using System.Collections.Generic;
//using System.Linq;
//using System.Text;
//using System.Text.Json;
//using System.Threading.Tasks;
//using System.Xml.Linq;

//static partial class FunctionTools
//{

// public const string SystemPrompt = @$"
//In this environment you have access to a set of tools you can use to answer the user's question. If there are multiple <invoke> tags, please consolidate them into a single <function_calls> block.

//You may call them like this:
//<function_calls>
// <invoke>
// <tool_name>$TOOL_NAME</tool_name>
// <parameters>
// <$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
// ...
// </parameters>
// </invoke>
//</function_calls>

//Here are the tools available:

//{PromptXml.ToolsAll}
//";

// public static class PromptXml
// {
// public const string ToolsAll = @$"
//{Today}
//{Sum}
//{DoPairwiseArithmetic}
//";

// public const string Today = @"
//<tool_description>
// <tool_name>Today</tool_name>
// <description>Date of target location.</description>
// <parameters>
// <parameter>
// <name>timeZoneId</name>
// <type>string</type>
// <description>TimeZone of localtion like 'Tokeyo Standard Time', 'Eastern Standard Time', etc.</description>
// </parameter>
// </parameters>
//</tool_description>
//";

// public const string Sum = @"
//<tool_description>
// <tool_name>Sum</tool_name>
// <description>Sum of two integer parameters.</description>
// <parameters>
// <parameter>
// <name>x</name>
// <type>int</type>
// <description>parameter1.</description>
// </parameter>
// <parameter>
// <name>y</name>
// <type>int</type>
// <description>parameter2.</description>
// </parameter>
// </parameters>
//</tool_description>
//";

// public const string DoPairwiseArithmetic = @"
//<tool_description>
// <tool_name>DoPairwiseArithmetic</tool_name>
// <description>Calculator function for doing basic arithmetic.
// Supports addition, subtraction, multiplication</description>
// <parameters>
// <parameter>
// <name>firstOperand</name>
// <type>double</type>
// <description>First operand (before the operator)</description>
// </parameter>
// <parameter>
// <name>secondOperand</name>
// <type>double</type>
// <description>Second operand (after the operator)</description>
// </parameter>
// <parameter>
// <name>operator</name>
// <type>string</type>
// <description>The operation to perform. Must be either +, -, *, or /</description>
// </parameter>
// </parameters>
//</tool_description>
//";


// }

//#pragma warning disable CS1998
// public static async ValueTask<string?> InvokeAsync(MessageResponse message)
// public static async ValueTask<Content[]> InvokeToolAsync(MessageResponse message)
// {
// var content = message.Content.FirstOrDefault(x => x.Text != null);
// if (content == null) return null;

// var text = content.Text;
// var tagStart = text .IndexOf("<function_calls>");
// if (tagStart == -1) return null;

// var functionCalls = text.Substring(tagStart) + "</function_calls>";
// var xmlResult = XElement.Parse(functionCalls);
// var result = new List<Content>();

// var sb = new StringBuilder();
// sb.AppendLine(functionCalls);
// sb.AppendLine("<function_results>");

// foreach (var item in xmlResult.Elements("invoke"))
// foreach (var item in message.Content)
// {
// var name = (string)item.Element("tool_name")!;
// switch (name)
// {
// case "Today":
// {
// var parameters = item.Element("parameters")!;

// var _0 = (string)parameters.Element("timeZoneId")!;

// BuildResult(sb, "Today", Today(_0));
// break;
// }
// case "Sum":
// {
// var parameters = item.Element("parameters")!;

// var _0 = (int)parameters.Element("x")!;
// var _1 = (int)parameters.Element("y")!;
// if (item.Type != ContentTypes.ToolUse) continue;

// BuildResult(sb, "Sum", Sum(_0, _1));
// break;
// }
// case "DoPairwiseArithmetic":
// switch (item.ToolUseName)
// {
// case "TimeOfDay":
// {
// var parameters = item.Element("parameters")!;

// var _0 = (double)parameters.Element("firstOperand")!;
// var _1 = (double)parameters.Element("secondOperand")!;
// var _2 = (string)parameters.Element("operator")!;
// // if (!item.ToolUseInput.TryGetValue("timeZone", out var _0)) _0 = default;
// var _0 = GetValueOrDefault<string>(item, "timeZone", default!);
// string? _callResult;
// bool? _isError = null;
// try
// {
// _callResult = TimeOfDay(_0).ToString();
// }
// catch (Exception ex)
// {
// _callResult = ex.Message;
// _isError = true;
// }

// result.Add(new Content
// {
// Type = ContentTypes.ToolResult,
// ToolUseId = item.ToolUseId,
// ToolResultContent = _callResult,
// ToolResultIsError = _isError
// });

// BuildResult(sb, "DoPairwiseArithmetic", DoPairwiseArithmetic(_0, _1, _2));
// break;
// }

// default:
// break;
// }
// }

// sb.Append("</function_results>"); // final assistant content cannot end with trailing whitespace

// return sb.ToString();
// return result.ToArray();

// static void BuildResult<T>(StringBuilder sb, string toolName, T result)
// static T GetValueOrDefault<T>(Content content, string name, T defaultValue)
// {
// sb.AppendLine(@$"
// <result>
// <tool_name>{toolName}</tool_name>
// <stdout>
// {result}
// </stdout>
// </result>
//");
// if (content.ToolUseInput.TryGetValue(name, out var stringValue))
// {
// return System.Text.Json.JsonSerializer.Deserialize<T>(stringValue)!;
// }
// else
// {
// return defaultValue;
// }
// }
// }

//#pragma warning restore CS1998
//}

Loading

0 comments on commit f7e51d7

Please sign in to comment.