Skip to content

Commit aa6ca66

Browse files
committed
Merged PR 49960: Merge MEAI updates from main into 9.5.0 release
2 parents f6bd93c + 1ba107a commit aa6ca66

File tree

17 files changed

+358
-462
lines changed

17 files changed

+358
-462
lines changed

eng/Versions.props

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -156,21 +156,21 @@
156156
<MicrosoftCodeAnalysisVersion>4.8.0</MicrosoftCodeAnalysisVersion>
157157
<MicrosoftCodeAnalysisAnalyzersVersion>3.3.4</MicrosoftCodeAnalysisAnalyzersVersion>
158158
<!-- AI templates -->
159-
<AspireVersion>9.1.0</AspireVersion>
160-
<AspireAzureAIOpenAIVersion>9.1.0-preview.1.25121.10</AspireAzureAIOpenAIVersion>
159+
<AspireVersion>9.2.1</AspireVersion>
160+
<AspireAzureAIOpenAIVersion>9.2.1-preview.1.25222.1</AspireAzureAIOpenAIVersion>
161161
<AzureAIProjectsVersion>1.0.0-beta.6</AzureAIProjectsVersion>
162162
<AzureAIOpenAIVersion>2.2.0-beta.4</AzureAIOpenAIVersion>
163163
<AzureIdentityVersion>1.13.2</AzureIdentityVersion>
164164
<AzureSearchDocumentsVersion>11.6.0</AzureSearchDocumentsVersion>
165-
<CommunityToolkitAspireHostingOllamaVersion>9.3.1-beta.260</CommunityToolkitAspireHostingOllamaVersion>
166-
<CommunityToolkitAspireHostingSqliteVersion>9.3.1-beta.260</CommunityToolkitAspireHostingSqliteVersion>
167-
<CommunityToolkitAspireMicrosoftEntityFrameworkCoreSqliteVersion>9.3.1-beta.260</CommunityToolkitAspireMicrosoftEntityFrameworkCoreSqliteVersion>
168-
<CommunityToolkitAspireOllamaSharpVersion>9.3.1-beta.260</CommunityToolkitAspireOllamaSharpVersion>
165+
<CommunityToolkitAspireHostingOllamaVersion>9.4.1-beta.277</CommunityToolkitAspireHostingOllamaVersion>
166+
<CommunityToolkitAspireHostingSqliteVersion>9.4.1-beta.277</CommunityToolkitAspireHostingSqliteVersion>
167+
<CommunityToolkitAspireMicrosoftEntityFrameworkCoreSqliteVersion>9.4.1-beta.277</CommunityToolkitAspireMicrosoftEntityFrameworkCoreSqliteVersion>
168+
<CommunityToolkitAspireOllamaSharpVersion>9.4.1-beta.277</CommunityToolkitAspireOllamaSharpVersion>
169169
<MicrosoftExtensionsServiceDiscoveryVersion>9.2.0</MicrosoftExtensionsServiceDiscoveryVersion>
170-
<MicrosoftSemanticKernelConnectorsAzureAISearchVersion>1.45.0-preview</MicrosoftSemanticKernelConnectorsAzureAISearchVersion>
171-
<MicrosoftSemanticKernelConnectorsQdrantVersion>1.45.0-preview</MicrosoftSemanticKernelConnectorsQdrantVersion>
172-
<MicrosoftSemanticKernelCoreVersion>1.45.0</MicrosoftSemanticKernelCoreVersion>
173-
<OllamaSharpVersion>5.1.12</OllamaSharpVersion>
170+
<MicrosoftSemanticKernelConnectorsAzureAISearchVersion>1.47.0-preview</MicrosoftSemanticKernelConnectorsAzureAISearchVersion>
171+
<MicrosoftSemanticKernelConnectorsQdrantVersion>1.47.0-preview</MicrosoftSemanticKernelConnectorsQdrantVersion>
172+
<MicrosoftSemanticKernelCoreVersion>1.47.0</MicrosoftSemanticKernelCoreVersion>
173+
<OllamaSharpVersion>5.1.13</OllamaSharpVersion>
174174
<OpenTelemetryVersion>1.9.0</OpenTelemetryVersion>
175175
<PdfPigVersion>0.1.9</PdfPigVersion>
176176
<SystemLinqAsyncVersion>6.0.1</SystemLinqAsyncVersion>

