From 0a5f070a5ce60b5e2b36ecf3adc5255fdb20609f Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:02:22 +0000 Subject: [PATCH 1/6] Add support for AzureAI tools --- .../Definition/AgentToolDefinition.cs | 26 +++++++ .../Extensions/AgentDefinitionExtensions.cs | 68 ++++++++++++++++++- .../AgentToolDefinitionExtensions.cs | 21 ++++++ 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs diff --git a/dotnet/src/Agents/Abstractions/Definition/AgentToolDefinition.cs b/dotnet/src/Agents/Abstractions/Definition/AgentToolDefinition.cs index 7c18ae624496..007daa8bb9fd 100644 --- a/dotnet/src/Agents/Abstractions/Definition/AgentToolDefinition.cs +++ b/dotnet/src/Agents/Abstractions/Definition/AgentToolDefinition.cs @@ -39,6 +39,19 @@ public string? Name } } + /// + /// The description of the tool. + /// + public string? Description + { + get => this._description; + set + { + Verify.NotNull(value); + this._description = value; + } + } + /// /// Gets or sets the configuration for the tool. /// @@ -56,9 +69,22 @@ public IDictionary? Configuration } } + /// + /// Gets the required configuration. + /// + /// Name of the configuration value. + public object GetRequiredConfiguration(string key) + { + Verify.NotNull(key); + Verify.True(this.Configuration!.ContainsKey(key), $"The configuration key '{key}' is required."); + + return this.Configuration[key]; + } + #region private private string? _type; private string? _name; + private string? _description; private IDictionary? _configuration; #endregion } diff --git a/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs b/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs index 362c103a16b7..5d1757fb976c 100644 --- a/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs +++ b/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs @@ -11,6 +11,16 @@ namespace Microsoft.SemanticKernel.Agents.AzureAI; /// internal static class AgentDefinitionExtensions { + private const string AzureAISearchType = "azure_aisearch"; + private const string AzureFunctionType = "azure_function"; + private const string BingGroundingType = "bing_grounding"; + private const string CodeInterpreterType = "code_interpreter"; + private const string FileSearchType = "file_search"; + private const string FunctionToolType = "function_tool"; + private const string MicrosoftFabricType = "microsoft_fabric"; + private const string OpenApiType = "openapi"; + private const string SharepointType = "sharepoint"; + /// /// Return the Azure AI tool definitions which corresponds with the provided . /// @@ -22,8 +32,15 @@ public static IEnumerable GetAzureToolDefinitions(this AgentDefi { return tool.Type switch { - "code_interpreter" => new CodeInterpreterToolDefinition(), - "file_search" => new FileSearchToolDefinition(), + AzureAISearchType => CreateAzureAISearchToolDefinition(tool), + AzureFunctionType => CreateAzureFunctionToolDefinition(tool), + BingGroundingType => CreateBingGroundingToolDefinition(tool), + CodeInterpreterType => CreateCodeInterpreterToolDefinition(tool), + FileSearchType => CreateFileSearchToolDefinition(tool), + FunctionToolType => CreateFunctionToolDefinition(tool), + MicrosoftFabricType => CreateMicrosoftFabricToolDefinition(tool), + OpenApiType => CreateOpenApiToolDefinition(tool), + SharepointType => CreateSharepointToolDefinition(tool), _ => throw new InvalidOperationException($"Unable to created Azure AI tool definition because of known tool type: {tool.Type}"), }; }); @@ -40,4 +57,51 @@ public static IEnumerable GetAzureToolDefinitions(this AgentDefi // TODO: Implement return null; } + + #region private + private static AzureAISearchToolDefinition CreateAzureAISearchToolDefinition(AgentToolDefinition tool) + { + return new AzureAISearchToolDefinition(); + } + + private static AzureFunctionToolDefinition CreateAzureFunctionToolDefinition(AgentToolDefinition tool) + { + return new AzureFunctionToolDefinition(); + } + + private static BingGroundingToolDefinition CreateBingGroundingToolDefinition(AgentToolDefinition tool) + { + return new BingGroundingToolDefinition(); + } + + private static CodeInterpreterToolDefinition CreateCodeInterpreterToolDefinition(AgentToolDefinition tool) + { + return new CodeInterpreterToolDefinition(); + } + + private static FileSearchToolDefinition CreateFileSearchToolDefinition(AgentToolDefinition tool) + { + return new FileSearchToolDefinition(); + } + + private static FunctionToolDefinition CreateFunctionToolDefinition(AgentToolDefinition tool) + { + return new FunctionToolDefinition(); + } + + private static MicrosoftFabricToolDefinition CreateMicrosoftFabricToolDefinition(AgentToolDefinition tool) + { + return new MicrosoftFabricToolDefinition(); + } + + private static OpenApiToolDefinition CreateOpenApiToolDefinition(AgentToolDefinition tool) + { + return new OpenApiToolDefinition(); + } + + private static SharepointToolDefinition CreateSharepointToolDefinition(AgentToolDefinition tool) + { + return new SharepointToolDefinition(); + } + #endregion } diff --git a/dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs b/dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs new file mode 100644 index 000000000000..b5a7ed7d9733 --- /dev/null +++ b/dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Projects; + +namespace Microsoft.SemanticKernel.Agents.AzureAI; + +/// +/// Provides extension methods for . +/// +internal static class AgentToolDefinitionExtensions +{ + internal static AzureFunctionBinding GetInputBinding(this AgentToolDefinition agentToolDefinition) + { + Verify.NotNull(agentToolDefinition.Configuration); + + string storageServiceEndpoint; + string queueName; + + return new AzureFunctionBinding(new AzureFunctionStorageQueue(storageServiceEndpoint, queueName)); + } +} From 7da0fb0e7b3d55064c6587ae55f6e0f88f700919 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 24 Feb 2025 20:06:54 +0000 Subject: [PATCH 2/6] Creating unit tests --- .../Definition/AgentToolDefinition.cs | 12 -- .../Extensions/AgentDefinitionExtensions.cs | 60 ++++++- .../AgentToolDefinitionExtensions.cs | 153 +++++++++++++++++- .../Definition/AzureAIAgentFactoryTests.cs | 5 +- .../Yaml/AzureAIKernelAgentYamlTests.cs | 149 +++++++++++++++++ 5 files changed, 355 insertions(+), 24 deletions(-) create mode 100644 dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs diff --git a/dotnet/src/Agents/Abstractions/Definition/AgentToolDefinition.cs b/dotnet/src/Agents/Abstractions/Definition/AgentToolDefinition.cs index 007daa8bb9fd..1ebb11352b5b 100644 --- a/dotnet/src/Agents/Abstractions/Definition/AgentToolDefinition.cs +++ b/dotnet/src/Agents/Abstractions/Definition/AgentToolDefinition.cs @@ -69,18 +69,6 @@ public IDictionary? Configuration } } - /// - /// Gets the required configuration. - /// - /// Name of the configuration value. - public object GetRequiredConfiguration(string key) - { - Verify.NotNull(key); - Verify.True(this.Configuration!.ContainsKey(key), $"The configuration key '{key}' is required."); - - return this.Configuration[key]; - } - #region private private string? _type; private string? _name; diff --git a/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs b/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs index 5d1757fb976c..189ecbd083bf 100644 --- a/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs +++ b/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs @@ -61,17 +61,33 @@ public static IEnumerable GetAzureToolDefinitions(this AgentDefi #region private private static AzureAISearchToolDefinition CreateAzureAISearchToolDefinition(AgentToolDefinition tool) { + Verify.NotNull(tool); + return new AzureAISearchToolDefinition(); } private static AzureFunctionToolDefinition CreateAzureFunctionToolDefinition(AgentToolDefinition tool) { - return new AzureFunctionToolDefinition(); + Verify.NotNull(tool); + Verify.NotNull(tool.Name); + Verify.NotNull(tool.Description); + + string name = tool.Name; + string description = tool.Description; + AzureFunctionBinding inputBinding = tool.GetInputBinding(); + AzureFunctionBinding outputBinding = tool.GetOutputBinding(); + BinaryData parameters = tool.GetParameters(); + + return new AzureFunctionToolDefinition(name, description, inputBinding, outputBinding, parameters); } private static BingGroundingToolDefinition CreateBingGroundingToolDefinition(AgentToolDefinition tool) { - return new BingGroundingToolDefinition(); + Verify.NotNull(tool); + + ToolConnectionList bingGrounding = tool.GetToolConnectionList(); + + return new BingGroundingToolDefinition(bingGrounding); } private static CodeInterpreterToolDefinition CreateCodeInterpreterToolDefinition(AgentToolDefinition tool) @@ -81,27 +97,57 @@ private static CodeInterpreterToolDefinition CreateCodeInterpreterToolDefinition private static FileSearchToolDefinition CreateFileSearchToolDefinition(AgentToolDefinition tool) { - return new FileSearchToolDefinition(); + Verify.NotNull(tool); + + return new FileSearchToolDefinition() + { + FileSearch = tool.GetFileSearchToolDefinitionDetails() + }; } private static FunctionToolDefinition CreateFunctionToolDefinition(AgentToolDefinition tool) { - return new FunctionToolDefinition(); + Verify.NotNull(tool); + Verify.NotNull(tool.Name); + Verify.NotNull(tool.Description); + + string name = tool.Name; + string description = tool.Description; + BinaryData parameters = tool.GetParameters(); + + return new FunctionToolDefinition(name, description, parameters); } private static MicrosoftFabricToolDefinition CreateMicrosoftFabricToolDefinition(AgentToolDefinition tool) { - return new MicrosoftFabricToolDefinition(); + Verify.NotNull(tool); + + ToolConnectionList fabricAiskill = tool.GetToolConnectionList(); + + return new MicrosoftFabricToolDefinition(fabricAiskill); } private static OpenApiToolDefinition CreateOpenApiToolDefinition(AgentToolDefinition tool) { - return new OpenApiToolDefinition(); + Verify.NotNull(tool); + Verify.NotNull(tool.Name); + Verify.NotNull(tool.Description); + + string name = tool.Name; + string description = tool.Description; + BinaryData spec = tool.GetSpecification(); + OpenApiAuthDetails auth = tool.GetOpenApiAuthDetails(); + + return new OpenApiToolDefinition(name, description, spec, auth); } private static SharepointToolDefinition CreateSharepointToolDefinition(AgentToolDefinition tool) { - return new SharepointToolDefinition(); + Verify.NotNull(tool); + + ToolConnectionList sharepointGrounding = tool.GetToolConnectionList(); + + return new SharepointToolDefinition(sharepointGrounding); } #endregion } diff --git a/dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs b/dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs index b5a7ed7d9733..b14d920d01c9 100644 --- a/dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs +++ b/dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; using Azure.AI.Projects; namespace Microsoft.SemanticKernel.Agents.AzureAI; @@ -13,9 +16,155 @@ internal static AzureFunctionBinding GetInputBinding(this AgentToolDefinition ag { Verify.NotNull(agentToolDefinition.Configuration); - string storageServiceEndpoint; - string queueName; + string storageServiceEndpoint = agentToolDefinition.GetRequiredConfiguration("input_storage_service_endpoint"); + string queueName = agentToolDefinition.GetRequiredConfiguration("input_queue_name"); return new AzureFunctionBinding(new AzureFunctionStorageQueue(storageServiceEndpoint, queueName)); } + + internal static AzureFunctionBinding GetOutputBinding(this AgentToolDefinition agentToolDefinition) + { + Verify.NotNull(agentToolDefinition.Configuration); + + string storageServiceEndpoint = agentToolDefinition.GetRequiredConfiguration("output_storage_service_endpoint"); + string queueName = agentToolDefinition.GetRequiredConfiguration("output_queue_name"); + + return new AzureFunctionBinding(new AzureFunctionStorageQueue(storageServiceEndpoint, queueName)); + } + + internal static BinaryData GetParameters(this AgentToolDefinition agentToolDefinition) + { + Verify.NotNull(agentToolDefinition.Configuration); + + var parameters = agentToolDefinition.GetConfiguration("parameters"); + + return parameters is not null ? new BinaryData(parameters) : s_noParams; + } + + internal static FileSearchToolDefinitionDetails GetFileSearchToolDefinitionDetails(this AgentToolDefinition agentToolDefinition) + { + var details = new FileSearchToolDefinitionDetails() + { + MaxNumResults = agentToolDefinition.GetConfiguration("max_num_results") + }; + + FileSearchRankingOptions? rankingOptions = agentToolDefinition.GetFileSearchRankingOptions(); + if (rankingOptions is not null) + { + details.RankingOptions = rankingOptions; + } + + return details; + } + + internal static ToolConnectionList GetToolConnectionList(this AgentToolDefinition agentToolDefinition) + { + Verify.NotNull(agentToolDefinition.Configuration); + + var toolConnections = agentToolDefinition.GetToolConnections(); + + var toolConnectionList = new ToolConnectionList(); + if (toolConnections is not null) + { + toolConnectionList.ConnectionList.AddRange(toolConnections); + } + return toolConnectionList; + } + + internal static BinaryData GetSpecification(this AgentToolDefinition agentToolDefinition) + { + Verify.NotNull(agentToolDefinition.Configuration); + + var specification = agentToolDefinition.GetRequiredConfiguration("specification"); + + return new BinaryData(specification); + } + + internal static OpenApiAuthDetails GetOpenApiAuthDetails(this AgentToolDefinition agentToolDefinition) + { + Verify.NotNull(agentToolDefinition.Configuration); + + var connectionId = agentToolDefinition.GetConfiguration("connection_id"); + if (string.IsNullOrEmpty(connectionId)) + { + return new OpenApiConnectionAuthDetails(new OpenApiConnectionSecurityScheme(connectionId)); + } + + var audience = agentToolDefinition.GetConfiguration("audience"); + if (!string.IsNullOrEmpty(audience)) + { + return new OpenApiManagedAuthDetails(new OpenApiManagedSecurityScheme(audience)); + } + + return new OpenApiAnonymousAuthDetails(); + } + + private static FileSearchRankingOptions? GetFileSearchRankingOptions(this AgentToolDefinition agentToolDefinition) + { + string? ranker = agentToolDefinition.GetConfiguration("ranker"); + float? scoreThreshold = agentToolDefinition.GetConfiguration("score_threshold"); + + if (ranker is not null && scoreThreshold is not null) + { + return new FileSearchRankingOptions(ranker, (float)scoreThreshold!); + } + + return null; + } + + private static List GetToolConnections(this AgentToolDefinition agentToolDefinition) + { + Verify.NotNull(agentToolDefinition.Configuration); + + var toolConnections = agentToolDefinition.GetRequiredConfiguration>("tool_connections"); + + return toolConnections.Select(connectionId => new ToolConnection(connectionId)).ToList(); + } + + private static T GetRequiredConfiguration(this AgentToolDefinition agentToolDefinition, string key) + { + Verify.NotNull(agentToolDefinition); + Verify.NotNull(agentToolDefinition.Configuration); + Verify.NotNull(key); + + if (agentToolDefinition.Configuration?.TryGetValue(key, out var value) ?? false) + { + if (value == null) + { + throw new ArgumentNullException($"The configuration key '{key}' must be a non null value."); + } + + try + { + return (T)Convert.ChangeType(value, typeof(T)); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"The configuration key '{key}' must be of type '{typeof(T)}'."); + } + } + + throw new ArgumentException($"The configuration key '{key}' is required."); + } + + private static T? GetConfiguration(this AgentToolDefinition agentToolDefinition, string key) + { + Verify.NotNull(agentToolDefinition); + Verify.NotNull(key); + + if (agentToolDefinition.Configuration?.TryGetValue(key, out var value) ?? false) + { + if (value == null) + { + return default; + } + + return (T?)Convert.ChangeType(value, typeof(T)); + } + + return default; + } + + private static BinaryData s_noParams = BinaryData.FromObjectAsJson(new { type = "object", properties = new { } }); + } diff --git a/dotnet/src/Agents/UnitTests/AzureAI/Definition/AzureAIAgentFactoryTests.cs b/dotnet/src/Agents/UnitTests/AzureAI/Definition/AzureAIAgentFactoryTests.cs index 97f4679e45ee..e1fccc8812f3 100644 --- a/dotnet/src/Agents/UnitTests/AzureAI/Definition/AzureAIAgentFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/AzureAI/Definition/AzureAIAgentFactoryTests.cs @@ -89,9 +89,9 @@ public async Task VerifyCanCreateAzureAIAgentAsync() } /// - /// Azure AI Agent response. + /// Azure AI Agent responses. /// - public const string AzureAIAgentResponse = + internal const string AzureAIAgentResponse = """ { "id": "asst_thdyqg4yVC9ffeILVdEWLONT", @@ -109,7 +109,6 @@ public async Task VerifyCanCreateAzureAIAgentAsync() "response_format": "auto" } """; - #region private private void SetupResponse(HttpStatusCode statusCode, string response) => #pragma warning disable CA2000 // Dispose objects before losing scope diff --git a/dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs b/dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs new file mode 100644 index 000000000000..e7f3f57bf0a4 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Azure.AI.Projects; +using Azure.Core.Pipeline; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.AzureAI; +using SemanticKernel.Agents.UnitTests.AzureAI.Definition; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Yaml; + +/// +/// Unit tests for with . +/// +public class AzureAIKernelAgentYamlTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Kernel _kernel; + + /// + /// Initializes a new instance of the class. + /// + public AzureAIKernelAgentYamlTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, disposeHandler: false); + + var builder = Kernel.CreateBuilder(); + + // Add Azure AI agents client + var client = new AIProjectClient( + "endpoint;subscription_id;resource_group_name;project_name", + new FakeTokenCredential(), + new AIProjectClientOptions() + { Transport = new HttpClientTransport(this._httpClient) }); + builder.Services.AddSingleton(client); + + this._kernel = builder.Build(); + } + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + this._messageHandlerStub.Dispose(); + this._httpClient.Dispose(); + } + + /// + /// Verify the request includes a tool of the specified when creating an Azure AI agent. + /// + [Theory] + [InlineData("code_interpreter")] + [InlineData("azure_aisearch")] + public async Task VerifyRequestIncludesToolAsync(string type) + { + // Arrange + var text = + $""" + type: azureai_agent + name: AzureAIAgent + description: AzureAIAgent Description + instructions: AzureAIAgent Instructions + model: + id: gpt-4o-mini + tools: + - type: {type} + """; + AzureAIAgentFactory factory = new(); + this.SetupResponse(HttpStatusCode.OK, AzureAIAgentFactoryTests.AzureAIAgentResponse); + + // Act + var agent = await factory.CreateAgentFromYamlAsync(this._kernel, text); + + // Assert + Assert.NotNull(agent); + var requestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(requestContent); + var requestJson = JsonSerializer.Deserialize(requestContent); + Assert.Equal(1, requestJson.GetProperty("tools").GetArrayLength()); + Assert.Equal(type, requestJson.GetProperty("tools")[0].GetProperty("type").GetString()); + } + + /// + /// Verify the request includes an Azure Function tool when creating an Azure AI agent. + /// + [Fact] + public async Task VerifyRequestIncludesAzureFunctionAsync() + { + // Arrange + var text = + """ + type: azureai_agent + name: AzureAIAgent + description: AzureAIAgent Description + instructions: AzureAIAgent Instructions + model: + id: gpt-4o-mini + tools: + - type: azure_function + name: function1 + description: function1 description + input_binding: + storage_service_endpoint: https://storage_service_endpoint + queue_name: queue_name + output_binding: + storage_service_endpoint: https://storage_service_endpoint + queue_name: queue_name + parameters: + - name: param1 + type: string + description: param1 description + - name: param2 + type: string + description: param2 description + """; + AzureAIAgentFactory factory = new(); + this.SetupResponse(HttpStatusCode.OK, AzureAIAgentFactoryTests.AzureAIAgentResponse); + + // Act + var agent = await factory.CreateAgentFromYamlAsync(this._kernel, text); + + // Assert + Assert.NotNull(agent); + var requestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(requestContent); + var requestJson = JsonSerializer.Deserialize(requestContent); + Assert.Equal(1, requestJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("azure_function", requestJson.GetProperty("tools")[0].GetProperty("type").GetString()); + } + + #region private + private void SetupResponse(HttpStatusCode statusCode, string response) => +#pragma warning disable CA2000 // Dispose objects before losing scope + this._messageHandlerStub.ResponseQueue.Enqueue(new(statusCode) + { + Content = new StringContent(response) + }); + #endregion +} From be7da13861348750ca6b75d014dd5ddcb7d071fc Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:22:28 +0000 Subject: [PATCH 3/6] Unit tests are passing for Azure AI Agent tool definitions --- .../Step08_AzureAIAgent_Declarative.cs | 96 +++++++-- .../Step07_Assistant_Declarative.cs | 121 +++++++++++ .../Step08_Declarative.cs | 184 ++++++++++++++++ .../Extensions/AgentDefinitionExtensions.cs | 14 +- .../AgentToolDefinitionExtensions.cs | 56 +++-- .../OpenAI/Extensions/KernelExtensions.cs | 7 +- .../ModelConfigurationExtensions.cs | 11 +- .../Yaml/AzureAIKernelAgentYamlTests.cs | 203 +++++++++++++++++- .../UnitTests/Yaml/KernelAgentYamlTests.cs | 6 +- dotnet/src/Agents/Yaml/AgentDefinitionYaml.cs | 2 + .../Yaml/AgentToolDefinitionTypeConverter.cs | 67 ++++++ .../Yaml/KernelAgentFactoryYamlExtensions.cs | 6 +- .../Yaml/ModelConfigurationTypeConverter.cs | 64 ++++++ .../samples/AgentUtilities/BaseAgentsTest.cs | 22 +- 14 files changed, 791 insertions(+), 68 deletions(-) create mode 100644 dotnet/samples/GettingStartedWithAgents/OpenAIAssistant/Step07_Assistant_Declarative.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Step08_Declarative.cs create mode 100644 dotnet/src/Agents/Yaml/AgentToolDefinitionTypeConverter.cs create mode 100644 dotnet/src/Agents/Yaml/ModelConfigurationTypeConverter.cs diff --git a/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs b/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs index acb868d26972..308a93624a81 100644 --- a/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs +++ b/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs @@ -21,6 +21,46 @@ public Step08_AzureAIAgent_Declarative(ITestOutputHelper output) : base(output) this._kernel = builder.Build(); } + [Fact] + public async Task AzureAIAgentWithConfigurationAsync() + { + var text = + $""" + type: azureai_agent + name: StoryAgent + description: Store Telling Agent + instructions: Tell a story suitable for children about the topic provided by the user. + model: + id: gpt-4o-mini + configuration: + connection_string: {TestConfiguration.AzureAI.ConnectionString} + """; + AzureAIAgentFactory factory = new(); + + var agent = await factory.CreateAgentFromYamlAsync(text) as AzureAIAgent; + + await InvokeAgentAsync(agent!, "Cats and Dogs"); + } + + [Fact] + public async Task AzureAIAgentWithKernelAsync() + { + var text = + """ + type: azureai_agent + name: StoryAgent + description: Store Telling Agent + instructions: Tell a story suitable for children about the topic provided by the user. + model: + id: gpt-4o-mini + """; + AzureAIAgentFactory factory = new(); + + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel) as AzureAIAgent; + + await InvokeAgentAsync(agent!, "Cats and Dogs"); + } + [Fact] public async Task AzureAIAgentWithCodeInterpreterAsync() { @@ -38,9 +78,9 @@ public async Task AzureAIAgentWithCodeInterpreterAsync() """; AzureAIAgentFactory factory = new(); - var agent = await factory.CreateAgentFromYamlAsync(this._kernel, text) as AzureAIAgent; + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel) as AzureAIAgent; - await InvokeAgentAsync(agent, "Use code to determine the values in the Fibonacci sequence that that are less then the value of 101?"); + await InvokeAgentAsync(agent!, "Use code to determine the values in the Fibonacci sequence that that are less then the value of 101?"); } [Fact] @@ -58,17 +98,49 @@ public async Task AzureAIAgentWithOpenApiAsync() - type: openapi name: RestCountriesAPI description: Web API version 3.1 for managing country items, based on previous implementations from restcountries.eu and restcountries.com. - schema: '{"openapi":"3.1.0","info":{"title":"Get Weather Data","description":"Retrieves current weather data for a location based on wttr.in.","version":"v1.0.0"},"servers":[{"url":"https://wttr.in"}],"auth":[],"paths":{"/{location}":{"get":{"description":"Get weather information for a specific location","operationId":"GetCurrentWeather","parameters":[{"name":"location","in":"path","description":"City or location to retrieve the weather for","required":true,"schema":{"type":"string"}},{"name":"format","in":"query","description":"Always use j1 value for this parameter","required":true,"schema":{"type":"string","default":"j1"}}],"responses":{"200":{"description":"Successful response","content":{"text/plain":{"schema":{"type":"string"}}}},"404":{"description":"Location not found"}},"deprecated":false}}},"components":{"schemes":{}}}' + specification: '{"openapi":"3.1.0","info":{"title":"Get Weather Data","description":"Retrieves current weather data for a location based on wttr.in.","version":"v1.0.0"},"servers":[{"url":"https://wttr.in"}],"auth":[],"paths":{"/{location}":{"get":{"description":"Get weather information for a specific location","operationId":"GetCurrentWeather","parameters":[{"name":"location","in":"path","description":"City or location to retrieve the weather for","required":true,"schema":{"type":"string"}},{"name":"format","in":"query","description":"Always use j1 value for this parameter","required":true,"schema":{"type":"string","default":"j1"}}],"responses":{"200":{"description":"Successful response","content":{"text/plain":{"schema":{"type":"string"}}}},"404":{"description":"Location not found"}},"deprecated":false}}},"components":{"schemes":{}}}' """; AzureAIAgentFactory factory = new(); - var agent = await factory.CreateAgentFromYamlAsync(this._kernel, text) as AzureAIAgent; + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel) as AzureAIAgent; + + await InvokeAgentAsync(agent!, "Use code to determine the values in the Fibonacci sequence that that are less then the value of 101?"); + } + + [Fact] + public async Task AzureAIAgentWithFunctionsAsync() + { + var text = + """ + type: azureai_agent + name: RestaurantHost + instructions: Answer questions about the menu. + description: This agent answers questions about the menu. + model: + id: gpt-4o-mini + options: + temperature: 0.4 + tools: + - type: function + name: MenuPlugin.GetSpecials + description: Retrieves the specials for the day. + - type: function + name: MenuPlugin.GetItemPrice + description: Retrieves the price of an item on the menu. + parameters: + """; + AzureAIAgentFactory factory = new(); + + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + this._kernel.Plugins.Add(plugin); + + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel) as AzureAIAgent; - await InvokeAgentAsync(agent, "Use code to determine the values in the Fibonacci sequence that that are less then the value of 101?"); + await InvokeAgentAsync(agent!, "What is the special soup and how much does it cost?"); } [Fact] - public async Task AzureAIAgentWithLocalFunctionsAsync() + public async Task AzureAIAgentWithFunctionsViaOptionsAsync() { var text = """ @@ -91,9 +163,9 @@ public async Task AzureAIAgentWithLocalFunctionsAsync() KernelPlugin plugin = KernelPluginFactory.CreateFromType(); this._kernel.Plugins.Add(plugin); - var agent = await factory.CreateAgentFromYamlAsync(this._kernel, text) as AzureAIAgent; + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel) as AzureAIAgent; - await InvokeAgentAsync(agent, "What is the special soup and how much does it cost?"); + await InvokeAgentAsync(agent!, "What is the special soup and how much does it cost?"); } #region private @@ -102,10 +174,10 @@ public async Task AzureAIAgentWithLocalFunctionsAsync() /// /// Invoke the agent with the user input. /// - private async Task InvokeAgentAsync(AzureAIAgent? agent, string input) + private async Task InvokeAgentAsync(AzureAIAgent agent, string input) { // Create a thread for the agent conversation. - AgentThread thread = await this.AgentsClient.CreateThreadAsync(metadata: SampleMetadata); + AgentThread thread = await agent.Client.CreateThreadAsync(metadata: SampleMetadata); // Respond to user input try @@ -114,8 +186,8 @@ private async Task InvokeAgentAsync(AzureAIAgent? agent, string input) } finally { - await this.AgentsClient.DeleteThreadAsync(thread.Id); - await this.AgentsClient.DeleteAgentAsync(agent!.Id); + await agent.Client.DeleteThreadAsync(thread.Id); + await agent.Client.DeleteAgentAsync(agent!.Id); } // Local function to invoke agent and display the conversation messages. diff --git a/dotnet/samples/GettingStartedWithAgents/OpenAIAssistant/Step07_Assistant_Declarative.cs b/dotnet/samples/GettingStartedWithAgents/OpenAIAssistant/Step07_Assistant_Declarative.cs new file mode 100644 index 000000000000..e52e1935fd21 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/OpenAIAssistant/Step07_Assistant_Declarative.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft. All rights reserved. +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI; + +namespace GettingStarted.OpenAIAssistants; + +/// +/// This example demonstrates how to declaratively create instances of . +/// +public class Step07_Assistant_Declarative : BaseAssistantTest +{ + public Step07_Assistant_Declarative(ITestOutputHelper output) : base(output) + { + var builder = Kernel.CreateBuilder(); + builder.Services.AddSingleton(this.Client); + this._kernel = builder.Build(); + } + + [Fact] + public async Task OpenAIAssistantAgentWithConfigurationForOpenAIAsync() + { + var text = + $""" + type: openai_assistant + name: StoryAgent + description: Store Telling Agent + instructions: Tell a story suitable for children about the topic provided by the user. + model: + id: gpt-4o-mini + configuration: + type: openai + api_key: {TestConfiguration.OpenAI.ApiKey} + """; + OpenAIAssistantAgentFactory factory = new(); + + var agent = await factory.CreateAgentFromYamlAsync(text) as OpenAIAssistantAgent; + + await InvokeAgentAsync(agent!, "Cats and Dogs"); + } + + [Fact] + public async Task OpenAIAssistantAgentWithConfigurationForAzureOpenAIAsync() + { + var text = + $""" + type: openai_assistant + name: StoryAgent + description: Store Telling Agent + instructions: Tell a story suitable for children about the topic provided by the user. + model: + id: gpt-4o-mini + configuration: + type: azure_openai + endpoint: {TestConfiguration.AzureOpenAI.Endpoint} + """; + OpenAIAssistantAgentFactory factory = new(); + + var agent = await factory.CreateAgentFromYamlAsync(text) as OpenAIAssistantAgent; + + await InvokeAgentAsync(agent!, "Cats and Dogs"); + } + + [Fact] + public async Task OpenAIAssistantAgentWithKernelAsync() + { + var text = + """ + type: openai_assistant + name: StoryAgent + description: Store Telling Agent + instructions: Tell a story suitable for children about the topic provided by the user. + model: + id: gpt-4o-mini + """; + OpenAIAssistantAgentFactory factory = new(); + + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel) as OpenAIAssistantAgent; + + await InvokeAgentAsync(agent!, "Cats and Dogs"); + } + + #region private + private readonly Kernel _kernel; + + /// + /// Invoke the agent with the user input. + /// + private async Task InvokeAgentAsync(OpenAIAssistantAgent agent, string input) + { + // Create a thread for the agent conversation. + string threadId = await agent.Client.CreateThreadAsync(metadata: SampleMetadata); + + try + { + await InvokeAgentAsync(input); + } + finally + { + await agent.Client.DeleteThreadAsync(threadId); + await agent.Client.DeleteAssistantAsync(agent.Id); + } + + // Local function to invoke agent and display the response. + async Task InvokeAgentAsync(string input) + { + ChatMessageContent message = new(AuthorRole.User, input); + await agent.AddChatMessageAsync(threadId, message); + this.WriteAgentChatMessage(message); + + await foreach (ChatMessageContent response in agent.InvokeAsync(threadId)) + { + WriteAgentChatMessage(response); + } + } + } + #endregion +} diff --git a/dotnet/samples/GettingStartedWithAgents/Step08_Declarative.cs b/dotnet/samples/GettingStartedWithAgents/Step08_Declarative.cs new file mode 100644 index 000000000000..358445ccc671 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Step08_Declarative.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel; +using Azure.AI.Projects; +using Azure.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.AzureAI; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI; + +namespace GettingStarted; + +/// +/// This example demonstrates how to declaratively create instances of . +/// +public class Step08_Declarative : BaseAgentsTest +{ + public Step08_Declarative(ITestOutputHelper output) : base(output) + { + var openaiClient = + this.UseOpenAIConfig ? + OpenAIAssistantAgent.CreateOpenAIClient(new ApiKeyCredential(this.ApiKey ?? throw new ConfigurationNotFoundException("OpenAI:ApiKey"))) : + !string.IsNullOrWhiteSpace(this.ApiKey) ? + OpenAIAssistantAgent.CreateAzureOpenAIClient(new ApiKeyCredential(this.ApiKey), new Uri(this.Endpoint!)) : + OpenAIAssistantAgent.CreateAzureOpenAIClient(new AzureCliCredential(), new Uri(this.Endpoint!)); + + var aiProjectClient = AzureAIAgent.CreateAzureAIClient(TestConfiguration.AzureAI.ConnectionString, new AzureCliCredential()); + + var builder = Kernel.CreateBuilder(); + builder.Services.AddSingleton(openaiClient); + builder.Services.AddSingleton(aiProjectClient); + AddChatCompletionToKernel(builder); + this._kernel = builder.Build(); + + this._kernelAgentFactory = new AggregatorKernelAgentFactory( + new ChatCompletionAgentFactory(), + new OpenAIAssistantAgentFactory(), + new AzureAIAgentFactory() + ); + } + + [Fact] + public async Task ChatCompletionAgentWithKernelAsync() + { + var text = + """ + type: chat_completion_agent + name: StoryAgent + description: Store Telling Agent + instructions: Tell a story suitable for children about the topic provided by the user. + """; + + var agent = await this._kernelAgentFactory.CreateAgentFromYamlAsync(text, this._kernel) as ChatCompletionAgent; + + await InvokeAgentAsync(agent!, "Cats and Dogs"); + } + + [Fact] + public async Task OpenAIAssistantAgentWithKernelAsync() + { + var text = + """ + type: openai_assistant + name: StoryAgent + description: Store Telling Agent + instructions: Tell a story suitable for children about the topic provided by the user. + model: + id: gpt-4o-mini + """; + + var agent = await this._kernelAgentFactory.CreateAgentFromYamlAsync(text, this._kernel) as OpenAIAssistantAgent; + + await InvokeAgentAsync(agent!, "Cats and Dogs"); + } + + [Fact] + public async Task AzureAIAgentWithKernelAsync() + { + var text = + """ + type: azureai_agent + name: StoryAgent + description: Store Telling Agent + instructions: Tell a story suitable for children about the topic provided by the user. + model: + id: gpt-4o-mini + """; + + var agent = await this._kernelAgentFactory.CreateAgentFromYamlAsync(text, this._kernel) as AzureAIAgent; + + await InvokeAgentAsync(agent!, "Cats and Dogs"); + } + + #region private + private readonly Kernel _kernel; + private readonly KernelAgentFactory _kernelAgentFactory; + + /// + /// Invoke the with the user input. + /// + private async Task InvokeAgentAsync(ChatCompletionAgent agent, string input) + { + ChatHistory chat = []; + ChatMessageContent message = new(AuthorRole.User, input); + chat.Add(message); + this.WriteAgentChatMessage(message); + + await foreach (ChatMessageContent response in agent.InvokeAsync(chat)) + { + chat.Add(response); + + this.WriteAgentChatMessage(response); + } + } + + /// + /// Invoke the with the user input. + /// + private async Task InvokeAgentAsync(OpenAIAssistantAgent agent, string input) + { + // Create a thread for the agent conversation. + string threadId = await agent.Client.CreateThreadAsync(metadata: SampleMetadata); + + try + { + await InvokeAgentAsync(input); + } + finally + { + await agent.Client.DeleteThreadAsync(threadId); + await agent.Client.DeleteAssistantAsync(agent.Id); + } + + // Local function to invoke agent and display the response. + async Task InvokeAgentAsync(string input) + { + ChatMessageContent message = new(AuthorRole.User, input); + await agent.AddChatMessageAsync(threadId, message); + this.WriteAgentChatMessage(message); + + await foreach (ChatMessageContent response in agent.InvokeAsync(threadId)) + { + WriteAgentChatMessage(response); + } + } + } + + /// + /// Invoke the with the user input. + /// + private async Task InvokeAgentAsync(AzureAIAgent agent, string input) + { + // Create a thread for the agent conversation. + AgentThread thread = await agent.Client.CreateThreadAsync(metadata: SampleMetadata); + + // Respond to user input + try + { + await InvokeAsync(input); + } + finally + { + await agent.Client.DeleteThreadAsync(thread.Id); + await agent.Client.DeleteAgentAsync(agent!.Id); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeAsync(string input) + { + ChatMessageContent message = new(AuthorRole.User, input); + await agent!.AddChatMessageAsync(thread.Id, message); + this.WriteAgentChatMessage(message); + + await foreach (ChatMessageContent response in agent.InvokeAsync(thread.Id)) + { + this.WriteAgentChatMessage(response); + } + } + } + #endregion +} diff --git a/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs b/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs index 189ecbd083bf..22b3f26a0622 100644 --- a/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs +++ b/dotnet/src/Agents/AzureAI/Extensions/AgentDefinitionExtensions.cs @@ -11,15 +11,15 @@ namespace Microsoft.SemanticKernel.Agents.AzureAI; /// internal static class AgentDefinitionExtensions { - private const string AzureAISearchType = "azure_aisearch"; + private const string AzureAISearchType = "azure_ai_search"; private const string AzureFunctionType = "azure_function"; private const string BingGroundingType = "bing_grounding"; private const string CodeInterpreterType = "code_interpreter"; private const string FileSearchType = "file_search"; - private const string FunctionToolType = "function_tool"; - private const string MicrosoftFabricType = "microsoft_fabric"; + private const string FunctionType = "function"; + private const string MicrosoftFabricType = "fabric_aiskill"; private const string OpenApiType = "openapi"; - private const string SharepointType = "sharepoint"; + private const string SharepointGroundingType = "sharepoint_grounding"; /// /// Return the Azure AI tool definitions which corresponds with the provided . @@ -37,10 +37,10 @@ public static IEnumerable GetAzureToolDefinitions(this AgentDefi BingGroundingType => CreateBingGroundingToolDefinition(tool), CodeInterpreterType => CreateCodeInterpreterToolDefinition(tool), FileSearchType => CreateFileSearchToolDefinition(tool), - FunctionToolType => CreateFunctionToolDefinition(tool), + FunctionType => CreateFunctionToolDefinition(tool), MicrosoftFabricType => CreateMicrosoftFabricToolDefinition(tool), OpenApiType => CreateOpenApiToolDefinition(tool), - SharepointType => CreateSharepointToolDefinition(tool), + SharepointGroundingType => CreateSharepointGroundingToolDefinition(tool), _ => throw new InvalidOperationException($"Unable to created Azure AI tool definition because of known tool type: {tool.Type}"), }; }); @@ -141,7 +141,7 @@ private static OpenApiToolDefinition CreateOpenApiToolDefinition(AgentToolDefini return new OpenApiToolDefinition(name, description, spec, auth); } - private static SharepointToolDefinition CreateSharepointToolDefinition(AgentToolDefinition tool) + private static SharepointToolDefinition CreateSharepointGroundingToolDefinition(AgentToolDefinition tool) { Verify.NotNull(tool); diff --git a/dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs b/dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs index b14d920d01c9..e334d83c1eed 100644 --- a/dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs +++ b/dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs @@ -14,30 +14,21 @@ internal static class AgentToolDefinitionExtensions { internal static AzureFunctionBinding GetInputBinding(this AgentToolDefinition agentToolDefinition) { - Verify.NotNull(agentToolDefinition.Configuration); - - string storageServiceEndpoint = agentToolDefinition.GetRequiredConfiguration("input_storage_service_endpoint"); - string queueName = agentToolDefinition.GetRequiredConfiguration("input_queue_name"); - - return new AzureFunctionBinding(new AzureFunctionStorageQueue(storageServiceEndpoint, queueName)); + return agentToolDefinition.GetAzureFunctionBinding("input_binding"); } internal static AzureFunctionBinding GetOutputBinding(this AgentToolDefinition agentToolDefinition) { - Verify.NotNull(agentToolDefinition.Configuration); - - string storageServiceEndpoint = agentToolDefinition.GetRequiredConfiguration("output_storage_service_endpoint"); - string queueName = agentToolDefinition.GetRequiredConfiguration("output_queue_name"); - - return new AzureFunctionBinding(new AzureFunctionStorageQueue(storageServiceEndpoint, queueName)); + return agentToolDefinition.GetAzureFunctionBinding("output_binding"); } internal static BinaryData GetParameters(this AgentToolDefinition agentToolDefinition) { Verify.NotNull(agentToolDefinition.Configuration); - var parameters = agentToolDefinition.GetConfiguration("parameters"); + var parameters = agentToolDefinition.GetConfiguration>("parameters"); + // TODO Needswork return parameters is not null ? new BinaryData(parameters) : s_noParams; } @@ -75,7 +66,7 @@ internal static BinaryData GetSpecification(this AgentToolDefinition agentToolDe { Verify.NotNull(agentToolDefinition.Configuration); - var specification = agentToolDefinition.GetRequiredConfiguration("specification"); + var specification = agentToolDefinition.GetRequiredConfiguration>("specification"); return new BinaryData(specification); } @@ -85,7 +76,7 @@ internal static OpenApiAuthDetails GetOpenApiAuthDetails(this AgentToolDefinitio Verify.NotNull(agentToolDefinition.Configuration); var connectionId = agentToolDefinition.GetConfiguration("connection_id"); - if (string.IsNullOrEmpty(connectionId)) + if (!string.IsNullOrEmpty(connectionId)) { return new OpenApiConnectionAuthDetails(new OpenApiConnectionSecurityScheme(connectionId)); } @@ -99,6 +90,23 @@ internal static OpenApiAuthDetails GetOpenApiAuthDetails(this AgentToolDefinitio return new OpenApiAnonymousAuthDetails(); } + private static AzureFunctionBinding GetAzureFunctionBinding(this AgentToolDefinition agentToolDefinition, string bindingType) + { + Verify.NotNull(agentToolDefinition.Configuration); + + var binding = agentToolDefinition.GetRequiredConfiguration>(bindingType); + if (!binding.TryGetValue("storage_service_endpoint", out var endpointValue) || endpointValue is not string storageServiceEndpoint) + { + throw new ArgumentException($"The configuration key '{bindingType}.storage_service_endpoint' is required."); + } + if (!binding.TryGetValue("queue_name", out var nameValue) || nameValue is not string queueName) + { + throw new ArgumentException($"The configuration key '{bindingType}.queue_name' is required."); + } + + return new AzureFunctionBinding(new AzureFunctionStorageQueue(storageServiceEndpoint, queueName)); + } + private static FileSearchRankingOptions? GetFileSearchRankingOptions(this AgentToolDefinition agentToolDefinition) { string? ranker = agentToolDefinition.GetConfiguration("ranker"); @@ -116,9 +124,9 @@ private static List GetToolConnections(this AgentToolDefinition { Verify.NotNull(agentToolDefinition.Configuration); - var toolConnections = agentToolDefinition.GetRequiredConfiguration>("tool_connections"); + var toolConnections = agentToolDefinition.GetRequiredConfiguration>("tool_connections"); - return toolConnections.Select(connectionId => new ToolConnection(connectionId)).ToList(); + return toolConnections.Select(connectionId => new ToolConnection(connectionId.ToString())).ToList(); } private static T GetRequiredConfiguration(this AgentToolDefinition agentToolDefinition, string key) @@ -140,7 +148,7 @@ private static T GetRequiredConfiguration(this AgentToolDefinition agentToolD } catch (InvalidCastException) { - throw new InvalidCastException($"The configuration key '{key}' must be of type '{typeof(T)}'."); + throw new InvalidCastException($"The configuration key '{key}' value must be of type '{typeof(T)}' but is '{value.GetType()}'."); } } @@ -159,12 +167,18 @@ private static T GetRequiredConfiguration(this AgentToolDefinition agentToolD return default; } - return (T?)Convert.ChangeType(value, typeof(T)); + try + { + return (T?)Convert.ChangeType(value, typeof(T)); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"The configuration key '{key}' value must be of type '{typeof(T?)}' but is '{value.GetType()}'."); + } } return default; } - private static BinaryData s_noParams = BinaryData.FromObjectAsJson(new { type = "object", properties = new { } }); - + private static readonly BinaryData s_noParams = BinaryData.FromObjectAsJson(new { type = "object", properties = new { } }); } diff --git a/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs index 5d8436108686..68bddb1034e8 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs @@ -35,7 +35,7 @@ public static OpenAIClient GetOpenAIClient(this Kernel kernel, AgentDefinition a { if (configuration.Type is null) { - throw new InvalidOperationException("OpenAI client type was not specified."); + throw new InvalidOperationException("OpenAI client type must be specified."); } var httpClient = kernel.GetAllServices().FirstOrDefault(); @@ -46,8 +46,11 @@ public static OpenAIClient GetOpenAIClient(this Kernel kernel, AgentDefinition a } else if (configuration.Type.Equals(AzureOpenAI, StringComparison.OrdinalIgnoreCase)) { - AzureOpenAIClientOptions clientOptions = OpenAIClientProvider.CreateAzureClientOptions(httpClient); var endpoint = configuration.GetEndpointUri(); + Verify.NotNull(endpoint, "Endpoint must be specified when using Azure OpenAI."); + + AzureOpenAIClientOptions clientOptions = OpenAIClientProvider.CreateAzureClientOptions(httpClient); + if (configuration.TryGetValue(ApiKey, out var apiKey) && apiKey is not null) { return new AzureOpenAIClient(endpoint, configuration.GetApiKeyCredential(), clientOptions); diff --git a/dotnet/src/Agents/OpenAI/Extensions/ModelConfigurationExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/ModelConfigurationExtensions.cs index 5ce92acdfd58..7799afbcfa7f 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/ModelConfigurationExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/ModelConfigurationExtensions.cs @@ -16,15 +16,14 @@ internal static class ModelConfigurationExtensions /// Gets the endpoint property as a from the specified . /// /// Model configuration - internal static Uri GetEndpointUri(this ModelConfiguration configuration) + internal static Uri? GetEndpointUri(this ModelConfiguration configuration) { - Verify.NotNull(configuration); - - if (!configuration.TryGetValue("endpoint", out var endpoint) || endpoint is null) + if (configuration.TryGetValue("endpoint", out var endpoint) && endpoint is not null) { - throw new InvalidOperationException("Endpoint was not specified."); + return new Uri(endpoint.ToString()!); } - return new Uri(endpoint.ToString()!); + + return null; } /// diff --git a/dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs b/dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs index e7f3f57bf0a4..f83c0758bd18 100644 --- a/dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs +++ b/dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs @@ -60,7 +60,7 @@ public void Dispose() /// [Theory] [InlineData("code_interpreter")] - [InlineData("azure_aisearch")] + [InlineData("azure_ai_search")] public async Task VerifyRequestIncludesToolAsync(string type) { // Arrange @@ -79,7 +79,7 @@ public async Task VerifyRequestIncludesToolAsync(string type) this.SetupResponse(HttpStatusCode.OK, AzureAIAgentFactoryTests.AzureAIAgentResponse); // Act - var agent = await factory.CreateAgentFromYamlAsync(this._kernel, text); + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel); // Assert Assert.NotNull(agent); @@ -127,7 +127,7 @@ public async Task VerifyRequestIncludesAzureFunctionAsync() this.SetupResponse(HttpStatusCode.OK, AzureAIAgentFactoryTests.AzureAIAgentResponse); // Act - var agent = await factory.CreateAgentFromYamlAsync(this._kernel, text); + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel); // Assert Assert.NotNull(agent); @@ -138,6 +138,203 @@ public async Task VerifyRequestIncludesAzureFunctionAsync() Assert.Equal("azure_function", requestJson.GetProperty("tools")[0].GetProperty("type").GetString()); } + /// + /// Verify the request includes a Function when creating an Azure AI agent. + /// + [Fact] + public async Task VerifyRequestIncludesFunctionAsync() + { + // Arrange + var text = + """ + type: azureai_agent + name: AzureAIAgent + description: AzureAIAgent Description + instructions: AzureAIAgent Instructions + model: + id: gpt-4o-mini + tools: + - type: function + name: function1 + description: function1 description + parameters: + - name: param1 + type: string + description: param1 description + - name: param2 + type: string + description: param2 description + """; + AzureAIAgentFactory factory = new(); + this.SetupResponse(HttpStatusCode.OK, AzureAIAgentFactoryTests.AzureAIAgentResponse); + + // Act + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel); + + // Assert + Assert.NotNull(agent); + var requestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(requestContent); + var requestJson = JsonSerializer.Deserialize(requestContent); + Assert.Equal(1, requestJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("function", requestJson.GetProperty("tools")[0].GetProperty("type").GetString()); + } + + /// + /// Verify the request includes a Bing Grounding tool when creating an Azure AI agent. + /// + [Fact] + public async Task VerifyRequestIncludesBingGroundingAsync() + { + // Arrange + var text = + """ + type: azureai_agent + name: AzureAIAgent + description: AzureAIAgent Description + instructions: AzureAIAgent Instructions + model: + id: gpt-4o-mini + tools: + - type: bing_grounding + tool_connections: + - connection_string + """; + AzureAIAgentFactory factory = new(); + this.SetupResponse(HttpStatusCode.OK, AzureAIAgentFactoryTests.AzureAIAgentResponse); + + // Act + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel); + + // Assert + Assert.NotNull(agent); + var requestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(requestContent); + var requestJson = JsonSerializer.Deserialize(requestContent); + Assert.Equal(1, requestJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("bing_grounding", requestJson.GetProperty("tools")[0].GetProperty("type").GetString()); + } + + /// + /// Verify the request includes a Microsoft Fabric tool when creating an Azure AI agent. + /// + [Fact] + public async Task VerifyRequestIncludesMicrosoftFabricAsync() + { + // Arrange + var text = + """ + type: azureai_agent + name: AzureAIAgent + description: AzureAIAgent Description + instructions: AzureAIAgent Instructions + model: + id: gpt-4o-mini + tools: + - type: fabric_aiskill + tool_connections: + - connection_string + """; + AzureAIAgentFactory factory = new(); + this.SetupResponse(HttpStatusCode.OK, AzureAIAgentFactoryTests.AzureAIAgentResponse); + + // Act + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel); + + // Assert + Assert.NotNull(agent); + var requestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(requestContent); + var requestJson = JsonSerializer.Deserialize(requestContent); + Assert.Equal(1, requestJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("fabric_aiskill", requestJson.GetProperty("tools")[0].GetProperty("type").GetString()); + } + + /// + /// Verify the request includes a Open API tool when creating an Azure AI agent. + /// + [Fact] + public async Task VerifyRequestIncludesOpenAPIAsync() + { + // Arrange + var text = + """ + type: azureai_agent + name: AzureAIAgent + description: AzureAIAgent Description + instructions: AzureAIAgent Instructions + model: + id: gpt-4o-mini + tools: + - type: openapi + name: function1 + description: function1 description + specification: {"openapi":"3.1.0","info":{"title":"Get Weather Data","description":"Retrieves current weather data for a location based on wttr.in.","version":"v1.0.0"},"servers":[{"url":"https://wttr.in"}],"auth":[],"paths":{"/{location}":{"get":{"description":"Get weather information for a specific location","operationId":"GetCurrentWeather","parameters":[{"name":"location","in":"path","description":"City or location to retrieve the weather for","required":true,"schema":{"type":"string"}},{"name":"format","in":"query","description":"Always use j1 value for this parameter","required":true,"schema":{"type":"string","default":"j1"}}],"responses":{"200":{"description":"Successful response","content":{"text/plain":{"schema":{"type":"string"}}}},"404":{"description":"Location not found"}},"deprecated":false}}},"components":{"schemes":{}}} + - type: openapi + name: function2 + description: function2 description + specification: {"openapi":"3.1.0","info":{"title":"Get Weather Data","description":"Retrieves current weather data for a location based on wttr.in.","version":"v1.0.0"},"servers":[{"url":"https://wttr.in"}],"auth":[],"paths":{"/{location}":{"get":{"description":"Get weather information for a specific location","operationId":"GetCurrentWeather","parameters":[{"name":"location","in":"path","description":"City or location to retrieve the weather for","required":true,"schema":{"type":"string"}},{"name":"format","in":"query","description":"Always use j1 value for this parameter","required":true,"schema":{"type":"string","default":"j1"}}],"responses":{"200":{"description":"Successful response","content":{"text/plain":{"schema":{"type":"string"}}}},"404":{"description":"Location not found"}},"deprecated":false}}},"components":{"schemes":{}}} + authentication: + connection_id: connection_id + - type: openapi + name: function3 + description: function3 description + specification: {"openapi":"3.1.0","info":{"title":"Get Weather Data","description":"Retrieves current weather data for a location based on wttr.in.","version":"v1.0.0"},"servers":[{"url":"https://wttr.in"}],"auth":[],"paths":{"/{location}":{"get":{"description":"Get weather information for a specific location","operationId":"GetCurrentWeather","parameters":[{"name":"location","in":"path","description":"City or location to retrieve the weather for","required":true,"schema":{"type":"string"}},{"name":"format","in":"query","description":"Always use j1 value for this parameter","required":true,"schema":{"type":"string","default":"j1"}}],"responses":{"200":{"description":"Successful response","content":{"text/plain":{"schema":{"type":"string"}}}},"404":{"description":"Location not found"}},"deprecated":false}}},"components":{"schemes":{}}} + authentication: + audience: audience + """; + AzureAIAgentFactory factory = new(); + this.SetupResponse(HttpStatusCode.OK, AzureAIAgentFactoryTests.AzureAIAgentResponse); + + // Act + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel); + + // Assert + Assert.NotNull(agent); + var requestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(requestContent); + var requestJson = JsonSerializer.Deserialize(requestContent); + Assert.Equal(3, requestJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("openapi", requestJson.GetProperty("tools")[0].GetProperty("type").GetString()); + Assert.Equal("openapi", requestJson.GetProperty("tools")[1].GetProperty("type").GetString()); + Assert.Equal("openapi", requestJson.GetProperty("tools")[2].GetProperty("type").GetString()); + } + + /// + /// Verify the request includes a Sharepoint tool when creating an Azure AI agent. + /// + [Fact] + public async Task VerifyRequestIncludesSharepointGroundingAsync() + { + // Arrange + var text = + """ + type: azureai_agent + name: AzureAIAgent + description: AzureAIAgent Description + instructions: AzureAIAgent Instructions + model: + id: gpt-4o-mini + tools: + - type: sharepoint_grounding + tool_connections: + - connection_string + """; + AzureAIAgentFactory factory = new(); + this.SetupResponse(HttpStatusCode.OK, AzureAIAgentFactoryTests.AzureAIAgentResponse); + + // Act + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel); + + // Assert + Assert.NotNull(agent); + var requestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(requestContent); + var requestJson = JsonSerializer.Deserialize(requestContent); + Assert.Equal(1, requestJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("sharepoint_grounding", requestJson.GetProperty("tools")[0].GetProperty("type").GetString()); + } + #region private private void SetupResponse(HttpStatusCode statusCode, string response) => #pragma warning disable CA2000 // Dispose objects before losing scope diff --git a/dotnet/src/Agents/UnitTests/Yaml/KernelAgentYamlTests.cs b/dotnet/src/Agents/UnitTests/Yaml/KernelAgentYamlTests.cs index fe43da2657cb..3bd38539957a 100644 --- a/dotnet/src/Agents/UnitTests/Yaml/KernelAgentYamlTests.cs +++ b/dotnet/src/Agents/UnitTests/Yaml/KernelAgentYamlTests.cs @@ -135,7 +135,7 @@ public async Task VerifyCanCreateChatCompletionAgentAsync() ChatCompletionAgentFactory factory = new(); // Act - var agent = await factory.CreateAgentFromYamlAsync(this._kernel, text); + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel); // Assert Assert.NotNull(agent); @@ -169,7 +169,7 @@ public async Task VerifyCanCreateOpenAIAssistantAsync() this.SetupResponse(HttpStatusCode.OK, OpenAIAssistantAgentFactoryTests.OpenAIAssistantResponse); // Act - var agent = await factory.CreateAgentFromYamlAsync(this._kernel, text); + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel); // Assert Assert.NotNull(agent); @@ -203,7 +203,7 @@ public async Task VerifyCanCreateAzureAIAgentAsync() this.SetupResponse(HttpStatusCode.OK, AzureAIAgentFactoryTests.AzureAIAgentResponse); // Act - var agent = await factory.CreateAgentFromYamlAsync(this._kernel, text); + var agent = await factory.CreateAgentFromYamlAsync(text, this._kernel); // Assert Assert.NotNull(agent); diff --git a/dotnet/src/Agents/Yaml/AgentDefinitionYaml.cs b/dotnet/src/Agents/Yaml/AgentDefinitionYaml.cs index 27bc1699b4c2..1275e640a3e8 100644 --- a/dotnet/src/Agents/Yaml/AgentDefinitionYaml.cs +++ b/dotnet/src/Agents/Yaml/AgentDefinitionYaml.cs @@ -19,6 +19,8 @@ public static AgentDefinition FromYaml(string text) var deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) .WithTypeConverter(new PromptExecutionSettingsTypeConverter()) + .WithTypeConverter(new ModelConfigurationTypeConverter()) + .WithTypeConverter(new AgentToolDefinitionTypeConverter()) .Build(); return deserializer.Deserialize(text); diff --git a/dotnet/src/Agents/Yaml/AgentToolDefinitionTypeConverter.cs b/dotnet/src/Agents/Yaml/AgentToolDefinitionTypeConverter.cs new file mode 100644 index 000000000000..c7c36a39812c --- /dev/null +++ b/dotnet/src/Agents/Yaml/AgentToolDefinitionTypeConverter.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Microsoft.SemanticKernel.Agents; + +/// +/// Allows custom deserialization for from YAML. +/// +internal sealed class AgentToolDefinitionTypeConverter : IYamlTypeConverter +{ + /// + public bool Accepts(Type type) + { + return type == typeof(AgentToolDefinition); + } + + /// + public object? ReadYaml(IParser parser, Type type) + { + s_deserializer ??= new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination. Otherwise, the "Property 'type' not found on type '{type.FullName}'" exception is thrown. + .Build(); + + parser.MoveNext(); // Move to the first property + + var agentToolDefinition = new AgentToolDefinition(); + while (parser.Current is not MappingEnd) + { + var propertyName = parser.Consume().Value; + switch (propertyName) + { + case "type": + agentToolDefinition.Type = s_deserializer.Deserialize(parser); + break; + case "name": + agentToolDefinition.Name = s_deserializer.Deserialize(parser); + break; + case "description": + agentToolDefinition.Description = s_deserializer.Deserialize(parser); + break; + default: + (agentToolDefinition.Configuration ??= new Dictionary()).Add(propertyName, s_deserializer.Deserialize(parser)); + break; + } + } + parser.MoveNext(); // Move past the MappingEnd event + return agentToolDefinition; + } + + /// + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + throw new NotImplementedException(); + } + + /// + /// The YamlDotNet deserializer instance. + /// + private static IDeserializer? s_deserializer; +} diff --git a/dotnet/src/Agents/Yaml/KernelAgentFactoryYamlExtensions.cs b/dotnet/src/Agents/Yaml/KernelAgentFactoryYamlExtensions.cs index 9f8842c78c96..b406bba77bed 100644 --- a/dotnet/src/Agents/Yaml/KernelAgentFactoryYamlExtensions.cs +++ b/dotnet/src/Agents/Yaml/KernelAgentFactoryYamlExtensions.cs @@ -14,16 +14,16 @@ public static class KernelAgentFactoryYamlExtensions /// Create a from the given YAML text. /// /// Kernel agent factory which will be used to create the agent - /// Kernel instance /// YAML in text format + /// Kernel instance /// Optional cancellation token - public static async Task CreateAgentFromYamlAsync(this KernelAgentFactory kernelAgentFactory, Kernel kernel, string text, CancellationToken cancellationToken = default) + public static async Task CreateAgentFromYamlAsync(this KernelAgentFactory kernelAgentFactory, string text, Kernel? kernel = null, CancellationToken cancellationToken = default) { var agentDefinition = AgentDefinitionYaml.FromYaml(text); agentDefinition.Type = agentDefinition.Type ?? (kernelAgentFactory.Types.Count > 0 ? kernelAgentFactory.Types[0] : null); return await kernelAgentFactory.CreateAsync( - kernel, + kernel ?? new Kernel(), agentDefinition, cancellationToken).ConfigureAwait(false); } diff --git a/dotnet/src/Agents/Yaml/ModelConfigurationTypeConverter.cs b/dotnet/src/Agents/Yaml/ModelConfigurationTypeConverter.cs new file mode 100644 index 000000000000..30fdae158be4 --- /dev/null +++ b/dotnet/src/Agents/Yaml/ModelConfigurationTypeConverter.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Microsoft.SemanticKernel.Agents; + +/// +/// Allows custom deserialization for from YAML. +/// +internal sealed class ModelConfigurationTypeConverter : IYamlTypeConverter +{ + /// + public bool Accepts(Type type) + { + return type == typeof(ModelConfiguration); + } + + /// + public object? ReadYaml(IParser parser, Type type) + { + s_deserializer ??= new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination. Otherwise, the "Property 'type' not found on type '{type.FullName}'" exception is thrown. + .Build(); + + parser.MoveNext(); // Move to the first property + + var modelConfiguration = new ModelConfiguration(); + while (parser.Current is not MappingEnd) + { + var propertyName = parser.Consume().Value; + switch (propertyName) + { + case "type": + modelConfiguration.Type = s_deserializer.Deserialize(parser); + break; + case "service_id": + modelConfiguration.ServiceId = s_deserializer.Deserialize(parser); + break; + default: + (modelConfiguration.ExtensionData ??= new Dictionary()).Add(propertyName, s_deserializer.Deserialize(parser)); + break; + } + } + parser.MoveNext(); // Move past the MappingEnd event + return modelConfiguration; + } + + /// + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + throw new NotImplementedException(); + } + + /// + /// The YamlDotNet deserializer instance. + /// + private static IDeserializer? s_deserializer; +} diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs index 7c9ee6a3c654..29b87d49105d 100644 --- a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs @@ -17,6 +17,17 @@ /// based on API's such as Open AI Assistants or Azure AI Agents. /// public abstract class BaseAgentsTest(ITestOutputHelper output) : BaseAgentsTest(output) +{ + /// + /// Gets the root client for the service. + /// + protected abstract TClient Client { get; } +} + +/// +/// Base class for samples that demonstrate the usage of agents. +/// +public abstract class BaseAgentsTest(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true) { /// /// Metadata key to indicate the assistant as created for a sample. @@ -37,17 +48,6 @@ public abstract class BaseAgentsTest(ITestOutputHelper output) : BaseAg { SampleMetadataKey, bool.TrueString } }); - /// - /// Gets the root client for the service. - /// - protected abstract TClient Client { get; } -} - -/// -/// Base class for samples that demonstrate the usage of agents. -/// -public abstract class BaseAgentsTest(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true) -{ /// /// Common method to write formatted agent chat content to the console. /// From fa65bcaae5328383fce6a194ef12ab02f1ea45b2 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Wed, 26 Feb 2025 09:45:03 +0000 Subject: [PATCH 4/6] Fix warnings --- .../Extensions/ModelConfigurationExtensions.cs | 18 ++++++------------ .../Yaml/AgentToolDefinitionTypeConverter.cs | 2 +- .../Yaml/ModelConfigurationTypeConverter.cs | 2 +- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Extensions/ModelConfigurationExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/ModelConfigurationExtensions.cs index 8e0a5f539c5b..d7974e3e70f2 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/ModelConfigurationExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/ModelConfigurationExtensions.cs @@ -20,12 +20,9 @@ internal static class ModelConfigurationExtensions { Verify.NotNull(configuration); - if (!configuration.ExtensionData.TryGetValue("endpoint", out var endpoint) || endpoint is null) - { - return new Uri(endpoint.ToString()!); - } - - return null; + return configuration.ExtensionData.TryGetValue("endpoint", out var value) && value is not null && value is string endpoint + ? new Uri(endpoint) + : null; } /// @@ -36,11 +33,8 @@ internal static ApiKeyCredential GetApiKeyCredential(this ModelConfiguration con { Verify.NotNull(configuration); - if (!configuration.ExtensionData.TryGetValue("api_key", out var apiKey) || apiKey is null) - { - throw new InvalidOperationException("API key was not specified."); - } - - return new ApiKeyCredential(apiKey.ToString()!); + return !configuration.ExtensionData.TryGetValue("api_key", out var apiKey) || apiKey is null + ? throw new InvalidOperationException("API key was not specified.") + : new ApiKeyCredential(apiKey.ToString()!); } } diff --git a/dotnet/src/Agents/Yaml/AgentToolDefinitionTypeConverter.cs b/dotnet/src/Agents/Yaml/AgentToolDefinitionTypeConverter.cs index c7c36a39812c..c5433d81f514 100644 --- a/dotnet/src/Agents/Yaml/AgentToolDefinitionTypeConverter.cs +++ b/dotnet/src/Agents/Yaml/AgentToolDefinitionTypeConverter.cs @@ -46,7 +46,7 @@ public bool Accepts(Type type) agentToolDefinition.Description = s_deserializer.Deserialize(parser); break; default: - (agentToolDefinition.Configuration ??= new Dictionary()).Add(propertyName, s_deserializer.Deserialize(parser)); + (agentToolDefinition.Configuration ??= new Dictionary()).Add(propertyName, s_deserializer.Deserialize(parser)); break; } } diff --git a/dotnet/src/Agents/Yaml/ModelConfigurationTypeConverter.cs b/dotnet/src/Agents/Yaml/ModelConfigurationTypeConverter.cs index 30fdae158be4..dff8df54e0c0 100644 --- a/dotnet/src/Agents/Yaml/ModelConfigurationTypeConverter.cs +++ b/dotnet/src/Agents/Yaml/ModelConfigurationTypeConverter.cs @@ -43,7 +43,7 @@ public bool Accepts(Type type) modelConfiguration.ServiceId = s_deserializer.Deserialize(parser); break; default: - (modelConfiguration.ExtensionData ??= new Dictionary()).Add(propertyName, s_deserializer.Deserialize(parser)); + (modelConfiguration.ExtensionData ??= new Dictionary()).Add(propertyName, s_deserializer.Deserialize(parser)); break; } } From cb46cca55564567bc746f0d4e5667eb4389e07af Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:26:51 +0000 Subject: [PATCH 5/6] More code review feedback --- .../Agents/Core/Definition/AggregatorKernelAgentFactory.cs | 4 ++-- dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/Core/Definition/AggregatorKernelAgentFactory.cs b/dotnet/src/Agents/Core/Definition/AggregatorKernelAgentFactory.cs index 28b7f6ae514f..46c1319f5536 100644 --- a/dotnet/src/Agents/Core/Definition/AggregatorKernelAgentFactory.cs +++ b/dotnet/src/Agents/Core/Definition/AggregatorKernelAgentFactory.cs @@ -11,7 +11,7 @@ namespace Microsoft.SemanticKernel.Agents; /// public sealed class AggregatorKernelAgentFactory : KernelAgentFactory { - private readonly KernelAgentFactory?[] _kernelAgentFactories; + private readonly KernelAgentFactory[] _kernelAgentFactories; /// Initializes the instance. /// Ordered instances to aggregate. @@ -37,7 +37,7 @@ public AggregatorKernelAgentFactory(params KernelAgentFactory[] kernelAgentFacto foreach (var kernelAgentFactory in this._kernelAgentFactories) { - if (kernelAgentFactory is not null && kernelAgentFactory.IsSupported(agentDefinition)) + if (kernelAgentFactory.IsSupported(agentDefinition)) { var kernelAgent = await kernelAgentFactory.TryCreateAsync(kernel, agentDefinition, cancellationToken).ConfigureAwait(false); if (kernelAgent is not null) diff --git a/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs index 6d97db3865b3..14a28b1e1b52 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs @@ -5,6 +5,7 @@ using System.Net.Http; using Azure.AI.OpenAI; using Azure.Identity; +using Microsoft.Extensions.DependencyInjection; using OpenAI; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -38,7 +39,7 @@ public static OpenAIClient GetOpenAIClient(this Kernel kernel, AgentDefinition a throw new InvalidOperationException("OpenAI client type must be specified."); } - var httpClient = kernel.GetAllServices().FirstOrDefault(); + var httpClient = kernel.Services.GetService(); if (configuration.Type.Equals(OpenAI, StringComparison.OrdinalIgnoreCase)) { OpenAIClientOptions clientOptions = OpenAIClientProvider.CreateOpenAIClientOptions(configuration.GetEndpointUri(), httpClient); From c11d486f427472338ebb0e7c00df59aa45fee5f9 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:29:00 +0000 Subject: [PATCH 6/6] More code review feedback --- dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs index 14a28b1e1b52..2c5001e7edd7 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs @@ -2,10 +2,9 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Net.Http; using Azure.AI.OpenAI; using Azure.Identity; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel.Http; using OpenAI; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -39,7 +38,9 @@ public static OpenAIClient GetOpenAIClient(this Kernel kernel, AgentDefinition a throw new InvalidOperationException("OpenAI client type must be specified."); } - var httpClient = kernel.Services.GetService(); +#pragma warning disable CA2000 // Dispose objects before losing scope, not applicable because the HttpClient is created elsewhere + var httpClient = HttpClientProvider.GetHttpClient(kernel.Services); +#pragma warning restore CA2000 // Dispose objects before losing scope if (configuration.Type.Equals(OpenAI, StringComparison.OrdinalIgnoreCase)) { OpenAIClientOptions clientOptions = OpenAIClientProvider.CreateOpenAIClientOptions(configuration.GetEndpointUri(), httpClient);