From 2bbe601eaa8449f99e350556d354ab96fc942132 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 13 Feb 2025 23:47:27 -0500 Subject: [PATCH] Update Azure.AI.Inference to 1.0.0-beta.3 --- eng/packages/General.props | 2 +- .../AzureAIInferenceChatClient.cs | 63 ++++++++++- ...soft.Extensions.AI.AzureAIInference.csproj | 1 - .../AzureAIInferenceChatClientTests.cs | 100 +++++++++++------- 4 files changed, 125 insertions(+), 41 deletions(-) diff --git a/eng/packages/General.props b/eng/packages/General.props index a19e9d2bb91..b33c4c25483 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -2,7 +2,7 @@ - + diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs index bb23e5da7e5..2f527612fab 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs @@ -33,6 +33,9 @@ public sealed class AzureAIInferenceChatClient : IChatClient /// The use for any serialization activities related to tool call arguments and results. private JsonSerializerOptions _toolCallJsonSerializerOptions = AIJsonUtilities.DefaultOptions; + /// Gets a ChatRole.Developer value. + private static ChatRole ChatRoleDeveloper { get; } = new("developer"); + /// Initializes a new instance of the class for the specified . /// The underlying client. /// The ID of the model to use. If null, it can be provided per request via . @@ -273,6 +276,7 @@ private static ChatRole ToChatRole(global::Azure.AI.Inference.ChatRole role) => role.Equals(global::Azure.AI.Inference.ChatRole.User) ? ChatRole.User : role.Equals(global::Azure.AI.Inference.ChatRole.Assistant) ? ChatRole.Assistant : role.Equals(global::Azure.AI.Inference.ChatRole.Tool) ? ChatRole.Tool : + role.Equals(global::Azure.AI.Inference.ChatRole.Developer) ? ChatRoleDeveloper : new ChatRole(role.ToString()); /// Converts an AzureAI finish reason to an Extensions finish reason. @@ -365,17 +369,40 @@ private ChatCompletionsOptions ToAzureAIOptions(IList chatContents, if (options.ResponseFormat is ChatResponseFormatText) { - result.ResponseFormat = new ChatCompletionsResponseFormatText(); + result.ResponseFormat = ChatCompletionsResponseFormat.CreateTextFormat(); } - else if (options.ResponseFormat is ChatResponseFormatJson) + else if (options.ResponseFormat is ChatResponseFormatJson json) { - result.ResponseFormat = new ChatCompletionsResponseFormatJSON(); + if (json.Schema is { } schema) + { + var tool = JsonSerializer.Deserialize(schema, JsonContext.Default.AzureAIChatToolJson)!; + result.ResponseFormat = ChatCompletionsResponseFormat.CreateJsonFormat( + json.SchemaName ?? "json_schema", + new Dictionary + { + ["type"] = _objectString, + ["properties"] = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool.Properties, JsonContext.Default.DictionaryStringJsonElement)), + ["required"] = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool.Required, JsonContext.Default.ListString)), + ["additionalProperties"] = _falseString, + }, + json.SchemaDescription); + } + else + { + result.ResponseFormat = ChatCompletionsResponseFormat.CreateJsonFormat(); + } } } return result; } + /// Cached for "object". + private static readonly BinaryData _objectString = BinaryData.FromString("\"object\""); + + /// Cached for "false". + private static readonly BinaryData _falseString = BinaryData.FromString("false"); + /// Converts an Extensions function to an AzureAI chat tool. private static ChatCompletionsToolDefinition ToAzureAIChatTool(AIFunction aiFunction) { @@ -401,6 +428,10 @@ private IEnumerable ToAzureAIInferenceChatMessages(IList GetContentParts(IList con parts.Add(new ChatMessageImageContentItem(new Uri(uri))); } + break; + + case DataContent dataContent when dataContent.MediaTypeStartsWith("audio/"): + if (dataContent.Data.HasValue) + { + AudioContentFormat format; + if (dataContent.MediaTypeStartsWith("audio/mpeg")) + { + format = AudioContentFormat.Mp3; + } + else if (dataContent.MediaTypeStartsWith("audio/wav")) + { + format = AudioContentFormat.Wav; + } + else + { + break; + } + + parts.Add(new ChatMessageAudioContentItem(BinaryData.FromBytes(dataContent.Data.Value), format)); + } + else if (dataContent.Uri is string uri) + { + parts.Add(new ChatMessageAudioContentItem(new Uri(uri))); + } + break; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj index 2b0d186c7b1..6ccd45cd5c6 100644 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj @@ -29,7 +29,6 @@ - diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs index 9169205b394..c86dbd756b5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs @@ -107,7 +107,12 @@ public void GetService_SuccessfullyReturnsUnderlyingClient() public async Task BasicRequestResponse_NonStreaming(bool multiContent) { const string Input = """ - {"messages":[{"content":"hello","role":"user"}],"max_tokens":10,"temperature":0.5,"model":"gpt-4o-mini"} + { + "messages": [{"role":"user", "content":"hello"}], + "max_tokens":10, + "temperature":0.5, + "model":"gpt-4o-mini" + } """; const string Output = """ @@ -178,7 +183,12 @@ [new ChatMessage(ChatRole.User, "hello".Select(c => (AIContent)new TextContent(c public async Task BasicRequestResponse_Streaming(bool multiContent) { const string Input = """ - {"messages":[{"content":"hello","role":"user"}],"max_tokens":20,"temperature":0.5,"stream":true,"model":"gpt-4o-mini"} + { + "messages": [{"role":"user", "content":"hello"}], + "max_tokens":20, + "temperature":0.5, + "stream":true, + "model":"gpt-4o-mini"} """; const string Output = """ @@ -248,7 +258,7 @@ public async Task AdditionalOptions_NonStreaming() { const string Input = """ { - "messages":[{"content":"hello","role":"user"}], + "messages":[{"role":"user", "content":"hello"}], "max_tokens":10, "temperature":0.5, "top_p":0.5, @@ -305,7 +315,7 @@ public async Task ResponseFormat_Text_NonStreaming() { const string Input = """ { - "messages":[{"content":"hello","role":"user"}], + "messages":[{"role":"user", "content":"hello"}], "model":"gpt-4o-mini", "response_format":{"type":"text"} } @@ -341,7 +351,7 @@ public async Task ResponseFormat_Json_NonStreaming() { const string Input = """ { - "messages":[{"content":"hello","role":"user"}], + "messages":[{"role":"user", "content":"hello"}], "model":"gpt-4o-mini", "response_format":{"type":"json_object"} } @@ -375,14 +385,32 @@ public async Task ResponseFormat_Json_NonStreaming() [Fact] public async Task ResponseFormat_JsonSchema_NonStreaming() { - // NOTE: Azure.AI.Inference doesn't yet expose JSON schema support, so it's currently - // mapped to "json_object" for the time being. - const string Input = """ { - "messages":[{"content":"hello","role":"user"}], + "messages":[{"role":"user", "content":"hello"}], "model":"gpt-4o-mini", - "response_format":{"type":"json_object"} + "response_format": + { + "type":"json_schema", + "json_schema": + { + "name": "DescribedObject", + "schema": + { + "type":"object", + "properties": + { + "description": + { + "type":"string" + } + }, + "required":["description"], + "additionalProperties":false + }, + "description":"An object with a description" + } + } } """; @@ -428,30 +456,30 @@ public async Task MultipleMessages_NonStreaming() { "messages": [ { - "content": "You are a really nice friend.", - "role": "system" + "role": "system", + "content": "You are a really nice friend." }, { - "content": "hello!", - "role": "user" + "role": "user", + "content": "hello!" }, { - "content": "hi, how are you?", - "role": "assistant" + "role": "assistant", + "content": "hi, how are you?" }, { - "content": "i\u0027m good. how are you?", - "role": "user" + "role": "user", + "content": "i\u0027m good. how are you?" }, { + "role": "assistant", "content": "", - "tool_calls": [{"id":"abcd123","type":"function","function":{"name":"GetMood","arguments":"null"}}], - "role": "assistant" + "tool_calls": [{"id":"abcd123","type":"function","function":{"name":"GetMood","arguments":"null"}}] }, { + "role": "tool", "content": "happy", - "tool_call_id": "abcd123", - "role": "tool" + "tool_call_id": "abcd123" } ], "temperature": 0.25, @@ -544,21 +572,21 @@ public async Task MultipleContent_NonStreaming() "messages": [ { + "role": "user", "content": [ { - "text": "Describe this picture.", - "type": "text" + "type": "text", + "text": "Describe this picture." }, { + "type": "image_url", "image_url": { "url": "http://dot.net/someimage.png" - }, - "type": "image_url" + } } - ], - "role":"user" + ] } ], "model": "gpt-4o-mini" @@ -598,12 +626,12 @@ public async Task NullAssistantText_ContentEmpty_NonStreaming() { "messages": [ { - "content": "", - "role": "assistant" + "role": "assistant", + "content": "" }, { - "content": "hello!", - "role": "user" + "role": "user", + "content": "hello!" } ], "model": "gpt-4o-mini" @@ -686,8 +714,8 @@ public async Task FunctionCallContent_NonStreaming(ChatToolMode mode) { "messages": [ { - "content": "How old is Alice?", - "role": "user" + "role": "user", + "content": "How old is Alice?" } ], "model": "gpt-4o-mini", @@ -797,8 +825,8 @@ public async Task FunctionCallContent_Streaming() { "messages": [ { - "content": "How old is Alice?", - "role": "user" + "role": "user", + "content": "How old is Alice?" } ], "stream": true,