Skip to content

Commit 329121c

Browse files
authored
Update Microsoft.Extensions.AI version and improve chat client for AWS updates (#3788)
* Update Microsoft.Extensions.AI version and improve chat client for AWS updates - Bumped version number to 9.4.3-preview.1.25230.7 - Renamed some methods and arguments to match other implementations - Added support for TextReasoningContent - Ensured underlying objects propagated out as RawRepresentation - Added message and response IDs, along with setting additional metadata - Propagate cached token counts to UsageDetails
1 parent 884c8a7 commit 329121c

10 files changed

+162
-68
lines changed

extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.NetFramework.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
</Choose>
3838

3939
<ItemGroup>
40-
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.4.0-preview.1.25207.5" />
40+
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.4.3-preview.1.25230.7" />
4141
</ItemGroup>
4242

4343
<ItemGroup>

extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.NetStandard.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
</Choose>
4242

4343
<ItemGroup>
44-
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.4.0-preview.1.25207.5" />
44+
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.4.3-preview.1.25230.7" />
4545
</ItemGroup>
4646

4747
<ItemGroup>

extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AWSSDK.Extensions.Bedrock.MEAI.nuspec

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<metadata>
44
<id>AWSSDK.Extensions.Bedrock.MEAI</id>
55
<title>AWSSDK - Bedrock integration with Microsoft.Extensions.AI.</title>
6-
<version>4.0.0.0-preview.15</version>
6+
<version>4.0.0.0-preview.16</version>
77
<authors>Amazon Web Services</authors>
88
<description>Implementations of Microsoft.Extensions.AI's abstractions for Bedrock.</description>
99
<language>en-US</language>
@@ -14,18 +14,18 @@
1414
<dependencies>
1515
<group targetFramework="net472">
1616
<dependency id="AWSSDK.Core" version="4.0.0.2" />
17-
<dependency id="AWSSDK.BedrockRuntime" version="4.0.0.0" />
18-
<dependency id="Microsoft.Extensions.AI.Abstractions" version="9.4.0-preview.1.25207.5" />
17+
<dependency id="AWSSDK.BedrockRuntime" version="4.0.0.1" />
18+
<dependency id="Microsoft.Extensions.AI.Abstractions" version="9.4.3-preview.1.25230.7" />
1919
</group>
2020
<group targetFramework="netstandard2.0">
2121
<dependency id="AWSSDK.Core" version="4.0.0.2" />
22-
<dependency id="AWSSDK.BedrockRuntime" version="4.0.0.0" />
23-
<dependency id="Microsoft.Extensions.AI.Abstractions" version="9.4.0-preview.1.25207.5" />
22+
<dependency id="AWSSDK.BedrockRuntime" version="4.0.0.1" />
23+
<dependency id="Microsoft.Extensions.AI.Abstractions" version="9.4.3-preview.1.25230.7" />
2424
</group>
2525
<group targetFramework="net8.0">
2626
<dependency id="AWSSDK.Core" version="4.0.0.2" />
27-
<dependency id="AWSSDK.BedrockRuntime" version="4.0.0.0" />
28-
<dependency id="Microsoft.Extensions.AI.Abstractions" version="9.4.0-preview.1.25207.5" />
27+
<dependency id="AWSSDK.BedrockRuntime" version="4.0.0.1" />
28+
<dependency id="Microsoft.Extensions.AI.Abstractions" version="9.4.3-preview.1.25230.7" />
2929
</group>
3030
</dependencies>
3131
</metadata>

extensions/src/AWSSDK.Extensions.Bedrock.MEAI/AmazonBedrockRuntimeExtensions.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,31 +26,31 @@ public static class AmazonBedrockRuntimeExtensions
2626

2727
/// <summary>Gets an <see cref="IChatClient"/> for the specified <see cref="IAmazonBedrockRuntime"/> instance.</summary>
2828
/// <param name="runtime">The runtime instance to be represented as an <see cref="IChatClient"/>.</param>
29-
/// <param name="modelId">
29+
/// <param name="defaultModelId">
3030
/// The default model ID to use when no model is specified in a request. If not specified,
3131
/// a model must be provided in the <see cref="ChatOptions.ModelId"/> passed to <see cref="IChatClient.GetResponseAsync"/>
3232
/// or <see cref="IChatClient.GetStreamingResponseAsync"/>.
3333
/// </param>
3434
/// <returns>A <see cref="IChatClient"/> instance representing the <see cref="IAmazonBedrockRuntime"/> instance.</returns>
3535
/// <exception cref="ArgumentNullException"><paramref name="runtime"/> is <see langword="null"/>.</exception>
36-
public static IChatClient AsChatClient(this IAmazonBedrockRuntime runtime, string? modelId = null) =>
37-
runtime is not null ? new BedrockChatClient(runtime, modelId) :
36+
public static IChatClient AsIChatClient(this IAmazonBedrockRuntime runtime, string? defaultModelId = null) =>
37+
runtime is not null ? new BedrockChatClient(runtime, defaultModelId) :
3838
throw new ArgumentNullException(nameof(runtime));
3939

4040
/// <summary>Gets an <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> for the specified <see cref="IAmazonBedrockRuntime"/> instance.</summary>
4141
/// <param name="runtime">The runtime instance to be represented as an <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/>.</param>
42-
/// <param name="modelId">
42+
/// <param name="defaultModelId">
4343
/// The default model ID to use when no model is specified in a request. If not specified,
4444
/// a model must be provided in the <see cref="EmbeddingGenerationOptions.ModelId"/> passed to <see cref="IEmbeddingGenerator{TInput, TEmbedding}.GenerateAsync"/>.
4545
/// </param>
46-
/// <param name="dimensions">
46+
/// <param name="defaultModelDimensions">
4747
/// The default number of dimensions to request be generated. This will be overridden by a <see cref="EmbeddingGenerationOptions.Dimensions"/>
4848
/// if that is specified to a request. If neither is specified, the default for the model will be used.
4949
/// </param>
5050
/// <returns>An <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> instance representing the <see cref="IAmazonBedrockRuntime"/> instance.</returns>
5151
/// <exception cref="ArgumentNullException"><paramref name="runtime"/> is <see langword="null"/>.</exception>
52-
public static IEmbeddingGenerator<string, Embedding<float>> AsEmbeddingGenerator(
53-
this IAmazonBedrockRuntime runtime, string? modelId = null, int? dimensions = null) =>
54-
runtime is not null ? new BedrockEmbeddingGenerator(runtime, modelId, dimensions) :
52+
public static IEmbeddingGenerator<string, Embedding<float>> AsIEmbeddingGenerator(
53+
this IAmazonBedrockRuntime runtime, string? defaultModelId = null, int? defaultModelDimensions = null) =>
54+
runtime is not null ? new BedrockEmbeddingGenerator(runtime, defaultModelId, defaultModelDimensions) :
5555
throw new ArgumentNullException(nameof(runtime));
5656
}

