diff --git a/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs b/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs index 606b9ef31ed8..98c6f684f010 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,14 +78,71 @@ 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; Assert.NotNull(agent); - 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] + public async Task AzureAIAgentWithOpenApiAsync() + { + var text = + """ + type: azureai_agent + name: AzureAIAgent + description: AzureAIAgent Description + instructions: AzureAIAgent Instructions + model: + id: gpt-4o-mini + tools: + - type: openapi + name: RestCountriesAPI + description: Web API version 3.1 for managing country items, based on previous implementations from restcountries.eu and restcountries.com. + 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(text, this._kernel) as AzureAIAgent; + Assert.NotNull(agent); + + await InvokeAgentAsync(agent!, "What country has Dublin as it's capital city?"); + } + + [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!, "What is the special soup and how much does it cost?"); } [Fact] - public async Task AzureAIAgentWithKernelFunctionsAsync() + public async Task AzureAIAgentWithFunctionsViaOptionsAsync() { var text = """ @@ -68,10 +165,10 @@ public async Task AzureAIAgentWithKernelFunctionsAsync() 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; Assert.NotNull(agent); - 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 @@ -83,7 +180,7 @@ public async Task AzureAIAgentWithKernelFunctionsAsync() 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); try { @@ -92,8 +189,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/Abstractions/Definition/AgentToolDefinition.cs b/dotnet/src/Agents/Abstractions/Definition/AgentToolDefinition.cs index 80c0495f9bf0..ab34df3df8d3 100644 --- a/dotnet/src/Agents/Abstractions/Definition/AgentToolDefinition.cs +++ b/dotnet/src/Agents/Abstractions/Definition/AgentToolDefinition.cs @@ -41,6 +41,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. /// @@ -61,6 +74,7 @@ public string? Name #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 83b557e4bedb..580609a5dd33 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_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 FunctionType = "function"; + private const string MicrosoftFabricType = "fabric_aiskill"; + private const string OpenApiType = "openapi"; + private const string SharepointGroundingType = "sharepoint_grounding"; + /// /// 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), + FunctionType => CreateFunctionToolDefinition(tool), + MicrosoftFabricType => CreateMicrosoftFabricToolDefinition(tool), + OpenApiType => CreateOpenApiToolDefinition(tool), + SharepointGroundingType => CreateSharepointGroundingToolDefinition(tool), _ => throw new NotSupportedException($"Unable to create Azure AI tool definition because of unsupported tool type: {tool.Type}"), }; }); @@ -40,4 +57,97 @@ public static IEnumerable GetAzureToolDefinitions(this AgentDefi // TODO: Implement return null; } + + #region private + private static AzureAISearchToolDefinition CreateAzureAISearchToolDefinition(AgentToolDefinition tool) + { + Verify.NotNull(tool); + + return new AzureAISearchToolDefinition(); + } + + private static AzureFunctionToolDefinition CreateAzureFunctionToolDefinition(AgentToolDefinition tool) + { + 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) + { + Verify.NotNull(tool); + + ToolConnectionList bingGrounding = tool.GetToolConnectionList(); + + return new BingGroundingToolDefinition(bingGrounding); + } + + private static CodeInterpreterToolDefinition CreateCodeInterpreterToolDefinition(AgentToolDefinition tool) + { + return new CodeInterpreterToolDefinition(); + } + + private static FileSearchToolDefinition CreateFileSearchToolDefinition(AgentToolDefinition tool) + { + Verify.NotNull(tool); + + return new FileSearchToolDefinition() + { + FileSearch = tool.GetFileSearchToolDefinitionDetails() + }; + } + + private static FunctionToolDefinition CreateFunctionToolDefinition(AgentToolDefinition tool) + { + 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) + { + Verify.NotNull(tool); + + ToolConnectionList fabricAiskill = tool.GetToolConnectionList(); + + return new MicrosoftFabricToolDefinition(fabricAiskill); + } + + private static OpenApiToolDefinition CreateOpenApiToolDefinition(AgentToolDefinition tool) + { + 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 CreateSharepointGroundingToolDefinition(AgentToolDefinition tool) + { + 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 new file mode 100644 index 000000000000..e334d83c1eed --- /dev/null +++ b/dotnet/src/Agents/AzureAI/Extensions/AgentToolDefinitionExtensions.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using Azure.AI.Projects; + +namespace Microsoft.SemanticKernel.Agents.AzureAI; + +/// +/// Provides extension methods for . +/// +internal static class AgentToolDefinitionExtensions +{ + internal static AzureFunctionBinding GetInputBinding(this AgentToolDefinition agentToolDefinition) + { + return agentToolDefinition.GetAzureFunctionBinding("input_binding"); + } + + internal static AzureFunctionBinding GetOutputBinding(this AgentToolDefinition agentToolDefinition) + { + return agentToolDefinition.GetAzureFunctionBinding("output_binding"); + } + + internal static BinaryData GetParameters(this AgentToolDefinition agentToolDefinition) + { + Verify.NotNull(agentToolDefinition.Configuration); + + var parameters = agentToolDefinition.GetConfiguration>("parameters"); + + // TODO Needswork + 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 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"); + 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.ToString())).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}' value must be of type '{typeof(T)}' but is '{value.GetType()}'."); + } + } + + 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; + } + + 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 readonly BinaryData s_noParams = BinaryData.FromObjectAsJson(new { type = "object", properties = new { } }); +} diff --git a/dotnet/src/Agents/AzureAI/Extensions/KernelExtensions.cs b/dotnet/src/Agents/AzureAI/Extensions/KernelExtensions.cs index eb12de0122df..202c7b48144b 100644 --- a/dotnet/src/Agents/AzureAI/Extensions/KernelExtensions.cs +++ b/dotnet/src/Agents/AzureAI/Extensions/KernelExtensions.cs @@ -33,7 +33,7 @@ public static AIProjectClient GetAIProjectClient(this Kernel kernel, AgentDefini { var httpClient = kernel.GetAllServices().FirstOrDefault(); AIProjectClientOptions clientOptions = AzureAIClientProvider.CreateAzureClientOptions(httpClient); - return new(connectionString!.ToString()!, new AzureCliCredential(), clientOptions); + return new(connectionString!.ToString()!, new DefaultAzureCredential(), clientOptions); } } diff --git a/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/KernelExtensions.cs index 84ab95f94c84..19b67098b0aa 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."); } #pragma warning disable CA2000 // Dispose objects before losing scope, not applicable because the HttpClient is created and may be used elsewhere @@ -49,14 +49,17 @@ 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.ExtensionData.TryGetValue(ApiKey, out var apiKey) && apiKey is not null) { return new AzureOpenAIClient(endpoint, configuration.GetApiKeyCredential(), clientOptions); } - return new AzureOpenAIClient(endpoint, new AzureCliCredential(), clientOptions); + return new AzureOpenAIClient(endpoint, new DefaultAzureCredential(), clientOptions); } throw new InvalidOperationException($"Invalid OpenAI client type '{configuration.Type}' was specified."); diff --git a/dotnet/src/Agents/OpenAI/Extensions/ModelConfigurationExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/ModelConfigurationExtensions.cs index a06617832cc5..d7974e3e70f2 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/ModelConfigurationExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/ModelConfigurationExtensions.cs @@ -16,15 +16,13 @@ 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.ExtensionData.TryGetValue("endpoint", out var endpoint) || endpoint is null) - { - throw new InvalidOperationException("Endpoint was not specified."); - } - return new Uri(endpoint.ToString()!); + return configuration.ExtensionData.TryGetValue("endpoint", out var value) && value is not null && value is string endpoint + ? new Uri(endpoint) + : null; } /// @@ -35,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/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..f83c0758bd18 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Yaml/AzureAIKernelAgentYamlTests.cs @@ -0,0 +1,346 @@ +// 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_ai_search")] + 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(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(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(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("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 + this._messageHandlerStub.ResponseQueue.Enqueue(new(statusCode) + { + Content = new StringContent(response) + }); + #endregion +} 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..c5433d81f514 --- /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 1752beca5277..f53b685f7f33 100644 --- a/dotnet/src/Agents/Yaml/KernelAgentFactoryYamlExtensions.cs +++ b/dotnet/src/Agents/Yaml/KernelAgentFactoryYamlExtensions.cs @@ -13,17 +13,17 @@ public static class KernelAgentFactoryYamlExtensions /// /// Create a from the given YAML text. /// - /// Kernel agent factory which will be used to create the agent. - /// Kernel instance. + /// Kernel agent factory which will be used to create the agent /// Text string containing the YAML representation of a kernel agent. - /// Optional cancellation token. - public static async Task CreateAgentFromYamlAsync(this KernelAgentFactory kernelAgentFactory, Kernel kernel, string text, CancellationToken cancellationToken = default) + /// Kernel instance + /// Optional cancellation token + 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..dff8df54e0c0 --- /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. ///