src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactory.cs renamed to src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactory.cs

Lines changed: 162 additions & 190 deletions
Large diffs are not rendered by default.

src/Libraries/Microsoft.Extensions.AI/Functions/AIFunctionFactoryOptions.cs renamed to src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/AIFunctionFactoryOptions.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,22 @@ public AIFunctionFactoryOptions()
107107
public Func<object?, Type?, CancellationToken, ValueTask<object?>>? MarshalResult { get; set; }
108108

109109
/// <summary>
110-
/// Gets or sets optional services used in the construction of the <see cref="AIFunction"/>.
110+
/// Gets or sets a delegate used with <see cref="AIFunctionFactory.Create(MethodInfo, Type, AIFunctionFactoryOptions?)"/> to create the receiver instance.
111111
/// </summary>
112112
/// <remarks>
113-
/// These services will be used to determine which parameters should be satisifed from dependency injection. As such,
114-
/// what services are satisfied via this provider should match what's satisfied via the provider passed into
115-
/// <see cref="AIFunction.InvokeAsync"/> via <see cref="AIFunctionArguments.Services"/>.
113+
/// <para>
114+
/// <see cref="AIFunctionFactory.Create(MethodInfo, Type, AIFunctionFactoryOptions?)"/> creates <see cref="AIFunction"/> instances that invoke an
115+
/// instance method on the specified <see cref="Type"/>. This delegate is used to create the instance of the type that will be used to invoke the method.
116+
/// By default if <see cref="CreateInstance"/> is <see langword="null"/>, <see cref="Activator.CreateInstance(Type)"/> is used. If
117+
/// <see cref="CreateInstance"/> is non-<see langword="null"/>, the delegate is invoked with the <see cref="Type"/> to be instantiated and the
118+
/// <see cref="AIFunctionArguments"/> provided to the <see cref="AIFunction.InvokeAsync"/> method.
119+
/// </para>
120+
/// <para>
121+
/// Each created instance will be used for a single invocation. If the object is <see cref="IAsyncDisposable"/> or <see cref="IDisposable"/>, it will
122+
/// be disposed of after the invocation completes.
123+
/// </para>
116124
/// </remarks>
117-
public IServiceProvider? Services { get; set; }
125+
public Func<Type, AIFunctionArguments, object>? CreateInstance { get; set; }
118126

119127
/// <summary>Provides configuration options produced by the <see cref="ConfigureParameterBinding"/> delegate.</summary>
120128
public readonly record struct ParameterBindingOptions

src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs

