Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e803079
fix: use allow list for configuration keys
baywet Sep 9, 2026
d2842d4
Apply batched suggestions from code review
baywet Sep 9, 2026
9508b4b
chore: formatting
baywet Sep 9, 2026
2e5089e
Potential fix for pull request finding 'Missed opportunity to use Sel…
baywet Sep 9, 2026
181ce0d
Address declarative workflow review feedback
Copilot Sep 9, 2026
4d42029
.NET: address workflow sensitivity review comments
baywet Sep 9, 2026
935532d
.NET: fix ChatClient factory options validation
baywet Sep 9, 2026
c1d1611
chore: formatting
baywet Sep 9, 2026
e0e8092
fix review feedback on declarative sensitivity APIs
Copilot Sep 9, 2026
c77957e
fix declarative review feedback
Copilot Sep 9, 2026
6f654c3
Merge branch 'main' into fix/configuration-keys
baywet Sep 10, 2026
2f38485
fix: context forwarding
baywet Sep 10, 2026
9a551d0
Fix declarative sensitivity review comments
Copilot Sep 10, 2026
1ea1ed2
Fix declarative review feedback
Copilot Sep 11, 2026
c7694fe
Merge remote-tracking branch 'origin/main' into fix/configuration-keys
Copilot Sep 11, 2026
8a23310
Merge main and resolve conflicts
Copilot Sep 11, 2026
93cc03e
Revert "Merge main and resolve conflicts"
Copilot Sep 11, 2026
386b512
Address declarative review feedback
Copilot Sep 15, 2026
6befdf8
Update dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Declarati…
baywet Sep 16, 2026
b57edf1
Update dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWork…
baywet Sep 16, 2026
2c5abfa
Remove unusable Power Fx Set opt-in
Copilot Sep 16, 2026
adacc40
Update dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/IWork…
baywet Sep 16, 2026
46a78b2
fix: pass the env var allow list down the prompt
baywet Sep 16, 2026
c5425a5
fix: missing sensitivity for state
baywet Sep 16, 2026
a27a86f
tests: adds additional tests to cover edge scenarios
baywet Sep 17, 2026
fc61f4d
chore: Change ReadStateWithSensitivityAsync to use PortableValue
baywet Sep 17, 2026
4a795f5
tests: fixes build failure because of copilot suggestion
baywet Sep 17, 2026
6d06022
fix: pass the sensitivity through the foreach loop
baywet Sep 17, 2026
132adde
tests: fixes build failure for netfx
baywet Sep 17, 2026
dac7c32
tests: fixes bound validation
baywet Sep 17, 2026
eded8ae
Merge branch 'main' into fix/configuration-keys
baywet Sep 17, 2026
c2cfbd6
chore: moves new class to dedicated file
baywet Sep 17, 2026
e0ef49c
tests: fixes netfx build issues
baywet Sep 17, 2026
bcb3ddb
fix: adds missing netfx conditional compilation directive
baywet Sep 17, 2026
adae16a
chore: removes noisy additional options
baywet Sep 18, 2026
5a19862
chore: restore missing defensive programming
baywet Sep 18, 2026
c51a4df
chore: refactors duplicated constructor
baywet Sep 18, 2026
2425356
chore: Initialize Sensitivities dictionary with case-insensitive comp…
baywet Sep 18, 2026
9db22b8
fix: null reference exception
baywet Sep 18, 2026
5cbd19e
chore: formatting
baywet Sep 18, 2026
1b6b6f8
tests: adds missing test to cover the fallback scenario
baywet Sep 18, 2026
ec87bdd
fix: removes back compat allow list
baywet Sep 18, 2026
602d8bb
tests: updates assertions to account for non back compat behaviour
baywet Sep 18, 2026
4152896
tests: inspect the inner exception to avoid debounce differences betw…
baywet Sep 18, 2026
c607b3c
chore: makes comparison case sensitive
baywet Sep 18, 2026
c8ff9ee
chore: makes comparison case sensitive
baywet Sep 18, 2026
6fdf451
fix: avoid calling sync over async to potentially cause deadlocks
baywet Sep 18, 2026
138de81
fix: adds compatibility deprecated method
baywet Sep 18, 2026
bf1c276
fix: binary compatibility
baywet Sep 18, 2026
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
Expand Up @@ -20,36 +20,69 @@ 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, engine, configuration, loggerFactory, null)
{
// BINARY COMPAT CONSTRUCTOR
}
/// <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="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>
/// <param name="allowedConfigurationVariables">Optional explicitly allowed environment variables referenced by the agent definition.</param>
public ChatClientPromptAgentFactory(
IChatClient chatClient,
IList<AIFunction>? functions,
RecalcEngine? engine,
IConfiguration? configuration,
ILoggerFactory? loggerFactory,
IEnumerable<string>? allowedConfigurationVariables)
: base(engine, configuration, allowedConfigurationVariables)
{
Throw.IfNull(chatClient);

this._chatClient = chatClient;
this._functions = functions;
this._loggerFactory = loggerFactory;
}

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

