mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.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:
@@ -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()
|
||||
{
|
||||
|
||||
+30
@@ -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)
|
||||
|
||||
+4
-2
@@ -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() };
|
||||
|
||||
|
||||
+4
-1
@@ -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)
|
||||
|
||||
+1
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
+95
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-7
@@ -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,
|
||||
|
||||
+8
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user