Skip to content

Commit 207fa90

Browse files
committed
Merged PR 51323: Backport most necessary commits from dotnet/extensions for 9.7
Pure cherry-picks
2 parents bbd9db7 + dd5fa17 commit 207fa90

File tree

40 files changed

+1304
-51
lines changed

40 files changed

+1304
-51
lines changed

scripts/ConfigureEvaluationTests.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ if ($Configure -and $Unconfigure) {
3131
Exit 1
3232
}
3333

34-
if (!(Test-Path $ConfigRoot)) {
34+
if (-not $ConfigRoot -or -not (Test-Path $ConfigRoot)) {
3535
$ConfigRoot = "$HOME/.config/dotnet-extensions"
3636
}
3737

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#pragma warning disable S2333 // Redundant modifiers should not be used
2727
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
2828
#pragma warning disable SA1202 // Public members should come before private members
29+
#pragma warning disable SA1203 // Constants should appear before fields
2930

3031
namespace Microsoft.Extensions.AI;
3132

@@ -825,6 +826,23 @@ static bool IsAsyncMethod(MethodInfo method)
825826
{
826827
try
827828
{
829+
if (value is string text && IsPotentiallyJson(text))
830+
{
831+
Debug.Assert(typeInfo.Type != typeof(string), "string parameters should not enter this branch.");
832+
833+
// Account for the parameter potentially being a JSON string.
834+
// The value is a string but the type is not. Try to deserialize it under the assumption that it's JSON.
835+
// If it's not, we'll fall through to the default path that makes it valid JSON and then tries to deserialize.
836+
try
837+
{
838+
return JsonSerializer.Deserialize(text, typeInfo);
839+
}
840+
catch (JsonException)
841+
{
842+
// If the string is not valid JSON, fall through to the round-trip.
843+
}
844+
}
845+
828846
string json = JsonSerializer.Serialize(value, serializerOptions.GetTypeInfo(value.GetType()));
829847
return JsonSerializer.Deserialize(json, typeInfo);
830848
}
@@ -1021,6 +1039,34 @@ private record struct DescriptorKey(
10211039
AIJsonSchemaCreateOptions SchemaOptions);
10221040
}
10231041

1042+
/// <summary>
1043+
/// Quickly checks if the specified string is potentially JSON
1044+
/// by checking if the first non-whitespace characters are valid JSON start tokens.
1045+
/// </summary>
1046+
/// <param name="value">The string to check.</param>
1047+
/// <returns>If <see langword="false"/> then the string is definitely not valid JSON.</returns>
1048+
private static bool IsPotentiallyJson(string value) => PotentiallyJsonRegex().IsMatch(value);
1049+
#if NET
1050+
[GeneratedRegex(PotentiallyJsonRegexString, RegexOptions.IgnorePatternWhitespace)]
1051+
private static partial Regex PotentiallyJsonRegex();
1052+
#else
1053+
private static Regex PotentiallyJsonRegex() => _potentiallyJsonRegex;
1054+
private static readonly Regex _potentiallyJsonRegex = new(PotentiallyJsonRegexString, RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
1055+
#endif
1056+
private const string PotentiallyJsonRegexString = """
1057+
^\s* # Optional whitespace at the start of the string
1058+
( null # null literal
1059+
| false # false literal
1060+
| true # true literal
1061+
| -?[0-9]# number
1062+
| " # string
1063+
| \[ # start array
1064+
| { # start object
1065+
| // # Start of single-line comment
1066+
| /\* # Start of multi-line comment
1067+
)
1068+
""";
1069+
10241070
/// <summary>
10251071
/// Removes characters from a .NET member name that shouldn't be used in an AI function name.
10261072
/// </summary>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Reflection;
7+
using System.Text.Json;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.Shared.Diagnostics;
11+
12+
#pragma warning disable SA1202 // Elements should be ordered by access
13+
14+
namespace Microsoft.Extensions.AI;
15+
16+
/// <summary>
17+
/// Provides an optional base class for an <see cref="AIFunction"/> that passes through calls to another instance.
18+
/// </summary>
19+
public class DelegatingAIFunction : AIFunction
20+
{
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="DelegatingAIFunction"/> class as a wrapper around <paramref name="innerFunction"/>.
23+
/// </summary>
24+
/// <param name="innerFunction">The inner AI function to which all calls are delegated by default.</param>
25+
/// <exception cref="ArgumentNullException"><paramref name="innerFunction"/> is <see langword="null"/>.</exception>
26+
protected DelegatingAIFunction(AIFunction innerFunction)
27+
{
28+
InnerFunction = Throw.IfNull(innerFunction);
29+
}
30+
31+
/// <summary>Gets the inner <see cref="AIFunction" />.</summary>
32+
protected AIFunction InnerFunction { get; }
33+
34+
/// <inheritdoc />
35+
public override string Name => InnerFunction.Name;
36+
37+
/// <inheritdoc />
38+
public override string Description => InnerFunction.Description;
39+
40+
/// <inheritdoc />
41+
public override JsonElement JsonSchema => InnerFunction.JsonSchema;
42+
43+
/// <inheritdoc />
44+
public override JsonElement? ReturnJsonSchema => InnerFunction.ReturnJsonSchema;
45+
46+
/// <inheritdoc />
47+
public override JsonSerializerOptions JsonSerializerOptions => InnerFunction.JsonSerializerOptions;
48+
49+
/// <inheritdoc />
50+
public override MethodInfo? UnderlyingMethod => InnerFunction.UnderlyingMethod;
51+
52+
/// <inheritdoc />
53+
public override IReadOnlyDictionary<string, object?> AdditionalProperties => InnerFunction.AdditionalProperties;
54+
55+
/// <inheritdoc />
56+
public override string ToString() => InnerFunction.ToString();
57+
58+
/// <inheritdoc />
59+
protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) =>
60+
InnerFunction.InvokeAsync(arguments, cancellationToken);
61+
}

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,6 +1355,58 @@
13551355
}
13561356
]
13571357
},
1358+
{
1359+
"Type": "class Microsoft.Extensions.AI.DelegatingAIFunction : Microsoft.Extensions.AI.AIFunction",
1360+
"Stage": "Stable",
1361+
"Methods": [
1362+
{
1363+
"Member": "Microsoft.Extensions.AI.DelegatingAIFunction.DelegatingAIFunction(Microsoft.Extensions.AI.AIFunction innerFunction);",
1364+
"Stage": "Stable"
1365+
},
1366+
{
1367+
"Member": "override System.Threading.Tasks.ValueTask<object?> Microsoft.Extensions.AI.DelegatingAIFunction.InvokeCoreAsync(Microsoft.Extensions.AI.AIFunctionArguments arguments, System.Threading.CancellationToken cancellationToken);",
1368+
"Stage": "Stable"
1369+
},
1370+
{
1371+
"Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.ToString();",
1372+
"Stage": "Experimental"
1373+
}
1374+
],
1375+
"Properties": [
1376+
{
1377+
"Member": "Microsoft.Extensions.AI.AIFunction Microsoft.Extensions.AI.DelegatingAIFunction.InnerFunction { get; }",
1378+
"Stage": "Stable"
1379+
},
1380+
{
1381+
"Member": "override System.Collections.Generic.IReadOnlyDictionary<string, object?> Microsoft.Extensions.AI.DelegatingAIFunction.AdditionalProperties { get; }",
1382+
"Stage": "Stable"
1383+
},
1384+
{
1385+
"Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.Description { get; }",
1386+
"Stage": "Stable"
1387+
},
1388+
{
1389+
"Member": "override System.Text.Json.JsonElement Microsoft.Extensions.AI.DelegatingAIFunction.JsonSchema { get; }",
1390+
"Stage": "Stable"
1391+
},
1392+
{
1393+
"Member": "override System.Text.Json.JsonSerializerOptions Microsoft.Extensions.AI.DelegatingAIFunction.JsonSerializerOptions { get; }",
1394+
"Stage": "Stable"
1395+
},
1396+
{
1397+
"Member": "override string Microsoft.Extensions.AI.DelegatingAIFunction.Name { get; }",
1398+
"Stage": "Stable"
1399+
},
1400+
{
1401+
"Member": "override System.Text.Json.JsonElement? Microsoft.Extensions.AI.DelegatingAIFunction.ReturnJsonSchema { get; }",
1402+
"Stage": "Stable"
1403+
},
1404+
{
1405+
"Member": "override System.Reflection.MethodInfo? Microsoft.Extensions.AI.DelegatingAIFunction.UnderlyingMethod { get; }",
1406+
"Stage": "Stable"
1407+
}
1408+
]
1409+
},
13581410
{
13591411
"Type": "class Microsoft.Extensions.AI.DelegatingChatClient : Microsoft.Extensions.AI.IChatClient, System.IDisposable",
13601412
"Stage": "Stable",

src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/BLEUEvaluator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public ValueTask<EvaluationResult> EvaluateAsync(
8686
});
8787

