Skip to content

Commit b115f96

Browse files
committed
Merge branch 'main' into noisyanalyzers1
2 parents 8b2ee42 + fe1cc44 commit b115f96

File tree

26 files changed

+473
-64
lines changed

26 files changed

+473
-64
lines changed

eng/packages/General.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<PackageVersion Include="Microsoft.ML.Tokenizers" Version="$(MicrosoftMLTokenizersVersion)" />
1717
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
1818
<PackageVersion Include="OllamaSharp" Version="5.1.9" />
19-
<PackageVersion Include="OpenAI" Version="2.4.0" />
19+
<PackageVersion Include="OpenAI" Version="2.5.0" />
2020
<PackageVersion Include="Polly" Version="8.4.2" />
2121
<PackageVersion Include="Polly.Core" Version="8.4.2" />
2222
<PackageVersion Include="Polly.Extensions" Version="8.4.2" />

src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## NOT YET RELEASED
44

55
- Added new `ChatResponseFormat.ForJsonSchema` overloads that export a JSON schema from a .NET type.
6+
- Added new `AITool.GetService` virtual method.
67
- Updated `TextReasoningContent` to include `ProtectedData` for representing encrypted/redacted content.
78
- Fixed `MinLength`/`MaxLength`/`Length` attribute mapping in nullable string properties during schema export.
89

src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,14 @@ protected DelegatingAIFunction(AIFunction innerFunction)
5656
/// <inheritdoc />
5757
protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) =>
5858
InnerFunction.InvokeAsync(arguments, cancellationToken);
59+
60+
/// <inheritdoc />
61+
public override object? GetService(Type serviceType, object? serviceKey = null)
62+
{
63+
_ = Throw.IfNull(serviceType);
64+
65+
return
66+
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
67+
InnerFunction.GetService(serviceType, serviceKey);
68+
}
5969
}

src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,14 @@ protected DelegatingAIFunctionDeclaration(AIFunctionDeclaration innerFunction)
4343

4444
/// <inheritdoc />
4545
public override string ToString() => InnerFunction.ToString();
46+
47+
/// <inheritdoc />
48+
public override object? GetService(Type serviceType, object? serviceKey = null)
49+
{
50+
_ = Throw.IfNull(serviceType);
51+
52+
return
53+
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
54+
InnerFunction.GetService(serviceType, serviceKey);
55+
}
4656
}