Lines changed: 75 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -157,30 +157,54 @@ void IDisposable.Dispose()
157157
}
158158
else if (input.Role == ChatRole.Assistant)
159159
{
160-
AssistantChatMessage message = new(ToOpenAIChatContent(input.Contents))
161-
{
162-
ParticipantName = input.AuthorName
163-
};
164-
160+
List<ChatMessageContentPart>? contentParts = null;
161+
List<ChatToolCall>? toolCalls = null;
162+
string? refusal = null;
165163
foreach (var content in input.Contents)
166164
{
167165
switch (content)
168166
{
169-
case ErrorContent errorContent when errorContent.ErrorCode is nameof(message.Refusal):
170-
message.Refusal = errorContent.Message;
167+
case ErrorContent ec when ec.ErrorCode == nameof(AssistantChatMessage.Refusal):
168+
refusal = ec.Message;
171169
break;
172170

173-
case FunctionCallContent callRequest:
174-
message.ToolCalls.Add(
175-
ChatToolCall.CreateFunctionToolCall(
176-
callRequest.CallId,
177-
callRequest.Name,
178-
new(JsonSerializer.SerializeToUtf8Bytes(
179-
callRequest.Arguments,
180-
options.GetTypeInfo(typeof(IDictionary<string, object?>))))));
171+
case FunctionCallContent fc:
172+
(toolCalls ??= []).Add(
173+
ChatToolCall.CreateFunctionToolCall(fc.CallId, fc.Name, new(JsonSerializer.SerializeToUtf8Bytes(
174+
fc.Arguments, options.GetTypeInfo(typeof(IDictionary<string, object?>))))));
181175
break;
176+
177+
default:
178+
if (ToChatMessageContentPart(content) is { } part)
179+
{
180+
(contentParts ??= []).Add(part);
181+
}
182+
183+
break;
184+
}
185+
}
186+
187+
AssistantChatMessage message;
188+
if (contentParts is not null)
189+
{
190+
message = new(contentParts);
191+
if (toolCalls is not null)
192+
{
193+
foreach (var toolCall in toolCalls)
194+
{
195+
message.ToolCalls.Add(toolCall);
196+
}
182197
}
183198
}
199+
else
200+
{
201+
message = toolCalls is not null ?
202+
new(toolCalls) :
203+
new(ChatMessageContentPart.CreateTextPart(string.Empty));
204+
}
205+
206+
message.ParticipantName = input.AuthorName;
207+
message.Refusal = refusal;
184208

185209
yield return message;
186210
}
@@ -191,38 +215,12 @@ void IDisposable.Dispose()
191215
private static List<ChatMessageContentPart> ToOpenAIChatContent(IList<AIContent> contents)
192216
{
193217
List<ChatMessageContentPart> parts = [];
218+
194219
foreach (var content in contents)
195220
{
196-
switch (content)
221+
if (ToChatMessageContentPart(content) is { } part)
197222
{
198-
case TextContent textContent:
199-
parts.Add(ChatMessageContentPart.CreateTextPart(textContent.Text));
200-
break;
201-
202-
case UriContent uriContent when uriContent.HasTopLevelMediaType("image"):
203-
parts.Add(ChatMessageContentPart.CreateImagePart(uriContent.Uri, GetImageDetail(content)));
204-
break;
205-
206-
case DataContent dataContent when dataContent.HasTopLevelMediaType("image"):
207-
parts.Add(ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, GetImageDetail(content)));
208-
break;
209-
210-
case DataContent dataContent when dataContent.HasTopLevelMediaType("audio"):
211-
var audioData = BinaryData.FromBytes(dataContent.Data);
212-
if (dataContent.MediaType.Equals("audio/mpeg", StringComparison.OrdinalIgnoreCase))
213-
{
214-
parts.Add(ChatMessageContentPart.CreateInputAudioPart(audioData, ChatInputAudioFormat.Mp3));
215-
}
216-
else if (dataContent.MediaType.Equals("audio/wav", StringComparison.OrdinalIgnoreCase))
217-
{
218-
parts.Add(ChatMessageContentPart.CreateInputAudioPart(audioData, ChatInputAudioFormat.Wav));
219-
}
220-
221-
break;
222-
223-
case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase):
224-
parts.Add(ChatMessageContentPart.CreateFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, $"{Guid.NewGuid():N}.pdf"));
225-
break;
223+
parts.Add(part);
226224
}
227225
}
228226

@@ -234,6 +232,39 @@ private static List<ChatMessageContentPart> ToOpenAIChatContent(IList<AIContent>
234232
return parts;
235233
}
236234

