Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public static async Task Main(string[] args)
WorkflowFactory workflowFactory = new("InvokeFoundryToolboxMcp.yaml", foundryEndpoint)
{
Configuration = workflowConfiguration,
AllowedEnvironmentVariables = [ToolboxMcpServerUrlSetting, DocsServerLabelSetting, WebSearchToolNameSetting],
McpToolHandler = mcpToolHandler
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Microsoft.Agents.ObjectModel;
using Microsoft.Agents.ObjectModel.Abstractions;
using Microsoft.Agents.ObjectModel.Analysis;
using Microsoft.Agents.ObjectModel.PowerFx;
using Microsoft.Agents.ObjectModel.Yaml;
using Microsoft.Extensions.Configuration;
using Microsoft.Shared.Diagnostics;
Expand All @@ -22,8 +25,9 @@ internal static class AgentBotElementYaml
/// </summary>
/// <param name="text">YAML representation of the <see cref="BotElement"/> to use to create the prompt function.</param>
/// <param name="configuration">Optional <see cref="IConfiguration"/> instance which provides environment variables to the template.</param>
/// <param name="allowedConfigurationVariables">Configuration keys that may be exposed when the YAML references them through <c>Env</c>.</param>
[RequiresDynamicCode("Calls YamlDotNet.Serialization.DeserializerBuilder.DeserializerBuilder()")]
public static GptComponentMetadata FromYaml(string text, IConfiguration? configuration = null)
public static GptComponentMetadata FromYaml(string text, IConfiguration? configuration = null, IEnumerable<string>? allowedConfigurationVariables = null)
{
Throw.IfNullOrEmpty(text);

Expand All @@ -35,7 +39,7 @@ public static GptComponentMetadata FromYaml(string text, IConfiguration? configu
throw new InvalidDataException($"Unsupported root element: {rootElement.GetType().Name}. Expected an {nameof(GptComponentMetadata)}.");
}

var botDefinition = WrapPromptAgentWithBot(promptAgent, configuration);
var botDefinition = WrapPromptAgentWithBot(promptAgent, configuration, allowedConfigurationVariables);

return botDefinition.Descendants().OfType<GptComponentMetadata>().First();
}
Expand All @@ -52,7 +56,7 @@ private sealed class AgentFeatureConfiguration : IFeatureConfiguration
public bool IsTenantFeatureEnabled(string featureName, bool defaultValue) => defaultValue;
}

public static BotDefinition WrapPromptAgentWithBot(this GptComponentMetadata element, IConfiguration? configuration = null)
public static BotDefinition WrapPromptAgentWithBot(this GptComponentMetadata element, IConfiguration? configuration = null, IEnumerable<string>? allowedConfigurationVariables = null)
{
var botBuilder =
new BotDefinition.Builder
Expand All @@ -67,25 +71,40 @@ public static BotDefinition WrapPromptAgentWithBot(this GptComponentMetadata ele
}
};

if (configuration is not null)
if (configuration is not null && allowedConfigurationVariables is not null)
{
foreach (var kvp in configuration.AsEnumerable().Where(kvp => kvp.Value is not null))
HashSet<string> allowedVariables = new(allowedConfigurationVariables, StringComparer.OrdinalIgnoreCase);
foreach (string variableName in GetReferencedEnvironmentVariableNames(element).Where(allowedVariables.Contains))
{
string? configurationValue = configuration[variableName];
if (configurationValue is null)
{
continue;
}

botBuilder.EnvironmentVariables.Add(new EnvironmentVariableDefinition.Builder()
{
SchemaName = kvp.Key,
SchemaName = variableName,
Id = Guid.NewGuid(),
DisplayName = kvp.Key,
DisplayName = variableName,
ValueComponent = new EnvironmentVariableValue.Builder()
{
Id = Guid.NewGuid(),
Value = kvp.Value!,
Value = configurationValue,
},
});
}
}

return botBuilder.Build();
}

