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