8888
metric.Value = score;
89-
string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s";
89+
string durationText = $"{duration.TotalSeconds.ToString("F4", CultureInfo.InvariantCulture)} s";
9090
metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText);
9191
metric.AddOrUpdateContext(context);
9292
metric.Interpretation = metric.Interpret();

src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/F1Evaluator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public ValueTask<EvaluationResult> EvaluateAsync(
7777
});
7878

7979
metric.Value = score;
80-
string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s";
80+
string durationText = $"{duration.TotalSeconds.ToString("F4", CultureInfo.InvariantCulture)} s";
8181
metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText);
8282
metric.AddOrUpdateContext(context);
8383
metric.Interpretation = metric.Interpret();

src/Libraries/Microsoft.Extensions.AI.Evaluation.NLP/GLEUEvaluator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public ValueTask<EvaluationResult> EvaluateAsync(
8686
});
8787

8888
metric.Value = score;
89-
string durationText = $"{duration.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s";
89+
string durationText = $"{duration.TotalSeconds.ToString("F4", CultureInfo.InvariantCulture)} s";
9090
metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText);
9191
metric.AddOrUpdateContext(context);
9292
metric.Interpretation = metric.Interpret();

src/Libraries/Microsoft.Extensions.AI.Evaluation/EvaluationMetricExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public static void AddOrUpdateChatMetadata(
177177

178178
if (duration is not null)
179179
{
180-
string durationText = $"{duration.Value.TotalSeconds.ToString("F2", CultureInfo.InvariantCulture)} s";
180+
string durationText = $"{duration.Value.TotalSeconds.ToString("F4", CultureInfo.InvariantCulture)} s";
181181
metric.AddOrUpdateMetadata(name: "evaluation-duration", value: durationText);
182182
}
183183
}

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,14 +237,16 @@ void IDisposable.Dispose()
237237
}
238238

