diff --git a/Anthropic.SDK.Tests/SkillsTests.cs b/Anthropic.SDK.Tests/SkillsTests.cs new file mode 100644 index 0000000..153c891 --- /dev/null +++ b/Anthropic.SDK.Tests/SkillsTests.cs @@ -0,0 +1,297 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Anthropic.SDK.Common; +using Anthropic.SDK.Constants; +using Anthropic.SDK.Messaging; + +namespace Anthropic.SDK.Tests +{ + [TestClass] + public class SkillsTests + { + [TestMethod] + public void TestContainerSerialization() + { + // Test that Container serializes correctly + var container = new Container + { + Skills = new List + { + new Skill + { + Type = "anthropic", + SkillId = "pptx", + Version = "latest" + } + } + }; + + var parameters = new MessageParameters + { + Model = AnthropicModels.Claude4Sonnet, + MaxTokens = 4096, + Messages = new List + { + new Message(RoleType.User, "Create a presentation about renewable energy") + }, + Container = container, + Tools = new List + { + new Function("code_execution", "code_execution_20250825", new Dictionary + { + { "name", "code_execution" } + }) + } + }; + + var json = JsonSerializer.Serialize(parameters, new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }); + + // Verify container is in the JSON + Assert.IsTrue(json.Contains("\"container\"")); + Assert.IsTrue(json.Contains("\"skills\"")); + Assert.IsTrue(json.Contains("\"pptx\"")); + Assert.IsTrue(json.Contains("\"anthropic\"")); + } + + [TestMethod] + public void TestContainerWithId() + { + // Test that Container with ID serializes correctly for container reuse + var container = new Container + { + Id = "container_abc123", + Skills = new List + { + new Skill + { + Type = "anthropic", + SkillId = "xlsx", + Version = "latest" + } + } + }; + + var parameters = new MessageParameters + { + Model = AnthropicModels.Claude4Sonnet, + MaxTokens = 4096, + Messages = new List + { + new Message(RoleType.User, "Continue with the spreadsheet") + }, + Container = container + }; + + var json = JsonSerializer.Serialize(parameters, new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }); + + // Verify container ID is in the JSON + Assert.IsTrue(json.Contains("\"id\"")); + Assert.IsTrue(json.Contains("\"container_abc123\"")); + } + + [TestMethod] + public void TestMultipleSkills() + { + // Test multiple skills (up to 8 allowed) + var container = new Container + { + Skills = new List + { + new Skill { Type = "anthropic", SkillId = "pptx", Version = "latest" }, + new Skill { Type = "anthropic", SkillId = "xlsx", Version = "latest" }, + new Skill { Type = "anthropic", SkillId = "docx", Version = "latest" }, + new Skill { Type = "anthropic", SkillId = "pdf", Version = "latest" } + } + }; + + var parameters = new MessageParameters + { + Model = AnthropicModels.Claude4Sonnet, + MaxTokens = 4096, + Messages = new List + { + new Message(RoleType.User, "Test multiple skills") + }, + Container = container + }; + + var json = JsonSerializer.Serialize(parameters, new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }); + + // Verify all skills are present + Assert.IsTrue(json.Contains("\"pptx\"")); + Assert.IsTrue(json.Contains("\"xlsx\"")); + Assert.IsTrue(json.Contains("\"docx\"")); + Assert.IsTrue(json.Contains("\"pdf\"")); + } + + [TestMethod] + public void TestCustomSkill() + { + // Test custom skill type + var container = new Container + { + Skills = new List + { + new Skill + { + Type = "custom", + SkillId = "my-custom-skill-id-123", + Version = "1.0.0" + } + } + }; + + var parameters = new MessageParameters + { + Model = AnthropicModels.Claude4Sonnet, + MaxTokens = 4096, + Messages = new List + { + new Message(RoleType.User, "Use my custom skill") + }, + Container = container + }; + + var json = JsonSerializer.Serialize(parameters, new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }); + + // Verify custom skill is present + Assert.IsTrue(json.Contains("\"custom\"")); + Assert.IsTrue(json.Contains("\"my-custom-skill-id-123\"")); + } + + [TestMethod] + public void TestBashCodeExecutionContentTypes() + { + // Test that bash code execution content types can be created + var bashOutput = new BashCodeExecutionOutputContent + { + FileId = "file_123abc" + }; + + Assert.AreEqual(ContentType.bash_code_execution_output, bashOutput.Type); + Assert.AreEqual("file_123abc", bashOutput.FileId); + + var bashResult = new BashCodeExecutionResultContent + { + Stdout = "Success output", + Stderr = "", + ReturnCode = 0, + Content = new List { bashOutput } + }; + + Assert.AreEqual(ContentType.bash_code_execution_result, bashResult.Type); + Assert.AreEqual("Success output", bashResult.Stdout); + Assert.AreEqual(0, bashResult.ReturnCode); + Assert.AreEqual(1, bashResult.Content.Count); + + var bashToolResult = new BashCodeExecutionToolResultContent + { + ToolUseId = "tool_use_123", + Content = bashResult + }; + + Assert.AreEqual(ContentType.bash_code_execution_tool_result, bashToolResult.Type); + Assert.AreEqual("tool_use_123", bashToolResult.ToolUseId); + + var bashError = new BashCodeExecutionToolResultErrorContent + { + ErrorCode = "execution_time_exceeded" + }; + + Assert.AreEqual(ContentType.bash_code_execution_tool_result_error, bashError.Type); + Assert.AreEqual("execution_time_exceeded", bashError.ErrorCode); + } + + [TestMethod] + public void TestContainerResponseDeserialization() + { + // Test that ContainerResponse can be deserialized from JSON + var json = @"{ + ""id"": ""msg_123"", + ""type"": ""message"", + ""role"": ""assistant"", + ""content"": [{""type"": ""text"", ""text"": ""Hello""}], + ""model"": ""claude-sonnet-4-5-20250929"", + ""stop_reason"": ""end_turn"", + ""usage"": { + ""input_tokens"": 10, + ""output_tokens"": 5 + }, + ""container"": { + ""id"": ""container_abc123"" + } + }"; + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { Extensions.ContentConverter.Instance } + }; + + var response = JsonSerializer.Deserialize(json, options); + + Assert.IsNotNull(response); + Assert.IsNotNull(response.Container); + Assert.AreEqual("container_abc123", response.Container.Id); + } + + [TestMethod] + public void TestBashCodeExecutionSerialization() + { + // Test that bash code execution content serializes correctly + var bashOutput = new BashCodeExecutionOutputContent + { + FileId = "file_xyz789" + }; + + var json = JsonSerializer.Serialize(bashOutput, new JsonSerializerOptions + { + WriteIndented = false, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }); + + Assert.IsTrue(json.Contains("\"type\":\"bash_code_execution_output\"")); + Assert.IsTrue(json.Contains("\"file_id\":\"file_xyz789\"")); + } + + [TestMethod] + public void TestBashCodeExecutionDeserialization() + { + // Test that bash code execution content deserializes correctly + var json = @"{ + ""type"": ""bash_code_execution_output"", + ""file_id"": ""file_test123"" + }"; + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Converters = { Extensions.ContentConverter.Instance } + }; + + var content = JsonSerializer.Deserialize(json, options); + + Assert.IsNotNull(content); + Assert.IsInstanceOfType(content, typeof(BashCodeExecutionOutputContent)); + var bashOutput = content as BashCodeExecutionOutputContent; + Assert.AreEqual("file_test123", bashOutput.FileId); + } + } +} diff --git a/Anthropic.SDK/Extensions/ContentConverter.cs b/Anthropic.SDK/Extensions/ContentConverter.cs index 03ce4e8..2814c13 100644 --- a/Anthropic.SDK/Extensions/ContentConverter.cs +++ b/Anthropic.SDK/Extensions/ContentConverter.cs @@ -48,6 +48,14 @@ public override ContentBase Read(ref Utf8JsonReader reader, Type typeToConvert, return JsonSerializer.Deserialize(root.GetRawText(), options); case "mcp_tool_result": return JsonSerializer.Deserialize(root.GetRawText(), options); + case "bash_code_execution_tool_result": + return JsonSerializer.Deserialize(root.GetRawText(), options); + case "bash_code_execution_result": + return JsonSerializer.Deserialize(root.GetRawText(), options); + case "bash_code_execution_output": + return JsonSerializer.Deserialize(root.GetRawText(), options); + case "bash_code_execution_tool_result_error": + return JsonSerializer.Deserialize(root.GetRawText(), options); // Add cases for other types as necessary default: throw new JsonException($"Unknown type {type}"); diff --git a/Anthropic.SDK/Messaging/Container.cs b/Anthropic.SDK/Messaging/Container.cs new file mode 100644 index 0000000..51714df --- /dev/null +++ b/Anthropic.SDK/Messaging/Container.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Anthropic.SDK.Messaging +{ + /// + /// Container configuration for Skills API + /// + public class Container + { + /// + /// Optional container ID to reuse an existing container from a previous request + /// + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Id { get; set; } + + /// + /// List of skills to enable in the container (max 8 skills) + /// + [JsonPropertyName("skills")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List Skills { get; set; } + } + + /// + /// Skill definition for use in containers + /// + public class Skill + { + /// + /// Type of skill - either "anthropic" for built-in skills or "custom" for custom skills + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// Unique identifier for the skill + /// For anthropic type: "pptx", "xlsx", "docx", "pdf" + /// For custom type: a custom string identifier + /// + [JsonPropertyName("skill_id")] + public string SkillId { get; set; } + + /// + /// Optional version to pin to a specific version (e.g., "latest") + /// + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Version { get; set; } + } + + /// + /// Container information returned in the response + /// + public class ContainerResponse + { + /// + /// The container ID that can be reused in subsequent requests + /// + [JsonPropertyName("id")] + public string Id { get; set; } + } +} diff --git a/Anthropic.SDK/Messaging/Content.cs b/Anthropic.SDK/Messaging/Content.cs index b90021e..798bcd1 100644 --- a/Anthropic.SDK/Messaging/Content.cs +++ b/Anthropic.SDK/Messaging/Content.cs @@ -493,4 +493,100 @@ public class WebSearchResultContent : ContentBase [JsonPropertyName("page_age")] public string PageAge { get; set; } } + + /// + /// Bash Code Execution Tool Result Content + /// + public class BashCodeExecutionToolResultContent : ContentBase + { + /// + /// Type of Content (bash_code_execution_tool_result, pre-set) + /// + [JsonPropertyName("type")] + public override ContentType Type => ContentType.bash_code_execution_tool_result; + + /// + /// Tool Use Id + /// + [JsonPropertyName("tool_use_id")] + public string ToolUseId { get; set; } + + /// + /// Content - can be either BashCodeExecutionResultContent or BashCodeExecutionToolResultErrorContent + /// + [JsonPropertyName("content")] + public ContentBase Content { get; set; } + } + + /// + /// Bash Code Execution Result Content + /// + public class BashCodeExecutionResultContent : ContentBase + { + /// + /// Type of Content (bash_code_execution_result, pre-set) + /// + [JsonPropertyName("type")] + public override ContentType Type => ContentType.bash_code_execution_result; + + /// + /// Standard output from the bash execution + /// + [JsonPropertyName("stdout")] + public string Stdout { get; set; } + + /// + /// Standard error from the bash execution + /// + [JsonPropertyName("stderr")] + public string Stderr { get; set; } + + /// + /// Return code from the bash execution + /// + [JsonPropertyName("return_code")] + public int ReturnCode { get; set; } + + /// + /// Array of output content blocks (files) + /// + [JsonPropertyName("content")] + public List Content { get; set; } + } + + /// + /// Bash Code Execution Output Content (represents a file) + /// + public class BashCodeExecutionOutputContent : ContentBase + { + /// + /// Type of Content (bash_code_execution_output, pre-set) + /// + [JsonPropertyName("type")] + public override ContentType Type => ContentType.bash_code_execution_output; + + /// + /// File ID that can be used to download the file + /// + [JsonPropertyName("file_id")] + public string FileId { get; set; } + } + + /// + /// Bash Code Execution Tool Result Error Content + /// + public class BashCodeExecutionToolResultErrorContent : ContentBase + { + /// + /// Type of Content (bash_code_execution_tool_result_error, pre-set) + /// + [JsonPropertyName("type")] + public override ContentType Type => ContentType.bash_code_execution_tool_result_error; + + /// + /// Error code describing the failure + /// + [JsonPropertyName("error_code")] + public string ErrorCode { get; set; } + } } diff --git a/Anthropic.SDK/Messaging/ContentType.cs b/Anthropic.SDK/Messaging/ContentType.cs index 44cf3fe..bec9981 100644 --- a/Anthropic.SDK/Messaging/ContentType.cs +++ b/Anthropic.SDK/Messaging/ContentType.cs @@ -37,6 +37,14 @@ public enum ContentType mcp_tool_use, - mcp_tool_result + mcp_tool_result, + + bash_code_execution_tool_result, + + bash_code_execution_result, + + bash_code_execution_output, + + bash_code_execution_tool_result_error } } diff --git a/Anthropic.SDK/Messaging/MessageParameters.cs b/Anthropic.SDK/Messaging/MessageParameters.cs index 2acc14b..e0a9dd3 100644 --- a/Anthropic.SDK/Messaging/MessageParameters.cs +++ b/Anthropic.SDK/Messaging/MessageParameters.cs @@ -46,6 +46,9 @@ public class MessageParameters : MessageCountTokenParameters [JsonPropertyName("mcp_servers")] public List MCPServers { get; set; } + [JsonPropertyName("container")] + public Container Container { get; set; } + /// /// Prompt Cache Type Definitions. Designed to be used as a bitwise assignment if you want to cache multiple types and are caching enough context. /// diff --git a/Anthropic.SDK/Messaging/MessageResponse.cs b/Anthropic.SDK/Messaging/MessageResponse.cs index d616b4c..bf01d9b 100644 --- a/Anthropic.SDK/Messaging/MessageResponse.cs +++ b/Anthropic.SDK/Messaging/MessageResponse.cs @@ -53,6 +53,9 @@ public class MessageResponse [JsonIgnore] public RateLimits RateLimits { get; set; } + + [JsonPropertyName("container")] + public ContainerResponse Container { get; set; } } public class StreamMessage diff --git a/README.md b/README.md index 06d4806..b466562 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Anthropic.SDK is an unofficial C# client designed for interacting with the Claud - [List Models](#list-models) - [Batching](#batching) - [Tools](#tools) + - [Skills](#skills) - [Computer Use](#computer-use) - [Vertex AI Support](#vertex-ai-support) - [Authentication](#authentication) @@ -1168,6 +1169,160 @@ Output From Json Mode } ``` +### Skills + +The `AnthropicClient` supports the Skills API, which allows Claude to use tools like PowerPoint, Excel, Word, and PDF generation through code execution in a containerized environment. Skills can be used with the `container` parameter in message requests. + +#### Basic Skills Usage + +```csharp +var client = new AnthropicClient(); + +// Create a container with skills +var container = new Container +{ + Skills = new List + { + new Skill + { + Type = "anthropic", + SkillId = "pptx", // Built-in PowerPoint skill + Version = "latest" + } + } +}; + +var parameters = new MessageParameters +{ + Model = AnthropicModels.Claude4Sonnet, + MaxTokens = 4096, + Messages = new List + { + new Message(RoleType.User, "Create a presentation about renewable energy") + }, + Container = container, + Tools = new List + { + new Function("code_execution", "code_execution_20250825", + new Dictionary { { "name", "code_execution" } }) + } +}; + +var response = await client.Messages.GetClaudeMessageAsync(parameters); + +// The response will include the container ID for reuse +var containerId = response.Container?.Id; +``` + +#### Reusing Containers + +Containers can be reused across multiple requests to maintain state: + +```csharp +// First request creates the container +var response1 = await client.Messages.GetClaudeMessageAsync(parameters); + +// Subsequent requests can reuse the same container +var containerWithId = new Container +{ + Id = response1.Container.Id, // Reuse the container + Skills = new List + { + new Skill { Type = "anthropic", SkillId = "xlsx", Version = "latest" } + } +}; + +var messages = new List +{ + new Message(RoleType.User, "Analyze this sales data"), + response1.Message, + new Message(RoleType.User, "What was the total revenue?") +}; + +var response2 = await client.Messages.GetClaudeMessageAsync(new MessageParameters +{ + Model = AnthropicModels.Claude4Sonnet, + MaxTokens = 4096, + Messages = messages, + Container = containerWithId +}); +``` + +#### Built-in Skills + +The following built-in Anthropic skills are available: +- **pptx**: Create and edit PowerPoint presentations +- **xlsx**: Create and analyze Excel spreadsheets +- **docx**: Create and edit Word documents +- **pdf**: Generate PDF documents + +#### Multiple Skills + +You can include up to 8 skills per request: + +```csharp +var container = new Container +{ + Skills = new List + { + new Skill { Type = "anthropic", SkillId = "pptx", Version = "latest" }, + new Skill { Type = "anthropic", SkillId = "xlsx", Version = "latest" }, + new Skill { Type = "anthropic", SkillId = "docx", Version = "latest" }, + new Skill { Type = "anthropic", SkillId = "pdf", Version = "latest" } + } +}; +``` + +#### Custom Skills + +Custom skills can also be used by specifying `Type = "custom"`: + +```csharp +var container = new Container +{ + Skills = new List + { + new Skill + { + Type = "custom", + SkillId = "my-custom-skill-id", + Version = "1.0.0" + } + } +}; +``` + +#### Working with Skill Results + +Skills execution results are returned as bash code execution content blocks: + +```csharp +foreach (var content in response.Content) +{ + if (content is BashCodeExecutionToolResultContent bashResult) + { + if (bashResult.Content is BashCodeExecutionResultContent result) + { + Console.WriteLine($"Return Code: {result.ReturnCode}"); + Console.WriteLine($"Stdout: {result.Stdout}"); + Console.WriteLine($"Stderr: {result.Stderr}"); + + // Access any file outputs + foreach (var output in result.Content) + { + Console.WriteLine($"File ID: {output.FileId}"); + // Use Files API to download the file + } + } + } +} +``` + +**Note:** Skills require the following beta headers: +- `code-execution-2025-08-25` - Enables code execution (required for Skills) +- `skills-2025-10-02` - Enables Skills API +- `files-api-2025-04-14` - For uploading/downloading files (when working with file outputs) + ### Computer Use The `AnthropicClient` supports calling computer use functionality, and in this repository is a demonstration application that should work reasonably well on Windows and mirrors in many ways the example application provided by Anthropic.