diff --git a/agent-samples/openai/OpenAIAssistants.yaml b/agent-samples/openai/OpenAIAssistants.yaml deleted file mode 100644 index 1318051120..0000000000 --- a/agent-samples/openai/OpenAIAssistants.yaml +++ /dev/null @@ -1,28 +0,0 @@ -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: gpt-4.1-mini - provider: OpenAI - apiType: Assistants - options: - temperature: 0.9 - topP: 0.95 - connection: - kind: ApiKey - key: =Env.OPENAI_API_KEY -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/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 630afbd6a5..333ba4262d 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -98,6 +98,7 @@ + @@ -402,6 +403,7 @@ + @@ -446,6 +448,7 @@ + diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/DeclarativeOpenAIAgents.csproj b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/DeclarativeOpenAIAgents.csproj new file mode 100644 index 0000000000..7fd4be0da4 --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/DeclarativeOpenAIAgents.csproj @@ -0,0 +1,25 @@ + + + + Exe + net10.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..2e66f08916 --- /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 Azure OpenAI as the backend. +// Unlike the ChatClient sample, this uses the OpenAIPromptAgentFactory which can create a ChatClient from the YAML model definition. + +using System.ComponentModel; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; + +string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); + +// Read command-line arguments +if (args.Length < 2) +{ + Console.WriteLine("Usage: DeclarativeOpenAIAgents "); + Console.WriteLine(" : The path to the YAML file containing the agent definition"); + Console.WriteLine(" : The prompt to send to the agent"); + return; +} + +string yamlFilePath = args[0]; +string 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 +string 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 configuration with the Azure OpenAI endpoint +IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AzureOpenAI:Endpoint"] = endpoint, + }) + .Build(); + +// Create the agent from the YAML definition. +// OpenAIPromptAgentFactory can create a ChatClient based on the model defined in the YAML file. +OpenAIPromptAgentFactory agentFactory = new( + new Uri(endpoint), + new AzureCliCredential(), + [AIFunctionFactory.Create(GetWeather, "GetWeather")], + configuration: configuration); +AIAgent? 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/OpenAI/Properties/launchSettings.json b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/Properties/launchSettings.json new file mode 100644 index 0000000000..d70b1eb090 --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "profiles": { + "OpenAI": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\openai\\OpenAI.yaml \"What is the weather in Cambridge, MA in °C?\"" + }, + "OpenAIChat": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\openai\\OpenAIChat.yaml \"What is the weather in Cambridge, MA in °C?\"" + }, + "OpenAIResponses": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\openai\\OpenAIResponses.yaml \"What is the weather in Cambridge, MA in °C?\"" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/README.md b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/README.md new file mode 100644 index 0000000000..2b93f40fe6 --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/README.md @@ -0,0 +1,63 @@ +# Declarative OpenAI Agents + +This sample demonstrates how to use the `OpenAIPromptAgentFactory` to create AI agents from YAML definitions. + +## Overview + +Unlike the `ChatClientPromptAgentFactory` which requires you to create an `IChatClient` upfront, the `OpenAIPromptAgentFactory` can create the chat client based on the model definition in the YAML file. This is useful when: + +- You want the model to be defined declaratively in the YAML file +- You need to support multiple models without changing code +- You want to use Azure OpenAI endpoints with token-based authentication + +## Prerequisites + +- .NET 10.0 SDK +- Azure OpenAI endpoint access +- Azure CLI installed and authenticated (`az login`) + +## Configuration + +Set the following environment variable: + +```bash +# Required: Azure OpenAI endpoint URL +set AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/ +``` + +## Running the Sample + +```bash +dotnet run -- +``` + +### Example + +```bash +dotnet run -- agent.yaml "What is the weather in Seattle?" +``` + +## Sample YAML Agent Definition + +Create a file named `agent.yaml` with the following content: + +```yaml +name: WeatherAgent +description: An agent that can provide weather information +model: + api: chat + configuration: + azure_deployment: gpt-4o-mini +instructions: | + You are a helpful assistant that provides weather information. + Use the GetWeather function when asked about weather conditions. +``` + +## Key Differences from ChatClient Sample + +| Feature | ChatClient Sample | OpenAI Sample | +|---------|------------------|---------------| +| Chat client creation | Manual (in code) | Automatic (from YAML model definition) | +| Model selection | Code-specified | YAML-specified | +| Factory class | `ChatClientPromptAgentFactory` | `OpenAIPromptAgentFactory` | +| Authentication | Passed to `AzureOpenAIClient` | Passed to factory constructor | diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/AzureAIPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/AzureAIPromptAgentFactory.cs new file mode 100644 index 0000000000..e8ec6d7028 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/AzureAIPromptAgentFactory.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Projects; +using Azure.Core; +using Microsoft.Agents.ObjectModel; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerFx; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// Provides an which creates instances of using a . +/// +public sealed class AzureAIPromptAgentFactory : PromptAgentFactory +{ + private readonly AIProjectClient? _projectClient; + private readonly TokenCredential? _tokenCredential; + + /// + /// Creates a new instance of the class with an associated . + /// + /// The instance to use for creating agents. + /// Optional , if none is provided a default instance will be created. + /// The instance to use for configuration. + public AzureAIPromptAgentFactory(AIProjectClient projectClient, RecalcEngine? engine = null, IConfiguration? configuration = null) : base(engine, configuration) + { + Throw.IfNull(projectClient); + + this._projectClient = projectClient; + } + + /// + /// Creates a new instance of the class with an associated . + /// + /// The to use for authenticating requests. + /// Optional , if none is provided a default instance will be created. + /// The instance to use for configuration. + public AzureAIPromptAgentFactory(TokenCredential tokenCredential, RecalcEngine? engine = null, IConfiguration? configuration = null) : base(engine, configuration) + { + Throw.IfNull(tokenCredential); + + this._tokenCredential = tokenCredential; + } + + /// + public override async Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default) + { + Throw.IfNull(promptAgent); + Throw.IfNullOrEmpty(promptAgent.Name); + + var projectClient = this._projectClient ?? this.CreateAIProjectClient(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."); + } + + return await projectClient.CreateAIAgentAsync( + name: promptAgent.Name, + model: modelId, + instructions: promptAgent.Instructions?.ToTemplateString() ?? string.Empty, + description: promptAgent.Description, + tools: promptAgent.GetAITools(), + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + private AIProjectClient CreateAIProjectClient(GptComponentMetadata promptAgent) + { + var externalModel = promptAgent.Model as CurrentModels; + var connection = externalModel?.Connection as RemoteConnection; + if (connection is not null) + { + var endpoint = connection.Endpoint?.Eval(this.Engine); + if (string.IsNullOrEmpty(endpoint)) + { + throw new InvalidOperationException("The endpoint must be specified in the agent definition model connection to create an AIProjectClient."); + } + if (this._tokenCredential is null) + { + throw new InvalidOperationException("A TokenCredential must be registered in the service provider to create an AIProjectClient."); + } + return new AIProjectClient(new Uri(endpoint), this._tokenCredential); + } + + throw new InvalidOperationException("A AIProjectClient must be registered in the service provider or a FoundryConnection must be specified in the agent definition model connection to create an AIProjectClient."); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/BaseOpenAIPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/BaseOpenAIPromptAgentFactory.cs new file mode 100644 index 0000000000..3f84776d67 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/BaseOpenAIPromptAgentFactory.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.ClientModel; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Agents.ObjectModel; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.PowerFx; +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 BaseOpenAIPromptAgentFactory : PromptAgentFactory +{ + /// + /// Creates a new instance of the class. + /// + protected BaseOpenAIPromptAgentFactory(RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(engine, configuration) + { + this.LoggerFactory = loggerFactory; + } + + /// + /// Creates a new instance of the class. + /// + protected BaseOpenAIPromptAgentFactory(Uri endpoint, TokenCredential tokenCredential, RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(engine, configuration) + { + 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 this.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 this.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 ResponsesClient? CreateResponseClient(GptComponentMetadata promptAgent) + { + var model = promptAgent.Model as CurrentModels; + var provider = model?.Provider?.Value ?? ModelProvider.OpenAI; + if (provider == ModelProvider.OpenAI) + { + return this.CreateResponsesClient(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 CreateAzureResponsesClient(promptAgent, this._endpoint, this._tokenCredential); + } + + return null; + } + + #region private + private readonly Uri? _endpoint; + private readonly TokenCredential? _tokenCredential; + + private 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 this.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 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 this.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 ResponsesClient CreateResponsesClient(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 this.CreateOpenAIClient(promptAgent).GetResponsesClient(modelId); + } + + private static ResponsesClient CreateAzureResponsesClient(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).GetResponsesClient(deploymentName); + } + + private 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!.Eval(this.Engine); + 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?.Eval(this.Engine); + 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/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..3af7c1d385 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/Microsoft.Agents.AI.Declarative.AzureAI.csproj @@ -0,0 +1,54 @@ + + + + preview + $(NoWarn);MEAI001;OPENAI001 + false + + + + true + true + true + + + + + + + Microsoft Agent Framework Declarative Foundry Agents + Provides Microsoft Agent Framework support for declarative Foundry agents. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIPromptAgentFactory.cs new file mode 100644 index 0000000000..162e9dcebb --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIPromptAgentFactory.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.Core; +using Microsoft.Agents.ObjectModel; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.PowerFx; +using Microsoft.Shared.Diagnostics; +using OpenAI.Chat; + +namespace Microsoft.Agents.AI; + +/// +/// Provides an which creates instances of using a . +/// +public sealed class OpenAIPromptAgentFactory : BaseOpenAIPromptAgentFactory +{ + /// + /// Creates a new instance of the class. + /// + public OpenAIPromptAgentFactory(IList? functions = null, RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(engine, configuration, loggerFactory) + { + this._functions = functions; + } + + /// + /// Creates a new instance of the class. + /// + public OpenAIPromptAgentFactory(ChatClient chatClient, IList? functions = null, RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(engine, configuration, loggerFactory) + { + Throw.IfNull(chatClient); + + this._chatClient = chatClient; + this._functions = functions; + } + + /// + /// Creates a new instance of the class. + /// + public OpenAIPromptAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList? functions = null, RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, engine, configuration, 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, + ChatOptions = promptAgent.GetChatOptions(this.Engine, 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/OpenAIResponsesPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIResponsesPromptAgentFactory.cs new file mode 100644 index 0000000000..d71365a855 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIResponsesPromptAgentFactory.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.Core; +using Microsoft.Agents.ObjectModel; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.PowerFx; +using Microsoft.Shared.Diagnostics; +using OpenAI.Responses; + +namespace Microsoft.Agents.AI; + +/// +/// Provides an which creates instances of using a . +/// +public sealed class OpenAIResponsesPromptAgentFactory : BaseOpenAIPromptAgentFactory +{ + /// + /// Creates a new instance of the class. + /// + public OpenAIResponsesPromptAgentFactory(IList? functions = null, RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(engine, configuration, loggerFactory) + { + this._functions = functions; + } + + /// + /// Creates a new instance of the class. + /// + public OpenAIResponsesPromptAgentFactory(ResponsesClient responsesClient, IList? functions = null, RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(engine, configuration, loggerFactory) + { + Throw.IfNull(responsesClient); + + this._responsesClient = responsesClient; + this._functions = functions; + } + + /// + /// Creates a new instance of the class. + /// + public OpenAIResponsesPromptAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList? functions = null, RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, engine, configuration, 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, + ChatOptions = promptAgent.GetChatOptions(this.Engine, this._functions), + }; + + var responseClient = this._responsesClient ?? this.CreateResponseClient(promptAgent); + if (responseClient is not null) + { + return new ChatClientAgent( + responseClient.AsIChatClient(), + options, + this.LoggerFactory); + } + + return null; + } + + #region private + private readonly ResponsesClient? _responsesClient; + private readonly IList? _functions; + #endregion +} diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs index 0da3f18f85..4255738c96 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs @@ -58,7 +58,7 @@ public static class PromptAgentExtensions /// /// Instance of /// Instance of - internal static List? GetAITools(this GptComponentMetadata promptAgent, IList? functions) + public static List? GetAITools(this GptComponentMetadata promptAgent, IList? functions = null) { return promptAgent.Tools.Select(tool => { 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 index 3b75b63236..c9f09ce90d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj @@ -39,6 +39,7 @@ + diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests/AzureAIPromptAgentFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests/AzureAIPromptAgentFactoryTests.cs new file mode 100644 index 0000000000..e19794f673 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests/AzureAIPromptAgentFactoryTests.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Azure.Core; +using Microsoft.Agents.ObjectModel; +using Moq; + +namespace Microsoft.Agents.AI.Declarative.AzureAI.UnitTests; + +/// +/// Unit tests for . +/// +public sealed class AzureAIPromptAgentFactoryTests +{ + [Fact] + public void Constructor_WithProjectClient_ThrowsForNull() + { + // Arrange & Act & Assert + Assert.Throws(() => new AzureAIPromptAgentFactory(projectClient: null!)); + } + + [Fact] + public void Constructor_WithTokenCredential_ThrowsForNull() + { + // Arrange & Act & Assert + Assert.Throws(() => new AzureAIPromptAgentFactory(tokenCredential: null!)); + } + + [Fact] + public async Task TryCreateAsync_ThrowsForNullPromptAgentAsync() + { + // Arrange + Mock mockCredential = new(); + AzureAIPromptAgentFactory factory = new(mockCredential.Object); + + // Act & Assert + await Assert.ThrowsAsync(() => factory.TryCreateAsync(null!)); + } + + [Fact] + public async Task TryCreateAsync_ThrowsForNullOrEmptyNameAsync() + { + // Arrange + Mock mockCredential = new(); + AzureAIPromptAgentFactory factory = new(mockCredential.Object); + GptComponentMetadata promptAgent = new(name: null!); + + // Act & Assert + await Assert.ThrowsAsync(() => factory.TryCreateAsync(promptAgent)); + } + + [Fact] + public async Task TryCreateAsync_ThrowsForEmptyNameAsync() + { + // Arrange + Mock mockCredential = new(); + AzureAIPromptAgentFactory factory = new(mockCredential.Object); + GptComponentMetadata promptAgent = new(name: string.Empty); + + // Act & Assert + await Assert.ThrowsAsync(() => factory.TryCreateAsync(promptAgent)); + } + + [Fact] + public async Task TryCreateAsync_ThrowsWhenModelIdIsNullAsync() + { + // Arrange + Mock mockCredential = new(); + AzureAIPromptAgentFactory factory = new(mockCredential.Object); + GptComponentMetadata promptAgent = new("TestAgent"); + + // Act & Assert + InvalidOperationException exception = await Assert.ThrowsAsync(() => factory.TryCreateAsync(promptAgent)); + Assert.Contains("AIProjectClient", exception.Message); + } + + [Fact] + public async Task TryCreateAsync_ThrowsWhenNoProjectClientAndNoConnectionAsync() + { + // Arrange + Mock mockCredential = new(); + AzureAIPromptAgentFactory factory = new(mockCredential.Object); + GptComponentMetadata promptAgent = CreateTestPromptAgentWithoutConnection(); + + // Act & Assert + InvalidOperationException exception = await Assert.ThrowsAsync(() => factory.TryCreateAsync(promptAgent)); + Assert.Contains("AIProjectClient must be registered", exception.Message); + } + + [Fact] + public async Task TryCreateAsync_ThrowsWhenEndpointIsEmptyAsync() + { + // Arrange + Mock mockCredential = new(); + AzureAIPromptAgentFactory factory = new(mockCredential.Object); + GptComponentMetadata promptAgent = CreateTestPromptAgentWithEmptyEndpoint(); + + // Act & Assert + InvalidOperationException exception = await Assert.ThrowsAsync(() => factory.TryCreateAsync(promptAgent)); + Assert.Contains("endpoint must be specified", exception.Message); + } + + private static GptComponentMetadata CreateTestPromptAgentWithoutConnection() + { + const string agentYaml = + """ + kind: Prompt + name: Test Agent + description: Test Description + instructions: You are a helpful assistant. + model: + id: gpt-4o + """; + + return AgentBotElementYaml.FromYaml(agentYaml); + } + + private static GptComponentMetadata CreateTestPromptAgentWithEmptyEndpoint() + { + const string agentYaml = + """ + kind: Prompt + name: Test Agent + description: Test Description + instructions: You are a helpful assistant. + model: + id: gpt-4o + connection: + kind: Remote + endpoint: "" + """; + + return AgentBotElementYaml.FromYaml(agentYaml); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests.csproj new file mode 100644 index 0000000000..fe9fb91041 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests.csproj @@ -0,0 +1,11 @@ + + + + $(NoWarn);IDE1006;VSTHRD200 + + + + + + + diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests/OpenAIPromptAgentFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests/OpenAIPromptAgentFactoryTests.cs new file mode 100644 index 0000000000..4cde729a88 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests/OpenAIPromptAgentFactoryTests.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Agents.ObjectModel; +using OpenAI.Chat; + +namespace Microsoft.Agents.AI.Declarative.AzureAI.UnitTests; + +/// +/// Unit tests for . +/// +public sealed class OpenAIPromptAgentFactoryTests +{ + [Fact] + public void Constructor_WithChatClient_ThrowsForNull() + { + // Arrange & Act & Assert + Assert.Throws(() => new OpenAIPromptAgentFactory(chatClient: null!)); + } + + [Fact] + public async Task TryCreateAsync_ThrowsForNullPromptAgentAsync() + { + // Arrange + OpenAIPromptAgentFactory factory = new(); + + // Act & Assert + await Assert.ThrowsAsync(() => factory.TryCreateAsync(null!)); + } + + [Fact] + public async Task TryCreateAsync_ThrowsWhenModelIsNullAsync() + { + // Arrange + OpenAIPromptAgentFactory factory = new(); + GptComponentMetadata promptAgent = new("TestAgent"); + + // Act & Assert + await Assert.ThrowsAsync(() => factory.TryCreateAsync(null!)); + } + + [Fact] + public async Task TryCreateAsync_ReturnsNull_WhenApiTypeIsUnknownAsync() + { + // Arrange + OpenAIPromptAgentFactory factory = new(); + GptComponentMetadata promptAgent = CreateTestPromptAgent(apiType: "Unknown"); + + // Act + AIAgent? result = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task TryCreateAsync_ReturnsNull_WhenApiTypeIsResponsesAsync() + { + // Arrange + OpenAIPromptAgentFactory factory = new(); + GptComponentMetadata promptAgent = CreateTestPromptAgent(apiType: "Responses"); + + // Act + AIAgent? result = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task TryCreateAsync_ReturnsNull_WhenApiTypeIsAssistantsAsync() + { + // Arrange + OpenAIPromptAgentFactory factory = new(); + GptComponentMetadata promptAgent = CreateTestPromptAgent(apiType: "Assistants"); + + // Act + AIAgent? result = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task TryCreateAsync_ReturnsChatClientAgent_WhenChatClientProvidedAsync() + { + // Arrange + ChatClient chatClient = new("gpt-4o", "test-api-key"); + OpenAIPromptAgentFactory factory = new(chatClient); + GptComponentMetadata promptAgent = CreateTestPromptAgent(apiType: "Chat"); + + // Act + AIAgent? result = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public async Task TryCreateAsync_ReturnsChatClientAgent_WithCorrectOptionsAsync() + { + // Arrange + ChatClient chatClient = new("gpt-4o", "test-api-key"); + OpenAIPromptAgentFactory factory = new(chatClient); + GptComponentMetadata promptAgent = CreateTestPromptAgent(apiType: "Chat"); + + // Act + AIAgent? result = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.NotNull(result); + ChatClientAgent agent = Assert.IsType(result); + Assert.Equal("Test Agent", agent.Name); + Assert.Equal("Test Description", agent.Description); + } + + private static GptComponentMetadata CreateTestPromptAgent(string apiType) + { + string agentYaml = + $""" + kind: Prompt + name: Test Agent + description: Test Description + instructions: You are a helpful assistant. + model: + id: gpt-4o + apiType: {apiType} + """; + + return AgentBotElementYaml.FromYaml(agentYaml); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests/OpenAIResponsesPromptAgentFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests/OpenAIResponsesPromptAgentFactoryTests.cs new file mode 100644 index 0000000000..16c6402f8f --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.AzureAI.UnitTests/OpenAIResponsesPromptAgentFactoryTests.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Agents.ObjectModel; +using OpenAI; +using OpenAI.Responses; + +namespace Microsoft.Agents.AI.Declarative.AzureAI.UnitTests; + +/// +/// Unit tests for . +/// +public sealed class OpenAIResponsesPromptAgentFactoryTests +{ + [Fact] + public void Constructor_WithResponsesClient_ThrowsForNull() + { + // Arrange & Act & Assert + Assert.Throws(() => new OpenAIResponsesPromptAgentFactory(responsesClient: null!)); + } + + [Fact] + public async Task TryCreateAsync_ThrowsForNullPromptAgentAsync() + { + // Arrange + OpenAIResponsesPromptAgentFactory factory = new(); + + // Act & Assert + await Assert.ThrowsAsync(() => factory.TryCreateAsync(null!)); + } + + [Fact] + public async Task TryCreateAsync_ReturnsNull_WhenModelIsNullAsync() + { + // Arrange + OpenAIResponsesPromptAgentFactory factory = new(); + GptComponentMetadata promptAgent = new("TestAgent"); + + // Act + AIAgent? result = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task TryCreateAsync_ReturnsNull_WhenApiTypeIsUnknownAsync() + { + // Arrange + OpenAIResponsesPromptAgentFactory factory = new(); + GptComponentMetadata promptAgent = CreateTestPromptAgent(apiType: "Unknown"); + + // Act + AIAgent? result = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task TryCreateAsync_ReturnsNull_WhenApiTypeIsChatAsync() + { + // Arrange + OpenAIResponsesPromptAgentFactory factory = new(); + GptComponentMetadata promptAgent = CreateTestPromptAgent(apiType: "Chat"); + + // Act + AIAgent? result = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task TryCreateAsync_ReturnsNull_WhenApiTypeIsAssistantsAsync() + { + // Arrange + OpenAIResponsesPromptAgentFactory factory = new(); + GptComponentMetadata promptAgent = CreateTestPromptAgent(apiType: "Assistants"); + + // Act + AIAgent? result = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task TryCreateAsync_ReturnsChatClientAgent_WhenResponsesClientProvidedAsync() + { + // Arrange + ResponsesClient responsesClient = new OpenAIClient("test-api-key").GetResponsesClient("gpt-4o"); + OpenAIResponsesPromptAgentFactory factory = new(responsesClient); + GptComponentMetadata promptAgent = CreateTestPromptAgent(apiType: "Responses"); + + // Act + AIAgent? result = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public async Task TryCreateAsync_ReturnsChatClientAgent_WithCorrectOptionsAsync() + { + // Arrange + ResponsesClient responsesClient = new OpenAIClient("test-api-key").GetResponsesClient("gpt-4o"); + OpenAIResponsesPromptAgentFactory factory = new(responsesClient); + GptComponentMetadata promptAgent = CreateTestPromptAgent(apiType: "Responses"); + + // Act + AIAgent? result = await factory.TryCreateAsync(promptAgent); + + // Assert + Assert.NotNull(result); + ChatClientAgent agent = Assert.IsType(result); + Assert.Equal("Test Agent", agent.Name); + Assert.Equal("Test Description", agent.Description); + } + + private static GptComponentMetadata CreateTestPromptAgent(string apiType) + { + string agentYaml = + $""" + kind: Prompt + name: Test Agent + description: Test Description + instructions: You are a helpful assistant. + model: + id: gpt-4o + apiType: {apiType} + """; + + return AgentBotElementYaml.FromYaml(agentYaml); + } +}