internal static ISet<string> GetReferencedEnvironmentVariableNames(GptComponentMetadata element)
{
var botDefinition = WrapPromptAgentWithBot(element);
SemanticModel semanticModel = botDefinition.GetSemanticModel(new PowerFxExpressionChecker(new AgentFeatureConfiguration()), new AgentFeatureConfiguration());

return semanticModel.GetAllEnvironmentVariablesReferencedInTheBot();
}
#endregion
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.ObjectModel;
Expand All @@ -20,20 +21,66 @@ public sealed class ChatClientPromptAgentFactory : PromptAgentFactory
/// <summary>
/// Creates a new instance of the <see cref="ChatClientPromptAgentFactory"/> class.
/// </summary>
public ChatClientPromptAgentFactory(IChatClient chatClient, IList<AIFunction>? functions = null, RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(engine, configuration)
/// <param name="chatClient">The chat client used by created agents.</param>
/// <param name="functions">Optional functions exposed as tools to created agents.</param>
/// <param name="engine">Optional Power Fx engine used to evaluate declarative expressions.</param>
/// <param name="configuration">Optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition.</param>
/// <param name="loggerFactory">Optional logger factory used by created agents.</param>
public ChatClientPromptAgentFactory(
IChatClient chatClient,
IList<AIFunction>? functions = null,
RecalcEngine? engine = null,
IConfiguration? configuration = null,
ILoggerFactory? loggerFactory = null)
: this(
chatClient,
functions,
new ChatClientPromptAgentFactoryOptions()
{
Engine = engine,
Configuration = configuration,
AllowedConfigurationVariables = configuration?.AsEnumerable().Select(static pair => pair.Key),
LoggerFactory = loggerFactory,
},
isValidated: true)
{
}

/// <summary>
/// Creates a new instance of the <see cref="ChatClientPromptAgentFactory"/> class.
/// </summary>
/// <param name="chatClient">The chat client used by created agents.</param>
/// <param name="options">Options used to configure the created agents and declarative expression evaluation.</param>
/// <param name="functions">Optional functions exposed as tools to created agents.</param>
/// <returns>The configured <see cref="ChatClientPromptAgentFactory"/> instance.</returns>
public static ChatClientPromptAgentFactory Create(
IChatClient chatClient,
ChatClientPromptAgentFactoryOptions options,
IList<AIFunction>? functions = null) =>
new(chatClient, functions, ValidateOptions(options), isValidated: true);

private ChatClientPromptAgentFactory(
IChatClient chatClient,
IList<AIFunction>? functions,
ChatClientPromptAgentFactoryOptions options,
bool isValidated) :
base(options.Engine, options.Configuration, options.AllowedConfigurationVariables, options.MaximumExpressionLength, options.MaximumCallDepth)
{
_ = isValidated;
Throw.IfNull(chatClient);

this._chatClient = chatClient;
this._functions = functions;
this._loggerFactory = loggerFactory;
this._loggerFactory = options.LoggerFactory;
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
}

/// <inheritdoc/>
public override Task<AIAgent?> TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
{
Throw.IfNull(promptAgent);

this.InitializeConfigurationVariables(promptAgent);

var options = new ChatClientAgentOptions()
{
Name = promptAgent.Name,
Expand All @@ -51,5 +98,44 @@ public ChatClientPromptAgentFactory(IChatClient chatClient, IList<AIFunction>? f
private readonly IChatClient _chatClient;
private readonly IList<AIFunction>? _functions;
private readonly ILoggerFactory? _loggerFactory;

private static ChatClientPromptAgentFactoryOptions ValidateOptions(ChatClientPromptAgentFactoryOptions? options) =>
Throw.IfNull(options);
#endregion
}

/// <summary>
/// Options for configuring <see cref="ChatClientPromptAgentFactory"/>.
/// </summary>
public sealed class ChatClientPromptAgentFactoryOptions
{
/// <summary>
/// Gets or sets configuration keys that may be exposed to Power Fx when the agent definition references them through <c>Env</c>.
/// </summary>
public IEnumerable<string>? AllowedConfigurationVariables { get; init; }
Comment thread
baywet marked this conversation as resolved.

/// <summary>
/// Gets or sets an optional Power Fx engine used to evaluate declarative expressions.
/// </summary>
public RecalcEngine? Engine { get; init; }

/// <summary>
/// Gets or sets optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition.
/// </summary>
public IConfiguration? Configuration { get; init; }

/// <summary>
/// Gets or sets an optional logger factory used by created agents.
/// </summary>
public ILoggerFactory? LoggerFactory { get; init; }

/// <summary>
/// Gets or sets an optional maximum length for Power Fx expressions evaluated by the factory-created engine.
/// </summary>
public int? MaximumExpressionLength { get; init; }

/// <summary>
/// Gets or sets an optional maximum nested call depth for Power Fx expressions evaluated by the factory-created engine.
/// </summary>
public int? MaximumCallDepth { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static Task<AIAgent> CreateFromYamlAsync(this PromptAgentFactory agentFac
Throw.IfNull(agentFactory);
Throw.IfNullOrEmpty(agentYaml);

var agentDefinition = AgentBotElementYaml.FromYaml(agentYaml);
var agentDefinition = agentFactory.FromYaml(agentYaml);

return agentFactory.CreateAsync(
agentDefinition,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.ObjectModel;
Expand All @@ -15,30 +18,83 @@ namespace Microsoft.Agents.AI;
/// </summary>
public abstract class PromptAgentFactory
{
private const int DefaultMaximumExpressionLength = 10000;

private readonly IConfiguration? _configuration;
private readonly HashSet<string> _allowedConfigurationVariables;

/// <summary>
/// Initializes a new instance of the <see cref="PromptAgentFactory"/> class.
/// </summary>
/// <param name="engine">Optional <see cref="RecalcEngine"/>, if none is provided a default instance will be created.</param>
/// <param name="configuration">Optional configuration to be added as variables to the <see cref="RecalcEngine"/>.</param>
/// <param name="configuration">Optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition.</param>
protected PromptAgentFactory(RecalcEngine? engine = null, IConfiguration? configuration = null)
: this(engine, configuration, allowedConfigurationVariables: null)
{
this.Engine = engine ?? new RecalcEngine();
}

/// <summary>
/// Initializes a new instance of the <see cref="PromptAgentFactory"/> class.
/// </summary>
/// <param name="engine">Optional <see cref="RecalcEngine"/>, if none is provided a default instance will be created.</param>
/// <param name="configuration">Optional configuration used to resolve explicitly allowed environment variables referenced by the agent definition.</param>
/// <param name="allowedConfigurationVariables">Configuration keys that may be exposed to Power Fx when the agent definition references them through <c>Env</c>.</param>
/// <param name="maximumExpressionLength">Optional maximum length for Power Fx expressions evaluated by the factory-created engine.</param>
/// <param name="maximumCallDepth">Optional maximum nested call depth for Power Fx expressions evaluated by the factory-created engine.</param>
protected PromptAgentFactory(
RecalcEngine? engine,
IConfiguration? configuration,
IEnumerable<string>? allowedConfigurationVariables,
int? maximumExpressionLength = null,
int? maximumCallDepth = null)
{
this.Engine = engine ?? new RecalcEngine(CreateConfig(maximumExpressionLength, maximumCallDepth));
this._configuration = configuration;
this._allowedConfigurationVariables = new(allowedConfigurationVariables ?? [], StringComparer.OrdinalIgnoreCase);
}

private static PowerFxConfig CreateConfig(int? maximumExpressionLength, int? maximumCallDepth)
{
PowerFxConfig config = new(Features.PowerFxV1)
{
MaximumExpressionLength = maximumExpressionLength ?? DefaultMaximumExpressionLength,
};

if (configuration is not null)
if (maximumCallDepth is not null)
{
foreach (var kvp in configuration.AsEnumerable())
{
this.Engine.UpdateVariable(kvp.Key, kvp.Value ?? string.Empty);
}
config.MaxCallDepth = maximumCallDepth.Value;
}

return config;
}

/// <summary>
/// Gets the Power Fx recalculation engine used to evaluate expressions in agent definitions.
/// This engine is configured with variables from the <see cref="IConfiguration"/> provided during construction.
/// This engine is configured with only explicitly allowed variables from the <see cref="IConfiguration"/> provided during construction.
/// </summary>
protected RecalcEngine Engine { get; }

/// <summary>
/// Adds allowed configuration values referenced through <c>Env</c> by the agent definition to the Power Fx engine.
/// </summary>
/// <param name="promptAgent">Definition of the agent to inspect.</param>
protected void InitializeConfigurationVariables(GptComponentMetadata promptAgent)
{
if (this._configuration is null || this._allowedConfigurationVariables.Count == 0)
{
return;
}

foreach (string variableName in AgentBotElementYaml.GetReferencedEnvironmentVariableNames(promptAgent).Where(variableName => this._allowedConfigurationVariables.Contains(variableName)))
{
this.Engine.UpdateVariable(variableName, this._configuration[variableName] ?? string.Empty);
}
}

[RequiresDynamicCode("Calls YamlDotNet.Serialization.DeserializerBuilder.DeserializerBuilder()")]
internal GptComponentMetadata FromYaml(string text) =>
AgentBotElementYaml.FromYaml(text, this._configuration, this._allowedConfigurationVariables);

/// <summary>
/// Create a <see cref="AIAgent"/> from the specified <see cref="GptComponentMetadata"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ public static Workflow Build<TInput>(
string rootId = WorkflowActionVisitor.Steps.Root(workflowElement);

WorkflowFormulaState state = new(options.CreateRecalcEngine());
state.Initialize(workflowElement.WrapWithBot(), options.Configuration);
state.Initialize(
workflowElement.WrapWithBot(),
options.Configuration,
options.AllowedEnvironmentVariables,
options.AllowProcessEnvironmentVariableFallback);
state.CaptureInitialState();
DeclarativeWorkflowExecutor<TInput> rootExecutor =
new(rootId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Agents.AI.Workflows.Observability;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -37,6 +38,16 @@ public sealed class DeclarativeWorkflowOptions(ResponseAgentProvider agentProvid
/// </summary>
public IConfiguration? Configuration { get; init; }

/// <summary>
/// Gets the configuration or process environment variable names that may be exposed through the workflow <c>Env</c> scope.
/// </summary>
public IEnumerable<string>? AllowedEnvironmentVariables { get; init; }

/// <summary>
/// Gets a value indicating whether the workflow may fall back to process environment variables for allowed <c>Env</c> names missing from <see cref="Configuration"/>.
/// </summary>
public bool AllowProcessEnvironmentVariableFallback { get; init; }

/// <summary>
/// Optionally identifies a continued workflow conversation.
/// </summary>
Expand All @@ -52,6 +63,11 @@ public sealed class DeclarativeWorkflowOptions(ResponseAgentProvider agentProvid
/// </summary>
public int? MaximumExpressionLength { get; init; }

/// <summary>
/// Gets a value indicating whether the Power Fx <c>Set</c> function is enabled.
/// </summary>
public bool EnableSetFunction { get; init; }

/// <summary>
/// Gets the <see cref="ILoggerFactory"/> used to create loggers for workflow components.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ internal static class DeclarativeWorkflowOptionsExtensions
private const int DefaultMaximumExpressionLength = 10000;

public static RecalcEngine CreateRecalcEngine(this DeclarativeWorkflowOptions? context) =>
RecalcEngineFactory.Create(context?.MaximumExpressionLength ?? DefaultMaximumExpressionLength, context?.MaximumCallDepth);
RecalcEngineFactory.Create(context?.MaximumExpressionLength ?? DefaultMaximumExpressionLength, context?.MaximumCallDepth, context?.EnableSetFunction ?? false);
}
Loading