extensions/src/AWSSDK.Extensions.Bedrock.MEAI/BedrockChatClient.cs

Lines changed: 121 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ internal sealed partial class BedrockChatClient : IChatClient
4646
/// Initializes a new instance of the <see cref="BedrockChatClient"/> class.
4747
/// </summary>
4848
/// <param name="runtime">The <see cref="IAmazonBedrockRuntime"/> instance to wrap.</param>
49-
/// <param name="modelId">Model ID to use as the default when no model ID is specified in a request.</param>
50-
public BedrockChatClient(IAmazonBedrockRuntime runtime, string? modelId)
49+
/// <param name="defaultModelId">Model ID to use as the default when no model ID is specified in a request.</param>
50+
public BedrockChatClient(IAmazonBedrockRuntime runtime, string? defaultModelId)
5151
{
5252
Debug.Assert(runtime is not null);
5353

5454
_runtime = runtime!;
55-
_modelId = modelId;
55+
_modelId = defaultModelId;
5656

57-
_metadata = new(AmazonBedrockRuntimeExtensions.ProviderName, defaultModelId: modelId);
57+
_metadata = new(AmazonBedrockRuntimeExtensions.ProviderName, defaultModelId: defaultModelId);
5858
}
5959

6060
public void Dispose()
@@ -85,7 +85,9 @@ public async Task<ChatResponse> GetResponseAsync(
8585

8686
ChatMessage result = new()
8787
{
88+
RawRepresentation = response.Output?.Message,
8889
Role = ChatRole.Assistant,
90+
MessageId = Guid.NewGuid().ToString("N"),
8991
};
9092

9193
if (response.Output?.Message?.Content is { } contents)
@@ -94,27 +96,44 @@ public async Task<ChatResponse> GetResponseAsync(
9496
{
9597
if (content.Text is string text)
9698
{
97-
result.Contents.Add(new TextContent(text));
99+
result.Contents.Add(new TextContent(text) { RawRepresentation = content });
100+
}
101+
102+
if (content.ReasoningContent is { ReasoningText.Text: not null } reasoningContent)
103+
{
104+
TextReasoningContent trc = new(reasoningContent.ReasoningText.Text) { RawRepresentation = content };
105+
106+
if (reasoningContent.ReasoningText.Signature is string signature)
107+
{
108+
(trc.AdditionalProperties ??= [])[nameof(reasoningContent.ReasoningText.Signature)] = signature;
109+
}
110+
111+
if (reasoningContent.RedactedContent is { } redactedContent)
112+
{
113+
(trc.AdditionalProperties ??= [])[nameof(reasoningContent.RedactedContent)] = redactedContent.ToArray();
114+
}
115+
116+
result.Contents.Add(trc);
98117
}
99118

100119
if (content.Image is { Source.Bytes: { } imageBytes, Format: { } imageFormat })
101120
{
102-
result.Contents.Add(new DataContent(imageBytes.ToArray(), GetMimeType(imageFormat)));
121+
result.Contents.Add(new DataContent(imageBytes.ToArray(), GetMimeType(imageFormat)) { RawRepresentation = content });
103122
}
104123

105124
if (content.Video is { Source.Bytes: { } videoBytes, Format: { } videoFormat })
106125
{
107-
result.Contents.Add(new DataContent(videoBytes.ToArray(), GetMimeType(videoFormat)));
126+
result.Contents.Add(new DataContent(videoBytes.ToArray(), GetMimeType(videoFormat)) { RawRepresentation = content });
108127
}
109128

110129
if (content.Document is { Source.Bytes: { } documentBytes, Format: { } documentFormat })
111130
{
112-
result.Contents.Add(new DataContent(documentBytes.ToArray(), GetMimeType(documentFormat)));
131+
result.Contents.Add(new DataContent(documentBytes.ToArray(), GetMimeType(documentFormat)) { RawRepresentation = content });
113132
}
114133

115134
if (content.ToolUse is { } toolUse)
116135
{
117-
result.Contents.Add(new FunctionCallContent(toolUse.ToolUseId, toolUse.Name, DocumentToDictionary(toolUse.Input)));
136+
result.Contents.Add(new FunctionCallContent(toolUse.ToolUseId, toolUse.Name, DocumentToDictionary(toolUse.Input)) { RawRepresentation = content });
118137
}
119138
}
120139
}
@@ -126,13 +145,11 @@ public async Task<ChatResponse> GetResponseAsync(
126145

127146
return new(result)
128147
{
148+
CreatedAt = DateTimeOffset.UtcNow,
129149
FinishReason = response.StopReason is not null ? GetChatFinishReason(response.StopReason) : null,
130-
Usage = response.Usage is TokenUsage usage ? new()
131-
{
132-
InputTokenCount = usage.InputTokens,
133-
OutputTokenCount = usage.OutputTokens,
134-
TotalTokenCount = usage.TotalTokens,
135-
} : null,
150+
RawRepresentation = response,
151+
ResponseId = Guid.NewGuid().ToString("N"),
152+
Usage = response.Usage is TokenUsage usage ? CreateUsageDetails(usage) : null,
136153
};
137154
}
138155

@@ -161,13 +178,19 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
161178
string? toolId = null;
162179
StringBuilder? toolInput = null;
163180
ChatFinishReason? finishReason = null;
181+
string messageId = Guid.NewGuid().ToString("N");
182+
string responseId = Guid.NewGuid().ToString("N");
164183
await foreach (var update in result.Stream.ConfigureAwait(false))
165184
{
166185
switch (update)
167186
{
168187
case MessageStartEvent messageStart:
169188
yield return new()
170189
{
190+
CreatedAt = DateTimeOffset.UtcNow,
191+
MessageId = messageId,
192+
RawRepresentation = update,
193+
ResponseId = responseId,
171194
Role = ChatRole.Assistant,
172195
FinishReason = finishReason,
173196
};
@@ -188,7 +211,35 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
188211
{
189212
yield return new(ChatRole.Assistant, text)
190213
{
214+
CreatedAt = DateTimeOffset.UtcNow,
215+
MessageId = messageId,
216+
RawRepresentation = update,
217+
FinishReason = finishReason,
218+
ResponseId = responseId,
219+
};
220+
}
221+
222+
if (contentBlockDelta.Delta.ReasoningContent is { Text: not null } reasoningContent)
223+
{
224+
TextReasoningContent trc = new(reasoningContent.Text);
225+
226+
if (reasoningContent.Signature is not null)
227+
{
228+
(trc.AdditionalProperties ??= [])[nameof(reasoningContent.Signature)] = reasoningContent.Signature;
229+
}
230+
231+
if (reasoningContent.RedactedContent is { } redactedContent)
232+
{
233+
(trc.AdditionalProperties ??= [])[nameof(reasoningContent.RedactedContent)] = redactedContent.ToArray();
234+
}
235+
236+
yield return new(ChatRole.Assistant, [trc])
237+
{
238+
CreatedAt = DateTimeOffset.UtcNow,
239+
MessageId = messageId,
191240
FinishReason = finishReason,
241+
RawRepresentation = update,
242+
ResponseId = responseId,
192243
};
193244
}
194245
break;
@@ -199,9 +250,13 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
199250
Dictionary<string, object?>? inputs = ParseToolInputs(toolInput?.ToString(), out Exception? parseError);
200251
yield return new()
201252
{
202-
Role = ChatRole.Assistant,
203-
FinishReason = finishReason,
204253
Contents = [new FunctionCallContent(toolId, toolName, inputs) { Exception = parseError }],
254+
CreatedAt = DateTimeOffset.UtcNow,
255+
MessageId = messageId,
256+
FinishReason = finishReason,
257+
RawRepresentation = update,
258+
ResponseId = responseId,
259+
Role = ChatRole.Assistant,
205260
};
206261
}
207262

@@ -224,26 +279,24 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
224279

225280
yield return new()
226281
{
227-
Role = ChatRole.Assistant,
228-
FinishReason = finishReason,
229282
AdditionalProperties = additionalProps,
283+
CreatedAt = DateTimeOffset.UtcNow,
284+
MessageId = messageId,
285+
FinishReason = finishReason,
286+
RawRepresentation = update,
287+
ResponseId = responseId,
288+
Role = ChatRole.Assistant,
230289
};
231290
break;
232291

233292
case ConverseStreamMetadataEvent metadata when metadata.Usage is TokenUsage usage:
234-
yield return new()
293+
yield return new(ChatRole.Assistant, [new UsageContent(CreateUsageDetails(usage))])
235294
{
236-
Role = ChatRole.Assistant,
295+
CreatedAt = DateTimeOffset.UtcNow,
237296
FinishReason = finishReason,
238-
Contents =
239-
[
240-
new UsageContent(new()
241-
{
242-
InputTokenCount = usage.InputTokens,
243-
OutputTokenCount = usage.OutputTokens,
244-
TotalTokenCount = usage.TotalTokens,
245-
})
246-
],
297+
MessageId = messageId,
298+
RawRepresentation = update,
299+
ResponseId = responseId,
247300
};
248301
break;
249302
}
@@ -266,6 +319,29 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
266319
null;
267320
}
268321