239239
/// <summary>Converts an Extensions function to an OpenAI assistants function tool.</summary>
240-
internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(AIFunction aiFunction)
240+
internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(AIFunction aiFunction, ChatOptions? options = null)
241241
{
242-
(BinaryData parameters, bool? strict) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction);
242+
bool? strict =
243+
OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ??
244+
OpenAIClientExtensions.HasStrict(options?.AdditionalProperties);
243245

244246
return new FunctionToolDefinition(aiFunction.Name)
245247
{
246248
Description = aiFunction.Description,
247-
Parameters = parameters,
249+
Parameters = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict),
248250
StrictParameterSchemaEnabled = strict,
249251
};
250252
}
@@ -296,7 +298,7 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(
296298
switch (tool)
297299
{
298300
case AIFunction aiFunction:
299-
runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction));
301+
runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options));
300302
break;
301303

302304
case HostedCodeInterpreterTool:
@@ -342,7 +344,8 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(
342344
runOptions.ResponseFormat = AssistantResponseFormat.CreateJsonSchemaFormat(
343345
jsonFormat.SchemaName,
344346
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
345-
jsonFormat.SchemaDescription);
347+
jsonFormat.SchemaDescription,
348+
OpenAIClientExtensions.HasStrict(options.AdditionalProperties));
346349
break;
347350

348351
case ChatResponseFormatJson jsonFormat:

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,17 @@ void IDisposable.Dispose()
101101
}
102102

103103
/// <summary>Converts an Extensions function to an OpenAI chat tool.</summary>
104-
internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction)
104+
internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? options = null)
105105
{
106-
(BinaryData parameters, bool? strict) = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction);
107-
108-
return ChatTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, parameters, strict);
106+
bool? strict =
107+
OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ??
108+
OpenAIClientExtensions.HasStrict(options?.AdditionalProperties);
109+
110+
return ChatTool.CreateFunctionTool(
111+
aiFunction.Name,
112+
aiFunction.Description,
113+
OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict),
114+
strict);
109115
}
110116

111117
/// <summary>Converts an Extensions chat message enumerable to an OpenAI chat message enumerable.</summary>
@@ -517,7 +523,7 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
517523
{
518524
if (tool is AIFunction af)
519525
{
520-
result.Tools.Add(ToOpenAIChatTool(af));
526+
result.Tools.Add(ToOpenAIChatTool(af, options));
521527
}
522528
}
523529

@@ -555,7 +561,8 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
555561
OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat(
556562
jsonFormat.SchemaName ?? "json_schema",
557563
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
558-
jsonFormat.SchemaDescription) :
564+
jsonFormat.SchemaDescription,
565+
OpenAIClientExtensions.HasStrict(options.AdditionalProperties)) :
559566
OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat();
560567
}
561568
}

0 commit comments

Comments
 (0)