235+
private static ChatMessageContentPart? ToChatMessageContentPart(AIContent content)
236+
{
237+
switch (content)
238+
{
239+
case TextContent textContent:
240+
return ChatMessageContentPart.CreateTextPart(textContent.Text);
241+
242+
case UriContent uriContent when uriContent.HasTopLevelMediaType("image"):
243+
return ChatMessageContentPart.CreateImagePart(uriContent.Uri, GetImageDetail(content));
244+
245+
case DataContent dataContent when dataContent.HasTopLevelMediaType("image"):
246+
return ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, GetImageDetail(content));
247+
248+
case DataContent dataContent when dataContent.HasTopLevelMediaType("audio"):
249+
var audioData = BinaryData.FromBytes(dataContent.Data);
250+
if (dataContent.MediaType.Equals("audio/mpeg", StringComparison.OrdinalIgnoreCase))
251+
{
252+
return ChatMessageContentPart.CreateInputAudioPart(audioData, ChatInputAudioFormat.Mp3);
253+
}
254+
else if (dataContent.MediaType.Equals("audio/wav", StringComparison.OrdinalIgnoreCase))
255+
{
256+
return ChatMessageContentPart.CreateInputAudioPart(audioData, ChatInputAudioFormat.Wav);
257+
}
258+
259+
break;
260+
261+
case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase):
262+
return ChatMessageContentPart.CreateFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, $"{Guid.NewGuid():N}.pdf");
263+
}
264+
265+
return null;
266+
}
267+
237268
private static ChatImageDetailLevel? GetImageDetail(AIContent content)
238269
{
239270
if (content.AdditionalProperties?.TryGetValue("detail", out object? value) is true)

src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientStructuredOutputExtensions.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@
77
using System.Reflection;
88
using System.Text.Json;
99
using System.Text.Json.Nodes;
10+
using System.Text.RegularExpressions;
1011
using System.Threading;
1112
using System.Threading.Tasks;
1213
using Microsoft.Shared.Diagnostics;
1314

1415
#pragma warning disable SA1118 // Parameter should not span multiple lines
16+
#pragma warning disable S2333 // Redundant modifiers should not be used
1517

1618
namespace Microsoft.Extensions.AI;
1719

1820
/// <summary>
1921
/// Provides extension methods on <see cref="IChatClient"/> that simplify working with structured output.
2022
/// </summary>
2123
/// <related type="Article" href="https://learn.microsoft.com/dotnet/ai/quickstarts/structured-output">Request a response with structured output.</related>
22-
public static class ChatClientStructuredOutputExtensions
24+
public static partial class ChatClientStructuredOutputExtensions
2325
{
2426
private static readonly AIJsonSchemaCreateOptions _inferenceOptions = new()
2527
{
@@ -197,7 +199,7 @@ public static async Task<ChatResponse<T>> GetResponseAsync<T>(
197199
// the LLM backend is meant to do whatever's needed to explain the schema to the LLM.
198200
options.ResponseFormat = ChatResponseFormat.ForJsonSchema(
199201
schema,
200-
schemaName: AIFunctionFactory.SanitizeMemberName(typeof(T).Name),
202+
schemaName: SanitizeMemberName(typeof(T).Name),
201203
schemaDescription: typeof(T).GetCustomAttribute<DescriptionAttribute>()?.Description);
202204
}
203205
else
@@ -246,4 +248,24 @@ private static bool SchemaRepresentsObject(JsonElement schemaElement)
246248
_ => JsonValue.Create(element)
247249
};
248250
}
251+
252+
/// <summary>
253+
/// Removes characters from a .NET member name that shouldn't be used in an AI function name.
254+
/// </summary>
255+
/// <param name="memberName">The .NET member name that should be sanitized.</param>
256+
/// <returns>
257+
/// Replaces non-alphanumeric characters in the identifier with the underscore character.
258+
/// Primarily intended to remove characters produced by compiler-generated method name mangling.
259+
/// </returns>
260+
private static string SanitizeMemberName(string memberName) =>
261+
InvalidNameCharsRegex().Replace(memberName, "_");
262+
263+
/// <summary>Regex that flags any character other than ASCII digits or letters or the underscore.</summary>
264+
#if NET
265+
[GeneratedRegex("[^0-9A-Za-z_]")]
266+
private static partial Regex InvalidNameCharsRegex();
267+
#else
268+
private static Regex InvalidNameCharsRegex() => _invalidNameCharsRegex;
269+
private static readonly Regex _invalidNameCharsRegex = new("[^0-9A-Za-z_]", RegexOptions.Compiled);
270+
#endif
249271
}

0 commit comments

Comments
 (0)