src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,14 @@
685685
"Member": "Microsoft.Extensions.AI.AITool.AITool();",
686686
"Stage": "Stable"
687687
},
688+
{
689+
"Member": "virtual object? Microsoft.Extensions.AI.AITool.GetService(System.Type serviceType, object? serviceKey = null);",
690+
"Stage": "Stable"
691+
},
692+
{
693+
"Member": "TService? Microsoft.Extensions.AI.AITool.GetService<TService>(object? serviceKey = null);",
694+
"Stage": "Stable"
695+
},
688696
{
689697
"Member": "override string Microsoft.Extensions.AI.AITool.ToString();",
690698
"Stage": "Stable"
@@ -1477,6 +1485,10 @@
14771485
"Member": "Microsoft.Extensions.AI.DelegatingAIFunction.DelegatingAIFunction(Microsoft.Extensions.AI.AIFunction innerFunction);",
14781486
"Stage": "Stable"
14791487
},
1488+
{
1489+
"Member": "override object? Microsoft.Extensions.AI.DelegatingAIFunction.GetService(System.Type serviceType, object? serviceKey = null);",
1490+
"Stage": "Stable"
1491+
},
14801492
{
14811493
"Member": "override System.Threading.Tasks.ValueTask<object?> Microsoft.Extensions.AI.DelegatingAIFunction.InvokeCoreAsync(Microsoft.Extensions.AI.AIFunctionArguments arguments, System.Threading.CancellationToken cancellationToken);",
14821494
"Stage": "Stable"

src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.Diagnostics;
67
using System.Text;
78
using Microsoft.Shared.Collections;
9+
using Microsoft.Shared.Diagnostics;
810

911
namespace Microsoft.Extensions.AI;
1012

@@ -29,6 +31,35 @@ protected AITool()
2931
/// <inheritdoc/>
3032
public override string ToString() => Name;
3133

34+
/// <summary>Asks the <see cref="AITool"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
35+
/// <param name="serviceType">The type of object being requested.</param>
36+
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
37+
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
38+
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
39+
/// <remarks>
40+
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="AITool"/>,
41+
/// including itself or any services it might be wrapping.
42+
/// </remarks>
43+
public virtual object? GetService(Type serviceType, object? serviceKey = null)
44+
{
45+
_ = Throw.IfNull(serviceType);
46+
47+
return
48+
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
49+
null;
50+
}
51+
52+
/// <summary>Asks the <see cref="AITool"/> for an object of type <typeparamref name="TService"/>.</summary>
53+
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
54+
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
55+
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
56+
/// <remarks>
57+
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="AITool"/>,
58+
/// including itself or any services it might be wrapping.
59+
/// </remarks>
60+
public TService? GetService<TService>(object? serviceKey = null) =>
61+
GetService(typeof(TService), serviceKey) is TService service ? service : default;
62+
3263
/// <summary>Gets the string to display in the debugger for this instance.</summary>
3364
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
3465
private string DebuggerDisplay

src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## NOT YET RELEASED
44

5+
- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`.
6+
57
## 9.9.0-preview.1.25458.4
68

79
- Updated tool mapping to recognize any `AIFunctionDeclaration`.

src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
## NOT YET RELEASED
44

5+
- Updated to depend on OpenAI 2.5.0.
56
- Added M.E.AI to OpenAI conversions for response format types.
67
- Added `ResponseTool` to `AITool` conversions.
8+
- Fixed the handling of `HostedCodeInterpreterTool` with Responses when no file IDs were provided.
9+
- Fixed an issue where requests would fail when AllowMultipleToolCalls was set with no tools provided.
10+
- Fixed an issue where requests would fail when an AuthorName was provided containing invalid characters.
711

812
## 9.9.0-preview.1.25458.4
913

10-
- Updated to depend on OpenAI 2.4.0
14+
- Updated to depend on OpenAI 2.4.0.
1115
- Updated tool mappings to recognize any `AIFunctionDeclaration`.
1216
- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`.
1317
- Fixed handling of annotated but empty content in the `AsIChatClient` for `AssistantClient`.

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

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Runtime.CompilerServices;
1111
using System.Text;
1212
using System.Text.Json;
13+
using System.Text.RegularExpressions;
1314
using System.Threading;
1415
using System.Threading.Tasks;
1516
using Microsoft.Shared.Diagnostics;
@@ -23,7 +24,7 @@
2324
namespace Microsoft.Extensions.AI;
2425

2526
/// <summary>Represents an <see cref="IChatClient"/> for an OpenAI <see cref="OpenAIClient"/> or <see cref="ChatClient"/>.</summary>
26-
internal sealed class OpenAIChatClient : IChatClient
27+
internal sealed partial class OpenAIChatClient : IChatClient
2728
{
2829
// These delegate instances are used to call the internal overloads of CompleteChatAsync and CompleteChatStreamingAsync that accept
2930
// a RequestOptions. These should be replaced once a better way to pass RequestOptions is available.
@@ -154,10 +155,11 @@ internal static ChatTool ToOpenAIChatTool(AIFunctionDeclaration aiFunction, Chat
154155
input.Role == OpenAIClientExtensions.ChatRoleDeveloper)
155156
{
156157
var parts = ToOpenAIChatContent(input.Contents);
158+
string? name = SanitizeAuthorName(input.AuthorName);
157159
yield return
158-
input.Role == ChatRole.System ? new SystemChatMessage(parts) { ParticipantName = input.AuthorName } :
159-
input.Role == OpenAIClientExtensions.ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = input.AuthorName } :
160-
new UserChatMessage(parts) { ParticipantName = input.AuthorName };
160+
input.Role == ChatRole.System ? new SystemChatMessage(parts) { ParticipantName = name } :
161+
input.Role == OpenAIClientExtensions.ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = name } :
162+
new UserChatMessage(parts) { ParticipantName = name };
161163
}
162164
else if (input.Role == ChatRole.Tool)
163165
{
@@ -230,7 +232,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunctionDeclaration aiFunction, Chat
230232
new(ChatMessageContentPart.CreateTextPart(string.Empty));
231233
}
232234

233-
message.ParticipantName = input.AuthorName;
235+
message.ParticipantName = SanitizeAuthorName(input.AuthorName);
234236
message.Refusal = refusal;
235237

236238
yield return message;
@@ -565,7 +567,6 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
565567
result.TopP ??= options.TopP;
566568
result.PresencePenalty ??= options.PresencePenalty;
567569
result.Temperature ??= options.Temperature;
568-
result.AllowParallelToolCalls ??= options.AllowMultipleToolCalls;
569570
result.Seed ??= options.Seed;
570571

571572
if (options.StopSequences is { Count: > 0 } stopSequences)
@@ -586,6 +587,11 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
586587
}
587588
}
588589

