-
Notifications
You must be signed in to change notification settings - Fork 761
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expose an AIJsonUtilities
class in M.E.AI and lower M.E.AI.Abstractions to STJv8
#5513
Changes from all commits
68a9d45
f6e6b98
514f846
aa76abf
1b1752b
fd85775
5ea90ed
b02f38b
40f9ec8
6e51c3d
b9dbf77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,6 +56,46 @@ public FunctionCallContent(string callId, string name, IDictionary<string, objec | |
[JsonIgnore] | ||
public Exception? Exception { get; set; } | ||
|
||
/// <summary> | ||
/// Creates a new instance of <see cref="FunctionCallContent"/> parsing arguments using a specified encoding and parser. | ||
/// </summary> | ||
/// <typeparam name="TEncoding">The encoding format from which to parse function call arguments.</typeparam> | ||
/// <param name="encodedArguments">The input arguments encoded in <typeparamref name="TEncoding"/>.</param> | ||
/// <param name="callId">The function call ID.</param> | ||
/// <param name="name">The function name.</param> | ||
/// <param name="argumentParser">The parsing implementation converting the encoding to a dictionary of arguments.</param> | ||
/// <param name="exceptionFilter">Filters potential parsing exceptions that should be caught and included in the result.</param> | ||
/// <returns>A new instance of <see cref="FunctionCallContent"/> containing the parse result.</returns> | ||
public static FunctionCallContent CreateFromParsedArguments<TEncoding>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change removes all "helper" code from the Abstractions repo with the exception of this method required by client impls. I used a callback approach to invert dependencies if that's acceptable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems ok, though at this point I'm not sure how much benefit this method really provides. |
||
TEncoding encodedArguments, | ||
string callId, | ||
string name, | ||
Func<TEncoding, IDictionary<string, object?>?> argumentParser, | ||
Func<Exception, bool>? exceptionFilter = null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need the filter? Could we just catch and wrap any exception from the parser and store it as the inner? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For JSON stuff we only wrap There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't seem worth promoting an extra delegate into the API for that. I'd suggest either we just hardcode ones we want to allow through and wrap everything else, or just wrap everything. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't hardcode exception types given that it's a generic helper, so I'll just remove the filter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We could; we do elsewhere around calling out to what's possibly user code. We can certainly choose not to.
Ok |
||
{ | ||
_ = Throw.IfNull(callId); | ||
_ = Throw.IfNull(name); | ||
_ = Throw.IfNull(encodedArguments); | ||
_ = Throw.IfNull(argumentParser); | ||
|
||
IDictionary<string, object?>? arguments = null; | ||
Exception? parsingException = null; | ||
|
||
try | ||
{ | ||
arguments = argumentParser(encodedArguments); | ||
} | ||
catch (Exception ex) when (exceptionFilter is null || exceptionFilter(ex)) | ||
{ | ||
parsingException = new InvalidOperationException("Error parsing function call arguments.", ex); | ||
} | ||
|
||
return new FunctionCallContent(callId, name, arguments) | ||
{ | ||
Exception = parsingException | ||
}; | ||
} | ||
|
||
/// <summary>Gets a string representing this instance to display in the debugger.</summary> | ||
private string DebuggerDisplay | ||
{ | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,12 +16,15 @@ | |
|
||
#pragma warning disable S1135 // Track uses of "TODO" tags | ||
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields | ||
#pragma warning disable SA1204 // Static elements should appear before instance elements | ||
|
||
namespace Microsoft.Extensions.AI; | ||
|
||
/// <summary>An <see cref="IChatClient"/> for an Azure AI Inference <see cref="ChatCompletionsClient"/>.</summary> | ||
public sealed partial class AzureAIInferenceChatClient : IChatClient | ||
{ | ||
private static readonly JsonElement _defaultParameterSchema = JsonDocument.Parse("{}").RootElement; | ||
eiriktsarpalis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// <summary>The underlying <see cref="ChatCompletionsClient" />.</summary> | ||
private readonly ChatCompletionsClient _chatCompletionsClient; | ||
|
||
|
@@ -93,14 +96,11 @@ public async Task<ChatCompletion> CompleteAsync( | |
{ | ||
if (toolCall is ChatCompletionsFunctionToolCall ftc && !string.IsNullOrWhiteSpace(ftc.Name)) | ||
{ | ||
Dictionary<string, object?>? arguments = FunctionCallHelpers.ParseFunctionCallArguments(ftc.Arguments, out Exception? parsingException); | ||
FunctionCallContent callContent = ParseCallContentFromJsonString(ftc.Arguments, toolCall.Id, ftc.Name); | ||
callContent.ModelId = response.Model; | ||
callContent.RawRepresentation = toolCall; | ||
|
||
returnMessage.Contents.Add(new FunctionCallContent(toolCall.Id, ftc.Name, arguments) | ||
{ | ||
ModelId = response.Model, | ||
Exception = parsingException, | ||
RawRepresentation = toolCall | ||
}); | ||
returnMessage.Contents.Add(callContent); | ||
} | ||
} | ||
} | ||
|
@@ -226,15 +226,14 @@ public async IAsyncEnumerable<StreamingChatCompletionUpdate> CompleteStreamingAs | |
FunctionCallInfo fci = entry.Value; | ||
if (!string.IsNullOrWhiteSpace(fci.Name)) | ||
{ | ||
var arguments = FunctionCallHelpers.ParseFunctionCallArguments( | ||
FunctionCallContent callContent = ParseCallContentFromJsonString( | ||
fci.Arguments?.ToString() ?? string.Empty, | ||
out Exception? parsingException); | ||
fci.CallId!, | ||
fci.Name!); | ||
|
||
completionUpdate.Contents.Add(new FunctionCallContent(fci.CallId!, fci.Name!, arguments) | ||
{ | ||
ModelId = modelId, | ||
Exception = parsingException | ||
}); | ||
callContent.ModelId = modelId; | ||
|
||
completionUpdate.Contents.Add(callContent); | ||
} | ||
} | ||
|
||
|
@@ -358,7 +357,7 @@ private ChatCompletionsOptions ToAzureAIOptions(IList<ChatMessage> chatContents, | |
} | ||
|
||
/// <summary>Converts an Extensions function to an AzureAI chat tool.</summary> | ||
private ChatCompletionsFunctionToolDefinition ToAzureAIChatTool(AIFunction aiFunction) | ||
private static ChatCompletionsFunctionToolDefinition ToAzureAIChatTool(AIFunction aiFunction) | ||
{ | ||
BinaryData resultParameters = AzureAIChatToolJson.ZeroFunctionParametersSchema; | ||
|
||
|
@@ -371,7 +370,7 @@ private ChatCompletionsFunctionToolDefinition ToAzureAIChatTool(AIFunction aiFun | |
{ | ||
tool.Properties.Add( | ||
parameter.Name, | ||
FunctionCallHelpers.InferParameterJsonSchema(parameter, aiFunction.Metadata, ToolCallJsonSerializerOptions)); | ||
parameter.Schema is JsonElement schema ? schema : _defaultParameterSchema); | ||
|
||
if (parameter.IsRequired) | ||
{ | ||
|
@@ -428,9 +427,10 @@ private IEnumerable<ChatRequestMessage> ToAzureAIInferenceChatMessages(IEnumerab | |
string? result = resultContent.Result as string; | ||
if (result is null && resultContent.Result is not null) | ||
{ | ||
JsonSerializerOptions options = ToolCallJsonSerializerOptions ?? JsonContext.Default.Options; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be a separate change if we decide to do it, but should ToolCallJsonSerializerOptions be made non-nullable like it is on the middleware clients, just defaulting it to the default options? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's possible, but it would require moving the defaults to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not understanding why this would require any movement. I'm simply suggesting changing: public JsonSerializerOptions? ToolCallJsonSerializerOptions { get; set; } to private JsonSerializerOptions _toolCallOptions = JsonContext.Default.Options;
...
public JsonSerializerOptions ToolCallJsonSerializerOptions
{
get => _toolCallOptions;
set => _toolCallOptions = Throw.IfNull(value);
} and then here instead of: JsonSerializerOptions options = ToolCallJsonSerializerOptions ?? JsonContext.Default.Options; it'd just be: JsonSerializerOptions options = ToolCallJsonSerializerOptions; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless we're trying to avoid exposing this options instance? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm just pointing out that unless we expose a default options instance on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't understand why. The current code doesn't require that, just substituting JsonContext.Default.Options when the user hasn't supplied their own. I'm simply suggesting moving around where we default back to that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see what you mean. It is appropriate to default to |
||
try | ||
{ | ||
result = FunctionCallHelpers.FormatFunctionResultAsJson(resultContent.Result, ToolCallJsonSerializerOptions); | ||
result = JsonSerializer.Serialize(resultContent.Result, options.GetTypeInfo(typeof(object))); | ||
} | ||
catch (NotSupportedException) | ||
{ | ||
|
@@ -461,7 +461,8 @@ private IEnumerable<ChatRequestMessage> ToAzureAIInferenceChatMessages(IEnumerab | |
{ | ||
if (content is FunctionCallContent callRequest && callRequest.CallId is not null && toolCalls?.ContainsKey(callRequest.CallId) is not true) | ||
{ | ||
string jsonArguments = FunctionCallHelpers.FormatFunctionParametersAsJson(callRequest.Arguments, ToolCallJsonSerializerOptions); | ||
JsonSerializerOptions serializerOptions = ToolCallJsonSerializerOptions ?? JsonContext.Default.Options; | ||
string jsonArguments = JsonSerializer.Serialize(callRequest.Arguments, serializerOptions.GetTypeInfo(typeof(IDictionary<string, object>))); | ||
(toolCalls ??= []).Add( | ||
callRequest.CallId, | ||
new ChatCompletionsFunctionToolCall( | ||
|
@@ -489,7 +490,14 @@ private IEnumerable<ChatRequestMessage> ToAzureAIInferenceChatMessages(IEnumerab | |
} | ||
} | ||
|
||
private static FunctionCallContent ParseCallContentFromJsonString(string json, string callId, string name) => | ||
FunctionCallContent.CreateFromParsedArguments(json, callId, name, | ||
argumentParser: static json => JsonSerializer.Deserialize(json, JsonContext.Default.IDictionaryStringObject), | ||
exceptionFilter: static ex => ex is JsonException); | ||
|
||
/// <summary>Source-generated JSON type information.</summary> | ||
[JsonSerializable(typeof(AzureAIChatToolJson))] | ||
[JsonSerializable(typeof(IDictionary<string, object?>))] | ||
[JsonSerializable(typeof(JsonElement))] | ||
private sealed partial class JsonContext : JsonSerializerContext; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If STJ revs, e.g. with a security patch, we'll need to remember to update this, or will it be automatically updated by darc? Do we need a corresponding entry in Version.Details.xml?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear to me that it would be updated automatically. Maybe @ViktorHofer knows.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Version.Details.xml is the input source for Maestro dependency flow which handles repo -> repo internal flow. If you want to get a version updated based on a new package being available on nuget.org, you would need to use Dependabot.
If you just want to make sure that you don't use a vulnerable version of STJ, then you could enable NuGet Audit in this repository which would flag the dependency at restore and build time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forgive my ignorance but if this extension doesn't have a dependency on .Net 9, why the package is a
9.x-preview
and not a8.x-preview
(targeting NET 8 dependencies only) ?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which package are you asking about?