322+
/// <summary>Creates a <see cref="UsageDetails"/> from a <see cref="TokenUsage"/>.</summary>
323+
private static UsageDetails CreateUsageDetails(TokenUsage usage)
324+
{
325+
UsageDetails ud = new()
326+
{
327+
InputTokenCount = usage.InputTokens,
328+
OutputTokenCount = usage.OutputTokens,
329+
TotalTokenCount = usage.TotalTokens,
330+
};
331+
332+
if (usage.CacheReadInputTokens is int cacheReadTokens)
333+
{
334+
(ud.AdditionalCounts ??= []).Add(nameof(usage.CacheReadInputTokens), cacheReadTokens);
335+
}
336+
337+
if (usage.CacheWriteInputTokens is int cacheWriteTokens)
338+
{
339+
(ud.AdditionalCounts ??= []).Add(nameof(usage.CacheWriteInputTokens), cacheWriteTokens);
340+
}
341+
342+
return ud;
343+
}
344+
269345
/// <summary>Converts a <see cref="StopReason"/> into a <see cref="ChatFinishReason"/>.</summary>
270346
private static ChatFinishReason GetChatFinishReason(StopReason stopReason) =>
271347
stopReason.Value switch
@@ -340,6 +416,21 @@ private static List<ContentBlock> CreateContents(ChatMessage message)
340416
contents.Add(new() { Text = tc.Text });
341417
break;
342418