590+
if (result.Tools.Count > 0)
591+
{
592+
result.AllowParallelToolCalls ??= options.AllowMultipleToolCalls;
593+
}
594+
589595
if (result.ToolChoice is null && result.Tools.Count > 0)
590596
{
591597
switch (options.ToolMode)
@@ -746,11 +752,41 @@ internal static void ConvertContentParts(ChatMessageContent content, IList<AICon
746752
_ => new ChatFinishReason(s),
747753
};
748754

755+
/// <summary>Sanitizes the author name to be appropriate for including as an OpenAI participant name.</summary>
756+
private static string? SanitizeAuthorName(string? name)
757+
{
758+
if (name is not null)
759+
{
760+
const int MaxLength = 64;
761+
762+
name = InvalidAuthorNameRegex().Replace(name, string.Empty);
763+
if (name.Length == 0)
764+
{
765+
name = null;
766+
}
767+
else if (name.Length > MaxLength)
768+
{
769+
name = name.Substring(0, MaxLength);
770+
}
771+
}
772+
773+
return name;
774+
}
775+
749776
/// <summary>POCO representing function calling info. Used to concatenation information for a single function call from across multiple streaming updates.</summary>
750777
private sealed class FunctionCallInfo
751778
{
752779
public string? CallId;
753780
public string? Name;
754781
public StringBuilder? Arguments;
755782
}
783+
784+
private const string InvalidAuthorNamePattern = @"[^a-zA-Z0-9_]+";
785+
#if NET
786+
[GeneratedRegex(InvalidAuthorNamePattern)]
787+
private static partial Regex InvalidAuthorNameRegex();
788+
#else
789+
private static Regex InvalidAuthorNameRegex() => _invalidAuthorNameRegex;
790+
private static readonly Regex _invalidAuthorNameRegex = new(InvalidAuthorNamePattern, RegexOptions.Compiled);
791+
#endif
756792
}

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,36 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.ClientModel;
6+
using System.ClientModel.Primitives;
57
using System.Collections.Generic;
68
using System.Linq;
9+
using System.Reflection;
710
using System.Threading;
811
using System.Threading.Tasks;
912
using Microsoft.Shared.Diagnostics;
1013
using OpenAI.Embeddings;
1114

1215
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
16+
#pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?)
1317

1418
namespace Microsoft.Extensions.AI;
1519

1620
/// <summary>An <see cref="IEmbeddingGenerator{String, Embedding}"/> for an OpenAI <see cref="EmbeddingClient"/>.</summary>
1721
internal sealed class OpenAIEmbeddingGenerator : IEmbeddingGenerator<string, Embedding<float>>
1822
{
23+
// This delegate instance is used to call the internal overload of GenerateEmbeddingsAsync that accepts
24+
// a RequestOptions. This should be replaced once a better way to pass RequestOptions is available.
25+
private static readonly Func<EmbeddingClient, IEnumerable<string>, OpenAI.Embeddings.EmbeddingGenerationOptions, RequestOptions, Task<ClientResult<OpenAIEmbeddingCollection>>>?
26+
_generateEmbeddingsAsync =
27+
(Func<EmbeddingClient, IEnumerable<string>, OpenAI.Embeddings.EmbeddingGenerationOptions, RequestOptions, Task<ClientResult<OpenAIEmbeddingCollection>>>?)
28+
typeof(EmbeddingClient)
29+
.GetMethod(
30+
nameof(EmbeddingClient.GenerateEmbeddingsAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
31+
null, [typeof(IEnumerable<string>), typeof(OpenAI.Embeddings.EmbeddingGenerationOptions), typeof(RequestOptions)], null)
32+
?.CreateDelegate(
33+
typeof(Func<EmbeddingClient, IEnumerable<string>, OpenAI.Embeddings.EmbeddingGenerationOptions, RequestOptions, Task<ClientResult<OpenAIEmbeddingCollection>>>));
34+
1935
/// <summary>Metadata about the embedding generator.</summary>
2036
private readonly EmbeddingGeneratorMetadata _metadata;
2137

@@ -48,7 +64,10 @@ public async Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(IEnumerab
4864
{
4965
OpenAI.Embeddings.EmbeddingGenerationOptions? openAIOptions = ToOpenAIOptions(options);
5066

51-
var embeddings = (await _embeddingClient.GenerateEmbeddingsAsync(values, openAIOptions, cancellationToken).ConfigureAwait(false)).Value;
67+
var t = _generateEmbeddingsAsync is not null ?
68+
_generateEmbeddingsAsync(_embeddingClient, values, openAIOptions, cancellationToken.ToRequestOptions(streaming: false)) :
69+
_embeddingClient.GenerateEmbeddingsAsync(values, openAIOptions, cancellationToken);
70+
var embeddings = (await t.ConfigureAwait(false)).Value;
5271

5372
return new(embeddings.Select(e =>
5473
new Embedding<float>(e.ToFloats())

0 commit comments

Comments
 (0)