diff --git a/agent-samples/README.md b/agent-samples/README.md new file mode 100644 index 0000000000..91e45605db --- /dev/null +++ b/agent-samples/README.md @@ -0,0 +1,3 @@ +# Declarative Agents + +This folder contains sample agent definitions than be ran using the [Declarative Agents](../dotnet/samples/GettingStarted/DeclarativeAgents) demo. diff --git a/agent-samples/azure/AzureOpenAIAssistants.yaml b/agent-samples/azure/AzureOpenAIAssistants.yaml new file mode 100644 index 0000000000..cf934ded5f --- /dev/null +++ b/agent-samples/azure/AzureOpenAIAssistants.yaml @@ -0,0 +1,25 @@ +kind: Prompt +name: Assistant +description: Helpful assistant +instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response. +model: + id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME + provider: AzureOpenAI + apiType: Assistants + options: + temperature: 0.9 + topP: 0.95 +outputSchema: + properties: + language: + type: string + required: true + description: The language of the answer. + answer: + type: string + required: true + description: The answer text. + type: + type: string + required: true + description: The type of the response. diff --git a/agent-samples/azure/AzureOpenAIChat.yaml b/agent-samples/azure/AzureOpenAIChat.yaml new file mode 100644 index 0000000000..8272b908ad --- /dev/null +++ b/agent-samples/azure/AzureOpenAIChat.yaml @@ -0,0 +1,25 @@ +kind: Prompt +name: Assistant +description: Helpful assistant +instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response. +model: + id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME + provider: AzureOpenAI + apiType: Chat + options: + temperature: 0.9 + topP: 0.95 +outputSchema: + properties: + language: + type: string + required: true + description: The language of the answer. + answer: + type: string + required: true + description: The answer text. + type: + type: string + required: true + description: The type of the response. diff --git a/agent-samples/azure/AzureOpenAIResponses.yaml b/agent-samples/azure/AzureOpenAIResponses.yaml new file mode 100644 index 0000000000..f29a7b7bbb --- /dev/null +++ b/agent-samples/azure/AzureOpenAIResponses.yaml @@ -0,0 +1,25 @@ +kind: Prompt +name: Assistant +description: Helpful assistant +instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response. +model: + id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME + provider: AzureOpenAI + apiType: Responses + options: + temperature: 0.9 + topP: 0.95 +outputSchema: + properties: + language: + type: string + required: true + description: The language of the answer. + answer: + type: string + required: true + description: The answer text. + type: + type: string + required: true + description: The type of the response. diff --git a/agent-samples/chatclient/Assistant.yaml b/agent-samples/chatclient/Assistant.yaml new file mode 100644 index 0000000000..3332d54540 --- /dev/null +++ b/agent-samples/chatclient/Assistant.yaml @@ -0,0 +1,18 @@ +kind: Prompt +name: Assistant +description: Helpful assistant +instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. +model: + options: + temperature: 0.9 + topP: 0.95 +outputSchema: + properties: + language: + type: string + required: true + description: The language of the answer. + answer: + type: string + required: true + description: The answer text. diff --git a/agent-samples/chatclient/GetWeather.yaml b/agent-samples/chatclient/GetWeather.yaml new file mode 100644 index 0000000000..798d2e4245 --- /dev/null +++ b/agent-samples/chatclient/GetWeather.yaml @@ -0,0 +1,26 @@ +kind: Prompt +name: Assistant +description: Helpful assistant +instructions: You are a helpful assistant. You answer questions using the tools provided. +model: + options: + temperature: 0.9 + topP: 0.95 + allowMultipleToolCalls: true + chatToolMode: auto +tools: + - kind: function + name: GetWeather + description: Get the weather for a given location. + parameters: + - name: location + type: string + description: The city and state, e.g. San Francisco, CA + required: true + - name: unit + type: string + description: The unit of temperature. Possible values are 'celsius' and 'fahrenheit'. + required: false + enum: + - celsius + - fahrenheit diff --git a/agent-samples/foundry/PersistentAgent.yaml b/agent-samples/foundry/PersistentAgent.yaml new file mode 100644 index 0000000000..5ff4514dd8 --- /dev/null +++ b/agent-samples/foundry/PersistentAgent.yaml @@ -0,0 +1,22 @@ +kind: Prompt +name: Assistant +description: Helpful assistant +instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. +model: + id: =Env.AZURE_FOUNDRY_PROJECT_MODEL_ID + options: + temperature: 0.9 + topP: 0.95 + connection: + kind: Remote + endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT +outputSchema: + properties: + language: + type: string + required: true + description: The language of the answer. + answer: + type: string + required: true + description: The answer text. diff --git a/agent-samples/openai/OpenAIAssistants.yaml b/agent-samples/openai/OpenAIAssistants.yaml new file mode 100644 index 0000000000..867639aa22 --- /dev/null +++ b/agent-samples/openai/OpenAIAssistants.yaml @@ -0,0 +1,28 @@ +kind: Prompt +name: Assistant +description: Helpful assistant +instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response. +model: + id: =Env.OPENAI_MODEL + provider: OpenAI + apiType: Assistants + options: + temperature: 0.9 + topP: 0.95 + connection: + kind: ApiKey + key: =Env.OPENAI_APIKEY +outputSchema: + properties: + language: + type: string + required: true + description: The language of the answer. + answer: + type: string + required: true + description: The answer text. + type: + type: string + required: true + description: The type of the response. diff --git a/agent-samples/openai/OpenAIChat.yaml b/agent-samples/openai/OpenAIChat.yaml new file mode 100644 index 0000000000..135bf8602b --- /dev/null +++ b/agent-samples/openai/OpenAIChat.yaml @@ -0,0 +1,28 @@ +kind: Prompt +name: Assistant +description: Helpful assistant +instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response. +model: + id: =Env.OPENAI_MODEL + provider: OpenAI + apiType: Chat + options: + temperature: 0.9 + topP: 0.95 + connection: + kind: ApiKey + key: =Env.OPENAI_APIKEY +outputSchema: + properties: + language: + type: string + required: true + description: The language of the answer. + answer: + type: string + required: true + description: The answer text. + type: + type: string + required: true + description: The type of the response. diff --git a/agent-samples/openai/OpenAIResponses.yaml b/agent-samples/openai/OpenAIResponses.yaml new file mode 100644 index 0000000000..78f331eea2 --- /dev/null +++ b/agent-samples/openai/OpenAIResponses.yaml @@ -0,0 +1,28 @@ +kind: Prompt +name: Assistant +description: Helpful assistant +instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response. +model: + id: =Env.OPENAI_MODEL + provider: OpenAI + apiType: Responses + options: + temperature: 0.9 + topP: 0.95 + connection: + kind: ApiKey + key: =Env.OPENAI_APIKEY +outputSchema: + properties: + language: + type: string + required: true + description: The language of the answer. + answer: + type: string + required: true + description: The answer text. + type: + type: string + required: true + description: The type of the response. diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props index 6b61196bbd..afb2250939 100644 --- a/dotnet/Directory.Build.props +++ b/dotnet/Directory.Build.props @@ -8,7 +8,7 @@ true 13 enable - $(NoWarn);NU5128 + $(NoWarn);NU5128;NU1900;NU1603 true net9.0;net8.0 net9.0 diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 004fc10f75..5d34ebc2ad 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -64,6 +64,7 @@ + @@ -80,6 +81,12 @@ + + + + + + @@ -296,6 +303,8 @@ + + @@ -313,6 +322,7 @@ + @@ -323,6 +333,7 @@ + diff --git a/dotnet/nuget.config b/dotnet/nuget.config index f7e74aa056..af03543b8f 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -13,4 +13,4 @@ - \ No newline at end of file + diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 65dbd78a29..099bb42dfb 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -12,7 +12,7 @@ 0.0.1 - $(NoWarn);CP0003 + $(NoWarn);CP0003;NU1900;NU1603 $(NoWarn);CP1002 diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step18_Declarative/Agent_Step18_Declarative.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step18_Declarative/Agent_Step18_Declarative.csproj new file mode 100644 index 0000000000..0bd9574dff --- /dev/null +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step18_Declarative/Agent_Step18_Declarative.csproj @@ -0,0 +1,25 @@ + + + + Exe + net9.0 + + enable + enable + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step18_Declarative/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step18_Declarative/Program.cs new file mode 100644 index 0000000000..6275b63b62 --- /dev/null +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step18_Declarative/Program.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create an agent from a YAML based declarative representation. + +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; + +// Create the chat client +IChatClient chatClient = new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .AsIChatClient(); + +// Define the agent using a YAML definition. +var text = + """ + kind: Prompt + name: Assistant + description: Helpful assistant + instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. + model: + options: + temperature: 0.9 + topP: 0.95 + outputSchema: + properties: + language: + type: string + required: true + description: The language of the answer. + answer: + type: string + required: true + description: The answer text. + """; + +// Create the agent from the YAML definition. +var agentFactory = new ChatClientAgentFactory(chatClient); +var agent = await agentFactory.CreateFromYamlAsync(text); + +// Invoke the agent and output the text result. +Console.WriteLine(await agent!.RunAsync("Tell me a joke about a pirate in English.")); + +// Invoke the agent with streaming support. +await foreach (var update in agent!.RunStreamingAsync("Tell me a joke about a pirate in French.")) +{ + Console.WriteLine(update); +} diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/DeclarativeAzureAgents.csproj b/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/DeclarativeAzureAgents.csproj new file mode 100644 index 0000000000..e607b92fb7 --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/DeclarativeAzureAgents.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + + enable + enable + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/Program.cs b/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/Program.cs new file mode 100644 index 0000000000..213cbf7815 --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/Program.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to load an AI agent from a YAML file and process a prompt using Azure OpenAI as the backend. + +using System.ComponentModel; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; + +// Read command-line arguments +if (args.Length < 2) +{ + Console.WriteLine("Usage: DeclarativeAgents "); + Console.WriteLine(" : The path to the YAML file containing the agent definition"); + Console.WriteLine(" : The prompt to send to the agent"); + return; +} + +var yamlFilePath = args[0]; +var prompt = args[1]; + +// Verify the YAML file exists +if (!File.Exists(yamlFilePath)) +{ + Console.WriteLine($"Error: File not found: {yamlFilePath}"); + return; +} + +// Read the YAML content from the file +var text = await File.ReadAllTextAsync(yamlFilePath); + +// TODO: Remove this workaround when the agent framework supports environment variable substitution in YAML files. +text = text.Replace("=Env.AZURE_OPENAI_DEPLOYMENT_NAME", deploymentName, StringComparison.OrdinalIgnoreCase); + +var endpointUri = new Uri(endpoint); +var tokenCredential = new AzureCliCredential(); + +// Create the agent from the YAML definition. +var agentFactory = new AggregatorAgentFactory( + [ + new OpenAIChatAgentFactory(endpointUri, tokenCredential), + new OpenAIResponseAgentFactory(endpointUri, tokenCredential), + new OpenAIAssistantAgentFactory(endpointUri, tokenCredential) + ]); +var agent = await agentFactory.CreateFromYamlAsync(text); + +// Example function tool that can be used by the agent. +[Description("Get the weather for a given location.")] +static string GetWeather( + [Description("The city and state, e.g. San Francisco, CA")] string location, + [Description("The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.")] string unit) + => $"The weather in {location} is cloudy with a high of {(unit.Equals("celsius", StringComparison.Ordinal) ? "15°C" : "59°F")}."; + +// Create agent run options +var options = new ChatClientAgentRunOptions(new() +{ + Tools = [AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather))] +}); + +// Invoke the agent and output the text result. +Console.WriteLine(await agent!.RunAsync(prompt, options: options)); diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj b/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj new file mode 100644 index 0000000000..6442fa3a7b --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj @@ -0,0 +1,25 @@ + + + + Exe + net9.0 + + enable + enable + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Program.cs b/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Program.cs new file mode 100644 index 0000000000..c3f998a836 --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Program.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to load an AI agent from a YAML file and process a prompt using Azure OpenAI as the backend. + +using System.ComponentModel; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; + +// Create the chat client +IChatClient chatClient = new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .AsIChatClient(); + +// Read command-line arguments +if (args.Length < 2) +{ + Console.WriteLine("Usage: DeclarativeAgents "); + Console.WriteLine(" : The path to the YAML file containing the agent definition"); + Console.WriteLine(" : The prompt to send to the agent"); + return; +} + +var yamlFilePath = args[0]; +var prompt = args[1]; + +// Verify the YAML file exists +if (!File.Exists(yamlFilePath)) +{ + Console.WriteLine($"Error: File not found: {yamlFilePath}"); + return; +} + +// Read the YAML content from the file +var text = await File.ReadAllTextAsync(yamlFilePath); + +// Example function tool that can be used by the agent. +[Description("Get the weather for a given location.")] +static string GetWeather( + [Description("The city and state, e.g. San Francisco, CA")] string location, + [Description("The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.")] string unit) + => $"The weather in {location} is cloudy with a high of {(unit.Equals("celsius", StringComparison.Ordinal) ? "15°C" : "59°F")}."; + +// Create the agent from the YAML definition. +var agentFactory = new ChatClientAgentFactory(chatClient, [AIFunctionFactory.Create(GetWeather, "GetWeather")]); +var agent = await agentFactory.CreateFromYamlAsync(text); + +// Invoke the agent and output the text result. +Console.WriteLine(await agent!.RunAsync(prompt)); diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/DeclarativeFoundryAgents.csproj b/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/DeclarativeFoundryAgents.csproj new file mode 100644 index 0000000000..e607b92fb7 --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/DeclarativeFoundryAgents.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + + enable + enable + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/Program.cs b/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/Program.cs new file mode 100644 index 0000000000..68a78fe01e --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/Program.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to load an AI agent from a YAML file and process a prompt using Foundry Agents as the backend. + +using System.ComponentModel; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set."); +var model = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_MODEL_ID") ?? "gpt-4.1-mini"; + +// Read command-line arguments +if (args.Length < 2) +{ + Console.WriteLine("Usage: DeclarativeAgents "); + Console.WriteLine(" : The path to the YAML file containing the agent definition"); + Console.WriteLine(" : The prompt to send to the agent"); + return; +} + +var yamlFilePath = args[0]; +var prompt = args[1]; + +// Verify the YAML file exists +if (!File.Exists(yamlFilePath)) +{ + Console.WriteLine($"Error: File not found: {yamlFilePath}"); + return; +} + +// Read the YAML content from the file +var text = await File.ReadAllTextAsync(yamlFilePath); + +// TODO: Remove this workaround when the agent framework supports environment variable substitution in YAML files. +text = text.Replace("=Env.AZURE_FOUNDRY_PROJECT_ENDPOINT", endpoint, StringComparison.OrdinalIgnoreCase); +text = text.Replace("=Env.AZURE_FOUNDRY_PROJECT_MODEL_ID", model, StringComparison.OrdinalIgnoreCase); + +// Example function tool that can be used by the agent. +[Description("Get the weather for a given location.")] +static string GetWeather( + [Description("The city and state, e.g. San Francisco, CA")] string location, + [Description("The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.")] string unit) + => $"The weather in {location} is cloudy with a high of {(unit.Equals("celsius", StringComparison.Ordinal) ? "15°C" : "59°F")}."; + +// Create the agent from the YAML definition. +var agentFactory = new FoundryPersistentAgentFactory(new AzureCliCredential()); +var agent = await agentFactory.CreateFromYamlAsync(text); + +// Create agent run options +var options = new ChatClientAgentRunOptions(new() +{ + Tools = [AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather))] +}); + +// Invoke the agent and output the text result. +Console.WriteLine(await agent!.RunAsync(prompt, options: options)); diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/DeclarativeOpenAIAgents.csproj b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/DeclarativeOpenAIAgents.csproj new file mode 100644 index 0000000000..e607b92fb7 --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/DeclarativeOpenAIAgents.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + + enable + enable + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/Program.cs b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/Program.cs new file mode 100644 index 0000000000..5aca8ef7cf --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/Program.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to load an AI agent from a YAML file and process a prompt using OpenAI as the backend. + +using System.ComponentModel; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set."); +var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini"; + +// Read command-line arguments +if (args.Length < 2) +{ + Console.WriteLine("Usage: DeclarativeAgents "); + Console.WriteLine(" : The path to the YAML file containing the agent definition"); + Console.WriteLine(" : The prompt to send to the agent"); + return; +} + +var yamlFilePath = args[0]; +var prompt = args[1]; + +// Verify the YAML file exists +if (!File.Exists(yamlFilePath)) +{ + Console.WriteLine($"Error: File not found: {yamlFilePath}"); + return; +} + +// Read the YAML content from the file +var text = await File.ReadAllTextAsync(yamlFilePath); + +// TODO: Remove this workaround when the agent framework supports environment variable substitution in YAML files. +text = text.Replace("=Env.OPENAI_APIKEY", apiKey, StringComparison.OrdinalIgnoreCase); +text = text.Replace("=Env.OPENAI_MODEL", model, StringComparison.OrdinalIgnoreCase); + +// Create the agent from the YAML definition. +var agentFactory = new AggregatorAgentFactory( + [ + new OpenAIChatAgentFactory(), + new OpenAIResponseAgentFactory(), + new OpenAIAssistantAgentFactory() + ]); +var agent = await agentFactory.CreateFromYamlAsync(text); + +// Example function tool that can be used by the agent. +[Description("Get the weather for a given location.")] +static string GetWeather( + [Description("The city and state, e.g. San Francisco, CA")] string location, + [Description("The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.")] string unit) + => $"The weather in {location} is cloudy with a high of {(unit.Equals("celsius", StringComparison.Ordinal) ? "15°C" : "59°F")}."; + +// Create agent run options +var options = new ChatClientAgentRunOptions(new() +{ + Tools = [AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather))] +}); + +// Invoke the agent and output the text result. +Console.WriteLine(await agent!.RunAsync(prompt, options: options)); diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/CodeInterpreterToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/CodeInterpreterToolExtensions.cs new file mode 100644 index 0000000000..45308f6bae --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/CodeInterpreterToolExtensions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Azure.AI.Agents.Persistent; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class CodeInterpreterToolExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of + internal static CodeInterpreterToolDefinition CreateCodeInterpreterToolDefinition(this CodeInterpreterTool tool) + { + Throw.IfNull(tool); + + return new CodeInterpreterToolDefinition(); + } + + /// + /// Collects the file IDs from the extension data of a . + /// + /// Instance of + internal static List? GetFileIds(this CodeInterpreterTool tool) + { + var fileIds = tool.ExtensionData?.GetPropertyOrNull(InitializablePropertyPath.Create("fileIds")); + return fileIds is not null + ? [.. fileIds.Values.Select(fileId => fileId.GetPropertyOrNull(InitializablePropertyPath.Create("value"))?.Value)] + : null; + } + + /// + /// Collects the data sources from the extension data of a . + /// + /// Instance of + internal static List? GetDataSources(this CodeInterpreterTool tool) + { + var dataSources = tool.ExtensionData?.GetPropertyOrNull(InitializablePropertyPath.Create("dataSources")); + return dataSources is not null + ? dataSources.Values.Select(dataSource => dataSource.CreateDataSource()).ToList() + : null; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/FileSearchToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/FileSearchToolExtensions.cs new file mode 100644 index 0000000000..52d1e69c04 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/FileSearchToolExtensions.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Azure.AI.Agents.Persistent; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class FileSearchToolExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of + internal static FileSearchToolDefinition CreateFileSearchToolDefinition(this FileSearchTool tool) + { + Throw.IfNull(tool); + + // TODO: Add support for FileSearchToolDefinitionDetails. + + return new FileSearchToolDefinition(); + } + + /// + /// Get the vector store IDs for the specified . + /// + /// Instance of + internal static List? GetVectorStoreIds(this FileSearchTool tool) + { + return tool.VectorStoreIds?.LiteralValue.ToList(); + } + + internal static IList? GetVectorStoreConfigurations(this FileSearchTool tool) + { + var dataSources = tool.ExtensionData?.GetPropertyOrNull(InitializablePropertyPath.Create("options.configurations")); + return dataSources?.Values.Select(value => value.CreateVectorStoreConfiguration()).ToList(); + } + + internal static VectorStoreConfigurations CreateVectorStoreConfiguration(this RecordDataValue value) + { + Throw.IfNull(value); + + var storeName = value.GetPropertyOrNull(InitializablePropertyPath.Create("storeName"))?.Value; + Throw.IfNullOrEmpty(storeName); + + var dataSources = value.GetDataSources(); + Throw.IfNull(dataSources); + + return new VectorStoreConfigurations(storeName, new VectorStoreConfiguration(dataSources)); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/FunctionToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/FunctionToolExtensions.cs new file mode 100644 index 0000000000..97281b79b7 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/FunctionToolExtensions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Azure.AI.Agents.Persistent; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +public static class FunctionToolExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of + internal static FunctionToolDefinition CreateFunctionToolDefinition(this InvokeClientTaskAction tool) + { + Throw.IfNull(tool); + Throw.IfNull(tool.Name); + + BinaryData parameters = tool.GetParameters(); + + return new FunctionToolDefinition( + name: tool.Name, + description: tool.Description, + parameters: parameters); + } + + /// + /// Creates the parameters schema for a . + /// + /// Instance of + internal static BinaryData GetParameters(this InvokeClientTaskAction tool) + { + Throw.IfNull(tool); + + var parameters = tool.ClientActionInputSchema?.GetSchema().ToString() ?? DefaultSchema; + + return new BinaryData(parameters); + } + + private const string DefaultSchema = "{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}"; +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/HostedCodeInterpreterToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/HostedCodeInterpreterToolExtensions.cs new file mode 100644 index 0000000000..da769856dd --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/HostedCodeInterpreterToolExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents.Persistent; +using Microsoft.Bot.ObjectModel; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Extension methods for . +/// +internal static class HostedCodeInterpreterToolExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of + internal static CodeInterpreterToolDefinition CreateHostedCodeInterpreterToolDefinition(this HostedCodeInterpreterTool tool) + { + Throw.IfNull(tool); + + return new CodeInterpreterToolDefinition(); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/HostedFileSearchToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/HostedFileSearchToolExtensions.cs new file mode 100644 index 0000000000..846e28f226 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/HostedFileSearchToolExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents.Persistent; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Extension methods for . +/// +internal static class HostedFileSearchToolExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of + internal static FileSearchToolDefinition CreateFileSearchToolDefinition(this HostedFileSearchTool tool) + { + Throw.IfNull(tool); + + // TODO: Add support for FileSearchToolDefinitionDetails. + + return new FileSearchToolDefinition(); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/HostedMcpServerToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/HostedMcpServerToolExtensions.cs new file mode 100644 index 0000000000..a879a105bc --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/HostedMcpServerToolExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using Azure.AI.Agents.Persistent; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Extension methods for . +/// +internal static class HostedMcpServerToolExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of + internal static MCPToolDefinition CreateMcpToolDefinition(this HostedMcpServerTool tool) + { + Throw.IfNull(tool); + Throw.IfNull(tool.ServerName); + Throw.IfNull(tool.ServerAddress); + + var definition = new MCPToolDefinition(tool.ServerName, tool.ServerAddress); + tool.AllowedTools?.ToList().ForEach(definition.AllowedTools.Add); + return definition; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/HostedWebSearchToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/HostedWebSearchToolExtensions.cs new file mode 100644 index 0000000000..f13c0ec2d4 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/HostedWebSearchToolExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents.Persistent; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Extension methods for . +/// +internal static class HostedWebSearchToolExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of + internal static BingGroundingToolDefinition CreateBingGroundingToolDefinition(this HostedWebSearchTool tool) + { + Throw.IfNull(tool); + + // TODO: Add support for BingGroundingSearchToolParameters. + var parameters = new BingGroundingSearchToolParameters([]); + + return new BingGroundingToolDefinition(parameters); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/McpServerToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/McpServerToolExtensions.cs new file mode 100644 index 0000000000..1e0668f846 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/McpServerToolExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Azure.AI.Agents.Persistent; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class McpServerToolExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of + internal static MCPToolDefinition CreateMcpToolDefinition(this McpServerTool tool) + { + Throw.IfNull(tool); + Throw.IfNull(tool.ServerName?.LiteralValue); + Throw.IfNull(tool.Connection); + + // TODO: Add support for additional properties + + var connection = tool.Connection as AnonymousConnection ?? throw new ArgumentException("Only AnonymousConnection is supported for MCP Server Tool connections.", nameof(tool)); + var serverUrl = connection.Endpoint?.LiteralValue; + Throw.IfNullOrEmpty(serverUrl, nameof(connection.Endpoint)); + + return new MCPToolDefinition(tool.ServerName?.LiteralValue, serverUrl); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/PromptAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/PromptAgentExtensions.cs new file mode 100644 index 0000000000..899fb26c5b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/PromptAgentExtensions.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; +using Azure.AI.Agents.Persistent; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class PromptAgentExtensions +{ + /// + /// Return the Foundry tool definitions which corresponds with the provided . + /// + /// Instance of + internal static IEnumerable GetToolDefinitions(this GptComponentMetadata promptAgent) + { + Throw.IfNull(promptAgent); + + return promptAgent.Tools.Select(tool => + { + return tool switch + { + CodeInterpreterTool => ((CodeInterpreterTool)tool).CreateCodeInterpreterToolDefinition(), + InvokeClientTaskAction => ((InvokeClientTaskAction)tool).CreateFunctionToolDefinition(), + FileSearchTool => ((FileSearchTool)tool).CreateFileSearchToolDefinition(), + WebSearchTool => ((WebSearchTool)tool).CreateBingGroundingToolDefinition(), + McpServerTool => ((McpServerTool)tool).CreateMcpToolDefinition(), + // TODO: Add other tool types as custom tools + // AzureAISearch + // AzureFunction + // OpenApi + _ => throw new NotSupportedException($"Unable to create tool definition because of unsupported tool type: {tool.Kind}"), + }; + }).ToList(); + } + + /// + /// Return the Foundry tool resources which corresponds with the provided . + /// + /// Instance of + internal static ToolResources GetToolResources(this GptComponentMetadata promptAgent) + { + Throw.IfNull(promptAgent); + + var toolResources = new ToolResources(); + + var codeInterpreter = promptAgent.GetCodeInterpreterToolResource(); + if (codeInterpreter is not null) + { + toolResources.CodeInterpreter = codeInterpreter; + } + + var fileSearch = promptAgent.GetFileSearchToolResource(); + if (fileSearch is not null) + { + toolResources.FileSearch = fileSearch; + } + + // TODO Handle MCP tool resources + + return toolResources; + } + + #region private + private static CodeInterpreterToolResource? GetCodeInterpreterToolResource(this GptComponentMetadata promptAgent) + { + Throw.IfNull(promptAgent); + + CodeInterpreterToolResource? resource = null; + + var codeInterpreter = (CodeInterpreterTool?)promptAgent.GetFirstAgentTool(); + if (codeInterpreter is not null) + { + var fileIds = codeInterpreter.GetFileIds(); + var dataSources = codeInterpreter.GetDataSources(); + if (fileIds is not null || dataSources is not null) + { + resource = new CodeInterpreterToolResource(); + fileIds?.ForEach(id => resource.FileIds.Add(id)); + dataSources?.ForEach(ds => resource.DataSources.Add(ds)); + } + } + + return resource; + } + + private static FileSearchToolResource? GetFileSearchToolResource(this GptComponentMetadata promptAgent) + { + Throw.IfNull(promptAgent); + + var fileSearch = (FileSearchTool?)promptAgent.GetFirstAgentTool(); + if (fileSearch is not null) + { + var vectorStoreIds = fileSearch.GetVectorStoreIds(); + var vectorStores = fileSearch.GetVectorStoreConfigurations(); + if (vectorStoreIds is not null || vectorStores is not null) + { + return new FileSearchToolResource(vectorStoreIds, vectorStores); + } + } + + return null; + } + + private static TaskAction? GetFirstAgentTool(this GptComponentMetadata promptAgent) + { + return promptAgent.Tools.FirstOrDefault(tool => tool is T); + } + #endregion +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/RecordDataTypeExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/RecordDataTypeExtensions.cs new file mode 100644 index 0000000000..24c172c888 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/RecordDataTypeExtensions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class RecordDataTypeExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + internal static BinaryData? AsBinaryData(this RecordDataType recordDataType) + { + Throw.IfNull(recordDataType); + + if (recordDataType.Properties.Count == 0) + { + return null; + } + + return BinaryData.FromObjectAsJson( + new + { + type = "json_schema", + schema = + new + { + type = "object", + properties = recordDataType.Properties.AsObjectDictionary(), + additionalProperties = false + } + } + ); + } +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/RecordDataValueExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/RecordDataValueExtensions.cs new file mode 100644 index 0000000000..a2c4d490d6 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/RecordDataValueExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Azure.AI.Agents.Persistent; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class RecordDataValueExtensions +{ + /// + /// Gets the data sources from the specified . + /// + internal static List? GetDataSources(this RecordDataValue value) + { + var dataSources = value.GetPropertyOrNull(InitializablePropertyPath.Create("options.data_sources")); + return dataSources?.Values.Select(dataSource => dataSource.CreateDataSource()).ToList(); + } + + /// + /// Creates a new instance of using the specified . + /// + internal static VectorStoreDataSource CreateDataSource(this RecordDataValue value) + { + Throw.IfNull(value); + + string? assetIdentifier = value.GetPropertyOrNull(InitializablePropertyPath.Create("assetIdentifier"))?.Value; + Throw.IfNullOrEmpty(assetIdentifier); + + string? assetType = value.GetPropertyOrNull(InitializablePropertyPath.Create("assetType"))?.Value; + Throw.IfNullOrEmpty(assetType); + + return new VectorStoreDataSource(assetIdentifier, new VectorStoreDataSourceAssetType(assetType)); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/WebSearchToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/WebSearchToolExtensions.cs new file mode 100644 index 0000000000..f4643f2ca1 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Extensions/WebSearchToolExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents.Persistent; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class WebSearchToolExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of + internal static BingGroundingToolDefinition CreateBingGroundingToolDefinition(this WebSearchTool tool) + { + Throw.IfNull(tool); + + // TODO: Add support for BingGroundingSearchToolParameters. + var parameters = new BingGroundingSearchToolParameters([]); + + return new BingGroundingToolDefinition(parameters); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/FoundryPersistentAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/FoundryPersistentAgentFactory.cs new file mode 100644 index 0000000000..90e7953926 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/FoundryPersistentAgentFactory.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Agents.Persistent; +using Azure.Core; +using Microsoft.Bot.ObjectModel; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// Provides an which creates instances of using a . +/// +public sealed class FoundryPersistentAgentFactory : AgentFactory +{ + private readonly PersistentAgentsClient? _agentClient; + private readonly TokenCredential? _tokenCredential; + + /// + /// Creates a new instance of the class. + /// + public FoundryPersistentAgentFactory(PersistentAgentsClient agentClient) + { + Throw.IfNull(agentClient); + + this._agentClient = agentClient; + } + + /// + /// Creates a new instance of the class. + /// + public FoundryPersistentAgentFactory(TokenCredential tokenCredential) + { + Throw.IfNull(tokenCredential); + + this._tokenCredential = tokenCredential; + } + + /// + public override async Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) + { + Throw.IfNull(promptAgent); + + var agentClient = this._agentClient ?? this.CreatePersistentAgentClient(promptAgent); + + var modelId = promptAgent.Model?.ModelNameHint; + if (string.IsNullOrEmpty(modelId)) + { + throw new InvalidOperationException("The model id must be specified in the agent definition model to create a foundry agent."); + } + + //var outputSchema = promptAgent.OutputType; TODO: Fix converting RecordDataType to BinaryData + var modelOptions = promptAgent.Model?.Options; + + return await agentClient.CreateAIAgentAsync( + model: modelId, + name: promptAgent.Name, + instructions: promptAgent.Instructions?.ToTemplateString(), + tools: promptAgent.GetToolDefinitions(), + toolResources: promptAgent.GetToolResources(), + temperature: (float?)modelOptions?.Temperature?.LiteralValue, + topP: (float?)modelOptions?.TopP?.LiteralValue, + //responseFormat: outputSchema.AsBinaryData(), TODO: Fix converting RecordDataType to BinaryData + metadata: promptAgent.Metadata?.ToDictionary(), + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + private PersistentAgentsClient CreatePersistentAgentClient(GptComponentMetadata promptAgent) + { + var externalModel = promptAgent.Model as CurrentModels; + var connection = externalModel?.Connection as RemoteConnection; + if (connection is not null) + { + var endpoint = connection.Endpoint?.LiteralValue; + if (string.IsNullOrEmpty(endpoint)) + { + throw new InvalidOperationException("The endpoint must be specified in the agent definition model connection to create an PersistentAgentsClient."); + } + if (this._tokenCredential is null) + { + throw new InvalidOperationException("A TokenCredential must be registered in the service provider to create an PersistentAgentsClient."); + } + return new PersistentAgentsClient(endpoint, this._tokenCredential); + } + + throw new InvalidOperationException("A PersistentAgentsClient must be registered in the service provider or a FoundryConnection must be specified in the agent definition model connection to create an PersistentAgentsClient."); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/JsonSchemaFunctionParameters.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/JsonSchemaFunctionParameters.cs new file mode 100644 index 0000000000..c406825d5b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/JsonSchemaFunctionParameters.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Declarative.AzureAI; + +/// +/// A class to describe the parameters of an in a JSON Schema friendly way. +/// +internal sealed class JsonSchemaFunctionParameters +{ + /// + /// The type of schema which is always "object" when describing function parameters. + /// + [JsonPropertyName("type")] + public string Type => "object"; + + /// + /// The list of required properties. + /// + [JsonPropertyName("required")] + public List Required { get; set; } = []; + + /// + /// A dictionary of properties, keyed by name => JSON Schema. + /// + [JsonPropertyName("properties")] + public Dictionary Properties { get; set; } = []; +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Microsoft.Agents.AI.Declarative.AzureAI.csproj b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Microsoft.Agents.AI.Declarative.AzureAI.csproj new file mode 100644 index 0000000000..4f8874ef07 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Microsoft.Agents.AI.Declarative.AzureAI.csproj @@ -0,0 +1,50 @@ + + + + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) + preview + $(NoWarn);MEAI001;OPENAI001 + + + + true + true + true + true + + + + + + + Microsoft Agent Framework Declarative AzureAI + Provides Microsoft Agent Framework support for declarative AzureAI agents. + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAgentFactory.cs new file mode 100644 index 0000000000..a729d1c025 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAgentFactory.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.ClientModel; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; +using OpenAI; +using OpenAI.Assistants; +using OpenAI.Chat; +using OpenAI.Responses; + +namespace Microsoft.Agents.AI; + +/// +/// Provides an abstract base class. +/// +public abstract class OpenAIAgentFactory : AgentFactory +{ + /// + /// Creates a new instance of the class. + /// + protected OpenAIAgentFactory(ILoggerFactory? loggerFactory) + { + this.LoggerFactory = loggerFactory; + } + + /// + /// Creates a new instance of the class. + /// + protected OpenAIAgentFactory(Uri endpoint, TokenCredential tokenCredential, ILoggerFactory? loggerFactory) + { + Throw.IfNull(endpoint); + Throw.IfNull(tokenCredential); + + this._endpoint = endpoint; + this._tokenCredential = tokenCredential; + this.LoggerFactory = loggerFactory; + } + + /// + /// Gets the instance used for creating loggers. + /// + protected ILoggerFactory? LoggerFactory { get; } + + /// + /// Creates a new instance of the class. + /// + protected ChatClient? CreateChatClient(GptComponentMetadata promptAgent) + { + var model = promptAgent.Model as CurrentModels; + var provider = model?.Provider?.Value ?? ModelProvider.OpenAI; + if (provider == ModelProvider.OpenAI) + { + return CreateOpenAIChatClient(promptAgent); + } + else if (provider == ModelProvider.AzureOpenAI) + { + Throw.IfNull(this._endpoint, "A endpoint must be specified to create an Azure OpenAI client"); + Throw.IfNull(this._tokenCredential, "A token credential must be specified to create an Azure OpenAI client"); + return CreateAzureOpenAIChatClient(promptAgent, this._endpoint, this._tokenCredential); + } + + return null; + } + + /// + /// Creates a new instance of the class. + /// + protected AssistantClient? CreateAssistantClient(GptComponentMetadata promptAgent) + { + var model = promptAgent.Model as CurrentModels; + var provider = model?.Provider?.Value ?? ModelProvider.OpenAI; + if (provider == ModelProvider.OpenAI) + { + return CreateOpenAIAssistantClient(promptAgent); + } + else if (provider == ModelProvider.AzureOpenAI) + { + Throw.IfNull(this._endpoint, "The connection endpoint must be specified to create an Azure OpenAI client."); + Throw.IfNull(this._tokenCredential, "A token credential must be specified to create an Azure OpenAI client"); + return CreateAzureOpenAIAssistantClient(promptAgent, this._endpoint, this._tokenCredential); + } + + return null; + } + + /// + /// Creates a new instance of the class. + /// + protected OpenAIResponseClient? CreateResponseClient(GptComponentMetadata promptAgent) + { + var model = promptAgent.Model as CurrentModels; + var provider = model?.Provider?.Value ?? ModelProvider.OpenAI; + if (provider == ModelProvider.OpenAI) + { + return CreateOpenAIResponseClient(promptAgent); + } + else if (provider == ModelProvider.AzureOpenAI) + { + Throw.IfNull(this._endpoint, "The connection endpoint must be specified to create an Azure OpenAI client."); + Throw.IfNull(this._tokenCredential, "A token credential must be specified to create an Azure OpenAI client"); + return CreateAzureOpenAIResponseClient(promptAgent, this._endpoint, this._tokenCredential); + } + + return null; + } + + #region private + private readonly Uri? _endpoint; + private readonly TokenCredential? _tokenCredential; + + private static ChatClient CreateOpenAIChatClient(GptComponentMetadata promptAgent) + { + var modelId = promptAgent.Model?.ModelNameHint; + Throw.IfNullOrEmpty(modelId, "The model id must be specified in the agent definition to create an OpenAI agent."); + + return CreateOpenAIClient(promptAgent).GetChatClient(modelId); + } + + private static ChatClient CreateAzureOpenAIChatClient(GptComponentMetadata promptAgent, Uri endpoint, TokenCredential tokenCredential) + { + var deploymentName = promptAgent.Model?.ModelNameHint; + Throw.IfNullOrEmpty(deploymentName, "The deployment name (using model.id) must be specified in the agent definition to create an Azure OpenAI agent."); + + return new AzureOpenAIClient(endpoint, tokenCredential).GetChatClient(deploymentName); + } + + private static AssistantClient CreateOpenAIAssistantClient(GptComponentMetadata promptAgent) + { + var modelId = promptAgent.Model?.ModelNameHint; + Throw.IfNullOrEmpty(modelId, "The model id must be specified in the agent definition to create an OpenAI agent."); + + return CreateOpenAIClient(promptAgent).GetAssistantClient(); + } + + private static AssistantClient CreateAzureOpenAIAssistantClient(GptComponentMetadata promptAgent, Uri endpoint, TokenCredential tokenCredential) + { + var deploymentName = promptAgent.Model?.ModelNameHint; + Throw.IfNullOrEmpty(deploymentName, "The deployment name (using model.id) must be specified in the agent definition to create an Azure OpenAI agent."); + + return new AzureOpenAIClient(endpoint, tokenCredential).GetAssistantClient(); + } + + private static OpenAIResponseClient CreateOpenAIResponseClient(GptComponentMetadata promptAgent) + { + var modelId = promptAgent.Model?.ModelNameHint; + Throw.IfNullOrEmpty(modelId, "The model id must be specified in the agent definition to create an OpenAI agent."); + + return CreateOpenAIClient(promptAgent).GetOpenAIResponseClient(modelId); + } + + private static OpenAIResponseClient CreateAzureOpenAIResponseClient(GptComponentMetadata promptAgent, Uri endpoint, TokenCredential tokenCredential) + { + var deploymentName = promptAgent.Model?.ModelNameHint; + Throw.IfNullOrEmpty(deploymentName, "The deployment name (using model.id) must be specified in the agent definition to create an Azure OpenAI agent."); + + return new AzureOpenAIClient(endpoint, tokenCredential).GetOpenAIResponseClient(deploymentName); + } + + private static OpenAIClient CreateOpenAIClient(GptComponentMetadata promptAgent) + { + var model = promptAgent.Model as CurrentModels; + + var keyConnection = model?.Connection as ApiKeyConnection; + Throw.IfNull(keyConnection, "A key connection must be specified when create an OpenAI client"); + + var apiKey = keyConnection.Key?.LiteralValue; + Throw.IfNullOrEmpty(apiKey, "The connection key must be specified in the agent definition to create an OpenAI client."); + + var clientOptions = new OpenAIClientOptions(); + var endpoint = keyConnection.Endpoint?.LiteralValue; + if (!string.IsNullOrEmpty(endpoint)) + { + clientOptions.Endpoint = new Uri(endpoint); + } + + return new OpenAIClient(new ApiKeyCredential(apiKey), clientOptions); + } + #endregion +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAssistantAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAssistantAgentFactory.cs new file mode 100644 index 0000000000..081a008881 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAssistantAgentFactory.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Agents.Persistent; +using Azure.Core; +using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; +using OpenAI; +using OpenAI.Assistants; + +namespace Microsoft.Agents.AI; + +/// +/// Provides an which creates instances of using a . +/// +public sealed class OpenAIAssistantAgentFactory : OpenAIAgentFactory +{ + /// + /// Creates a new instance of the class. + /// + public OpenAIAssistantAgentFactory(IList? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) + { + this._functions = functions; + } + + /// + /// Creates a new instance of the class. + /// + public OpenAIAssistantAgentFactory(AssistantClient assistantClient, IList? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) + { + Throw.IfNull(assistantClient); + + this._assistantClient = assistantClient; + this._functions = functions; + } + + /// + /// Creates a new instance of the class. + /// + public OpenAIAssistantAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList? functions = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, loggerFactory) + { + this._functions = functions; + } + + /// + public override async Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) + { + Throw.IfNull(promptAgent); + + var model = promptAgent.Model as CurrentModels; + var apiType = model?.ApiType; + if (apiType?.IsUnknown() == false || apiType?.UnknownValue?.Equals(API_TYPE_ASSISTANTS, StringComparison.OrdinalIgnoreCase) == false) + { + return null; + } + + var options = new ChatClientAgentOptions() + { + Name = promptAgent.Name, + Description = promptAgent.Description, + Instructions = promptAgent.Instructions?.ToTemplateString(), + ChatOptions = promptAgent.GetChatOptions(this._functions), + }; + + AssistantClient? assistantClient = this._assistantClient ?? this.CreateAssistantClient(promptAgent); + if (assistantClient is not null) + { + var modelId = promptAgent.Model?.ModelNameHint; + Throw.IfNullOrEmpty(modelId, "The model id must be specified in the agent definition to create an OpenAI Assistant."); + Throw.IfNullOrEmpty(promptAgent.Instructions?.ToTemplateString(), "The instructions must be specified in the agent definition to create an OpenAI Assistant."); + + return await assistantClient.CreateAIAgentAsync( + modelId, + options + ).ConfigureAwait(false); + } + + return null; + } + + #region private + private readonly AssistantClient? _assistantClient; + private readonly IList? _functions; + + private const string API_TYPE_ASSISTANTS = "ASSISTANTS"; + #endregion +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIChatAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIChatAgentFactory.cs new file mode 100644 index 0000000000..587d39aae2 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIChatAgentFactory.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Agents.Persistent; +using Azure.Core; +using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; +using OpenAI.Chat; + +namespace Microsoft.Agents.AI; + +/// +/// Provides an which creates instances of using a . +/// +public sealed class OpenAIChatAgentFactory : OpenAIAgentFactory +{ + /// + /// Creates a new instance of the class. + /// + public OpenAIChatAgentFactory(IList? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) + { + this._functions = functions; + } + + /// + /// Creates a new instance of the class. + /// + public OpenAIChatAgentFactory(ChatClient chatClient, IList? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) + { + Throw.IfNull(chatClient); + + this._chatClient = chatClient; + this._functions = functions; + } + + /// + /// Creates a new instance of the class. + /// + public OpenAIChatAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList? functions = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, loggerFactory) + { + this._functions = functions; + } + + /// + public override async Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) + { + Throw.IfNull(promptAgent); + + var model = promptAgent.Model as CurrentModels; + var apiType = model?.ApiType; + if (apiType?.IsUnknown() == true || apiType?.Value != ModelApiType.Chat) + { + return null; + } + + var options = new ChatClientAgentOptions() + { + Name = promptAgent.Name, + Description = promptAgent.Description, + Instructions = promptAgent.Instructions?.ToTemplateString(), + ChatOptions = promptAgent.GetChatOptions(this._functions), + }; + + ChatClient? chatClient = this._chatClient ?? this.CreateChatClient(promptAgent); + if (chatClient is not null) + { + return new ChatClientAgent( + chatClient.AsIChatClient(), + options, + this.LoggerFactory); + } + + return null; + } + + #region private + private readonly ChatClient? _chatClient; + private readonly IList? _functions; + #endregion +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIResponseAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIResponseAgentFactory.cs new file mode 100644 index 0000000000..1e5ddfc98b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIResponseAgentFactory.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Agents.Persistent; +using Azure.Core; +using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; +using OpenAI.Responses; + +namespace Microsoft.Agents.AI; + +/// +/// Provides an which creates instances of using a . +/// +public sealed class OpenAIResponseAgentFactory : OpenAIAgentFactory +{ + /// + /// Creates a new instance of the class. + /// + public OpenAIResponseAgentFactory(IList? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) + { + this._functions = functions; + } + + /// + /// Creates a new instance of the class. + /// + public OpenAIResponseAgentFactory(OpenAIResponseClient responseClient, IList? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) + { + Throw.IfNull(responseClient); + + this._responseClient = responseClient; + this._functions = functions; + } + + /// + /// Creates a new instance of the class. + /// + public OpenAIResponseAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList? functions = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, loggerFactory) + { + this._functions = functions; + } + + /// + public override async Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) + { + Throw.IfNull(promptAgent); + + var model = promptAgent.Model as CurrentModels; + var apiType = model?.ApiType; + if (apiType?.IsUnknown() == true || apiType?.Value != ModelApiType.Responses) + { + return null; + } + + var options = new ChatClientAgentOptions() + { + Name = promptAgent.Name, + Description = promptAgent.Description, + Instructions = promptAgent.Instructions?.ToTemplateString(), + ChatOptions = promptAgent.GetChatOptions(this._functions), + }; + + var responseClient = this._responseClient ?? this.CreateResponseClient(promptAgent); + if (responseClient is not null) + { + return new ChatClientAgent( + responseClient.AsIChatClient(), + options, + this.LoggerFactory); + } + + return null; + } + + #region private + private readonly OpenAIResponseClient? _responseClient; + private readonly IList? _functions; + #endregion +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/AgentBotElementYaml.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/AgentBotElementYaml.cs new file mode 100644 index 0000000000..808bf76462 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/AgentBotElementYaml.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using Microsoft.Bot.ObjectModel; +using Microsoft.Bot.ObjectModel.Abstractions; +using Microsoft.Bot.ObjectModel.Yaml; +using Microsoft.Extensions.Configuration; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// Helper methods for creating from YAML. +/// +internal static class AgentBotElementYaml +{ + /// + /// Convert the given YAML text to a model. + /// + /// YAML representation of the to use to create the prompt function. + /// Optional instance which provides environment variables to the template. + [RequiresDynamicCode("Calls YamlDotNet.Serialization.DeserializerBuilder.DeserializerBuilder()")] + public static GptComponentMetadata FromYaml(string text, IConfiguration? configuration = null) + { + Throw.IfNullOrEmpty(text); + + using var yamlReader = new StringReader(text); + BotElement rootElement = YamlSerializer.Deserialize(yamlReader) ?? throw new InvalidDataException("Text does not contain a valid agent definition."); + + if (rootElement is not GptComponentMetadata promptAgent) + { + throw new InvalidDataException($"Unsupported root element: {rootElement.GetType().Name}. Expected an {nameof(GptComponentMetadata)}."); + } + + var botDefinition = WrapPromptAgentWithBot(promptAgent, configuration); + + return botDefinition.Descendants().OfType().First(); + } + + #region private + private sealed class AgentFeatureConfiguration : IFeatureConfiguration + { + public long GetInt64Value(string settingName, long defaultValue) => defaultValue; + + public string GetStringValue(string settingName, string defaultValue) => defaultValue; + + public bool IsEnvironmentFeatureEnabled(string featureName, bool defaultValue) => true; + + public bool IsTenantFeatureEnabled(string featureName, bool defaultValue) => defaultValue; + } + + public static BotDefinition WrapPromptAgentWithBot(this GptComponentMetadata element, IConfiguration? configuration = null) + { + var botBuilder = + new BotDefinition.Builder + { + Components = + { + new GptComponent.Builder + { + SchemaName = "default-schema", + Metadata = element.ToBuilder(), + } + } + }; + + if (configuration is not null) + { + foreach (var kvp in configuration.AsEnumerable().Where(kvp => kvp.Value is not null)) + { + botBuilder.EnvironmentVariables.Add(new EnvironmentVariableDefinition.Builder() + { + SchemaName = kvp.Key, + Id = Guid.NewGuid(), + DisplayName = kvp.Key, + ValueComponent = new EnvironmentVariableValue.Builder() + { + Id = Guid.NewGuid(), + Value = kvp.Value!, + }, + }); + } + } + + return botBuilder.Build(); + } + #endregion +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/AgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/AgentFactory.cs new file mode 100644 index 0000000000..653c14bdf8 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/AgentFactory.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// Represents a factory for creating instances. +/// +public abstract class AgentFactory +{ + /// + /// Create a from the specified . + /// + /// Definition of the agent to create. + /// Optional cancellation token. + /// The created , if null the agent type is not supported. + public async Task CreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) + { + Throw.IfNull(promptAgent); + + var agent = await this.TryCreateAsync(promptAgent, cancellationToken).ConfigureAwait(false); + return agent ?? throw new NotSupportedException($"Agent type {promptAgent.Kind} is not supported."); + } + + /// + /// Tries to create a from the specified . + /// + /// Definition of the agent to create. + /// Optional cancellation token. + /// The created , if null the agent type is not supported. + public abstract Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/AggregatorAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/AggregatorAgentFactory.cs new file mode 100644 index 0000000000..e9ddcdd6d8 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/AggregatorAgentFactory.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// Provides a which aggregates multiple agent factories. +/// +public sealed class AggregatorAgentFactory : AgentFactory +{ + private readonly AgentFactory[] _agentFactories; + + /// Initializes the instance. + /// Ordered instances to aggregate. + /// + /// Where multiple instances are provided, the first factory that supports the will be used. + /// + public AggregatorAgentFactory(params AgentFactory[] agentFactories) + { + Throw.IfNullOrEmpty(agentFactories); + + foreach (AgentFactory agentFactory in agentFactories) + { + Throw.IfNull(agentFactory, nameof(agentFactories)); + } + + this._agentFactories = agentFactories; + } + + /// + public override async Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) + { + Throw.IfNull(promptAgent); + + foreach (var agentFactory in this._agentFactories) + { + var agent = await agentFactory.TryCreateAsync(promptAgent, cancellationToken).ConfigureAwait(false); + if (agent is not null) + { + return agent; + } + } + + return null; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientAgentFactory.cs new file mode 100644 index 0000000000..aa32da4112 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientAgentFactory.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// Provides an which creates instances of . +/// +public sealed class ChatClientAgentFactory : AgentFactory +{ + /// + /// Creates a new instance of the class. + /// + public ChatClientAgentFactory(IChatClient chatClient, IList? functions = null, ILoggerFactory? loggerFactory = null) + { + Throw.IfNull(chatClient); + + this._chatClient = chatClient; + this._functions = functions; + this._loggerFactory = loggerFactory; + } + + /// + public override Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) + { + Throw.IfNull(promptAgent); + + var options = new ChatClientAgentOptions() + { + Name = promptAgent.Name, + Description = promptAgent.Description, + Instructions = promptAgent.Instructions?.ToTemplateString(), + ChatOptions = promptAgent.GetChatOptions(this._functions), + }; + + var agent = new ChatClientAgent(this._chatClient, options, this._loggerFactory); + + return Task.FromResult(agent); + } + + #region private + private readonly IChatClient _chatClient; + private readonly IList? _functions; + private readonly ILoggerFactory? _loggerFactory; + #endregion +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/CodeInterpreterToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/CodeInterpreterToolExtensions.cs new file mode 100644 index 0000000000..e6f13d5f54 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/CodeInterpreterToolExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class CodeInterpreterToolExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of + internal static HostedCodeInterpreterTool AsCodeInterpreterTool(this CodeInterpreterTool tool) + { + Throw.IfNull(tool); + + return new HostedCodeInterpreterTool(); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/FileSearchToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/FileSearchToolExtensions.cs new file mode 100644 index 0000000000..5e1cb1bb5f --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/FileSearchToolExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class FileSearchToolExtensions +{ + /// + /// Create a from a . + /// + /// Instance of + internal static HostedFileSearchTool CreateFileSearchTool(this FileSearchTool tool) + { + Throw.IfNull(tool); + + return new HostedFileSearchTool() + { + MaximumResultCount = (int?)tool.MaximumResultCount?.LiteralValue, + Inputs = tool.VectorStoreIds?.LiteralValue.Select(id => (AIContent)new HostedVectorStoreContent(id)).ToList(), + }; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/FunctionToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/FunctionToolExtensions.cs new file mode 100644 index 0000000000..2c54d7e749 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/FunctionToolExtensions.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class FunctionToolExtensions +{ + /// + /// Creates a from a . + /// + /// + /// If a matching function already exists in the provided list, it will be returned. + /// Otherwise, a new function declaration will be created. + /// + /// Instance of + /// Instance of + internal static AITool CreateOrGetAITool(this InvokeClientTaskAction tool, IList? functions) + { + Throw.IfNull(tool); + Throw.IfNull(tool.Name); + + // use the tool from the provided list if it exists + if (functions is not null) + { + var function = functions.FirstOrDefault(f => tool.Matches(f)); + + if (function is not null) + { + return function; + } + } + + return AIFunctionFactory.CreateDeclaration( + name: tool.Name, + description: tool.Description, + jsonSchema: tool.ClientActionInputSchema?.GetSchema() ?? s_defaultSchema); + } + + /// + /// Checks if a matches an . + /// + /// Instance of + /// Instance of + internal static bool Matches(this InvokeClientTaskAction tool, AIFunction aiFunc) + { + Throw.IfNull(tool); + Throw.IfNull(aiFunc); + + return tool.Name == aiFunc.Name; + } + + private static readonly JsonElement s_defaultSchema = JsonDocument.Parse("{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}").RootElement; +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/McpServerToolApprovalModeExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/McpServerToolApprovalModeExtensions.cs new file mode 100644 index 0000000000..ee5632368b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/McpServerToolApprovalModeExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.AI; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class McpServerToolApprovalModeExtensions +{ + /// + /// Converts a to a . + /// + /// Instance of + internal static HostedMcpServerToolApprovalMode AsHostedMcpServerToolApprovalMode(this McpServerToolApprovalMode mode) + { + return mode switch + { + McpServerToolNeverRequireApprovalMode => HostedMcpServerToolApprovalMode.NeverRequire, + McpServerToolAlwaysRequireApprovalMode => HostedMcpServerToolApprovalMode.AlwaysRequire, + McpServerToolRequireSpecificApprovalMode specificMode => + HostedMcpServerToolApprovalMode.RequireSpecific( + specificMode?.AlwaysRequireApprovalToolNames?.LiteralValue ?? [], + specificMode?.NeverRequireApprovalToolNames?.LiteralValue ?? [] + ), + _ => HostedMcpServerToolApprovalMode.AlwaysRequire, + }; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/McpServerToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/McpServerToolExtensions.cs new file mode 100644 index 0000000000..763e402625 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/McpServerToolExtensions.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class McpServerToolExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of + internal static HostedMcpServerTool CreateHostedMcpTool(this McpServerTool tool) + { + Throw.IfNull(tool); + Throw.IfNull(tool.ServerName?.LiteralValue); + Throw.IfNull(tool.Connection); + + var connection = tool.Connection as AnonymousConnection ?? throw new ArgumentException("Only AnonymousConnection is supported for MCP Server Tool connections.", nameof(tool)); + var serverUrl = connection.Endpoint?.LiteralValue; + Throw.IfNullOrEmpty(serverUrl, nameof(connection.Endpoint)); + + return new HostedMcpServerTool(tool.ServerName.LiteralValue, serverUrl) + { + ServerDescription = tool.ServerDescription?.LiteralValue, + AllowedTools = tool.AllowedTools?.LiteralValue, + ApprovalMode = tool.ApprovalMode?.AsHostedMcpServerToolApprovalMode(), + }; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/ModelOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/ModelOptionsExtensions.cs new file mode 100644 index 0000000000..7ad4d26a6b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/ModelOptionsExtensions.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class ModelOptionsExtensions +{ + /// + /// Converts the 'chatToolMode' property from a to a . + /// + /// Instance of + internal static ChatToolMode? AsChatToolMode(this ModelOptions modelOptions) + { + Throw.IfNull(modelOptions); + + var mode = modelOptions.ExtensionData?.GetPropertyOrNull(InitializablePropertyPath.Create("chatToolMode"))?.Value; + if (mode is null) + { + return null; + } + + return mode switch + { + "auto" => ChatToolMode.Auto, + "none" => ChatToolMode.None, + "require_any" => ChatToolMode.RequireAny, + _ => ChatToolMode.RequireSpecific(mode), + }; + } + + /// + /// Retrieves the 'additional_properties' property from a . + /// + /// Instance of + /// List of properties which should not be included in additional properties. + internal static AdditionalPropertiesDictionary? GetAdditionalProperties(this ModelOptions modelOptions, string[] excludedProperties) + { + Throw.IfNull(modelOptions); + + var options = modelOptions.ExtensionData; + if (options is null || options.Properties.Count == 0) + { + return null; + } + + var additionalProperties = options.Properties + .Where(kvp => !excludedProperties.Contains(kvp.Key)) + .ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value?.ToObject()); + + if (additionalProperties is null || additionalProperties.Count == 0) + { + return null; + } + + return new AdditionalPropertiesDictionary(additionalProperties); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs new file mode 100644 index 0000000000..dacc83bc63 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +public static class PromptAgentExtensions +{ + /// + /// Retrieves the 'options' property from a as a instance. + /// + /// Instance of + /// Instance of + public static ChatOptions? GetChatOptions(this GptComponentMetadata promptAgent, IList? functions) + { + Throw.IfNull(promptAgent); + + var outputSchema = promptAgent.OutputType; + var modelOptions = promptAgent.Model?.Options; + + var tools = promptAgent.GetAITools(functions); + + if (modelOptions is null && tools is null) + { + return null; + } + + return new ChatOptions() + { + Instructions = promptAgent.ResponseInstructions?.ToTemplateString(), + Temperature = (float?)modelOptions?.Temperature?.LiteralValue, + MaxOutputTokens = (int?)modelOptions?.MaxOutputTokens?.LiteralValue, + TopP = (float?)modelOptions?.TopP?.LiteralValue, + TopK = (int?)modelOptions?.TopK?.LiteralValue, + FrequencyPenalty = (float?)modelOptions?.FrequencyPenalty?.LiteralValue, + PresencePenalty = (float?)modelOptions?.PresencePenalty?.LiteralValue, + Seed = modelOptions?.Seed?.LiteralValue, + ResponseFormat = outputSchema?.AsChatResponseFormat(), + ModelId = promptAgent.Model?.ModelNameHint, + StopSequences = modelOptions?.StopSequences, + AllowMultipleToolCalls = modelOptions?.AllowMultipleToolCalls?.LiteralValue, + ToolMode = modelOptions?.AsChatToolMode(), + Tools = tools, + AdditionalProperties = modelOptions?.GetAdditionalProperties(s_chatOptionProperties), + }; + } + + /// + /// Retrieves the 'tools' property from a . + /// + /// Instance of + /// Instance of + internal static List? GetAITools(this GptComponentMetadata promptAgent, IList? functions) + { + return promptAgent.Tools.Select(tool => + { + return tool switch + { + CodeInterpreterTool => ((CodeInterpreterTool)tool).AsCodeInterpreterTool(), + InvokeClientTaskAction => ((InvokeClientTaskAction)tool).CreateOrGetAITool(functions), + McpServerTool => ((McpServerTool)tool).CreateHostedMcpTool(), + FileSearchTool => ((FileSearchTool)tool).CreateFileSearchTool(), + WebSearchTool => ((WebSearchTool)tool).CreateWebSearchTool(), + _ => throw new NotSupportedException($"Unable to create tool definition because of unsupported tool type: {tool.Kind}, supported tool types are: {string.Join(",", s_validToolKinds)}"), + }; + }).ToList() ?? []; + } + + #region private + private const string CodeInterpreterKind = "codeInterpreter"; + private const string FileSearchKind = "fileSearch"; + private const string FunctionKind = "function"; + private const string WebSearchKind = "webSearch"; + private const string McpKind = "mcp"; + + private static readonly string[] s_validToolKinds = + [ + CodeInterpreterKind, + FileSearchKind, + FunctionKind, + WebSearchKind, + McpKind + ]; + + private static readonly string[] s_chatOptionProperties = + [ + "allowMultipleToolCalls", + "conversationId", + "chatToolMode", + "frequencyPenalty", + "additionalInstructions", + "maxOutputTokens", + "modelId", + "presencePenalty", + "responseFormat", + "seed", + "stopSequences", + "temperature", + "topK", + "topP", + "toolMode", + "tools", + ]; + + #endregion +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PropertyInfoExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PropertyInfoExtensions.cs new file mode 100644 index 0000000000..a62fddec88 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PropertyInfoExtensions.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +public static class PropertyInfoExtensions +{ + /// + /// Creates a of and + /// from an of and . + /// + /// A read-only dictionary of property names and their corresponding objects. + public static Dictionary AsObjectDictionary(this IReadOnlyDictionary properties) + { + var result = new Dictionary(); + + foreach (var property in properties) + { + result[property.Key] = BuildPropertySchema(property.Value); + } + + return result; + } + + #region private + private static Dictionary BuildPropertySchema(PropertyInfo propertyInfo) + { + var propertySchema = new Dictionary(); + + // Map the DataType to JSON schema type and add type-specific properties + switch (propertyInfo.Type) + { + case StringDataType: + propertySchema["type"] = "string"; + break; + case NumberDataType: + propertySchema["type"] = "number"; + break; + case BooleanDataType: + propertySchema["type"] = "boolean"; + break; + case DateTimeDataType: + propertySchema["type"] = "string"; + propertySchema["format"] = "date-time"; + break; + case DateDataType: + propertySchema["type"] = "string"; + propertySchema["format"] = "date"; + break; + case TimeDataType: + propertySchema["type"] = "string"; + propertySchema["format"] = "time"; + break; + case RecordDataType nestedRecordType: +#pragma warning disable IL2026, IL3050 + // For nested records, recursively build the schema + var nestedSchema = nestedRecordType.GetSchema(); + var nestedJson = JsonSerializer.Serialize(nestedSchema, ElementSerializer.CreateOptions()); + var nestedDict = JsonSerializer.Deserialize>(nestedJson, ElementSerializer.CreateOptions()); +#pragma warning restore IL2026, IL3050 + if (nestedDict != null) + { + return nestedDict; + } + propertySchema["type"] = "object"; + break; + case TableDataType tableType: + propertySchema["type"] = "array"; + // TableDataType has Properties like RecordDataType + propertySchema["items"] = new Dictionary + { + ["type"] = "object", + ["properties"] = AsObjectDictionary(tableType.Properties), + ["additionalProperties"] = false + }; + break; + default: + propertySchema["type"] = "string"; + break; + } + + // Add description if available + if (!string.IsNullOrEmpty(propertyInfo.Description)) + { + propertySchema["description"] = propertyInfo.Description; + } + + return propertySchema; + } + #endregion +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/RecordDataTypeExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/RecordDataTypeExtensions.cs new file mode 100644 index 0000000000..e4b696d9cd --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/RecordDataTypeExtensions.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +public static class RecordDataTypeExtensions +{ + /// + /// Creates a from a . + /// + /// Instance of + internal static ChatResponseFormat? AsChatResponseFormat(this RecordDataType recordDataType) + { + Throw.IfNull(recordDataType); + + if (recordDataType.Properties.Count == 0) + { + return null; + } + + // TODO: Consider adding schemaName and schemaDescription parameters to this method. + return ChatResponseFormat.ForJsonSchema( + schema: recordDataType.GetSchema(), + schemaName: recordDataType.GetSchemaName(), + schemaDescription: recordDataType.GetSchemaDescription()); + } + + /// + /// Converts a to a . + /// + /// Instance of +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + public static JsonElement GetSchema(this RecordDataType recordDataType) + { + Throw.IfNull(recordDataType); + + var schemaObject = new Dictionary + { + ["type"] = "object", + ["properties"] = recordDataType.Properties.AsObjectDictionary(), + ["additionalProperties"] = false + }; + + var json = JsonSerializer.Serialize(schemaObject, ElementSerializer.CreateOptions()); + return JsonSerializer.Deserialize(json); + } +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + + /// + /// Retrieves the 'schemaName' property from a . + /// + internal static string? GetSchemaName(this RecordDataType recordDataType) + { + Throw.IfNull(recordDataType); + + return recordDataType.ExtensionData?.GetPropertyOrNull(InitializablePropertyPath.Create("schemaName"))?.Value; + } + + /// + /// Retrieves the 'schemaDescription' property from a . + /// + internal static string? GetSchemaDescription(this RecordDataType recordDataType) + { + Throw.IfNull(recordDataType); + + return recordDataType.ExtensionData?.GetPropertyOrNull(InitializablePropertyPath.Create("schemaDescription"))?.Value; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/RecordDataValueExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/RecordDataValueExtensions.cs new file mode 100644 index 0000000000..6351b7badb --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/RecordDataValueExtensions.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +public static class RecordDataValueExtensions +{ + /// + /// Retrieves a 'number' property from a + /// + /// Instance of + /// Path of the property to retrieve + public static decimal? GetNumber(this RecordDataValue recordData, string propertyPath) + { + Throw.IfNull(recordData); + + var numberValue = recordData.GetPropertyOrNull(InitializablePropertyPath.Create(propertyPath)); + return numberValue?.Value; + } + + /// + /// Retrieves a nullable boolean value from the specified property path within the given record data. + /// + /// Instance of + /// Path of the property to retrieve + public static bool? GetBoolean(this RecordDataValue recordData, string propertyPath) + { + Throw.IfNull(recordData); + + var booleanValue = recordData.GetPropertyOrNull(InitializablePropertyPath.Create(propertyPath)); + return booleanValue?.Value; + } + + /// + /// Converts a to a . + /// + /// Instance of + public static IReadOnlyDictionary ToDictionary(this RecordDataValue recordData) + { + Throw.IfNull(recordData); + + return recordData.Properties.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value?.ToString() ?? string.Empty + ); + } + + /// + /// Retrieves the 'schema' property from a . + /// + /// Instance of +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + public static JsonElement? GetSchema(this RecordDataValue recordData) + { + Throw.IfNull(recordData); + + try + { + var schemaStr = recordData.GetPropertyOrNull(InitializablePropertyPath.Create("json_schema.schema")); + if (schemaStr?.Value is not null) + { + return JsonSerializer.Deserialize(schemaStr.Value); + } + } + catch (InvalidCastException) + { + // Ignore and try next + } + + var responseFormRec = recordData.GetPropertyOrNull(InitializablePropertyPath.Create("json_schema.schema")); + if (responseFormRec is not null) + { + var json = JsonSerializer.Serialize(responseFormRec, ElementSerializer.CreateOptions()); + return JsonSerializer.Deserialize(json); + } + + return null; + } +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + + internal static object? ToObject(this DataValue? value) + { + if (value is null) + { + return null; + } + return value switch + { + StringDataValue s => s.Value, + NumberDataValue n => n.Value, + BooleanDataValue b => b.Value, + TableDataValue t => t.Values.Select(v => v.ToObject()).ToList(), + RecordDataValue r => r.Properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToObject()), + _ => throw new NotSupportedException($"Unsupported DataValue type: {value.GetType().FullName}"), + }; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/WebSearchToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/WebSearchToolExtensions.cs new file mode 100644 index 0000000000..e6ee360308 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/WebSearchToolExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +internal static class WebSearchToolExtensions +{ + /// + /// Create a from a . + /// + /// Instance of + internal static HostedWebSearchTool CreateWebSearchTool(this WebSearchTool tool) + { + Throw.IfNull(tool); + + return new HostedWebSearchTool(); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/YamlAgentFactoryExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/YamlAgentFactoryExtensions.cs new file mode 100644 index 0000000000..2bbda44a71 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/YamlAgentFactoryExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// Extension methods for to support YAML based agent definitions. +/// +public static class YamlAgentFactoryExtensions +{ + /// + /// Create a from the given agent YAML. + /// + /// which will be used to create the agent. + /// Text string containing the YAML representation of an . + /// Optional cancellation token + [RequiresDynamicCode("Calls YamlDotNet.Serialization.DeserializerBuilder.DeserializerBuilder()")] + public static Task CreateFromYamlAsync(this AgentFactory agentFactory, string agentYaml, CancellationToken cancellationToken = default) + { + Throw.IfNull(agentFactory); + Throw.IfNullOrEmpty(agentYaml); + + var agentDefinition = AgentBotElementYaml.FromYaml(agentYaml); + + return agentFactory.CreateAsync( + agentDefinition, + cancellationToken); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj b/dotnet/src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj new file mode 100644 index 0000000000..72b2d0d743 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj @@ -0,0 +1,43 @@ + + + + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) + preview + $(NoWarn);MEAI001 + + + + true + true + true + true + + + + + + + + + + + + + Microsoft Agent Framework Declarative + Provides Microsoft Agent Framework support for declarative agents. + + + + + + + + + + + + + + + diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs index e20d1ab448..6520ff117f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs @@ -28,6 +28,8 @@ public static class AGUIEndpointRouteBuilderExtensions /// The URL pattern for the endpoint. /// The agent instance. /// An for the mapped endpoint. + [RequiresUnreferencedCode("Dynamic code may be required for this endpoint.")] + [RequiresDynamicCode("Dynamic code may be required for this endpoint.")] public static IEndpointConventionBuilder MapAGUI( this IEndpointRouteBuilder endpoints, [StringSyntax("route")] string pattern, diff --git a/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj b/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj index e3d7f00aa1..bf447db538 100644 --- a/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj +++ b/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj @@ -35,6 +35,7 @@ + diff --git a/dotnet/src/Shared/IntegrationTests/FoundryProjectConfiguration.cs b/dotnet/src/Shared/IntegrationTests/FoundryProjectConfiguration.cs new file mode 100644 index 0000000000..cce0d8afe1 --- /dev/null +++ b/dotnet/src/Shared/IntegrationTests/FoundryProjectConfiguration.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Shared.IntegrationTests; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. +#pragma warning disable CA1812 // Internal class that is apparently never instantiated. + +internal sealed class FoundryProjectConfiguration +{ + public string Endpoint { get; set; } + + public string ModelId { get; set; } + + public string BingConnectionId { get; set; } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/AzureOpenAIDeclarativeAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/AzureOpenAIDeclarativeAgentTests.cs new file mode 100644 index 0000000000..86d6c91542 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/AzureOpenAIDeclarativeAgentTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel; +using System.IO; +using System.Threading.Tasks; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Declarative.IntegrationTests; + +/// +/// Tests for declarative agents created using . +/// +public sealed class AzureOpenAIDeclarativeAgentTests(ITestOutputHelper output) : BaseIntegrationTest(output) +{ + [Fact] + public async Task CanCreateAndRunChatAgentAsync() + { + // Example function tool that can be used by the agent. + [Description("Get the weather for a given location.")] + static string GetWeather( + [Description("The city and state, e.g. San Francisco, CA")] string location, + [Description("The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.")] string unit) + => $"The weather in {location} is cloudy with a high of {(unit.Equals("celsius", StringComparison.Ordinal) ? "15°C" : "59°F")}."; + + // Arrange + var endpointUri = new Uri(this.FoundryConfiguration.Endpoint); + var tokenCredential = new AzureCliCredential(); + var agentFactory = new AggregatorAgentFactory( + [ + new OpenAIChatAgentFactory(endpointUri, tokenCredential), + new OpenAIResponseAgentFactory(endpointUri, tokenCredential), + new OpenAIAssistantAgentFactory(endpointUri, tokenCredential) + ]); + var agentYaml = File.ReadAllText("../../../../../../agent-samples/azure/AzureOpenAIChat.yaml"); + agentYaml = agentYaml.Replace("=Env.AZURE_OPENAI_DEPLOYMENT_NAME", this.FoundryConfiguration.DeploymentName); + + // Create agent run options + var options = new ChatClientAgentRunOptions(new() + { + Tools = [AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather))] + }); + + // Act + var agent = await agentFactory.CreateFromYamlAsync(agentYaml); + var response = await agent!.RunAsync("What is the weather in Cambridge, MA in °C?", options: options); + this.Output.WriteLine($"Agent Response: {response.Text}"); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(response); + Assert.NotEmpty(response.Text); + } + + [Fact] + public async Task CanCreateAndRunResponsesAgentAsync() + { + // Example function tool that can be used by the agent. + [Description("Get the weather for a given location.")] + static string GetWeather( + [Description("The city and state, e.g. San Francisco, CA")] string location, + [Description("The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.")] string unit) + => $"The weather in {location} is cloudy with a high of {(unit.Equals("celsius", StringComparison.Ordinal) ? "15°C" : "59°F")}."; + + // Arrange + var endpointUri = new Uri(this.FoundryConfiguration.Endpoint); + var tokenCredential = new AzureCliCredential(); + var agentFactory = new AggregatorAgentFactory( + [ + new OpenAIChatAgentFactory(endpointUri, tokenCredential), + new OpenAIResponseAgentFactory(endpointUri, tokenCredential), + new OpenAIAssistantAgentFactory(endpointUri, tokenCredential) + ]); + var agentYaml = File.ReadAllText("../../../../../../agent-samples/azure/AzureOpenAIResponses.yaml"); + agentYaml = agentYaml.Replace("=Env.AZURE_OPENAI_DEPLOYMENT_NAME", this.FoundryConfiguration.DeploymentName); + + // Create agent run options + var options = new ChatClientAgentRunOptions(new() + { + Tools = [AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather))] + }); + + // Act + var agent = await agentFactory.CreateFromYamlAsync(agentYaml); + var response = await agent!.RunAsync("What is the weather in Cambridge, MA in °C?", options: options); + this.Output.WriteLine($"Agent Response: {response.Text}"); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(response); + Assert.NotEmpty(response.Text); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/BaseIntegrationTest.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/BaseIntegrationTest.cs new file mode 100644 index 0000000000..68d7723676 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/BaseIntegrationTest.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Shared.IntegrationTests; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Declarative.IntegrationTests; + +/// +/// Base class for integration tests. +/// +public abstract class BaseIntegrationTest : IDisposable +{ + private IConfigurationRoot? _configuration; + private AzureAIConfiguration? _foundryConfiguration; + private OpenAIConfiguration? _openAIConfiguration; + private FoundryProjectConfiguration? _foundryProjectConfiguration; + + protected IConfigurationRoot Configuration => this._configuration ??= InitializeConfig(); + + internal AzureAIConfiguration FoundryConfiguration + { + get + { + this._foundryConfiguration ??= this.Configuration.GetSection("AzureAI").Get(); + Assert.NotNull(this._foundryConfiguration); + return this._foundryConfiguration; + } + } + + internal OpenAIConfiguration OpenAIConfiguration + { + get + { + this._openAIConfiguration ??= this.Configuration.GetSection("OpenAI").Get(); + Assert.NotNull(this._openAIConfiguration); + return this._openAIConfiguration; + } + } + + internal FoundryProjectConfiguration FoundryProjectConfiguration + { + get + { + this._foundryProjectConfiguration ??= this.Configuration.GetSection("FoundryProject").Get(); + Assert.NotNull(this._foundryProjectConfiguration); + return this._foundryProjectConfiguration; + } + } + + public TestOutputAdapter Output { get; } + + protected BaseIntegrationTest(ITestOutputHelper output) + { + this.Output = new TestOutputAdapter(output); + Console.SetOut(this.Output); + } + + public void Dispose() + { + this.Dispose(isDisposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool isDisposing) + { + if (isDisposing) + { + this.Output.Dispose(); + } + } + + private static IConfigurationRoot InitializeConfig() => + new ConfigurationBuilder() + .AddJsonFile("appsettings.Development.json", true) + .AddEnvironmentVariables() + .AddUserSecrets(Assembly.GetExecutingAssembly()) + .Build(); +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/ChatClientDeclarativeAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/ChatClientDeclarativeAgentTests.cs new file mode 100644 index 0000000000..236d8ed52f --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/ChatClientDeclarativeAgentTests.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel; +using System.IO; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Declarative.IntegrationTests; + +/// +/// Tests for declarative agents created using . +/// +public sealed class ChatClientDeclarativeAgentTests(ITestOutputHelper output) : BaseIntegrationTest(output) +{ + [Fact] + public async Task CanCreateAndRunAssistantAgentAsync() + { + // Arrange + var chatClient = this.CreateIChatClient(); + var agentFactory = new ChatClientAgentFactory(chatClient); + var agentYaml = File.ReadAllText("../../../../../../agent-samples/chatclient/Assistant.yaml"); + + // Act + var agent = await agentFactory.CreateFromYamlAsync(agentYaml); + var response = await agent!.RunAsync("Tell me a joke about a pirate in Italian."); + this.Output.WriteLine($"Agent Response: {response.Text}"); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(response); + Assert.NotEmpty(response.Text); + } + + [Fact] + public async Task CanCreateAndRunGetWeatherAgentAsync() + { + // Example function tool that can be used by the agent. + [Description("Get the weather for a given location.")] + static string GetWeather( + [Description("The city and state, e.g. San Francisco, CA")] string location, + [Description("The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.")] string unit) + => $"The weather in {location} is cloudy with a high of {(unit.Equals("celsius", StringComparison.Ordinal) ? "15°C" : "59°F")}."; + + // Arrange + var chatClient = this.CreateIChatClient(); + var agentFactory = new ChatClientAgentFactory(chatClient, [AIFunctionFactory.Create(GetWeather, "GetWeather")]); + var agentYaml = File.ReadAllText("../../../../../../agent-samples/chatclient/GetWeather.yaml"); + + // Act + var agent = await agentFactory.CreateFromYamlAsync(agentYaml); + var response = await agent!.RunAsync("What is the weather in Cambridge, MA in °C?"); + this.Output.WriteLine($"Agent Response: {response.Text}"); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(response); + Assert.NotEmpty(response.Text); + } + + private IChatClient CreateIChatClient() + { + var endpoint = this.FoundryConfiguration.Endpoint; + var deploymentName = this.FoundryConfiguration.DeploymentName; + + // Create the chat client + return new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .AsIChatClient(); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/FoundryDeclarativeAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/FoundryDeclarativeAgentTests.cs new file mode 100644 index 0000000000..a345031f4d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/FoundryDeclarativeAgentTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel; +using System.IO; +using System.Threading.Tasks; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Declarative.IntegrationTests; + +/// +/// Tests for declarative agents created using . +/// +public sealed class FoundryDeclarativeAgentTests(ITestOutputHelper output) : BaseIntegrationTest(output) +{ + [Fact] + public async Task CanCreateAndRunPersistentAgentAsync() + { + // Example function tool that can be used by the agent. + [Description("Get the weather for a given location.")] + static string GetWeather( + [Description("The city and state, e.g. San Francisco, CA")] string location, + [Description("The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.")] string unit) + => $"The weather in {location} is cloudy with a high of {(unit.Equals("celsius", StringComparison.Ordinal) ? "15°C" : "59°F")}."; + + // Arrange + var agentFactory = new FoundryPersistentAgentFactory(new AzureCliCredential()); + var agentYaml = File.ReadAllText("../../../../../../agent-samples/foundry/PersistentAgent.yaml"); + agentYaml = agentYaml.Replace("=Env.AZURE_FOUNDRY_PROJECT_ENDPOINT", this.FoundryProjectConfiguration.Endpoint); + agentYaml = agentYaml.Replace("=Env.AZURE_FOUNDRY_PROJECT_MODEL_ID", this.FoundryProjectConfiguration.ModelId); + + // Create agent run options + var options = new ChatClientAgentRunOptions(new() + { + Tools = [AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather))] + }); + + // Act + var agent = await agentFactory.CreateFromYamlAsync(agentYaml); + var response = await agent!.RunAsync("What is the weather in Cambridge, MA in °C?", options: options); + this.Output.WriteLine($"Agent Response: {response.Text}"); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(response); + Assert.NotEmpty(response.Text); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/Microsoft.Agents.AI.Declarative.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/Microsoft.Agents.AI.Declarative.IntegrationTests.csproj new file mode 100644 index 0000000000..9b84b6e4c5 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/Microsoft.Agents.AI.Declarative.IntegrationTests.csproj @@ -0,0 +1,36 @@ + + + + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) + True + true + $(NoWarn);CS1591 + + + + true + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/OpenAIDeclarativeAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/OpenAIDeclarativeAgentTests.cs new file mode 100644 index 0000000000..a0b2548ea2 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/OpenAIDeclarativeAgentTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Declarative.IntegrationTests; + +/// +/// Tests for declarative agents created using . +/// +public sealed class OpenAIDeclarativeAgentTests(ITestOutputHelper output) : BaseIntegrationTest(output) +{ + [Fact] + public async Task CanCreateAndRunChatAgentAsync() + { + // Example function tool that can be used by the agent. + [Description("Get the weather for a given location.")] + static string GetWeather( + [Description("The city and state, e.g. San Francisco, CA")] string location, + [Description("The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.")] string unit) + => $"The weather in {location} is cloudy with a high of {(unit.Equals("celsius", StringComparison.Ordinal) ? "15°C" : "59°F")}."; + + // Arrange + var agentFactory = new AggregatorAgentFactory( + [ + new OpenAIChatAgentFactory(), + new OpenAIResponseAgentFactory(), + new OpenAIAssistantAgentFactory() + ]); + var agentYaml = File.ReadAllText("../../../../../../agent-samples/openai/OpenAIChat.yaml"); + agentYaml = agentYaml.Replace("=Env.OPENAI_APIKEY", this.OpenAIConfiguration.ApiKey); + agentYaml = agentYaml.Replace("=Env.OPENAI_MODEL", this.OpenAIConfiguration.ChatModelId); + + // Create agent run options + var options = new ChatClientAgentRunOptions(new() + { + Tools = [AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather))] + }); + + // Act + var agent = await agentFactory.CreateFromYamlAsync(agentYaml); + var response = await agent!.RunAsync("What is the weather in Cambridge, MA in °C?", options: options); + this.Output.WriteLine($"Agent Response: {response.Text}"); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(response); + Assert.NotEmpty(response.Text); + } + + [Fact] + public async Task CanCreateAndRunResponsesAgentAsync() + { + // Example function tool that can be used by the agent. + [Description("Get the weather for a given location.")] + static string GetWeather( + [Description("The city and state, e.g. San Francisco, CA")] string location, + [Description("The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.")] string unit) + => $"The weather in {location} is cloudy with a high of {(unit.Equals("celsius", StringComparison.Ordinal) ? "15°C" : "59°F")}."; + + // Arrange + var agentFactory = new AggregatorAgentFactory( + [ + new OpenAIChatAgentFactory(), + new OpenAIResponseAgentFactory(), + new OpenAIAssistantAgentFactory() + ]); + var agentYaml = File.ReadAllText("../../../../../../agent-samples/openai/OpenAIResponses.yaml"); + agentYaml = agentYaml.Replace("=Env.OPENAI_APIKEY", this.OpenAIConfiguration.ApiKey); + agentYaml = agentYaml.Replace("=Env.OPENAI_MODEL", this.OpenAIConfiguration.ChatModelId); + + // Create agent run options + var options = new ChatClientAgentRunOptions(new() + { + Tools = [AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather))] + }); + + // Act + var agent = await agentFactory.CreateFromYamlAsync(agentYaml); + var response = await agent!.RunAsync("What is the weather in Cambridge, MA in °C?", options: options); + this.Output.WriteLine($"Agent Response: {response.Text}"); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(response); + Assert.NotEmpty(response.Text); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/TestOutputAdapter.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/TestOutputAdapter.cs new file mode 100644 index 0000000000..d57d054aaf --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.IntegrationTests/TestOutputAdapter.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Declarative.IntegrationTests; + +public sealed class TestOutputAdapter(ITestOutputHelper output) : TextWriter, ILogger, ILoggerFactory +{ + private readonly Stack _scopes = []; + + public override Encoding Encoding { get; } = Encoding.UTF8; + + public void AddProvider(ILoggerProvider provider) => throw new NotSupportedException(); + + public ILogger CreateLogger(string categoryName) => this; + + public bool IsEnabled(LogLevel logLevel) => true; + + public override void WriteLine(object? value) => this.SafeWrite($"{value}"); + + public override void WriteLine(string? format, params object?[] arg) => this.SafeWrite(string.Format(format ?? string.Empty, arg)); + + public override void WriteLine(string? value) => this.SafeWrite(value ?? string.Empty); + + public override void Write(object? value) => this.SafeWrite($"{value}"); + + public override void Write(char[]? buffer) => this.SafeWrite(new string(buffer)); + + public IDisposable BeginScope(TState state) where TState : notnull + { + this._scopes.Push($"{state}"); + return new LoggerScope(() => this._scopes.Pop()); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + string message = formatter(state, exception); + string scope = this._scopes.Count > 0 ? $"[{this._scopes.Peek()}] " : string.Empty; + output.WriteLine($"{scope}{message}"); + } + + private void SafeWrite(string value) + { + try + { + output.WriteLine(value ?? string.Empty); + } + catch (InvalidOperationException exception) when (exception.Message == "There is no currently active test.") + { + // This exception is thrown when the test output is accessed outside of a test context. + // We can ignore it since we are not in a test context. + } + } + + private sealed class LoggerScope(Action action) : IDisposable + { + private bool _disposed; + + public void Dispose() + { + if (!this._disposed) + { + action.Invoke(); + this._disposed = true; + } + } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs new file mode 100644 index 0000000000..0d9774cd42 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json.Serialization; +using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Agents.AI.Declarative.UnitTests; + +/// +/// Unit tests for +/// +public sealed class AgentBotElementYamlTests +{ + [Theory] + [InlineData(PromptAgents.AgentWithEverything)] + [InlineData(PromptAgents.AgentWithApiKeyConnection)] + [InlineData(PromptAgents.AgentWithEnvironmentVariables)] + [InlineData(PromptAgents.AgentWithOutputSchema)] + [InlineData(PromptAgents.OpenAIChatAgent)] + [InlineData(PromptAgents.AgentWithCurrentModels)] + [InlineData(PromptAgents.AgentWithRemoteConnection)] + public void FromYaml_DoesNotThrow(string text) + { + // Arrange & Act + var agent = AgentBotElementYaml.FromYaml(text); + + // Assert + Assert.NotNull(agent); + } + + [Fact] + public void FromYaml_Properties() + { + // Arrange & Act + var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything); + + // Assert + Assert.NotNull(agent); + Assert.Equal("AgentName", agent.Name); + Assert.Equal("Agent description", agent.Description); + Assert.Equal("You are a helpful assistant.", agent.Instructions?.ToTemplateString()); + Assert.NotNull(agent.Model); + Assert.True(agent.Tools.Length > 0); + } + + [Fact] + public void FromYaml_CurrentModels() + { + // Arrange & Act + var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithCurrentModels); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(agent.Model); + Assert.Equal("gpt-4o", agent.Model.ModelNameHint); + Assert.NotNull(agent.Model.Options); + Assert.Equal(0.7f, (float?)agent.Model.Options?.Temperature?.LiteralValue); + Assert.Equal(0.9f, (float?)agent.Model.Options?.TopP?.LiteralValue); + + // Assert contents using extension methods + Assert.Equal(1024, agent.Model.Options?.MaxOutputTokens?.LiteralValue); + Assert.Equal(50, agent.Model.Options?.TopK?.LiteralValue); + Assert.Equal(0.7f, (float?)agent.Model.Options?.FrequencyPenalty?.LiteralValue); + Assert.Equal(0.7f, (float?)agent.Model.Options?.PresencePenalty?.LiteralValue); + Assert.Equal(42, agent.Model.Options?.Seed?.LiteralValue); + Assert.Equal(PromptAgents.s_stopSequences, agent.Model.Options?.StopSequences); + Assert.True(agent.Model.Options?.AllowMultipleToolCalls?.LiteralValue); + Assert.Equal(ChatToolMode.Auto, agent.Model.Options?.AsChatToolMode()); + } + + [Fact] + public void FromYaml_OutputSchema() + { + // Arrange & Act + var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithOutputSchema); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(agent.OutputType); + var responseFormat = agent.OutputType.AsChatResponseFormat() as ChatResponseFormatJson; + Assert.NotNull(responseFormat); + Assert.NotNull(responseFormat.Schema); + } + + [Fact] + public void FromYaml_CodeInterpreter() + { + // Arrange & Act + var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything); + + // Assert + Assert.NotNull(agent); + var tools = agent.Tools; + var codeInterpreterTools = tools.Where(t => t is CodeInterpreterTool).ToArray(); + Assert.Single(codeInterpreterTools); + var codeInterpreterTool = codeInterpreterTools[0] as CodeInterpreterTool; + Assert.NotNull(codeInterpreterTool); + } + + [Fact] + public void FromYaml_FunctionTool() + { + // Arrange & Act + var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything); + + // Assert + Assert.NotNull(agent); + var tools = agent.Tools; + var functionTools = tools.Where(t => t is InvokeClientTaskAction).ToArray(); + Assert.Single(functionTools); + var functionTool = functionTools[0] as InvokeClientTaskAction; + Assert.NotNull(functionTool); + Assert.Equal("GetWeather", functionTool.Name); + Assert.Equal("Get the weather for a given location.", functionTool.Description); + // TODO check schema + } + + [Fact] + public void FromYaml_MCP() + { + // Arrange & Act + var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything); + + // Assert + Assert.NotNull(agent); + var tools = agent.Tools; + var mcpTools = tools.Where(t => t is McpServerTool).ToArray(); + Assert.Single(mcpTools); + var mcpTool = mcpTools[0] as McpServerTool; + Assert.NotNull(mcpTool); + Assert.Equal("PersonInfoTool", mcpTool.ServerName?.LiteralValue); + var connection = mcpTool.Connection as AnonymousConnection; + Assert.NotNull(connection); + Assert.Equal("https://my-mcp-endpoint.com/api", connection.Endpoint?.LiteralValue); + } + + [Fact] + public void FromYaml_WebSearchTool() + { + // Arrange & Act + var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything); + + // Assert + Assert.NotNull(agent); + var tools = agent.Tools; + var webSearchTools = tools.Where(t => t is WebSearchTool).ToArray(); + Assert.Single(webSearchTools); + Assert.NotNull(webSearchTools[0] as WebSearchTool); + } + + [Fact] + public void FromYaml_FileSearchTool() + { + // Arrange & Act + var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything); + + // Assert + Assert.NotNull(agent); + var tools = agent.Tools; + var fileSearchTools = tools.Where(t => t is FileSearchTool).ToArray(); + Assert.Single(fileSearchTools); + var fileSearchTool = fileSearchTools[0] as FileSearchTool; + Assert.NotNull(fileSearchTool); + + // Verify vector store content property exists and has correct values + Assert.NotNull(fileSearchTool.VectorStoreIds); + Assert.Equal(3, fileSearchTool.VectorStoreIds.LiteralValue.Length); + Assert.Equal("1", fileSearchTool.VectorStoreIds.LiteralValue[0]); + Assert.Equal("2", fileSearchTool.VectorStoreIds.LiteralValue[1]); + Assert.Equal("3", fileSearchTool.VectorStoreIds.LiteralValue[2]); + } + + [Fact] + public void FromYaml_ApiKeyConnection() + { + // Arrange & Act + var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithApiKeyConnection); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(agent.Model); + var model = agent.Model as CurrentModels; + Assert.NotNull(model); + Assert.NotNull(model.Connection); + Assert.IsType(model.Connection); + var connection = model.Connection as ApiKeyConnection; + Assert.NotNull(connection); + Assert.Equal("https://my-azure-openai-endpoint.openai.azure.com/", connection.Endpoint?.LiteralValue); + Assert.Equal("my-api-key", connection.Key?.LiteralValue); + } + + [Fact] + public void FromYaml_RemoteConnection() + { + // Arrange & Act + var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithRemoteConnection); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(agent.Model); + var model = agent.Model as CurrentModels; + Assert.NotNull(model); + Assert.NotNull(model.Connection); + Assert.IsType(model.Connection); + var connection = model.Connection as RemoteConnection; + Assert.NotNull(connection); + Assert.Equal("https://my-azure-openai-endpoint.openai.azure.com/", connection.Endpoint?.LiteralValue); + } + + [Fact] + public void FromYaml_WithEnvironmentVariables() + { + // Arrange + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["OpenAIEndpoint"] = "endpoint", + ["OpenAIModelId"] = "modelId", + ["OpenAIApiKey"] = "apiKey" + }) + .Build(); + + // Act + var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEnvironmentVariables, configuration); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(agent.Model); + var model = agent.Model as CurrentModels; + Assert.NotNull(model); + Assert.NotNull(model.Connection); + Assert.IsType(model.Connection); + //Assert.Equal("https://my-azure-openai-endpoint.openai.azure.com/", agent.Model.Connection.Endpoint?.LiteralValue); + //Assert.Equal("apiKey", connection.Key?.LiteralValue); + //Assert.Equal("modelId", model.Id); + } + + /// + /// Represents information about a person, including their name, age, and occupation, matched to the JSON schema used in the agent. + /// + [Description("Information about a person including their name, age, and occupation")] + public class PersonInfo + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("age")] + public int? Age { get; set; } + + [JsonPropertyName("occupation")] + public string? Occupation { get; set; } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs new file mode 100644 index 0000000000..b9078ee0b0 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Moq; + +namespace Microsoft.Agents.AI.Declarative.UnitTests.ChatClient; + +/// +/// Unit tests for . +/// +public sealed class ChatClientAgentFactoryTests +{ + private readonly Mock _mockChatClient; + + public ChatClientAgentFactoryTests() + { + this._mockChatClient = new(); + } + + [Fact] + public async Task TryCreateAsync_WithChatClientInConstructor_CreatesAgentAsync() + { + // Arrange + var promptAgent = PromptAgents.CreateTestPromptAgent(); + ChatClientAgentFactory factory = new(this._mockChatClient.Object); + + // Act + AIAgent? agent = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + Assert.Equal("Test Agent", agent.Name); + Assert.Equal("Test Description", agent.Description); + } + + [Fact] + public async Task TryCreateAsync_Creates_ChatClientAgentAsync() + { + // Arrange + var promptAgent = PromptAgents.CreateTestPromptAgent(); + ChatClientAgentFactory factory = new(this._mockChatClient.Object); + + // Act + AIAgent? agent = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + var chatClientAgent = agent as ChatClientAgent; + Assert.NotNull(chatClientAgent); + Assert.Equal("You are a helpful assistant.", chatClientAgent.Instructions); + Assert.NotNull(chatClientAgent.ChatClient); + Assert.NotNull(chatClientAgent.ChatOptions); + } + + [Fact] + public async Task TryCreateAsync_Creates_ChatOptionsAsync() + { + // Arrange + var promptAgent = PromptAgents.CreateTestPromptAgent(); + ChatClientAgentFactory factory = new(this._mockChatClient.Object); + + // Act + AIAgent? agent = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + var chatClientAgent = agent as ChatClientAgent; + Assert.NotNull(chatClientAgent?.ChatOptions); + Assert.Equal("Provide detailed and accurate responses.", chatClientAgent?.ChatOptions?.Instructions); + Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.Temperature); + Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.FrequencyPenalty); + Assert.Equal(1024, chatClientAgent?.ChatOptions?.MaxOutputTokens); + Assert.Equal(0.9F, chatClientAgent?.ChatOptions?.TopP); + Assert.Equal(50, chatClientAgent?.ChatOptions?.TopK); + Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.PresencePenalty); + Assert.Equal(42L, chatClientAgent?.ChatOptions?.Seed); + Assert.NotNull(chatClientAgent?.ChatOptions?.ResponseFormat); + Assert.Equal("gpt-4o", chatClientAgent?.ChatOptions?.ModelId); + Assert.Equal(["###", "END", "STOP"], chatClientAgent?.ChatOptions?.StopSequences); + Assert.True(chatClientAgent?.ChatOptions?.AllowMultipleToolCalls); + Assert.Equal(ChatToolMode.Auto, chatClientAgent?.ChatOptions?.ToolMode); + Assert.Equal("customValue", chatClientAgent?.ChatOptions?.AdditionalProperties?["customProperty"]); + } + + [Fact] + public async Task TryCreateAsync_Creates_ToolsAsync() + { + // Arrange + var promptAgent = PromptAgents.CreateTestPromptAgent(); + ChatClientAgentFactory factory = new(this._mockChatClient.Object); + + // Act + AIAgent? agent = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + var chatClientAgent = agent as ChatClientAgent; + Assert.NotNull(chatClientAgent?.ChatOptions?.Tools); + var tools = chatClientAgent?.ChatOptions?.Tools; + Assert.Equal(5, tools?.Count); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj new file mode 100644 index 0000000000..dcdded59ce --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj @@ -0,0 +1,18 @@ + + + + $(ProjectsTargetFrameworks) + $(NoWarn);IDE1006 + + + + + + + + + + + + + diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/PromptAgents.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/PromptAgents.cs new file mode 100644 index 0000000000..075b134981 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/PromptAgents.cs @@ -0,0 +1,326 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Bot.ObjectModel; + +namespace Microsoft.Agents.AI.Declarative.UnitTests; +internal static class PromptAgents +{ + internal const string AgentWithEverything = + """ + kind: Prompt + name: AgentName + description: Agent description + instructions: You are a helpful assistant. + model: + id: gpt-4o + options: + temperature: 0.7 + maxOutputTokens: 1024 + topP: 0.9 + topK: 50 + frequencyPenalty: 0.0 + presencePenalty: 0.0 + seed: 42 + responseFormat: text + stopSequences: + - "###" + - "END" + - "STOP" + allowMultipleToolCalls: true + tools: + - kind: codeInterpreter + inputs: + - kind: HostedFileContent + FileId: fileId123 + - kind: function + name: GetWeather + description: Get the weather for a given location. + parameters: + - name: location + type: string + description: The city and state, e.g. San Francisco, CA + required: true + - name: unit + type: string + description: The unit of temperature. Possible values are 'celsius' and 'fahrenheit'. + required: false + enum: + - celsius + - fahrenheit + - kind: mcp + serverName: PersonInfoTool + serverDescription: Get information about a person. + connection: + kind: AnonymousConnection + endpoint: https://my-mcp-endpoint.com/api + allowedTools: + - "GetPersonInfo" + - "UpdatePersonInfo" + - "DeletePersonInfo" + approvalMode: + kind: HostedMcpServerToolRequireSpecificApprovalMode + AlwaysRequireApprovalToolNames: + - "UpdatePersonInfo" + - "DeletePersonInfo" + NeverRequireApprovalToolNames: + - "GetPersonInfo" + - kind: webSearch + name: WebSearchTool + description: Search the web for information. + - kind: fileSearch + name: FileSearchTool + description: Search files for information. + ranker: default + scoreThreshold: 0.5 + maxResults: 5 + maxContentLength: 2000 + vectorStoreIds: + - 1 + - 2 + - 3 + """; + + internal const string AgentWithOutputSchema = + """ + kind: Prompt + name: Translation Assistant + description: A helpful assistant that translates text to a specified language. + model: + id: gpt-4o + options: + temperature: 0.9 + topP: 0.95 + instructions: You are a helpful assistant. You answer questions in {language}. You return your answers in a JSON format. + additionalInstructions: You must always respond in the specified language. + tools: + - kind: codeInterpreter + template: + format: PowerFx # Mustache is the other option + parser: None # Prompty and XML are the other options + inputSchema: + properties: + language: string + outputSchema: + properties: + language: + type: string + required: true + description: The language of the answer. + answer: + type: string + required: true + description: The answer text. + """; + + internal const string AgentWithApiKeyConnection = + """ + kind: Prompt + name: AgentName + description: Agent description + instructions: You are a helpful assistant. + model: + id: gpt-4o + connection: + kind: ApiKey + endpoint: https://my-azure-openai-endpoint.openai.azure.com/ + key: my-api-key + """; + + internal const string AgentWithRemoteConnection = + """ + kind: Prompt + name: AgentName + description: Agent description + instructions: You are a helpful assistant. + model: + id: gpt-4o + connection: + kind: Remote + endpoint: https://my-azure-openai-endpoint.openai.azure.com/ + """; + + internal const string AgentWithEnvironmentVariables = + """ + kind: Prompt + name: AgentName + description: Agent description + instructions: You are a helpful assistant. + model: + id: =Env.OpenAIModelId + connection: + kind: apiKey + endpoint: =Env.OpenAIEndpoint + key: =Env.OpenAIApiKey + """; + + internal const string OpenAIChatAgent = + """ + kind: Prompt + name: Assistant + description: Helpful assistant + instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. + model: + id: =Env.OPENAI_MODEL + options: + temperature: 0.9 + topP: 0.95 + connection: + kind: apiKey + key: =Env.OPENAI_APIKEY + outputSchema: + properties: + language: + type: string + required: true + description: The language of the answer. + answer: + type: string + required: true + description: The answer text. + """; + + internal const string AgentWithCurrentModels = + """ + kind: Prompt + name: AgentName + description: Agent description + instructions: You are a helpful assistant. + model: + id: gpt-4o + options: + temperature: 0.7 + maxOutputTokens: 1024 + topP: 0.9 + topK: 50 + frequencyPenalty: 0.7 + presencePenalty: 0.7 + seed: 42 + responseFormat: text + stopSequences: + - "###" + - "END" + - "STOP" + allowMultipleToolCalls: true + chatToolMode: auto + """; + + internal const string AgentWithCurrentModelsSnakeCase = + """ + kind: Prompt + name: AgentName + description: Agent description + instructions: You are a helpful assistant. + model: + id: gpt-4o + options: + temperature: 0.7 + max_output_tokens: 1024 + top_p: 0.9 + top_k: 50 + frequency_penalty: 0.7 + presence_penalty: 0.7 + seed: 42 + response_format: text + stop_sequences: + - "###" + - "END" + - "STOP" + allow_multiple_tool_calls: true + chat_tool_mode: auto + """; + + internal static readonly string[] s_stopSequences = ["###", "END", "STOP"]; + + internal static GptComponentMetadata CreateTestPromptAgent(string? publisher = "OpenAI", string? apiType = "Chat") + { + string agentYaml = + $""" + kind: Prompt + name: Test Agent + description: Test Description + instructions: You are a helpful assistant. + additionalInstructions: Provide detailed and accurate responses. + model: + id: gpt-4o + publisher: {publisher} + apiType: {apiType} + options: + modelId: gpt-4o + temperature: 0.7 + maxOutputTokens: 1024 + topP: 0.9 + topK: 50 + frequencyPenalty: 0.7 + presencePenalty: 0.7 + seed: 42 + responseFormat: text + stopSequences: + - "###" + - "END" + - "STOP" + allowMultipleToolCalls: true + chatToolMode: auto + customProperty: customValue + connection: + kind: apiKey + endpoint: https://my-azure-openai-endpoint.openai.azure.com/ + key: my-api-key + tools: + - kind: codeInterpreter + - kind: function + name: GetWeather + description: Get the weather for a given location. + parameters: + - name: location + type: string + description: The city and state, e.g. San Francisco, CA + required: true + - name: unit + type: string + description: The unit of temperature. Possible values are 'celsius' and 'fahrenheit'. + required: false + enum: + - celsius + - fahrenheit + - kind: mcp + serverName: PersonInfoTool + serverDescription: Get information about a person. + allowedTools: + - "GetPersonInfo" + - "UpdatePersonInfo" + - "DeletePersonInfo" + approvalMode: + kind: HostedMcpServerToolRequireSpecificApprovalMode + AlwaysRequireApprovalToolNames: + - "UpdatePersonInfo" + - "DeletePersonInfo" + NeverRequireApprovalToolNames: + - "GetPersonInfo" + connection: + kind: AnonymousConnection + endpoint: https://my-mcp-endpoint.com/api + - kind: webSearch + name: WebSearchTool + description: Search the web for information. + - kind: fileSearch + name: FileSearchTool + description: Search files for information. + vectorStoreIds: + - 1 + - 2 + - 3 + outputSchema: + properties: + language: + type: string + required: true + description: The language of the answer. + answer: + type: string + required: true + description: The answer text. + """; + + return AgentBotElementYaml.FromYaml(agentYaml); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json new file mode 100644 index 0000000000..350fd25434 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Microsoft.Agents.AI.Hosting.A2A.UnitTests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:54921;http://localhost:54922" + } + } +} \ No newline at end of file