419+
case TextReasoningContent trc:
420+
contents.Add(new()
421+
{
422+
ReasoningContent = new()
423+
{
424+
ReasoningText = new()
425+
{
426+
Text = trc.Text,
427+
Signature = trc.AdditionalProperties?[nameof(ReasoningContentBlock.ReasoningText.Signature)] as string,
428+
},
429+
RedactedContent = trc.AdditionalProperties?[nameof(ReasoningContentBlock.RedactedContent)] is byte[] array ? new(array) : null,
430+
}
431+
});
432+
break;
433+
343434
case DataContent dc:
344435
if (GetImageFormat(dc.MediaType) is ImageFormat imageFormat)
345436
{

extensions/src/AWSSDK.Extensions.Bedrock.MEAI/BedrockEmbeddingGenerator.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,17 @@ internal sealed partial class BedrockEmbeddingGenerator : IEmbeddingGenerator<st
3939
/// Initializes a new instance of the <see cref="BedrockEmbeddingGenerator"/> class.
4040
/// </summary>
4141
/// <param name="runtime">The <see cref="IAmazonBedrockRuntime"/> instance to wrap.</param>
42-
/// <param name="modelId">Model ID to use as the default when no model ID is specified in a request.</param>
43-
/// <param name="dimensions">Number of dimensions to use when no number of dimensions is specified in a request.</param>
44-
public BedrockEmbeddingGenerator(IAmazonBedrockRuntime runtime, string? modelId, int? dimensions)
42+
/// <param name="defaultModelId">Model ID to use as the default when no model ID is specified in a request.</param>
43+
/// <param name="defaultModelDimensions">Number of dimensions to use when no number of dimensions is specified in a request.</param>
44+
public BedrockEmbeddingGenerator(IAmazonBedrockRuntime runtime, string? defaultModelId, int? defaultModelDimensions)
4545
{
4646
Debug.Assert(runtime is not null);
4747

4848
_runtime = runtime!;
49-
_modelId = modelId;
50-
_dimensions = dimensions;
49+
_modelId = defaultModelId;
50+
_dimensions = defaultModelDimensions;
5151

52-
_metadata = new(AmazonBedrockRuntimeExtensions.ProviderName, defaultModelId: modelId, defaultModelDimensions: dimensions);
52+
_metadata = new(AmazonBedrockRuntimeExtensions.ProviderName, defaultModelId: defaultModelId, defaultModelDimensions: defaultModelDimensions);
5353
}
5454

5555
public void Dispose()
@@ -116,7 +116,11 @@ public async Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(
116116

117117
if (totaltokens is not null)
118118
{
119-
embeddings.Usage = new() { InputTokenCount = totaltokens.Value };
119+
embeddings.Usage = new()
120+
{
121+
InputTokenCount = totaltokens.Value,
122+
TotalTokenCount = totaltokens.Value,
123+
};
120124
}
121125

122126
return embeddings;

0 commit comments

Comments
 (0)