.NET Workflows - Declarative update for AzureAgentProvider usage (#907)

* Updated

* Update dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Constant / override

* Namespace

* Protect against race

* Simplify collection usage

* Rollback name resolution

* One more rollback

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Chris
2025-09-25 10:17:59 -07:00
committed by GitHub
Unverified
parent 9355329dfd
commit f527fbe6ce
12 changed files with 170 additions and 29 deletions
@@ -1,5 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
@@ -43,15 +42,20 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
}
/// <inheritdoc/>
public override async Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default)
public override Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default)
{
await this.GetAgentsClient().Messages.CreateMessageAsync(
// TODO: Switch to asynchronous "CreateMessageAsync", when fix properly applied:
// BUG: https://github.com/Azure/azure-sdk-for-net/issues/52571
// PR: https://github.com/Azure/azure-sdk-for-net/pull/52653
this.GetAgentsClient().Messages.CreateMessage(
conversationId,
role: s_roleMap[conversationMessage.Role.Value.ToUpperInvariant()],
contentBlocks: GetContent(),
attachments: null,
metadata: GetMetadata(),
cancellationToken).ConfigureAwait(false);
cancellationToken);
return Task.CompletedTask;
Dictionary<string, string>? GetMetadata()
{
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows.Declarative.Interpreter;
@@ -36,6 +37,35 @@ internal static class IWorkflowContextExtensions
public static FormulaValue ReadState(this IWorkflowContext context, string key, string? scopeName = null) =>
DeclarativeContext(context).State.Get(key, scopeName);
public static async ValueTask QueueConversationUpdateAsync(this IWorkflowContext context, string conversationId)
{
RecordValue conversation = (RecordValue)context.ReadState(SystemScope.Names.Conversation, VariableScopeNames.System);
conversation.UpdateField("Id", FormulaValue.New(conversationId));
await context.QueueSystemUpdateAsync(SystemScope.Names.Conversation, conversation).ConfigureAwait(false);
await context.QueueSystemUpdateAsync(SystemScope.Names.ConversationId, FormulaValue.New(conversationId)).ConfigureAwait(false);
await context.AddEventAsync(new ConversationUpdateEvent(conversationId)).ConfigureAwait(false);
}
// Ensure "System.Conversation.Id" and "System.ConversationId" are properly initialized when referenced.
public static async ValueTask EnsureWorkflowConversationAsync(this IWorkflowContext context, WorkflowAgentProvider agentProvider, StringExpression expression, CancellationToken cancellationToken)
{
if (expression.IsVariableReference &&
expression.VariableReference.IsVariableReferenceWithScope(VariableScopeNames.System, out string? variableName))
{
if (string.Equals(variableName, SystemScope.Names.Conversation, StringComparison.Ordinal) ||
string.Equals(variableName, SystemScope.Names.ConversationId, StringComparison.Ordinal))
{
FormulaValue variableValue = context.ReadState(SystemScope.Names.ConversationId, VariableScopeNames.System);
if (variableValue is BlankValue)
{
string conversationId = await agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false);
await context.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false);
}
}
}
}
private static DeclarativeWorkflowContext DeclarativeContext(IWorkflowContext context)
{
if (context is not DeclarativeWorkflowContext declarativeContext)
@@ -17,8 +17,10 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode
{
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
StringExpression conversationExpression = Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}");
string conversationId = this.Evaluator.GetValue(conversationExpression).Value;
Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}");
await context.EnsureWorkflowConversationAsync(agentProvider, this.Model.ConversationId, cancellationToken).ConfigureAwait(false);
string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value;
ChatMessage newMessage = new(this.Model.Role.Value.ToChatRole(), [.. this.GetContent()]) { AdditionalProperties = this.GetMetadata() };
@@ -17,7 +17,10 @@ internal sealed class CopyConversationMessagesExecutor(CopyConversationMessages
{
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string conversationId = this.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value;
Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}");
await context.EnsureWorkflowConversationAsync(agentProvider, this.Model.ConversationId, cancellationToken).ConfigureAwait(false);
string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value;
DataValue? inputMessages = this.GetInputMessages();
if (inputMessages is not null)
@@ -11,7 +11,6 @@ using Microsoft.Bot.ObjectModel;
using Microsoft.Bot.ObjectModel.Abstractions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.PowerFx.Types;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
@@ -79,12 +78,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
{
conversationId = assignValue;
RecordValue conversation = (RecordValue)context.ReadState(SystemScope.Names.Conversation, VariableScopeNames.System);
conversation.UpdateField("Id", FormulaValue.New(conversationId));
await context.QueueSystemUpdateAsync(SystemScope.Names.Conversation, conversation).ConfigureAwait(false);
await context.QueueSystemUpdateAsync(SystemScope.Names.ConversationId, FormulaValue.New(conversationId)).ConfigureAwait(false);
await context.AddEventAsync(new ConversationUpdateEvent(conversationId)).ConfigureAwait(false);
await context.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false);
}
}
}
@@ -16,7 +16,9 @@ internal sealed class RetrieveConversationMessageExecutor(RetrieveConversationMe
{
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string conversationId = this.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value;
Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}");
await context.EnsureWorkflowConversationAsync(agentProvider, this.Model.ConversationId, cancellationToken).ConfigureAwait(false);
string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value;
string messageId = this.Evaluator.GetValue(Throw.IfNull(this.Model.MessageId, $"{nameof(this.Model)}.{nameof(this.Model.MessageId)}")).Value;
ChatMessage message = await agentProvider.GetMessageAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false);
@@ -18,7 +18,9 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM
{
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string conversationId = this.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value;
Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}");
await context.EnsureWorkflowConversationAsync(agentProvider, this.Model.ConversationId, cancellationToken).ConfigureAwait(false);
string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value;
ChatMessage[] messages = await agentProvider.GetMessagesAsync(
conversationId,
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.Workflows.Declarative.Extensions;
@@ -14,7 +13,7 @@ using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.Workflows.Declarative.PowerFx;
internal sealed record class WorkflowTypeInfo(FrozenSet<string> EnvironmentVariables, IEnumerable<VariableInformationDiagnostic> UserVariables);
internal sealed record class WorkflowTypeInfo(ISet<string> EnvironmentVariables, IList<VariableInformationDiagnostic> UserVariables);
internal static class WorkflowDiagnostics
{
@@ -26,8 +25,8 @@ internal static class WorkflowDiagnostics
return
new WorkflowTypeInfo(
semanticModel.GetAllEnvironmentVariablesReferencedInTheBot().ToFrozenSet(),
semanticModel.GetVariables(workflowElement.SchemaName.Value).Where(x => !x.IsSystemVariable).Select(v => v.ToDiagnostic()));
semanticModel.GetAllEnvironmentVariablesReferencedInTheBot(),
[.. semanticModel.GetVariables(workflowElement.SchemaName.Value).Where(x => !x.IsSystemVariable).Select(v => v.ToDiagnostic())]);
}
public static void Initialize<TElement>(this WorkflowFormulaState scopes, TElement workflowElement, IConfiguration? configuration) where TElement : BotElement, IDialogBase
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Threading.Tasks;
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Configuration;
using Shared.IntegrationTests;
using Xunit.Abstractions;
namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests;
public sealed class AzureAgentProviderTest(ITestOutputHelper output) : WorkflowTest(output)
{
private AzureAIConfiguration? _configuration;
[Fact]
public async Task ConversationTestAsync()
{
// Arrange
AzureAgentProvider provider = new(this.Configuration.Endpoint, new AzureCliCredential());
// Act
string conversationId = await provider.CreateConversationAsync();
// Assert
Assert.NotEmpty(conversationId);
// Arrange & Act
for (int index = 0; index < 3; ++index)
{
await provider.CreateMessageAsync(conversationId, new ChatMessage(ChatRole.User, $"Message #{index * 2}"));
await provider.CreateMessageAsync(conversationId, new ChatMessage(ChatRole.Assistant, $"Message #{(index * 2) + 1}"));
}
// Act
ChatMessage[] messages = await provider.GetMessagesAsync(conversationId).ToArrayAsync();
// Assert
Assert.Equal(6, messages.Length);
Assert.NotNull(messages[3].MessageId);
// Act
ChatMessage message = await provider.GetMessageAsync(conversationId, messages[3].MessageId!);
// Assert
Assert.NotNull(message);
Assert.Equal(messages[3].Text, message.Text);
}
[Fact]
public async Task GetAgentTestAsync()
{
// Arrange
AzureAgentProvider provider = new(this.Configuration.Endpoint, new AzureCliCredential());
string agentName = $"TestAgent-{DateTime.UtcNow:yyMMdd-HHmmss-fff}";
string agent1Id = await this.CreateAgentAsync();
string agent2Id = await this.CreateAgentAsync(agentName);
// Act
AIAgent agent1 = await provider.GetAgentAsync(agent1Id);
// Assert
Assert.Equal(agent1Id, agent1.Id);
// Act
AIAgent agent2 = await provider.GetAgentAsync(agent2Id);
// Assert
Assert.Equal(agent2Id, agent2.Id);
// Act & Assert
await Assert.ThrowsAsync<DeclarativeActionException>(() => provider.GetAgentAsync(agentName));
}
private async ValueTask<string> CreateAgentAsync(string? name = null)
{
PersistentAgentsClient client = new(this.Configuration.Endpoint, new AzureCliCredential());
PersistentAgent agent = await client.Administration.CreateAgentAsync(this.Configuration.DeploymentName, name: name);
return agent.Id;
}
private AzureAIConfiguration Configuration
{
get
{
if (this._configuration is null)
{
this._configuration ??= InitializeConfig().GetSection("AzureAI").Get<AzureAIConfiguration>();
Assert.NotNull(this._configuration);
}
return this._configuration;
}
}
}
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
@@ -99,12 +98,6 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
return testcase;
}
private static IConfigurationRoot InitializeConfig() =>
new ConfigurationBuilder()
.AddUserSecrets(Assembly.GetExecutingAssembly())
.AddEnvironmentVariables()
.Build();
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Reflection;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.Configuration;
using Xunit.Abstractions;
namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
@@ -34,4 +36,10 @@ public abstract class WorkflowTest : IDisposable
}
internal static string FormatVariablePath(string variableName, string? scope = null) => $"{scope ?? VariableScopeNames.Topic}.{variableName}";
protected static IConfigurationRoot InitializeConfig() =>
new ConfigurationBuilder()
.AddUserSecrets(Assembly.GetExecutingAssembly())
.AddEnvironmentVariables()
.Build();
}
+11 -2
View File
@@ -1,5 +1,8 @@
#
# This workflow demonstrates a single agent interaction based on user input.
# This workflow demonstrates sequential agent interaction to develop product marketing copy.
#
# Example input:
# An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours.
#
# Any Foundry Agent may be used to provide the response.
# See: ./setup/QuestionAgent.yaml
@@ -11,13 +14,19 @@ trigger:
id: workflow_demo
actions:
- kind: AddConversationMessage
id: add_input_message
conversationId: =System.ConversationId
content:
- type: Text
value: {System.LastMessage.Text}
- kind: InvokeAzureAgent
id: invoke_analyst
conversationId: =System.ConversationId
agent:
name: =Env.FOUNDRY_AGENT_ANSWER
input:
messages: =[UserMessage(System.LastMessageText)]
additionalInstructions: |-
You are a marketing analyst. Given a product description, identify:
- Key features