From 44585156efe8c38677c9f94afba5277b24ac8066 Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:11:59 +0000 Subject: [PATCH] .NET: Integrate RecalcEngine to compute Env variables (#2093) * Integrate RecalcEngine to compute Env variables * Update dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAgentFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIResponseAgentFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI.Declarative/AgentFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix some copilot logged issues * Address code review feedback --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../azure/AzureOpenAIAssistants.yaml | 2 +- agent-samples/azure/AzureOpenAIChat.yaml | 2 +- agent-samples/azure/AzureOpenAIResponses.yaml | 2 +- agent-samples/foundry/PersistentAgent.yaml | 2 +- agent-samples/openai/OpenAIAssistants.yaml | 2 +- agent-samples/openai/OpenAIChat.yaml | 2 +- agent-samples/openai/OpenAIResponses.yaml | 2 +- .../DeclarativeAgents/Azure/Program.cs | 4 -- .../Azure/Properties/launchSettings.json | 16 +++++++ .../ChatClient/Properties/launchSettings.json | 12 ++++++ .../DeclarativeAgents/Foundry/Program.cs | 14 ++++--- .../Foundry/Properties/launchSettings.json | 12 ++++++ .../DeclarativeAgents/OpenAI/Program.cs | 18 ++++---- .../OpenAI/Properties/launchSettings.json | 16 +++++++ .../FoundryPersistentAgentFactory.cs | 7 ++-- .../OpenAIAgentFactory.cs | 29 ++++++------- .../OpenAIAssistantAgentFactory.cs | 7 ++-- .../OpenAIChatAgentFactory.cs | 7 ++-- .../OpenAIResponseAgentFactory.cs | 9 ++-- .../AgentFactory.cs | 25 +++++++++++ .../ChatClient/ChatClientAgentFactory.cs | 3 +- .../Extensions/StringExpressionExtensions.cs | 42 +++++++++++++++++++ .../AgentBotElementYamlTests.cs | 21 ++++++++-- .../PromptAgents.cs | 2 +- 24 files changed, 202 insertions(+), 56 deletions(-) create mode 100644 dotnet/samples/GettingStarted/DeclarativeAgents/Azure/Properties/launchSettings.json create mode 100644 dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Properties/launchSettings.json create mode 100644 dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/Properties/launchSettings.json create mode 100644 dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/Properties/launchSettings.json create mode 100644 dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs diff --git a/agent-samples/azure/AzureOpenAIAssistants.yaml b/agent-samples/azure/AzureOpenAIAssistants.yaml index cf934ded5f..f973d05acc 100644 --- a/agent-samples/azure/AzureOpenAIAssistants.yaml +++ b/agent-samples/azure/AzureOpenAIAssistants.yaml @@ -3,7 +3,7 @@ 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 + id: gpt-4o-mini provider: AzureOpenAI apiType: Assistants options: diff --git a/agent-samples/azure/AzureOpenAIChat.yaml b/agent-samples/azure/AzureOpenAIChat.yaml index 8272b908ad..d02e0c6039 100644 --- a/agent-samples/azure/AzureOpenAIChat.yaml +++ b/agent-samples/azure/AzureOpenAIChat.yaml @@ -3,7 +3,7 @@ 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 + id: gpt-4o-mini provider: AzureOpenAI apiType: Chat options: diff --git a/agent-samples/azure/AzureOpenAIResponses.yaml b/agent-samples/azure/AzureOpenAIResponses.yaml index f29a7b7bbb..006c1476f4 100644 --- a/agent-samples/azure/AzureOpenAIResponses.yaml +++ b/agent-samples/azure/AzureOpenAIResponses.yaml @@ -3,7 +3,7 @@ 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 + id: gpt-4o-mini provider: AzureOpenAI apiType: Responses options: diff --git a/agent-samples/foundry/PersistentAgent.yaml b/agent-samples/foundry/PersistentAgent.yaml index 5ff4514dd8..2de2ea069e 100644 --- a/agent-samples/foundry/PersistentAgent.yaml +++ b/agent-samples/foundry/PersistentAgent.yaml @@ -3,7 +3,7 @@ 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 + id: gpt-4.1-mini options: temperature: 0.9 topP: 0.95 diff --git a/agent-samples/openai/OpenAIAssistants.yaml b/agent-samples/openai/OpenAIAssistants.yaml index 867639aa22..c1f20beb38 100644 --- a/agent-samples/openai/OpenAIAssistants.yaml +++ b/agent-samples/openai/OpenAIAssistants.yaml @@ -3,7 +3,7 @@ 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 + id: gpt-4.1-mini provider: OpenAI apiType: Assistants options: diff --git a/agent-samples/openai/OpenAIChat.yaml b/agent-samples/openai/OpenAIChat.yaml index 135bf8602b..832ef4eb15 100644 --- a/agent-samples/openai/OpenAIChat.yaml +++ b/agent-samples/openai/OpenAIChat.yaml @@ -3,7 +3,7 @@ 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 + id: gpt-4.1-mini provider: OpenAI apiType: Chat options: diff --git a/agent-samples/openai/OpenAIResponses.yaml b/agent-samples/openai/OpenAIResponses.yaml index 78f331eea2..efe822233e 100644 --- a/agent-samples/openai/OpenAIResponses.yaml +++ b/agent-samples/openai/OpenAIResponses.yaml @@ -3,7 +3,7 @@ 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 + id: gpt-4.1-mini provider: OpenAI apiType: Responses options: diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/Program.cs b/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/Program.cs index 213cbf7815..1231484c77 100644 --- a/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/Program.cs +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/Program.cs @@ -8,7 +8,6 @@ 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) @@ -32,9 +31,6 @@ if (!File.Exists(yamlFilePath)) // 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(); diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/Properties/launchSettings.json b/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/Properties/launchSettings.json new file mode 100644 index 0000000000..3c1b8c8325 --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/Azure/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "profiles": { + "Chat": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\azure\\AzureOpenAIChat.yaml \"What is the weather in Cambridge, MA in °C?\"" + }, + "Responses": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\azure\\AzureOpenAIResponses.yaml \"What is the weather in Cambridge, MA in °C?\"" + }, + "Assistants": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\azure\\AzureOpenAIAssistants.yaml \"What is the weather in Cambridge, MA in °C?\"" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Properties/launchSettings.json b/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Properties/launchSettings.json new file mode 100644 index 0000000000..5ec486626c --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "GetWeather": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\chatclient\\GetWeather.yaml \"What is the weather in Cambridge, MA in °C?\"" + }, + "Assistant": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\chatclient\\Assistant.yaml \"Tell me a joke about a pirate in Italian.\"" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/Program.cs b/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/Program.cs index 68a78fe01e..3a65bdad4b 100644 --- a/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/Program.cs +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/Program.cs @@ -6,9 +6,9 @@ using System.ComponentModel; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; 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) @@ -32,9 +32,13 @@ if (!File.Exists(yamlFilePath)) // 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); +// Set up configuration with the Azure Foundry project endpoint +IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AZURE_FOUNDRY_PROJECT_ENDPOINT"] = endpoint + }) + .Build(); // Example function tool that can be used by the agent. [Description("Get the weather for a given location.")] @@ -44,7 +48,7 @@ static string GetWeather( => $"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 agentFactory = new FoundryPersistentAgentFactory(new AzureCliCredential(), configuration); var agent = await agentFactory.CreateFromYamlAsync(text); // Create agent run options diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/Properties/launchSettings.json b/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/Properties/launchSettings.json new file mode 100644 index 0000000000..ee5dc4fb3d --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/Foundry/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "PersistentAgent": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\foundry\\PersistentAgent.yaml \"What is the weather in Cambridge, MA in °C?\"" + }, + "MicrosoftLearnAgent": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\foundry\\MicrosoftLearnAgent.yaml \"Tell me a joke about a pirate in Italian.\"" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/Program.cs b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/Program.cs index 5aca8ef7cf..0f6cf328b7 100644 --- a/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/Program.cs +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/Program.cs @@ -5,9 +5,9 @@ using System.ComponentModel; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; 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) @@ -31,16 +31,20 @@ if (!File.Exists(yamlFilePath)) // 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); +// Set up configuration with the OpenAI API key +IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["OPENAI_APIKEY"] = apiKey + }) + .Build(); // Create the agent from the YAML definition. var agentFactory = new AggregatorAgentFactory( [ - new OpenAIChatAgentFactory(), - new OpenAIResponseAgentFactory(), - new OpenAIAssistantAgentFactory() + new OpenAIChatAgentFactory(configuration: configuration), + new OpenAIResponseAgentFactory(configuration: configuration), + new OpenAIAssistantAgentFactory(configuration: configuration) ]); var agent = await agentFactory.CreateFromYamlAsync(text); 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..85b9d08303 --- /dev/null +++ b/dotnet/samples/GettingStarted/DeclarativeAgents/OpenAI/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "profiles": { + "Chat": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\openai\\OpenAIChat.yaml \"What is the weather in Cambridge, MA in °C?\"" + }, + "Responses": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\openai\\OpenAIResponses.yaml \"What is the weather in Cambridge, MA in °C?\"" + }, + "Assistants": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\openai\\OpenAIAssistants.yaml \"What is the weather in Cambridge, MA in °C?\"" + } + } +} \ No newline at end of file diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/FoundryPersistentAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/FoundryPersistentAgentFactory.cs index 90e7953926..505c627263 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/FoundryPersistentAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/FoundryPersistentAgentFactory.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Azure.AI.Agents.Persistent; using Azure.Core; using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.Configuration; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -20,7 +21,7 @@ public sealed class FoundryPersistentAgentFactory : AgentFactory /// /// Creates a new instance of the class. /// - public FoundryPersistentAgentFactory(PersistentAgentsClient agentClient) + public FoundryPersistentAgentFactory(PersistentAgentsClient agentClient, IConfiguration? configuration = null) : base(configuration) { Throw.IfNull(agentClient); @@ -30,7 +31,7 @@ public sealed class FoundryPersistentAgentFactory : AgentFactory /// /// Creates a new instance of the class. /// - public FoundryPersistentAgentFactory(TokenCredential tokenCredential) + public FoundryPersistentAgentFactory(TokenCredential tokenCredential, IConfiguration? configuration = null) : base(configuration) { Throw.IfNull(tokenCredential); @@ -72,7 +73,7 @@ public sealed class FoundryPersistentAgentFactory : AgentFactory var connection = externalModel?.Connection as RemoteConnection; if (connection is not null) { - var endpoint = connection.Endpoint?.LiteralValue; + 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 PersistentAgentsClient."); diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAgentFactory.cs index a729d1c025..a598292e75 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAgentFactory.cs @@ -4,6 +4,7 @@ using System.ClientModel; using Azure.AI.OpenAI; using Azure.Core; using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; using OpenAI; @@ -21,7 +22,7 @@ public abstract class OpenAIAgentFactory : AgentFactory /// /// Creates a new instance of the class. /// - protected OpenAIAgentFactory(ILoggerFactory? loggerFactory) + protected OpenAIAgentFactory(IConfiguration? configuration, ILoggerFactory? loggerFactory) : base(configuration) { this.LoggerFactory = loggerFactory; } @@ -29,7 +30,7 @@ public abstract class OpenAIAgentFactory : AgentFactory /// /// Creates a new instance of the class. /// - protected OpenAIAgentFactory(Uri endpoint, TokenCredential tokenCredential, ILoggerFactory? loggerFactory) + protected OpenAIAgentFactory(Uri endpoint, TokenCredential tokenCredential, IConfiguration? configuration, ILoggerFactory? loggerFactory) : base(configuration) { Throw.IfNull(endpoint); Throw.IfNull(tokenCredential); @@ -53,7 +54,7 @@ public abstract class OpenAIAgentFactory : AgentFactory var provider = model?.Provider?.Value ?? ModelProvider.OpenAI; if (provider == ModelProvider.OpenAI) { - return CreateOpenAIChatClient(promptAgent); + return this.CreateOpenAIChatClient(promptAgent); } else if (provider == ModelProvider.AzureOpenAI) { @@ -74,7 +75,7 @@ public abstract class OpenAIAgentFactory : AgentFactory var provider = model?.Provider?.Value ?? ModelProvider.OpenAI; if (provider == ModelProvider.OpenAI) { - return CreateOpenAIAssistantClient(promptAgent); + return this.CreateOpenAIAssistantClient(promptAgent); } else if (provider == ModelProvider.AzureOpenAI) { @@ -95,7 +96,7 @@ public abstract class OpenAIAgentFactory : AgentFactory var provider = model?.Provider?.Value ?? ModelProvider.OpenAI; if (provider == ModelProvider.OpenAI) { - return CreateOpenAIResponseClient(promptAgent); + return this.CreateOpenAIResponseClient(promptAgent); } else if (provider == ModelProvider.AzureOpenAI) { @@ -111,12 +112,12 @@ public abstract class OpenAIAgentFactory : AgentFactory private readonly Uri? _endpoint; private readonly TokenCredential? _tokenCredential; - private static ChatClient CreateOpenAIChatClient(GptComponentMetadata promptAgent) + 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 CreateOpenAIClient(promptAgent).GetChatClient(modelId); + return this.CreateOpenAIClient(promptAgent).GetChatClient(modelId); } private static ChatClient CreateAzureOpenAIChatClient(GptComponentMetadata promptAgent, Uri endpoint, TokenCredential tokenCredential) @@ -127,12 +128,12 @@ public abstract class OpenAIAgentFactory : AgentFactory return new AzureOpenAIClient(endpoint, tokenCredential).GetChatClient(deploymentName); } - private static AssistantClient CreateOpenAIAssistantClient(GptComponentMetadata promptAgent) + 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 CreateOpenAIClient(promptAgent).GetAssistantClient(); + return this.CreateOpenAIClient(promptAgent).GetAssistantClient(); } private static AssistantClient CreateAzureOpenAIAssistantClient(GptComponentMetadata promptAgent, Uri endpoint, TokenCredential tokenCredential) @@ -143,12 +144,12 @@ public abstract class OpenAIAgentFactory : AgentFactory return new AzureOpenAIClient(endpoint, tokenCredential).GetAssistantClient(); } - private static OpenAIResponseClient CreateOpenAIResponseClient(GptComponentMetadata promptAgent) + private 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); + return this.CreateOpenAIClient(promptAgent).GetOpenAIResponseClient(modelId); } private static OpenAIResponseClient CreateAzureOpenAIResponseClient(GptComponentMetadata promptAgent, Uri endpoint, TokenCredential tokenCredential) @@ -159,18 +160,18 @@ public abstract class OpenAIAgentFactory : AgentFactory return new AzureOpenAIClient(endpoint, tokenCredential).GetOpenAIResponseClient(deploymentName); } - private static OpenAIClient CreateOpenAIClient(GptComponentMetadata promptAgent) + 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?.LiteralValue; + 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?.LiteralValue; + var endpoint = keyConnection.Endpoint?.Eval(this.Engine); if (!string.IsNullOrEmpty(endpoint)) { clientOptions.Endpoint = new Uri(endpoint); diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAssistantAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAssistantAgentFactory.cs index 081a008881..621736e6dd 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAssistantAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIAssistantAgentFactory.cs @@ -7,6 +7,7 @@ using Azure.AI.Agents.Persistent; using Azure.Core; using Microsoft.Bot.ObjectModel; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; using OpenAI; @@ -22,7 +23,7 @@ public sealed class OpenAIAssistantAgentFactory : OpenAIAgentFactory /// /// Creates a new instance of the class. /// - public OpenAIAssistantAgentFactory(IList? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) + public OpenAIAssistantAgentFactory(IList? functions = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(configuration, loggerFactory) { this._functions = functions; } @@ -30,7 +31,7 @@ public sealed class OpenAIAssistantAgentFactory : OpenAIAgentFactory /// /// Creates a new instance of the class. /// - public OpenAIAssistantAgentFactory(AssistantClient assistantClient, IList? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) + public OpenAIAssistantAgentFactory(AssistantClient assistantClient, IList? functions = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(configuration, loggerFactory) { Throw.IfNull(assistantClient); @@ -41,7 +42,7 @@ public sealed class OpenAIAssistantAgentFactory : OpenAIAgentFactory /// /// Creates a new instance of the class. /// - public OpenAIAssistantAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList? functions = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, loggerFactory) + public OpenAIAssistantAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList? functions = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, configuration, loggerFactory) { this._functions = functions; } diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIChatAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIChatAgentFactory.cs index 587d39aae2..f27c8ca6a5 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIChatAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIChatAgentFactory.cs @@ -7,6 +7,7 @@ using Azure.AI.Agents.Persistent; using Azure.Core; using Microsoft.Bot.ObjectModel; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; using OpenAI.Chat; @@ -21,7 +22,7 @@ public sealed class OpenAIChatAgentFactory : OpenAIAgentFactory /// /// Creates a new instance of the class. /// - public OpenAIChatAgentFactory(IList? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) + public OpenAIChatAgentFactory(IList? functions = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(configuration, loggerFactory) { this._functions = functions; } @@ -29,7 +30,7 @@ public sealed class OpenAIChatAgentFactory : OpenAIAgentFactory /// /// Creates a new instance of the class. /// - public OpenAIChatAgentFactory(ChatClient chatClient, IList? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) + public OpenAIChatAgentFactory(ChatClient chatClient, IList? functions = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(configuration, loggerFactory) { Throw.IfNull(chatClient); @@ -40,7 +41,7 @@ public sealed class OpenAIChatAgentFactory : OpenAIAgentFactory /// /// Creates a new instance of the class. /// - public OpenAIChatAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList? functions = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, loggerFactory) + public OpenAIChatAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList? functions = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, configuration, loggerFactory) { this._functions = functions; } diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIResponseAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIResponseAgentFactory.cs index 1e5ddfc98b..6da009135d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIResponseAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative.AzureAI/OpenAIResponseAgentFactory.cs @@ -7,6 +7,7 @@ using Azure.AI.Agents.Persistent; using Azure.Core; using Microsoft.Bot.ObjectModel; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; using OpenAI.Responses; @@ -21,7 +22,7 @@ public sealed class OpenAIResponseAgentFactory : OpenAIAgentFactory /// /// Creates a new instance of the class. /// - public OpenAIResponseAgentFactory(IList? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) + public OpenAIResponseAgentFactory(IList? functions = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(configuration, loggerFactory) { this._functions = functions; } @@ -29,7 +30,7 @@ public sealed class OpenAIResponseAgentFactory : OpenAIAgentFactory /// /// Creates a new instance of the class. /// - public OpenAIResponseAgentFactory(OpenAIResponseClient responseClient, IList? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory) + public OpenAIResponseAgentFactory(OpenAIResponseClient responseClient, IList? functions = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(configuration, loggerFactory) { Throw.IfNull(responseClient); @@ -38,9 +39,9 @@ public sealed class OpenAIResponseAgentFactory : OpenAIAgentFactory } /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// - public OpenAIResponseAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList? functions = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, loggerFactory) + public OpenAIResponseAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList? functions = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, configuration, loggerFactory) { this._functions = functions; } diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/AgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/AgentFactory.cs index 653c14bdf8..0b60cc86c8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/AgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/AgentFactory.cs @@ -4,6 +4,8 @@ using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerFx; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -13,6 +15,29 @@ namespace Microsoft.Agents.AI; /// public abstract class AgentFactory { + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + protected AgentFactory(IConfiguration? configuration = null) + { + this.Engine = new RecalcEngine(); + + if (configuration is not null) + { + foreach (var kvp in configuration.AsEnumerable()) + { + this.Engine.UpdateVariable(kvp.Key, kvp.Value ?? string.Empty); + } + } + } + + /// + /// Gets the Power Fx recalculation engine used to evaluate expressions in agent definitions. + /// This engine is configured with variables from the provided during construction. + /// + protected RecalcEngine Engine { get; } + /// /// Create a from the specified . /// diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientAgentFactory.cs index aa32da4112..1a9a3b7a4b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientAgentFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientAgentFactory.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.ObjectModel; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; @@ -18,7 +19,7 @@ public sealed class ChatClientAgentFactory : AgentFactory /// /// Creates a new instance of the class. /// - public ChatClientAgentFactory(IChatClient chatClient, IList? functions = null, ILoggerFactory? loggerFactory = null) + public ChatClientAgentFactory(IChatClient chatClient, IList? functions = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(configuration) { Throw.IfNull(chatClient); diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs new file mode 100644 index 0000000000..6353bf3d42 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; + +namespace Microsoft.Bot.ObjectModel; + +/// +/// Extension methods for . +/// +public static class StringExpressionExtensions +{ + /// + /// Evaluates the given using the provided . + /// + /// Expression to evaluate. + /// Recalc engine to use for evaluation. + /// The evaluated string value, or null if the expression is null or cannot be evaluated. + public static string? Eval(this StringExpression? expression, RecalcEngine engine) + { + if (expression is null) + { + return null; + } + + if (expression.IsLiteral) + { + return expression.LiteralValue?.ToString(); + } + else if (expression.IsExpression) + { + return engine.Eval(expression.ExpressionText!).ToString(); + } + else if (expression.IsVariableReference) + { + var stringValue = engine.Eval(expression.VariableReference!.VariableName) as StringValue; + return stringValue?.Value; + } + + return null; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs index 86ec4bc571..d54304897c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs @@ -6,6 +6,8 @@ using System.Text.Json.Serialization; using Microsoft.Bot.ObjectModel; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; namespace Microsoft.Agents.AI.Declarative.UnitTests; @@ -218,7 +220,6 @@ public sealed class AgentBotElementYamlTests .AddInMemoryCollection(new Dictionary { ["OpenAIEndpoint"] = "endpoint", - ["OpenAIModelId"] = "modelId", ["OpenAIApiKey"] = "apiKey" }) .Build(); @@ -233,9 +234,10 @@ public sealed class AgentBotElementYamlTests 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); + ApiKeyConnection connection = (model.Connection as ApiKeyConnection)!; + Assert.NotNull(connection); + Assert.Equal("endpoint", Eval(connection.Endpoint!, configuration)); + Assert.Equal("apiKey", Eval(connection.Key!, configuration)); } /// @@ -253,4 +255,15 @@ public sealed class AgentBotElementYamlTests [JsonPropertyName("occupation")] public string? Occupation { get; set; } } + + private static string? Eval(StringExpression expression, IConfiguration configuration) + { + RecalcEngine engine = new(); + foreach (var kvp in configuration.AsEnumerable()) + { + engine.UpdateVariable(kvp.Key, kvp.Value ?? string.Empty); + } + + return expression.Eval(engine); + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/PromptAgents.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/PromptAgents.cs index 075b134981..513026aa41 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/PromptAgents.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/PromptAgents.cs @@ -146,7 +146,7 @@ internal static class PromptAgents description: Agent description instructions: You are a helpful assistant. model: - id: =Env.OpenAIModelId + id: gpt-4o connection: kind: apiKey endpoint: =Env.OpenAIEndpoint