diff --git a/README.md b/README.md
index 7dc1bb3..e5b294b 100644
--- a/README.md
+++ b/README.md
@@ -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
---
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ /// The time zone to get the current time for, such as UTC, US/Pacific, Europe/London.
+ [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` or `ValueTask`. 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
+ // ...
+ ///
+ /// Retrieves the HTML from the specified URL.
+ ///
+ /// The URL to retrieve the HTML from.
+ [ClaudiaFunction]
+ static async Task 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
+{
///
/// 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.
///
@@ -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.
diff --git a/sandbox/ConsoleApp1/GeneratedMock.cs b/sandbox/ConsoleApp1/GeneratedMock.cs
index 2fd55bd..4ae9b9d 100644
--- a/sandbox/ConsoleApp1/GeneratedMock.cs
+++ b/sandbox/ConsoleApp1/GeneratedMock.cs
@@ -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 tags, please consolidate them into a single block.
-//You may call them like this:
-//
-//
-// $TOOL_NAME
-//
-// <$PARAMETER_NAME>$PARAMETER_VALUE$PARAMETER_NAME>
-// ...
-//
-//
-//
-//Here are the tools available:
-
-//{PromptXml.ToolsAll}
-//";
-
-// public static class PromptXml
-// {
-// public const string ToolsAll = @$"
-//{Today}
-//{Sum}
-//{DoPairwiseArithmetic}
-//";
-
-// public const string Today = @"
-//
-// Today
-// Date of target location.
-//
-//
-// timeZoneId
-// string
-// TimeZone of localtion like 'Tokeyo Standard Time', 'Eastern Standard Time', etc.
-//
-//
-//
-//";
-
-// public const string Sum = @"
-//
-// Sum
-// Sum of two integer parameters.
-//
-//
-// x
-// int
-// parameter1.
-//
-//
-// y
-// int
-// parameter2.
-//
-//
-//
-//";
-
-// public const string DoPairwiseArithmetic = @"
-//
-// DoPairwiseArithmetic
-// Calculator function for doing basic arithmetic.
-// Supports addition, subtraction, multiplication
-//
-//
-// firstOperand
-// double
-// First operand (before the operator)
-//
-//
-// secondOperand
-// double
-// Second operand (after the operator)
-//
-//
-// operator
-// string
-// The operation to perform. Must be either +, -, *, or /
-//
-//
-//
-//";
-
-
-// }
//#pragma warning disable CS1998
-// public static async ValueTask InvokeAsync(MessageResponse message)
+// public static async ValueTask 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("");
-// if (tagStart == -1) return null;
-
-// var functionCalls = text.Substring(tagStart) + "";
-// var xmlResult = XElement.Parse(functionCalls);
+// var result = new List();
-// var sb = new StringBuilder();
-// sb.AppendLine(functionCalls);
-// sb.AppendLine("");
-
-// 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(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(""); // final assistant content cannot end with trailing whitespace
-
-// return sb.ToString();
+// return result.ToArray();
-// static void BuildResult(StringBuilder sb, string toolName, T result)
+// static T GetValueOrDefault(Content content, string name, T defaultValue)
// {
-// sb.AppendLine(@$"
-//
-// {toolName}
-//
-// {result}
-//
-//
-//");
+// if (content.ToolUseInput.TryGetValue(name, out var stringValue))
+// {
+// return System.Text.Json.JsonSerializer.Deserialize(stringValue)!;
+// }
+// else
+// {
+// return defaultValue;
+// }
// }
// }
+
//#pragma warning restore CS1998
//}
diff --git a/sandbox/ConsoleApp1/Program.cs b/sandbox/ConsoleApp1/Program.cs
index d25a2ad..a42c24e 100644
--- a/sandbox/ConsoleApp1/Program.cs
+++ b/sandbox/ConsoleApp1/Program.cs
@@ -1,330 +1,52 @@
using Claudia;
+using ConsoleApp1;
using System;
+using System.Collections.Specialized;
+using System.Data;
using System.Linq;
using System.Net.Http;
using System.Net.NetworkInformation;
+using System.Runtime.CompilerServices;
using System.Text;
+using System.Text.Json;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
-// function calling
-// https://github.com/anthropics/anthropic-cookbook/blob/main/function_calling/function_calling.ipynb
-var anthropic = new Anthropic();
-
-//var userInput = """
-//Translate and summarize this Japanese site to English.
-//https://scrapbox.io/hadashiA/ZLogger_v2%E3%81%AE%E6%96%B0%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%83%81%E3%83%A3%E3%83%BC%E3%83%89%E3%83%AD%E3%82%AE%E3%83%B3%E3%82%B0%E4%BD%93%E9%A8%93
-//""";
-
-//var message = await anthropic.Messages.CreateAsync(new()
-//{
-// Model = Models.Claude3Haiku,
-// MaxTokens = 1024,
-// System = SystemPrompts.Claude3 + "\n" + FunctionTools.SystemPrompt,
-// StopSequences = [StopSequnces.CloseFunctionCalls],
-// Messages = [
-// new() { Role = Roles.User, Content = userInput },
-// ],
-//});
-
-//var partialAssistantMessage = await FunctionTools.InvokeAsync(message);
-
-//var callResult = await anthropic.Messages.CreateAsync(new()
-//{
-// Model = Models.Claude3Haiku,
-// MaxTokens = 1024,
-// System = SystemPrompts.Claude3 + "\n" + FunctionTools.SystemPrompt + "\n" + "Return message from assistant should be humanreadable so don't use xml tags, and json.",
-// Messages = [
-// new() { Role = Roles.User, Content = userInput },
-// new() { Role = Roles.Assistant, Content = partialAssistantMessage! },
-// ],
-//});
-
-//Console.WriteLine(callResult);
+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 Seattle and Tokyo?
- Incidentally multiply 1,984,135 by 9,343,116.
-"""
-};
-
+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,
- System = FunctionTools.SystemPrompt, // set generated prompt
- StopSequences = [StopSequnces.CloseFunctionCalls], // set as stop sequence
+ Tools = FunctionTools.AllTools, // use generated Tools
Messages = [input],
});
-var partialAssistantMessage = await FunctionTools.InvokeAsync(message);
+var toolResult = await FunctionTools.InvokeToolAsync(message);
-var callResult = await anthropic.Messages.CreateAsync(new()
+var response = await anthropic.Messages.CreateAsync(new()
{
Model = Models.Claude3Haiku,
MaxTokens = 1024,
- System = FunctionTools.SystemPrompt,
+ Tools = [ToolUseSamples.Tools.Calculator],
Messages = [
input,
- new() { Role = Roles.Assistant, Content = partialAssistantMessage! } // set as Assistant
+ new() { Role = Roles.Assistant, Content = message.Content },
+ new() { Role = Roles.User, Content = toolResult! }
],
});
-Console.WriteLine(callResult);
-
-
-//var systemPrompt = """
-//In this environment you have access to a set of tools you can use to answer the user's question.
-
-//You may call them like this:
-//
-//
-// $TOOL_NAME
-//
-// <$PARAMETER_NAME>$PARAMETER_VALUE$PARAMETER_NAME>
-// ...
-//
-//
-//
-
-//Here are the tools available:
-//
-//
-// calculator
-//
-// Calculator function for doing basic arithmetic.
-// Supports addition, subtraction, multiplication
-//
-//
-//
-// first_operand
-// int
-// First operand (before the operator)
-//
-//
-// second_operand
-// int
-// Second operand (after the operator)
-//
-//
-// operator
-// str
-// The operation to perform. Must be either +, -, *, or /
-//
-//
-//
-//
-//""";
-
-
-//var message = await anthropic.Messages.CreateAsync(new()
-//{
-// Model = Models.Claude3Opus,
-// MaxTokens = 1024,
-// System = systemPrompt,
-// StopSequences = ["\n\nHuman:", "\n\nAssistant", ""],
-// Messages = [new() { Role = "user", Content = "Multiply 1,984,135 by 9,343,116" }],
-//});
-
-//// Result XML::
-
-
-//var text = message.Content[0].Text!;
-//var tagStart = text.IndexOf("");
-//var xmlResult = XElement.Parse(text.Substring(tagStart) + message.StopSequence);
-//var parameters = xmlResult.Descendants("parameters").Elements();
-
-//var first = (double)parameters.First(x => x.Name == "first_operand");
-//var second = (double)parameters.First(x => x.Name == "second_operand");
-//var operation = (string)parameters.First(x => x.Name == "operator");
-
-//var result = DoPairwiseArithmetic(first, second, operation);
-
-//Console.WriteLine(result);
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-//var imageBytes = File.ReadAllBytes(@"dish.jpg");
-
-//var anthropic = new Anthropic();
-
-//var message = await anthropic.Messages.CreateAsync(new()
-//{
-// Model = "claude-3-opus-20240229",
-// MaxTokens = 1024,
-// Messages = [new()
-// {
-// Role = "user",
-// Content = [
-// new()
-// {
-// Type = "image",
-// Source = new()
-// {
-// Type = "base64",
-// MediaType = "image/jpeg",
-// Data = imageBytes
-// }
-// },
-// new()
-// {
-// Type = "text",
-// Text = "Describe this image."
-// }
-// ]
-// }],
-//});
-//Console.WriteLine(message);
-
-//var simple = await anthropic.Messages.CreateAsync(new()
-//{
-// Model = Models.Claude3Opus,
-// MaxTokens = 1024,
-// Messages = [new()
-// {
-// Role = Roles.User,
-// Content = [
-// new(imageBytes, "image/jpeg"),
-// "Describe this image."
-// ]
-// }],
-//});
-//Console.WriteLine(simple);
-
-//// convert to array.
-//var array = await stream.ToObservable().ToArrayAsync();
-
-//// filterling and execute.
-//await stream.ToObservable()
-// .OfType()
-// .Where(x => x.Delta.Text != null)
-// .ForEachAsync(x =>
-// {
-// Console.WriteLine(x.Delta.Text);
-// });
-
-//// branching query
-//var branch = stream.ToObservable().Publish();
-
-//var messageStartTask = branch.OfType().FirstAsync();
-//var messageDeltaTask = branch.OfType().FirstAsync();
-
-//branch.Connect(); // start consume stream
-
-//Console.WriteLine((await messageStartTask));
-//Console.WriteLine((await messageDeltaTask));
-
-
-
-
-
-
-
-//Console.WriteLine("---");
-
-//Console.WriteLine(sb.ToString());
-
-// Counting Tokens
-//var anthropic = new Anthropic();
-
-//var msg = await anthropic.Messages.CreateAsync(new()
-//{
-// Model = Models.Claude3Opus,
-// MaxTokens = 1024,
-// Messages = [new() { Role = "user", Content = "Hello, Claude." }]
-//});
-
-//// Usage { InputTokens = 11, OutputTokens = 18 }
-//Console.WriteLine(msg.Usage);
-
-
-
-//Messages = [new() { Role = "user", Content = "Hello, Claude. Responses, please break line after each word." }]
-
-
-//// error
-
-//try
-//{
-// var msg = await anthropic.Messages.CreateAsync(new()
-// {
-// Model = Models.Claude3Opus,
-// MaxTokens = 1024,
-// Messages = [new() { Role = "user", Content = "Hello, Claude" }]
-// });
-//}
-//catch (ClaudiaException ex)
-//{
-// Console.WriteLine((int)ex.Status); // 400(ErrorCode.InvalidRequestError)
-// Console.WriteLine(ex.Name); // invalid_request_error
-// Console.WriteLine(ex.Message); // Field required. Input:...
-//}
-
-// retry
-
-// Configure the default for all requests:
-//var anthropic = new Anthropic
-//{
-// MaxRetries = 0, // default is 2
-//};
-
-//// Or, configure per-request:
-//await anthropic.Messages.CreateAsync(new()
-//{
-// MaxTokens = 1024,
-// Messages = [new() { Role = "user", Content = "Hello, Claude" }],
-// Model = "claude-3-opus-20240229"
-//}, new()
-//{
-// MaxRetries = 5
-//});
-
-// timeout
-
-//// Configure the default for all requests:
-//var anthropic = new Anthropic
-//{
-// Timeout = TimeSpan.FromSeconds(20) // 20 seconds (default is 10 minutes)
-//};
-
-//// Override per-request:
-//await anthropic.Messages.CreateAsync(new()
-//{
-// MaxTokens = 1024,
-// Messages = [new() { Role = "user", Content = "Hello, Claude" }],
-// Model = "claude-3-opus-20240229"
-//}, new()
-//{
-// Timeout = TimeSpan.FromSeconds(5)
-//});
+// The current time in Los Angeles is 10:45 AM.
+Console.WriteLine(response.Content.ToString());
public static partial class FunctionTools
{
- // Sample of anthropic-tools https://github.com/anthropics/anthropic-tools#basetool
-
///
/// 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.
///
@@ -368,7 +90,33 @@ static async Task GetHtmlFromWeb(string url)
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
+
+ ///
+ /// Sum of two parameters.
+ ///
+ /// x.
+ /// y.
+ [ClaudiaFunction]
+ static int Sum(int x, int y = 100)
+ {
+ return x + y;
+ }
+
+ ///
+ /// Choose which fruits
+ ///
+ /// Fruits basket.
+ /// Fruits basket2.
+ [ClaudiaFunction]
+ static string ChooseFruit(Fruits basket, Fruits more = Fruits.Grape)
+ {
+ return basket.ToString();
+ }
}
+public enum Fruits
+{
+ Orange, Grape
+}
diff --git a/sandbox/ConsoleApp1/ToolUseSamples.cs b/sandbox/ConsoleApp1/ToolUseSamples.cs
new file mode 100644
index 0000000..a97de88
--- /dev/null
+++ b/sandbox/ConsoleApp1/ToolUseSamples.cs
@@ -0,0 +1,108 @@
+using Claudia;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ConsoleApp1;
+
+public static partial class ToolUseSamples
+{
+ // https://github.com/anthropics/anthropic-cookbook/blob/main/tool_use/calculator_tool.ipynb
+ public static async Task TryCalculateAsync()
+ {
+ var anthropic = new Anthropic();
+ anthropic.HttpClient.DefaultRequestHeaders.Add("anthropic-beta", "tools-2024-04-04");
+
+ await ChatWithClaude("What is the result of 1,984,135 * 9,343,116?");
+ await ChatWithClaude("Calculate (12851 - 593) * 301 + 76");
+ await ChatWithClaude("What is 15910385 divided by 193053?");
+
+ async Task ChatWithClaude(string userMessage)
+ {
+ Console.WriteLine("==================================================");
+ Console.WriteLine($"User Message: {userMessage}");
+ Console.WriteLine("==================================================");
+
+ var message = await anthropic.Messages.CreateAsync(new()
+ {
+ Model = Models.Claude3Haiku,
+ MaxTokens = 1024,
+ Tools = [ToolUseSamples.Tools.Calculator],
+ Messages = [new() { Role = Roles.User, Content = userMessage }]
+ });
+
+ Console.WriteLine("Initial Response:");
+ Console.WriteLine($"Stop Reason: {message.StopReason}");
+ Console.WriteLine($"Content: {message.Content}");
+
+ var toolResult = await ToolUseSamples.InvokeToolAsync(message);
+
+ var response = await anthropic.Messages.CreateAsync(new()
+ {
+ Model = Models.Claude3Haiku,
+ MaxTokens = 1024,
+ Tools = [ToolUseSamples.Tools.Calculator],
+ Messages = [
+ new() { Role = Roles.User, Content = userMessage },
+ new() { Role = Roles.Assistant, Content = message.Content },
+ new() { Role = Roles.User, Content = toolResult! }
+ ],
+ });
+
+ Console.WriteLine(response.Content.ToString());
+ }
+ }
+
+
+ ///
+ /// A simple calculator that performs basic arithmetic operations.
+ ///
+ /// The mathematical expression to evaluate (e.g., '2 + 3 * 4').
+ [ClaudiaFunction]
+ static double Calculator(string expression)
+ {
+ // cheap calculator, only calc 32bit.
+ var dt = new DataTable();
+ return Convert.ToDouble(dt.Compute(expression, ""));
+ }
+
+ // https://github.com/anthropics/anthropic-cookbook/blob/main/tool_use/customer_service_agent.ipynb
+
+
+
+ /////
+ ///// Retrieves customer information based on their customer ID. Returns the customer's name, email, and phone number.
+ /////
+ ///// The unique identifier for the customer.
+ //static string GetCustomerInfo(string customerId)
+ //{
+ // // Simulated customer data
+ // var customers = new Dictionary {
+ // { "C1", new Customer(name: "John Doe", email: "john@example.com", phone: "123-456-7890") },
+ // { "C2", new Customer(name: "Jane Smith", email: "jane@example.com", phone: "987-654-3210") },
+ // };
+
+ // return customers.TryGetValue(customerId, out var customer) ? customer.ToString() : "Customer not found";
+ //}
+
+
+ /////
+ ///// Retrieves the details of a specific order based on the order ID. Returns the order ID, product name, quantity, price, and order status.
+ /////
+ ///// Retrieves the details of a specific order based on the order ID. Returns the order ID, product name, quantity, price, and order status.
+ //static string GetOrderDetails(string orderId)
+ //{
+ //}
+
+ /////
+ ///// The unique identifier for the order to be cancelled.
+ /////
+ ///// Cancels an order based on the provided order ID. Returns a confirmation message if the cancellation is successful.
+ //static string CancelOrder(string orderId)
+ //{
+ //}
+
+ //record class Customer(string name, string email, string phone);
+}
diff --git a/src/Claudia.FunctionGenerator/Emitter.cs b/src/Claudia.FunctionGenerator/Emitter.cs
index ab14b25..6e66471 100644
--- a/src/Claudia.FunctionGenerator/Emitter.cs
+++ b/src/Claudia.FunctionGenerator/Emitter.cs
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using System.Text;
using System.Xml.Linq;
+using static Claudia.FunctionGenerator.Emitter;
namespace Claudia.FunctionGenerator;
@@ -32,7 +33,13 @@ internal void Emit()
sb.AppendLine("{");
sb.AppendLine("");
- EmitCore(parseResult);
+ // new, beta: https://docs.anthropic.com/claude/docs/tool-use
+ EmitToolCallingCore(parseResult);
+
+ sb.AppendLine();
+
+ // legacy
+ EmitXmlCallingCore(parseResult);
sb.AppendLine("}");
@@ -40,7 +47,13 @@ internal void Emit()
}
}
- void EmitCore(ParseResult parseResult)
+ void EmitToolCallingCore(ParseResult parseResult)
+ {
+ EmitTools(parseResult);
+ EmitToolInvoke(parseResult);
+ }
+
+ void EmitXmlCallingCore(ParseResult parseResult)
{
var toolsAll = string.Join(Environment.NewLine, parseResult.Methods.Select(x => "{" + x.Name + "}"));
@@ -94,6 +107,210 @@ public static class PromptXml
EmitInvoke(parseResult);
}
+ void EmitToolInvoke(ParseResult parseResult)
+ {
+ var methodInvoke = BuildToolLocalMethodInvoke(parseResult.Methods);
+
+ var code = $$""""
+#pragma warning disable CS1998
+ public static async ValueTask InvokeToolAsync(MessageResponse message)
+ {
+ var result = new Contents();
+
+ foreach (var item in message.Content)
+ {
+ if (item.Type != ContentTypes.ToolUse) continue;
+
+ switch (item.ToolUseName)
+ {
+{{methodInvoke}}
+ default:
+ break;
+ }
+ }
+
+ return result;
+
+ static T GetValueOrDefault(Content content, string name, T defaultValue)
+ {
+ if (content.ToolUseInput!.TryGetValue(name, out var stringValue))
+ {
+ if (typeof(T) == typeof(Boolean))
+ {
+ var v = bool.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(SByte))
+ {
+ var v = SByte.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(Byte))
+ {
+ var v = Byte.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(Int16))
+ {
+ var v = Int16.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(UInt16))
+ {
+ var v = UInt16.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(Int32))
+ {
+ var v = Int32.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(UInt32))
+ {
+ var v = UInt32.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(Int64))
+ {
+ var v = Int64.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(UInt64))
+ {
+ var v = UInt64.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(Decimal))
+ {
+ var v = Decimal.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(Single))
+ {
+ var v = Single.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(Double))
+ {
+ var v = Double.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(String))
+ {
+ return (T)(object)stringValue;
+ }
+ else if (typeof(T) == typeof(DateTime))
+ {
+ var v = DateTime.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(DateTimeOffset))
+ {
+ var v = DateTimeOffset.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(Guid))
+ {
+ var v = Guid.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else if (typeof(T) == typeof(TimeSpan))
+ {
+ var v = TimeSpan.Parse(stringValue);
+ return Unsafe.As(ref v);
+ }
+ else
+ {
+ if (typeof(T).IsEnum)
+ {
+ return (T)Enum.Parse(typeof(T), stringValue);
+ }
+ throw new NotSupportedException();
+ }
+ }
+ else
+ {
+ return defaultValue;
+ }
+ }
+ }
+#pragma warning restore CS1998
+"""";
+
+ sb.AppendLine(code);
+ }
+
+ string BuildToolLocalMethodInvoke(Method[] methods)
+ {
+ var sb = new StringBuilder();
+
+ foreach (var method in methods)
+ {
+ var returnType = method.Symbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var isTask = (returnType.StartsWith("global::System.Threading.Tasks.Task") || returnType.StartsWith("global::System.Threading.Tasks.ValueTask"));
+
+ var i = 0;
+ var parameterParseString = new StringBuilder();
+ var parameterNames = new StringBuilder();
+ foreach (var p in method.Symbol.Parameters)
+ {
+ var defaultValue = "default!";
+ if (p.HasExplicitDefaultValue && p.ExplicitDefaultValue != null)
+ {
+ if (p.Type.TypeKind == TypeKind.Enum)
+ {
+ defaultValue = $"({p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}){p.ExplicitDefaultValue}";
+ }
+ else
+ {
+ defaultValue = p.ExplicitDefaultValue.ToString();
+ }
+ }
+ var parameterType = p.Type.ToDisplayString();
+ parameterNames.Append((i != 0) ? $", _{i}" : $"_{i}");
+ parameterParseString.AppendLine($" var _{i} = GetValueOrDefault<{parameterType}>(item, \"{p.Name}\", {defaultValue});");
+ i++;
+ }
+
+ var methodCall = $"{method.Name}({parameterNames})";
+ if (isTask)
+ {
+ methodCall = $"(await {methodCall})";
+ }
+
+ sb.AppendLine($$"""
+ case "{{method.Name}}":
+ {
+{{parameterParseString}}
+ string? _callResult;
+ bool? _isError = null;
+ try
+ {
+ _callResult = {{methodCall}}.ToString();
+ }
+ catch (Exception ex)
+ {
+ _callResult = ex.Message;
+ _isError = true;
+ }
+
+ result.Add(new Content
+ {
+ Type = ContentTypes.ToolResult,
+ ToolResultId = item.ToolUseId,
+ ToolResultContent = _callResult,
+ ToolResultIsError = _isError
+ });
+
+ break;
+ }
+""");
+ }
+
+
+ return sb.ToString();
+ }
+
void EmitToolDescription(Method method)
{
var docComment = method.Syntax.GetDocumentationCommentTriviaSyntax()!;
@@ -144,7 +361,14 @@ void EmitInvoke(ParseResult parseResult)
foreach (var p in method.Symbol.Parameters)
{
parameterNames.Append((i != 0) ? $", _{i}" : $"_{i}");
- parameterParseString.AppendLine($" var _{i++} = ({p.Type.ToDisplayString()})parameters.Element(\"{p.Name}\")!;");
+ if (p.Type.TypeKind == TypeKind.Enum)
+ {
+ parameterParseString.AppendLine($" var _{i++} = Enum.Parse<{p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>((string)parameters.Element(\"{p.Name}\")!);");
+ }
+ else
+ {
+ parameterParseString.AppendLine($" var _{i++} = ({p.Type.ToDisplayString()})parameters.Element(\"{p.Name}\")!;");
+ }
}
parameterParseString.AppendLine();
if (isTask)
@@ -206,6 +430,116 @@ static void BuildResult(StringBuilder sb, string toolName, T result)
sb.AppendLine(code);
}
+ void EmitTools(ParseResult parseResult)
+ {
+ var allTools = string.Join(", ", parseResult.Methods.Select(x => $"Tools.{x.Name}"));
+ sb.AppendLine($$"""
+ public static readonly Tool[] AllTools = new[] { {{allTools}} };
+
+ public static class Tools
+ {
+""");
+
+ // Emit Tool
+ foreach (var method in parseResult.Methods)
+ {
+ var docComment = method.Syntax.GetDocumentationCommentTriviaSyntax()!;
+ var description = RemoveStringNewLine(docComment.GetSummary().Replace("\"", "'"));
+
+ // property
+ var inputSchema = new StringBuilder();
+ if (method.Symbol.Parameters.Length != 0)
+ {
+ var propBuilder = new StringBuilder();
+ var paramRequired = new List();
+ foreach (var p in docComment.GetParams())
+ {
+ var paramDescription = RemoveStringNewLine(p.Description.Replace("\"", "'"));
+
+ // type retrieve from method symbol
+ var name = p.Name;
+ var pSymbol = method.Symbol.Parameters.First(x => x.Name == name);
+ var paramType = "string";
+
+ var enumMembers = "null";
+ if (pSymbol.Type.TypeKind == TypeKind.Enum)
+ {
+ enumMembers = string.Join(", ", pSymbol.Type.GetMembers().Where(x => x.Name != ".ctor").Select(x => "\"" + x.Name + "\""));
+ enumMembers = $"new [] {{ {enumMembers} }}";
+ }
+
+ // mapping jsonschema paramtype https://json-schema.org/understanding-json-schema/reference/type
+ switch (pSymbol.Type.SpecialType)
+ {
+ case SpecialType.System_Boolean:
+ paramType = "boolean";
+ break;
+ case SpecialType.System_SByte:
+ case SpecialType.System_Byte:
+ case SpecialType.System_Int16:
+ case SpecialType.System_UInt16:
+ case SpecialType.System_Int32:
+ case SpecialType.System_UInt32:
+ case SpecialType.System_Int64:
+ case SpecialType.System_UInt64:
+ case SpecialType.System_Decimal:
+ case SpecialType.System_Single:
+ case SpecialType.System_Double:
+ paramType = "number";
+ break;
+ default:
+ break;
+ }
+
+ propBuilder.AppendLine($$"""
+ {
+ "{{name}}", new ToolProperty()
+ {
+ Type = "{{paramType}}",
+ Description = "{{paramDescription}}",
+ Enum = {{enumMembers}}
+ }
+ },
+""");
+
+ if (!pSymbol.HasExplicitDefaultValue)
+ {
+ paramRequired.Add("\"" + name + "\"");
+ }
+ }
+ var required = string.Join(", ", paramRequired);
+ if (required.Length != 0)
+ {
+ required = "Required = new [] { " + required + " }";
+ }
+
+ inputSchema.AppendLine($$"""
+ InputSchema = new InputSchema
+ {
+ Type = "object",
+ Properties = new System.Collections.Generic.Dictionary
+ {
+{{propBuilder}}
+ },
+ {{required}}
+ }
+""");
+ }
+
+ sb.AppendLine($$"""
+ public static readonly Tool {{method.Name}} = new Tool
+ {
+ Name = "{{method.Name}}",
+ Description = "{{description}}",
+{{inputSchema}}
+ };
+
+""");
+ }
+
+ sb.AppendLine(" }"); // close Tools
+ }
+
static void AddSource(SourceProductionContext context, ISymbol targetSymbol, string code, string fileExtension = ".g.cs")
{
var fullType = targetSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
@@ -234,6 +568,7 @@ static void AddSource(SourceProductionContext context, ISymbol targetSymbol, str
#pragma warning disable CA1050
using Claudia;
+using System.Runtime.CompilerServices;
using System;
using System.Linq;
using System.Text;
@@ -258,4 +593,25 @@ static void AddSource(SourceProductionContext context, ISymbol targetSymbol, str
var sourceCode = sb.ToString();
context.AddSource($"{fullType}{fileExtension}", sourceCode);
}
+
+ static string RemoveStringNewLine(string str)
+ {
+ var sb = new StringBuilder();
+ var first = true;
+ using var sr = new StringReader(str);
+ string line = default!;
+ while ((line = sr.ReadLine()) != null)
+ {
+ if (first)
+ {
+ first = false;
+ }
+ else
+ {
+ sb.Append(" ");
+ }
+ sb.Append(line.Trim());
+ }
+ return sb.ToString();
+ }
}
\ No newline at end of file
diff --git a/src/Claudia.FunctionGenerator/Parser.cs b/src/Claudia.FunctionGenerator/Parser.cs
index 64959ce..9cb3503 100644
--- a/src/Claudia.FunctionGenerator/Parser.cs
+++ b/src/Claudia.FunctionGenerator/Parser.cs
@@ -130,6 +130,10 @@ internal ParseResult[] Parse()
{
break;
}
+ if (p.Type.TypeKind == TypeKind.Enum)
+ {
+ break;
+ }
hasError = true;
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ParameterTypeIsNotSupported, method.Locations[0], method.Name, p.Name, p.Type.Name));
diff --git a/src/Claudia/Anthropic.cs b/src/Claudia/Anthropic.cs
index 250647a..1907293 100644
--- a/src/Claudia/Anthropic.cs
+++ b/src/Claudia/Anthropic.cs
@@ -68,7 +68,6 @@ async Task IMessages.CreateAsync(MessageRequest request, Reques
{
request.Stream = null;
using var msg = await SendRequestAsync(request, overrideOptions, cancellationToken).ConfigureAwait(ConfigureAwait);
-
var result = await RequestWithAsync(msg, cancellationToken, overrideOptions, static (x, ct, _) => x.Content.ReadFromJsonAsync(AnthropicJsonSerialzierContext.Default.Options, ct), null).ConfigureAwait(ConfigureAwait);
return result!;
}
diff --git a/src/Claudia/AnthropicJsonSerialzierContext.cs b/src/Claudia/AnthropicJsonSerialzierContext.cs
index c408f00..119e1ee 100644
--- a/src/Claudia/AnthropicJsonSerialzierContext.cs
+++ b/src/Claudia/AnthropicJsonSerialzierContext.cs
@@ -27,6 +27,9 @@ namespace Claudia;
[JsonSerializable(typeof(ContentBlockStop))]
[JsonSerializable(typeof(MessageStartBody))]
[JsonSerializable(typeof(MessageDeltaBody))]
+[JsonSerializable(typeof(Tool))]
+[JsonSerializable(typeof(InputSchema))]
+[JsonSerializable(typeof(ToolProperty))]
public partial class AnthropicJsonSerialzierContext : JsonSerializerContext
{
}
\ No newline at end of file
diff --git a/src/Claudia/Constants.cs b/src/Claudia/Constants.cs
index 78f75ba..b39d32b 100644
--- a/src/Claudia/Constants.cs
+++ b/src/Claudia/Constants.cs
@@ -46,6 +46,8 @@ public static class ContentTypes
{
public const string Text = "text";
public const string Image = "image";
+ public const string ToolUse = "tool_use";
+ public const string ToolResult = "tool_result";
}
public static class StopSequnces
@@ -62,6 +64,8 @@ public static class StopReasons
public const string MaxTokens = "max_tokens";
/// one of your provided custom stop_sequences was generated
public const string StopSequence = "stop_sequence";
+
+ public const string ToolUse = "tool_use";
}
public static class SystemPrompts
diff --git a/src/Claudia/MessageRequest.cs b/src/Claudia/MessageRequest.cs
index ffc2b3e..3d20f02 100644
--- a/src/Claudia/MessageRequest.cs
+++ b/src/Claudia/MessageRequest.cs
@@ -2,6 +2,7 @@
using System.Text.Json;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
+using System.Text;
namespace Claudia;
@@ -90,6 +91,11 @@ public override string ToString()
{
return JsonSerializer.Serialize(this, AnthropicJsonSerialzierContext.Default.Options);
}
+
+ // 2024-04-04 beta: https://docs.anthropic.com/claude/docs/tool-use
+ [JsonPropertyName("tools")]
+ public Tool[]? Tools { get; set; }
+
}
public record class Message
{
@@ -127,7 +133,7 @@ public override string ToString()
}
else
{
- return base.ToString() ?? "";
+ return "[" + string.Join(", ", this.Select(x => x.ToString())) + "]";
}
}
}
@@ -145,6 +151,35 @@ public record class Content
[JsonPropertyName("source")]
public Source? Source { get; set; }
+ #region tool_use response
+
+ /// A unique identifier for this particular tool use block. This will be used to match up the tool results later.
+ [JsonPropertyName("id")]
+ public string? ToolUseId { get; set; }
+
+ /// The name of the tool being used.
+ [JsonPropertyName("name")]
+ public string? ToolUseName { get; set; }
+
+ /// An object containing the input being passed to the tool, conforming to the tool's input_schema.
+ [JsonPropertyName("input")]
+ public Dictionary? ToolUseInput { get; set; }
+
+ /// The result of the tool.
+ [JsonPropertyName("content")]
+ public Contents? ToolResultContent { get; set; }
+
+ /// The id of the tool use request this is a result for.
+ [JsonPropertyName("tool_use_id")]
+ public string? ToolResultId { get; set; }
+
+ /// Set to true if the tool execution resulted in an error.
+ [JsonPropertyName("is_error")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public bool? ToolResultIsError { get; set; }
+
+ #endregion
+
public static implicit operator Content(string text) => new Content(text);
public Content()
@@ -180,6 +215,30 @@ public override string ToString()
{
return $"{Source.Type}(Source.Data.Length)";
}
+ else if (ToolUseId != null)
+ {
+ var sb = new StringBuilder();
+ sb.Append(ToolUseName);
+ sb.Append("(");
+ if (ToolUseInput != null)
+ {
+ var first = true;
+ foreach (var item in ToolUseInput)
+ {
+ if (first)
+ {
+ first = true;
+ }
+ else
+ {
+ sb.Append(", ");
+ }
+ sb.Append(item.Key + ": " + item.Value);
+ }
+ }
+ sb.Append(")");
+ return sb.ToString();
+ }
else
{
return base.ToString() ?? "";
@@ -214,3 +273,40 @@ public record class Source
[JsonPropertyName("data")]
public required ReadOnlyMemory Data { get; set; } // Base64
}
+
+// https://docs.anthropic.com/claude/docs/tool-use
+public record class Tool
+{
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ [JsonPropertyName("description")]
+ public required string Description { get; set; }
+
+ [JsonPropertyName("input_schema")]
+ public InputSchema? InputSchema { get; set; }
+}
+
+public record class InputSchema
+{
+ [JsonPropertyName("type")]
+ public required string Type { get; set; }
+
+ [JsonPropertyName("properties")]
+ public Dictionary? Properties { get; set; }
+
+ [JsonPropertyName("required")]
+ public string[]? Required { get; set; }
+}
+
+public record class ToolProperty
+{
+ [JsonPropertyName("type")]
+ public required string Type { get; set; }
+
+ [JsonPropertyName("enum")]
+ public string[]? Enum { get; set; }
+
+ [JsonPropertyName("description")]
+ public required string Description { get; set; }
+}
\ No newline at end of file
diff --git a/src/Claudia/MessageResponse.cs b/src/Claudia/MessageResponse.cs
index 393c55e..5bfa186 100644
--- a/src/Claudia/MessageResponse.cs
+++ b/src/Claudia/MessageResponse.cs
@@ -24,10 +24,10 @@ public class MessageResponse
///
/// Content generated by the model.
- /// This is an array of content blocks, each of which has a type that determines its shape. Currently, the only type in responses is "text".
+ /// This is an array of content blocks, each of which has a type that determines its shape.
///
[JsonPropertyName("content")]
- public required Content[] Content { get; set; }
+ public required Contents Content { get; set; }
///
/// The model that handled the request.
@@ -55,7 +55,7 @@ public class MessageResponse
public override string ToString()
{
- if (Content.Length == 1 && Content[0].Text != null)
+ if (Content.Count == 1 && Content[0].Text != null)
{
return Content[0].Text!;
}