this.InitializeConfigurationVariables(promptAgent);

var options = new ChatClientAgentOptions()
{
Name = promptAgent.Name,
Description = promptAgent.Description,
ChatOptions = promptAgent.GetChatOptions(this.Engine, this._functions),
ChatOptions = await promptAgent.GetChatOptionsAsync(this.Engine, this._functions, cancellationToken: cancellationToken).ConfigureAwait(false),
};

var agent = new ChatClientAgent(this._chatClient, options, this._loggerFactory);

Declarative.FeatureUsageMarker.MarkUsed();
return Task.FromResult<AIAgent?>(agent);
return agent;
}

#region private
private readonly IChatClient _chatClient;
private readonly IList<AIFunction>? _functions;
private readonly ILoggerFactory? _loggerFactory;

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

using System.Threading;
using System.Threading.Tasks;
using Microsoft.PowerFx;
using Microsoft.PowerFx.Types;

Expand All @@ -15,8 +17,9 @@ internal static class BoolExpressionExtensions
/// </summary>
/// <param name="expression">Expression to evaluate.</param>
/// <param name="engine">Recalc engine to use for evaluation.</param>
/// <param name="cancellationToken">Cancellation token to observe while evaluating the expression.</param>
/// <returns>The evaluated boolean value, or null if the expression is null or cannot be evaluated.</returns>
internal static bool? Eval(this BoolExpression? expression, RecalcEngine? engine)
internal static async Task<bool?> EvalAsync(this BoolExpression? expression, RecalcEngine? engine, CancellationToken cancellationToken = default)
{
if (expression is null)
{
Expand All @@ -35,11 +38,11 @@ internal static class BoolExpressionExtensions

if (expression.IsExpression)
{
return engine.Eval(expression.ExpressionText!).AsBoolean();
return (await engine.EvalAsync(expression.ExpressionText!, cancellationToken).ConfigureAwait(false)).AsBoolean();
}
else if (expression.IsVariableReference)
{
var formulaValue = engine.Eval(expression.VariableReference!.VariableName);
var formulaValue = await engine.EvalAsync(expression.VariableReference!.VariableName, cancellationToken).ConfigureAwait(false);
if (formulaValue is BooleanValue booleanValue)
{
return booleanValue.Value;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.PowerFx;
using Microsoft.PowerFx.Types;

Expand All @@ -16,8 +18,9 @@ internal static class IntExpressionExtensions
/// </summary>
/// <param name="expression">Expression to evaluate.</param>
/// <param name="engine">Recalc engine to use for evaluation.</param>
/// <param name="cancellationToken">Cancellation token to observe while evaluating the expression.</param>
/// <returns>The evaluated integer value, or null if the expression is null or cannot be evaluated.</returns>
internal static long? Eval(this IntExpression? expression, RecalcEngine? engine)
internal static async Task<long?> EvalAsync(this IntExpression? expression, RecalcEngine? engine, CancellationToken cancellationToken = default)
{
if (expression is null)
{
Expand All @@ -36,11 +39,11 @@ internal static class IntExpressionExtensions

if (expression.IsExpression)
{
return (long)engine.Eval(expression.ExpressionText!).AsDouble();
return (long)(await engine.EvalAsync(expression.ExpressionText!, cancellationToken).ConfigureAwait(false)).AsDouble();
}
else if (expression.IsVariableReference)
{
var formulaValue = engine.Eval(expression.VariableReference!.VariableName);
var formulaValue = await engine.EvalAsync(expression.VariableReference!.VariableName, cancellationToken).ConfigureAwait(false);
if (formulaValue is NumberValue numberValue)
{
return (long)numberValue.Value;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.PowerFx;
using Microsoft.PowerFx.Types;

Expand All @@ -16,8 +18,9 @@ internal static class NumberExpressionExtensions
/// </summary>
/// <param name="expression">Expression to evaluate.</param>
/// <param name="engine">Recalc engine to use for evaluation.</param>
/// <param name="cancellationToken">Cancellation token to observe while evaluating the expression.</param>
/// <returns>The evaluated number value, or null if the expression is null or cannot be evaluated.</returns>
internal static double? Eval(this NumberExpression? expression, RecalcEngine? engine)
internal static async Task<double?> EvalAsync(this NumberExpression? expression, RecalcEngine? engine, CancellationToken cancellationToken = default)
{
if (expression is null)
{
Expand All @@ -36,11 +39,11 @@ internal static class NumberExpressionExtensions

if (expression.IsExpression)
{
return engine.Eval(expression.ExpressionText!).AsDouble();
return (await engine.EvalAsync(expression.ExpressionText!, cancellationToken).ConfigureAwait(false)).AsDouble();
}
else if (expression.IsVariableReference)
{
var formulaValue = engine.Eval(expression.VariableReference!.VariableName);
var formulaValue = await engine.EvalAsync(expression.VariableReference!.VariableName, cancellationToken).ConfigureAwait(false);
if (formulaValue is NumberValue numberValue)
{
return numberValue.Value;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx;
using Microsoft.Shared.Diagnostics;
Expand All @@ -19,7 +22,20 @@ public static class PromptAgentExtensions
/// <param name="promptAgent">Instance of <see cref="GptComponentMetadata"/></param>
/// <param name="engine">Instance of <see cref="RecalcEngine"/></param>
/// <param name="functions">Instance of <see cref="IList{AIFunction}"/></param>
[Obsolete("Use GetChatOptionsAsync instead. This method calls into async methods and might cause deadlocks")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static ChatOptions? GetChatOptions(this GptComponentMetadata promptAgent, RecalcEngine? engine, IList<AIFunction>? functions)
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
=> promptAgent.GetChatOptionsAsync(engine, functions).GetAwaiter().GetResult();
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
/// <summary>
/// Retrieves the 'options' property from a <see cref="GptComponentMetadata"/> as a <see cref="ChatOptions"/> instance.
/// </summary>
/// <param name="promptAgent">Instance of <see cref="GptComponentMetadata"/></param>
/// <param name="engine">Instance of <see cref="RecalcEngine"/></param>
/// <param name="functions">Instance of <see cref="IList{AIFunction}"/></param>
/// <param name="cancellationToken">Cancellation token to observe while retrieving chat options.</param>
public static async Task<ChatOptions?> GetChatOptionsAsync(this GptComponentMetadata promptAgent, RecalcEngine? engine, IList<AIFunction>? functions, CancellationToken cancellationToken = default)
{
Throw.IfNull(promptAgent);

Expand All @@ -36,17 +52,17 @@ public static class PromptAgentExtensions
return new ChatOptions()
{
Instructions = promptAgent.Instructions?.ToTemplateString(),
Temperature = (float?)modelOptions?.Temperature?.Eval(engine),
MaxOutputTokens = (int?)modelOptions?.MaxOutputTokens?.Eval(engine),
TopP = (float?)modelOptions?.TopP?.Eval(engine),
TopK = (int?)modelOptions?.TopK?.Eval(engine),
FrequencyPenalty = (float?)modelOptions?.FrequencyPenalty?.Eval(engine),
PresencePenalty = (float?)modelOptions?.PresencePenalty?.Eval(engine),
Seed = modelOptions?.Seed?.Eval(engine),
Temperature = modelOptions?.Temperature is { } temperature ? (float?)await temperature.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
MaxOutputTokens = modelOptions?.MaxOutputTokens is { } maxOutputTokens ? (int?)await maxOutputTokens.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
TopP = modelOptions?.TopP is { } topP ? (float?)await topP.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
TopK = modelOptions?.TopK is { } topK ? (int?)await topK.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
FrequencyPenalty = modelOptions?.FrequencyPenalty is { } frequencyPenalty ? (float?)await frequencyPenalty.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
PresencePenalty = modelOptions?.PresencePenalty is { } presencePenalty ? (float?)await presencePenalty.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
Seed = modelOptions?.Seed is { } seed ? (int?)await seed.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
ResponseFormat = outputSchema?.AsChatResponseFormat(),
ModelId = promptAgent.Model?.ModelNameHint,
StopSequences = modelOptions?.StopSequences,
AllowMultipleToolCalls = modelOptions?.AllowMultipleToolCalls?.Eval(engine),
AllowMultipleToolCalls = modelOptions?.AllowMultipleToolCalls is { } allowMultipleToolCalls ? await allowMultipleToolCalls.EvalAsync(engine, cancellationToken: cancellationToken).ConfigureAwait(false) : default,
ToolMode = modelOptions?.AsChatToolMode(),
Tools = tools,
AdditionalProperties = modelOptions?.GetAdditionalProperties(s_chatOptionProperties),
Expand Down
Loading
Loading