.NET Workflows - Fix converation behaviors for declarative worfklows (#1237)

* Updated

* Passing

* Ready

* Update dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/ConversationMessages.yaml

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

* Comment

* Code analysis

* Unit-tests/provider signature

* Comment

* Consistent

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Chris
2025-10-06 22:04:41 +00:00
committed by GitHub
co-authored by Copilot
parent f1694b0507
commit f81b4a5abe
28 changed files with 202 additions and 138 deletions
@@ -42,20 +42,21 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
}
/// <inheritdoc/>
public override Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default)
public override Task<ChatMessage> CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default)
{
// 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);
PersistentThreadMessage newMessage =
this.GetAgentsClient().Messages.CreateMessage(
conversationId,
role: s_roleMap[conversationMessage.Role.Value.ToUpperInvariant()],
contentBlocks: GetContent(),
attachments: null,
metadata: GetMetadata(),
cancellationToken);
return Task.CompletedTask;
return Task.FromResult(ToChatMessage(newMessage));
Dictionary<string, string>? GetMetadata()
{
@@ -12,6 +12,11 @@ public sealed class ConversationUpdateEvent : WorkflowEvent
/// </summary>
public string ConversationId { get; }
/// <summary>
/// Is the conversation associated with the workflow.
/// </summary>
public bool IsWorkflow { get; internal init; }
/// <summary>
/// Initializes a new instance of <see cref="ConversationUpdateEvent"/>.
/// </summary>
@@ -40,7 +40,7 @@ internal static class AgentProviderExtensions
agent.RunStreamingAsync(null, options, cancellationToken);
// Enable "autoSend" behavior if this is the workflow conversation.
bool isWorkflowConversation = context.IsWorkflowConversation(conversationId);
bool isWorkflowConversation = context.IsWorkflowConversation(conversationId, out string? workflowConversationId);
autoSend |= isWorkflowConversation;
// Process the agent response updates.
@@ -64,7 +64,7 @@ internal static class AgentProviderExtensions
await context.AddEventAsync(new AgentRunResponseEvent(executorId, response)).ConfigureAwait(false);
}
if (autoSend && !isWorkflowConversation && conversationId is not null)
if (autoSend && !isWorkflowConversation && workflowConversationId is not null)
{
// Copy messages with content that aren't function calls or results.
IEnumerable<ChatMessage> messages =
@@ -75,7 +75,7 @@ internal static class AgentProviderExtensions
!message.Contents.OfType<FunctionResultContent>().Any());
foreach (ChatMessage message in messages)
{
await agentProvider.CreateMessageAsync(conversationId, message, cancellationToken).ConfigureAwait(false);
await agentProvider.CreateMessageAsync(workflowConversationId, message, cancellationToken).ConfigureAwait(false);
}
}
@@ -38,25 +38,39 @@ 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)
public static async ValueTask QueueConversationUpdateAsync(this IWorkflowContext context, string conversationId, bool isExternal = false)
{
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);
}
public static bool IsWorkflowConversation(this IWorkflowContext context, string? conversationId)
{
if (string.IsNullOrWhiteSpace(conversationId))
if (isExternal)
{
return false;
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);
}
StringValue workflowId = (StringValue)context.ReadState(SystemScope.Names.ConversationId, VariableScopeNames.System);
return workflowId.Value.Equals(conversationId, StringComparison.Ordinal);
await context.AddEventAsync(new ConversationUpdateEvent(conversationId) { IsWorkflow = isExternal }).ConfigureAwait(false);
}
public static bool IsWorkflowConversation(
this IWorkflowContext context,
string? conversationId,
out string? workflowConversationId)
{
FormulaValue idValue = context.ReadState(SystemScope.Names.ConversationId, VariableScopeNames.System);
switch (idValue)
{
case BlankValue:
case ErrorValue:
workflowConversationId = null;
return false;
case StringValue stringValue when stringValue.Value.Length > 0:
workflowConversationId = stringValue.Value;
return workflowConversationId.Equals(conversationId, StringComparison.Ordinal);
default:
// Something has gone terribly wrong.
throw new DeclarativeActionException($"Invalid '{SystemScope.Names.ConversationId}' value type: {idValue.GetType().Name}.");
}
}
private static DeclarativeWorkflowContext DeclarativeContext(IWorkflowContext context)
@@ -37,7 +37,7 @@ internal sealed class DeclarativeWorkflowExecutor<TInput>(
{
conversationId = await options.AgentProvider.CreateConversationAsync(cancellationToken: default).ConfigureAwait(false);
}
await declarativeContext.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false);
await declarativeContext.QueueConversationUpdateAsync(conversationId, isExternal: true).ConfigureAwait(false);
await options.AgentProvider.CreateMessageAsync(conversationId, input, cancellationToken: default).ConfigureAwait(false);
await declarativeContext.SetLastMessageAsync(input).ConfigureAwait(false);
@@ -66,7 +66,7 @@ public abstract class RootExecutor<TInput> : Executor<TInput>, IResettableExecut
{
this._conversationId = await this._agentProvider.CreateConversationAsync(cancellationToken: default).ConfigureAwait(false);
}
await declarativeContext.QueueConversationUpdateAsync(this._conversationId).ConfigureAwait(false);
await declarativeContext.QueueConversationUpdateAsync(this._conversationId, isExternal: true).ConfigureAwait(false);
await this._agentProvider.CreateMessageAsync(this._conversationId, input, cancellationToken: default).ConfigureAwait(false);
await declarativeContext.SetLastMessageAsync(input).ConfigureAwait(false);
@@ -22,7 +22,8 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode
ChatMessage newMessage = new(this.Model.Role.Value.ToChatRole(), [.. this.GetContent()]) { AdditionalProperties = this.GetMetadata() };
await agentProvider.CreateMessageAsync(conversationId, newMessage, cancellationToken).ConfigureAwait(false);
// Capture the created message, which includes the assigned ID.
newMessage = await agentProvider.CreateMessageAsync(conversationId, newMessage, cancellationToken).ConfigureAwait(false);
await this.AssignAsync(this.Model.Message?.Path, newMessage.ToRecord(), context).ConfigureAwait(false);
@@ -33,7 +33,7 @@ public abstract class WorkflowAgentProvider
/// <param name="conversationId">The identifier of the target conversation.</param>
/// <param name="conversationMessage">The message being added.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
public abstract Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default);
public abstract Task<ChatMessage> CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a specific message from a conversation.