2024-06-28 16:30:09 -05:00
using System.Net.Http.Headers ;
using System.Text ;
using System.Text.Json ;
using AnthropicClient.Json ;
using AnthropicClient.Models ;
2024-07-01 22:46:02 -05:00
using AnthropicClient.Utils ;
2024-06-28 16:30:09 -05:00
namespace AnthropicClient ;
/// <summary>
/// Represents a client for interacting with the Anthropic API.
/// </summary>
public interface IAnthropicApiClient
{
/// <summary>
2024-07-03 16:57:27 -05:00
/// Creates a message asynchronously.
2024-06-28 16:30:09 -05:00
/// </summary>
2024-07-03 16:57:27 -05:00
/// <param name="request">The message request to create.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/>.</returns>
Task < AnthropicResult < MessageResponse >> CreateMessageAsync ( MessageRequest request );
2024-06-28 16:30:09 -05:00
/// <summary>
2024-07-03 16:57:27 -05:00
/// Creates a message asynchronously and streams the response.
2024-06-28 16:30:09 -05:00
/// </summary>
2024-07-03 16:57:27 -05:00
/// <param name="request">The message request to create.</param>
/// <returns>An asynchronous enumerable that yields the response event by event.</returns>
IAsyncEnumerable < AnthropicEvent > CreateMessageAsync ( StreamMessageRequest request );
2024-06-28 16:30:09 -05:00
}
/// <inheritdoc cref="IAnthropicApiClient"/>
public class AnthropicApiClient : IAnthropicApiClient
{
private const string BaseUrl = "https://api.anthropic.com/v1/" ;
private const string ApiKeyHeader = "x-api-key" ;
private const string MessagesEndpoint = "messages" ;
private const string JsonContentType = "application/json" ;
private const string EventPrefix = "event:" ;
private const string DataPrefix = "data:" ;
private readonly Dictionary < string , string > _defaultHeaders = new ()
{
{ "anthropic-version" , "2023-06-01" },
};
private readonly HttpClient _httpClient ;
/// <summary>
/// Initializes a new instance of the <see cref="AnthropicApiClient"/> class.
/// </summary>
/// <param name="apiKey">The API key to use for the client.</param>
/// <param name="httpClient">The HTTP client to use for the client.</param>
/// <exception cref="ArgumentNullException">Thrown when the API key or HTTP client is null.</exception>
/// <returns>A new instance of the <see cref="AnthropicApiClient"/> class.</returns>
public AnthropicApiClient ( string apiKey , HttpClient httpClient )
{
ArgumentValidator . ThrowIfNull ( apiKey , nameof ( apiKey ));
ArgumentValidator . ThrowIfNull ( httpClient , nameof ( httpClient ));
_httpClient = httpClient ;
_httpClient . BaseAddress = new Uri ( BaseUrl );
_httpClient . DefaultRequestHeaders . Add ( ApiKeyHeader , apiKey );
_httpClient . DefaultRequestHeaders
. Accept
. Add ( new MediaTypeWithQualityHeaderValue ( JsonContentType ));
foreach ( var pair in _defaultHeaders )
{
_httpClient . DefaultRequestHeaders . Add ( pair . Key , pair . Value );
}
}
/// <inheritdoc />
2024-07-03 16:57:27 -05:00
public async Task < AnthropicResult < MessageResponse >> CreateMessageAsync ( MessageRequest request )
2024-06-28 16:30:09 -05:00
{
var response = await SendRequestAsync ( request );
var anthropicHeaders = new AnthropicHeaders ( response . Headers );
var responseContent = await response . Content . ReadAsStringAsync ();
if ( response . IsSuccessStatusCode is false )
{
var error = Deserialize < AnthropicError >( responseContent ) ?? new AnthropicError ();
2024-07-03 16:57:27 -05:00
return AnthropicResult < MessageResponse >. Failure ( error , anthropicHeaders );
2024-06-28 16:30:09 -05:00
}
2024-07-03 16:57:27 -05:00
var msgResponse = Deserialize < MessageResponse >( responseContent ) ?? new MessageResponse ();
2024-07-01 22:46:02 -05:00
2024-07-01 21:00:27 -05:00
if ( request . Tools is not null && request . Tools . Count > 0 )
{
2024-07-03 16:57:27 -05:00
msgResponse . ToolCall = GetToolCall ( msgResponse , request . Tools );
2024-07-01 21:00:27 -05:00
}
2024-07-01 22:46:02 -05:00
2024-07-03 16:57:27 -05:00
return AnthropicResult < MessageResponse >. Success ( msgResponse , anthropicHeaders );
2024-06-28 16:30:09 -05:00
}
/// <inheritdoc />
2024-07-03 16:57:27 -05:00
public async IAsyncEnumerable < AnthropicEvent > CreateMessageAsync ( StreamMessageRequest request )
2024-06-28 16:30:09 -05:00
{
var response = await SendRequestAsync ( request );
2024-07-19 21:15:59 -05:00
if ( response . IsSuccessStatusCode is false )
{
var error = Deserialize < AnthropicError >( await response . Content . ReadAsStringAsync ()) ?? new AnthropicError ();
yield return new AnthropicEvent ( EventType . Error , new ErrorEventData ( error . Error ));
yield break ;
}
2024-06-28 16:30:09 -05:00
var anthropicHeaders = new AnthropicHeaders ( response . Headers );
using var responseContent = await response . Content . ReadAsStreamAsync ();
using var streamReader = new StreamReader ( responseContent );
2024-07-03 16:57:27 -05:00
MessageResponse ? msgResponse = null ;
2024-06-28 16:30:09 -05:00
Content ? content = null ;
var toolInputJsonStringBuilder = new StringBuilder ();
var currentEvent = new AnthropicEvent ();
do
{
var line = await streamReader . ReadLineAsync ();
2024-07-01 22:46:02 -05:00
2024-06-28 16:30:09 -05:00
// I know...this is not pretty, but here is why...
// as events are being yielded I want to also
2024-07-03 16:57:27 -05:00
// build up the complete response
2024-06-28 16:30:09 -05:00
// so I can yield it as a special event to make tool
// calling easier to handle
2024-07-01 22:46:02 -05:00
2024-07-03 16:57:27 -05:00
// initialize response on message start
2024-06-28 16:30:09 -05:00
if ( currentEvent . Type is EventType . MessageStart && currentEvent . Data is MessageStartEventData msgStartData )
{
2024-07-03 16:57:27 -05:00
msgResponse = msgStartData . Message ;
2024-06-28 16:30:09 -05:00
}
// initialize content block on content block start
if ( currentEvent . Type is EventType . ContentBlockStart && currentEvent . Data is ContentStartEventData contentStartData )
{
content = contentStartData . ContentBlock ;
}
// update content block with deltas based on
// current content type and delta type
if ( currentEvent . Type is EventType . ContentBlockDelta && currentEvent . Data is ContentDeltaEventData contentDeltaData )
{
if ( content is TextContent textContent && contentDeltaData . Delta is TextDelta textDelta )
{
var newText = textContent . Text + textDelta . Text ;
content = new TextContent ( newText );
}
if ( content is ToolUseContent toolUseContent && contentDeltaData . Delta is JsonDelta jsonDelta )
{
toolInputJsonStringBuilder . Append ( jsonDelta . PartialJson );
}
}
// finalize content block on content block stop
2024-07-03 16:57:27 -05:00
// and add it to the response
2024-06-28 16:30:09 -05:00
if ( currentEvent . Type is EventType . ContentBlockStop )
{
2024-07-03 16:57:27 -05:00
if ( content is not null && msgResponse is not null )
2024-06-28 16:30:09 -05:00
{
if ( content is TextContent textContent )
{
2024-07-03 16:57:27 -05:00
msgResponse . Content . Add ( textContent );
2024-06-28 16:30:09 -05:00
}
if ( content is ToolUseContent toolUseContent )
{
var input = Deserialize < Dictionary < string , object? >>( toolInputJsonStringBuilder . ToString ());
var newToolUseContent = new ToolUseContent ()
{
Id = toolUseContent . Id ,
Name = toolUseContent . Name ,
Input = input !,
};
2024-07-03 16:57:27 -05:00
msgResponse . Content . Add ( newToolUseContent );
2024-06-28 16:30:09 -05:00
}
content = null ;
}
}
2024-07-03 16:57:27 -05:00
// update response with message delta data
2024-06-28 16:30:09 -05:00
if (
2024-07-01 22:46:02 -05:00
currentEvent . Type is EventType . MessageDelta &&
2024-06-28 16:30:09 -05:00
currentEvent . Data is MessageDeltaEventData msgDeltaData &&
2024-07-03 16:57:27 -05:00
msgResponse is not null
2024-06-28 16:30:09 -05:00
)
{
2024-07-03 16:57:27 -05:00
var existingUsage = msgResponse . Usage ;
var newUsage = new Usage ()
2024-06-29 22:07:49 -05:00
{
InputTokens = existingUsage . InputTokens + msgDeltaData . Usage . InputTokens ,
OutputTokens = existingUsage . OutputTokens + msgDeltaData . Usage . OutputTokens ,
2024-08-16 09:21:43 -05:00
CacheCreationInputTokens = existingUsage . CacheCreationInputTokens + msgDeltaData . Usage . CacheCreationInputTokens ,
CacheReadInputTokens = existingUsage . CacheReadInputTokens + msgDeltaData . Usage . CacheReadInputTokens ,
2024-06-29 22:07:49 -05:00
};
2024-07-03 16:57:27 -05:00
msgResponse = new MessageResponse ()
2024-06-28 16:30:09 -05:00
{
2024-07-03 16:57:27 -05:00
Id = msgResponse . Id ,
Model = msgResponse . Model ,
Role = msgResponse . Role ,
2024-06-28 16:30:09 -05:00
StopReason = msgDeltaData . Delta . StopReason ,
StopSequence = msgDeltaData . Delta . StopSequence ,
2024-07-03 16:57:27 -05:00
Type = msgResponse . Type ,
2024-06-29 22:07:49 -05:00
Usage = newUsage ,
2024-07-03 16:57:27 -05:00
Content = msgResponse . Content ,
2024-06-28 16:30:09 -05:00
};
2024-07-01 21:00:27 -05:00
if ( request . Tools is not null && request . Tools . Count > 0 )
{
2024-07-03 16:57:27 -05:00
msgResponse . ToolCall = GetToolCall ( msgResponse , request . Tools );
2024-07-01 21:00:27 -05:00
}
2024-06-28 16:30:09 -05:00
}
2024-07-03 16:57:27 -05:00
// yield response on message stop
if ( currentEvent . Type is EventType . MessageStop && msgResponse is not null )
2024-06-28 16:30:09 -05:00
{
2024-07-03 16:57:27 -05:00
var eventData = new MessageCompleteEventData ( msgResponse , anthropicHeaders );
2024-06-28 16:30:09 -05:00
yield return new AnthropicEvent ( EventType . MessageComplete , eventData );
2024-07-03 16:57:27 -05:00
msgResponse = null ;
2024-06-28 16:30:09 -05:00
}
if ( line is null )
{
2024-07-03 00:11:48 -05:00
if ( string . IsNullOrWhiteSpace ( currentEvent . Type ) is false )
{
yield return currentEvent ;
currentEvent = new AnthropicEvent ();
}
2024-06-28 16:30:09 -05:00
break ;
}
if ( line == string . Empty )
{
yield return currentEvent ;
currentEvent = new AnthropicEvent ();
}
if ( line . StartsWith ( EventPrefix ))
{
var eventType = line . Substring ( EventPrefix . Length ). Trim ();
currentEvent = new AnthropicEvent ( eventType , currentEvent . Data );
continue ;
}
if ( line . StartsWith ( DataPrefix ))
{
var eventData = line . Substring ( DataPrefix . Length ). Trim ();
var eventDataJson = Deserialize < EventData >( eventData );
currentEvent = new AnthropicEvent ( currentEvent . Type , eventDataJson !);
continue ;
}
} while ( true );
}
2024-07-03 16:57:27 -05:00
private ToolCall ? GetToolCall ( MessageResponse response , List < Tool > tools )
2024-07-01 21:00:27 -05:00
{
var toolUse = response . Content . OfType < ToolUseContent >(). FirstOrDefault ();
if ( toolUse is null )
{
return null ;
}
var tool = tools . FirstOrDefault ( t => t . Name == toolUse . Name );
if ( tool is null )
{
return null ;
}
return new ToolCall ( tool , toolUse );
}
2024-07-03 16:57:27 -05:00
private async Task < HttpResponseMessage > SendRequestAsync ( BaseMessageRequest request )
2024-06-28 16:30:09 -05:00
{
var requestContent = new StringContent ( Serialize ( request ), Encoding . UTF8 , JsonContentType );
return await _httpClient . PostAsync ( MessagesEndpoint , requestContent );
}
private string Serialize < T >( T obj ) => JsonSerializer . Serialize ( obj , JsonSerializationOptions . DefaultOptions );
private T ? Deserialize < T >( string json ) => JsonSerializer . Deserialize < T >( json , JsonSerializationOptions . DefaultOptions );
}