diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..723ed2d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,90 @@ +# Contributing to Anthropic.Net + +Thank you for your interest in contributing to Anthropic.Net! We welcome contributions from the community to help improve this SDK. + +## Development Environment + +To get started, ensure you have the following installed: + +- **.NET SDK**: Version 8.0 or later (see `global.json`). +- **IDE**: Visual Studio 2022, VS Code, or JetBrains Rider. +- **PowerShell**: Required for running build scripts. + +## Getting Started + +1. **Fork** the repository on GitHub. +2. **Clone** your fork locally: + + ```bash + git clone https://github.com/YOUR-USERNAME/anthropic.net.git + cd anthropic.net + ``` + +3. **Restore** dependencies and tools: + + ```bash + dotnet tool restore + dotnet restore + ``` + +## Building and Testing + +The project uses [Cake](https://cakebuild.net/) for build automation. You can run the build script using the dotnet tool: + +```bash +dotnet cake --target=Build +``` + +To run tests: + +```bash +dotnet cake --target=Test +``` + +To run the full CI pipeline (Build, Test, Pack): + +```bash +dotnet cake --target=Default +``` + +## Code Style + +We enforce code style using `.editorconfig` and StyleCop analyzers. + +- **Naming**: PascalCase for public members, camelCase for private fields/parameters. +- **Formatting**: Standard C# conventions (K&R braces, 4-space indentation). +- **Headers**: File headers are not required. +- **Warnings**: Treat warnings as errors is enabled in CI. Ensure your code builds without warnings. + +Run `dotnet format` to automatically fix style issues. + +## Pull Request Process + +1. Create a new branch for your feature or fix: + + ```bash + git checkout -b feature/my-awesome-feature + ``` + +2. Commit your changes with clear, descriptive messages. +3. Push your branch to your fork. +4. Open a Pull Request (PR) against the `main` branch of the `tinonetic/anthropic.net` repository. +5. Ensure all CI checks pass. + +## Release Process (Maintainers Only) + +Releases are automated via GitHub Actions and MinVer. + +1. **Versioning**: Determine the next version number (SemVer). +2. **Tagging**: Create and push a git tag for the new version (e.g., `v2.0.0`). + + ```bash + git tag v2.0.0 + git push origin v2.0.0 + ``` + +3. **Publishing**: Go to GitHub Releases, draft a new release for the tag, and click "Publish". This triggers the NuGet publication workflow. + +## License + +By contributing, you agree that your contributions will be licensed under the project's [MIT License](LICENSE.md). diff --git a/Source/Anthropic.Net/IAnthropicApiClient.cs b/Source/Anthropic.Net/IAnthropicApiClient.cs index d1b74bf..faa3276 100644 --- a/Source/Anthropic.Net/IAnthropicApiClient.cs +++ b/Source/Anthropic.Net/IAnthropicApiClient.cs @@ -7,7 +7,7 @@ namespace Anthropic.Net; /// /// The Anthropic API client interface. /// -internal interface IAnthropicApiClient +public interface IAnthropicApiClient { /// /// Sends a prompt to the Anthropic API for completion. diff --git a/Tests/Anthropic.Net.Tests/ApiClientTests.cs b/Tests/Anthropic.Net.Tests/ApiClientTests.cs index 14db254..7fccf49 100644 --- a/Tests/Anthropic.Net.Tests/ApiClientTests.cs +++ b/Tests/Anthropic.Net.Tests/ApiClientTests.cs @@ -3,6 +3,8 @@ namespace Anthropic.Net.Test; using System.Net; using Anthropic.Net.Constants; using Anthropic.Net.Models.Messages; +using Anthropic.Net.Models.Messages.Streaming; +using Anthropic.Net.Models.Messages.Streaming.StreamingEvents; using NSubstitute; using Shouldly; using Xunit; @@ -82,4 +84,53 @@ public async Task TestMessageAsync_ValidRequestAsync() response.Content[0].Type.ShouldBe("text"); ((TextContentBlock)response.Content[0]).Text.ShouldBe("Hello, world!"); } + [Fact] + public async Task TestStreamMessageAsync_ValidRequestAsync() + { + // Arrange + var mockSseResponse = """ + event: message_start + data: {"type": "message_start", "message": {"id": "msg_1", "type": "message", "role": "assistant", "content": [], "model": "claude-3-sonnet-20240229", "stop_reason": null, "stop_sequence": null, "usage": {"input_tokens": 25, "output_tokens": 1}}} + + event: content_block_start + data: {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}} + + event: content_block_delta + data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hello"}} + + event: content_block_stop + data: {"type": "content_block_stop", "index": 0} + + event: message_delta + data: {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": null}, "usage": {"output_tokens": 15}} + + event: message_stop + data: {"type": "message_stop"} + """; + + var messageHandler = new MockHttpMessageHandler(mockSseResponse, HttpStatusCode.OK); + var httpClientFactory = Substitute.For(); + _ = httpClientFactory.CreateClient().Returns( + new HttpClient(messageHandler) + { + BaseAddress = _baseAddress, + }); + + var sut = new AnthropicApiClient("test-api-key", httpClientFactory); + + // Act + var messages = new List { Message.FromUser("Hello") }; + var events = new List(); + await foreach (var ev in sut.StreamMessageAsync(new MessageRequest(AnthropicModels.Claude3Sonnet, messages))) + { + events.Add(ev); + } + + // Assert + events.ShouldNotBeEmpty(); + events.Count.ShouldBe(6); + events[0].ShouldBeOfType(); + events[2].ShouldBeOfType(); + ((ContentBlockDeltaEvent)events[2]).Delta.Text.ShouldBe("Hello"); + } }