From 74879489a4e94ff752ac0fb554710acfea8d2ee5 Mon Sep 17 00:00:00 2001
From: Chris <66376200+crickman@users.noreply.github.com>
Date: Mon, 15 Sep 2025 13:41:07 -0700
Subject: [PATCH] .NET Workflow - Integrated updated CPS Object Model (#681)
* Checkpoint
* Update workflows/DeepResearch.yaml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Comment
* Fix comment
* Update package version
* Fix nuget haxx
* Checkpoint
* Code complete
* Testing
* Message content workaround
* Add sequential flow
* Checkpoint
* Integration test project
* Checkpoint
* Checkpoint cleanup
* Complete
* Update package
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
dotnet/Directory.Packages.props | 10 +-
dotnet/agent-framework-dotnet.slnx | 7 +-
.../Declarative/DeclarativeWorkflow.csproj | 1 -
.../Workflows/Declarative/Program.cs | 276 +++++++++++-------
.../AzureAgentProvider.cs | 181 ++++++++++++
.../DeclarativeWorkflowBuilder.cs | 3 +-
.../DeclarativeWorkflowEvents.cs | 64 ----
.../Entities/EntityExtractionResult.cs | 24 ++
.../Entities/EntityExtractor.cs | 173 +++++++++++
.../Events/ConversationUpdateEvent.cs | 20 ++
.../Events/DeclarativeActionCompleteEvent.cs | 40 +++
.../Events/DeclarativeActionInvokeEvent.cs | 34 +++
.../Events/InputRequest.cs | 19 ++
.../Events/InputResponse.cs | 23 ++
.../Events/MessageActivityEvent.cs | 19 ++
.../Exceptions/DeclarativeModelException.cs | 2 +-
.../Extensions/ChatMessageExtensions.cs | 232 ++++++++++++++-
.../Extensions/DataValueExtensions.cs | 34 ++-
.../Extensions/FormulaValueExtensions.cs | 57 ++--
.../Extensions/IWorkflowContextExtensions.cs | 20 ++
.../Extensions/StringExtensions.cs | 2 +-
.../Extensions/TemplateExtensions.cs | 8 +-
.../Interpreter/DeclarativeActionExecutor.cs | 47 +--
.../DeclarativeWorkflowExecutor.cs | 2 +-
.../Interpreter/DeclarativeWorkflowModel.cs | 47 ++-
.../Interpreter/DeclarativeWorkflowState.cs | 6 +-
.../Interpreter/DelegateActionExecutor.cs | 32 +-
.../Interpreter/DurableProperty.cs | 17 ++
.../Interpreter/ExecutorResultMessage.cs | 16 +
.../Interpreter/WorkflowActionVisitor.cs | 162 +++++++---
.../AddConversationMessageExecutor.cs | 66 +++++
.../AnswerQuestionWithAIExecutor.cs | 126 --------
.../ObjectModel/ConditionGroupExecutor.cs | 25 +-
.../CopyConversationMessagesExecutor.cs | 45 +++
.../ObjectModel/CreateConversationExecutor.cs | 21 ++
.../ObjectModel/EditTableExecutor.cs | 2 +-
.../ObjectModel/EditTableV2Executor.cs | 4 +-
.../ObjectModel/ForeachExecutor.cs | 12 +-
.../ObjectModel/GotoExecutor.cs | 18 ++
.../ObjectModel/InvokeAzureAgentExecutor.cs | 138 +++++++++
.../ObjectModel/ParseValueExecutor.cs | 2 +-
.../ObjectModel/QuestionExecutor.cs | 145 +++++++++
.../RetrieveConversationMessageExecutor.cs | 27 ++
.../RetrieveConversationMessagesExecutor.cs | 67 +++++
.../ObjectModel/SendActivityExecutor.cs | 15 +-
.../SetMultipleVariablesExecutor.cs | 39 +++
.../ObjectModel/SetVariableExecutor.cs | 2 +-
.../PowerFx/Functions/UserMessage.cs | 36 +++
.../PowerFx/RecalcEngineFactory.cs | 2 +
.../PowerFx/SystemScope.cs | 6 -
.../PowerFx/TypeSchema.cs | 33 +++
.../PowerFx/WorkflowDiagnostics.cs | 2 +-
.../WorkflowAgentProvider.cs | 91 +++---
.../Agents/BasicAgent.yaml | 5 +
.../DeclarativeWorkflowTest.cs | 114 ++++++++
.../Framework/AgentFactory.cs | 50 ++++
.../Framework/AgentFixture.cs | 26 ++
.../Framework/TestOutputAdapter.cs | 73 +++++
.../Framework/Testcase.cs | 59 ++++
.../Framework/WorkflowEvents.cs | 23 ++
.../Framework/WorkflowHarness.cs | 17 ++
.../Framework/WorkflowTest.cs | 37 +++
...kflows.Declarative.IntegrationTests.csproj | 37 +++
.../Testcases/InvokeAgent.json | 12 +
.../Testcases/SendActivity.json | 12 +
.../Workflows/AddMessages.yaml | 49 ++++
.../Workflows/GetMessage.yaml | 23 ++
.../Workflows/GetMessages.yaml | 22 ++
.../Workflows/InvokeAgent.yaml | 15 +
.../Workflows/SendActivity.yaml | 6 +-
.../DeclarativeWorkflowTest.cs | 9 +-
.../Extensions/FormulaValueExtensionsTests.cs | 28 +-
.../ObjectModel/SendActivityExecutorTest.cs | 2 +-
.../ObjectModel/WorkflowActionExecutorTest.cs | 8 +-
workflows/DeepResearch.yaml | 250 +++++++++-------
workflows/HumanInLoop.yaml | 44 +++
workflows/Marketing.yaml | 46 +++
workflows/MathChat.yaml | 22 +-
workflows/Question.yaml | 24 --
79 files changed, 2830 insertions(+), 685 deletions(-)
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs
delete mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowEvents.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractionResult.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractor.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/ConversationUpdateEvent.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompleteEvent.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokeEvent.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputRequest.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputResponse.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/MessageActivityEvent.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DurableProperty.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/ExecutorResultMessage.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs
delete mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AnswerQuestionWithAIExecutor.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CreateConversationExecutor.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/GotoExecutor.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/QuestionExecutor.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetMultipleVariablesExecutor.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/Functions/UserMessage.cs
create mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/TypeSchema.cs
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/BasicAgent.yaml
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/TestOutputAdapter.cs
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/Testcase.cs
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Microsoft.Agents.Workflows.Declarative.IntegrationTests.csproj
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/SendActivity.json
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/AddMessages.yaml
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessage.yaml
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessages.yaml
create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml
rename workflows/HelloWorld.yaml => dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml (86%)
create mode 100644 workflows/HumanInLoop.yaml
create mode 100644 workflows/Marketing.yaml
delete mode 100644 workflows/Question.yaml
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 907a58ece8..184812cab2 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -86,14 +86,16 @@
-
-
-
-
+
+
+
+
+
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 324d63cb19..2b07e3346d 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -78,9 +78,9 @@
-
+
+
-
@@ -96,8 +96,8 @@
-
+
@@ -265,6 +265,7 @@
+
diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj
index 96a9d5df1d..fa80c24ff2 100644
--- a/dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Declarative/DeclarativeWorkflow.csproj
@@ -7,7 +7,6 @@
$(ProjectsDebugTargetFrameworks)
enable
disable
- 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
$(NoWarn);CA1812
diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs
index ffacf3123a..914365b779 100644
--- a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs
@@ -11,6 +11,7 @@ using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Agents.Workflows;
using Microsoft.Agents.Workflows.Declarative;
+using Microsoft.Agents.Workflows.Declarative.Events;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
@@ -39,17 +40,11 @@ internal sealed class Program
private async Task ExecuteAsync()
{
// Read and parse the declarative workflow.
- Notify($"WORKFLOW: Parsing {Path.GetFullPath(this.WorkflowFile)}");
+ Notify($"\nWORKFLOW: Parsing {Path.GetFullPath(this.WorkflowFile)}");
Stopwatch timer = Stopwatch.StartNew();
- // Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file.
- DeclarativeWorkflowOptions options =
- new(new FoundryAgentProvider(this.FoundryEndpoint, new AzureCliCredential()))
- {
- Configuration = this.Configuration
- };
- Workflow workflow = DeclarativeWorkflowBuilder.Build(this.WorkflowFile, options);
+ Workflow workflow = this.CreateWorkflow();
Notify($"\nWORKFLOW: Defined {timer.Elapsed}");
@@ -57,23 +52,69 @@ internal sealed class Program
// Run the workflow, just like any other workflow
string input = this.GetWorkflowInput();
- StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
- await this.MonitorWorkflowRunAsync(run);
- Notify("\nWORKFLOW: Done!");
+ CheckpointManager checkpointManager = new();
+ Checkpointed run = await InProcessExecution.StreamAsync(workflow, input, checkpointManager);
+
+ bool isComplete = false;
+ InputResponse? response = null;
+ do
+ {
+ ExternalRequest? inputRequest = await this.MonitorWorkflowRunAsync(run, response);
+ if (inputRequest is not null)
+ {
+ Notify("\nWORKFLOW: Yield");
+
+ if (this.LastCheckpoint is null)
+ {
+ throw new InvalidOperationException("Checkpoint information missing after external request.");
+ }
+
+ // Process the external request.
+ response = HandleExternalRequest(inputRequest);
+
+ // Let's resume on an entirely new workflow instance to demonstrate checkpoint portability.
+ workflow = this.CreateWorkflow();
+
+ // Restore the latest checkpoint.
+ Debug.WriteLine($"RESTORE #{this.LastCheckpoint.CheckpointId}");
+ Notify("\nWORKFLOW: Restore");
+ run = await InProcessExecution.ResumeStreamAsync(workflow, this.LastCheckpoint, checkpointManager);
+ }
+ else
+ {
+ isComplete = true;
+ }
+ }
+ while (!isComplete);
+
+ Notify("\nWORKFLOW: Done!\n");
+ }
+
+ private Workflow CreateWorkflow()
+ {
+ // Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file.
+ DeclarativeWorkflowOptions options =
+ new(new AzureAgentProvider(this.FoundryEndpoint, new AzureCliCredential()))
+ {
+ Configuration = this.Configuration
+ };
+ Workflow workflow = DeclarativeWorkflowBuilder.Build(this.WorkflowFile, options);
+ return workflow;
}
private const string DefaultWorkflow = "HelloWorld.yaml";
private const string ConfigKeyFoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT";
- private static readonly Dictionary s_nameCache = [];
- private static readonly HashSet s_fileCache = [];
+ private static Dictionary NameCache { get; } = [];
+ private static HashSet FileCache { get; } = [];
private string WorkflowFile { get; }
private string? WorkflowInput { get; }
private string FoundryEndpoint { get; }
private PersistentAgentsClient FoundryClient { get; }
private IConfiguration Configuration { get; }
+ private CheckpointInfo? LastCheckpoint { get; set; }
private Program(string[] args)
{
@@ -86,112 +127,147 @@ internal sealed class Program
this.FoundryClient = new PersistentAgentsClient(this.FoundryEndpoint, new AzureCliCredential());
}
- private async Task MonitorWorkflowRunAsync(StreamingRun run)
+ private async Task MonitorWorkflowRunAsync(Checkpointed run, InputResponse? response = null)
{
string? messageId = null;
- await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
+ await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync().ConfigureAwait(false))
{
- if (evt is ExecutorInvokedEvent executorInvoked)
+ switch (workflowEvent)
{
- Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}");
- }
- else if (evt is ExecutorCompletedEvent executorCompleted)
- {
- Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}");
- }
- if (evt is DeclarativeActionInvokeEvent actionInvoked)
- {
- Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]");
- }
- else if (evt is DeclarativeActionCompleteEvent actionComplete)
- {
- Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]");
- }
- else if (evt is ExecutorFailureEvent executorFailure)
- {
- Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
- }
- else if (evt is ConversationUpdateEvent invokeEvent)
- {
- Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
- }
- else if (evt is AgentRunUpdateEvent streamEvent)
- {
- if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal))
- {
- messageId = streamEvent.Update.MessageId;
+ case ExecutorInvokedEvent executorInvoked:
+ Debug.WriteLine($"EXECUTOR ENTER #{executorInvoked.ExecutorId}");
+ break;
- if (messageId is not null)
- {
- string? agentId = streamEvent.Update.AuthorName;
- if (agentId is not null)
- {
- if (!s_nameCache.TryGetValue(agentId, out string? realName))
- {
- PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId);
- s_nameCache[agentId] = agent.Name;
- realName = agent.Name;
- }
- agentId = realName;
- }
- agentId ??= nameof(ChatRole.Assistant);
- Console.ForegroundColor = ConsoleColor.Cyan;
- Console.Write($"\n{agentId.ToUpperInvariant()}:");
- Console.ForegroundColor = ConsoleColor.DarkGray;
- Console.WriteLine($" [{messageId}]");
- }
- }
+ case ExecutorCompletedEvent executorCompleted:
+ Debug.WriteLine($"EXECUTOR EXIT #{executorCompleted.ExecutorId}");
+ break;
- ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate;
- switch (chatUpdate?.RawRepresentation)
- {
- case MessageContentUpdate messageUpdate:
- string? fileId = messageUpdate.ImageFileId ?? messageUpdate.TextAnnotation?.OutputFileId;
- if (fileId is not null && s_fileCache.Add(fileId))
- {
- BinaryData content = await this.FoundryClient.Files.GetFileContentAsync(fileId);
- await DownloadFileContentAsync(Path.GetFileName(messageUpdate.TextAnnotation?.TextToReplace ?? "response.png"), content);
- }
- break;
- }
- try
- {
- Console.ResetColor();
- Console.Write(streamEvent.Data);
- }
- finally
- {
- Console.ResetColor();
- }
- }
- else if (evt is AgentRunResponseEvent messageEvent)
- {
- try
- {
- Console.WriteLine();
- if (messageEvent.Response.AgentId is null)
+ case DeclarativeActionInvokeEvent actionInvoked:
+ Debug.WriteLine($"ACTION ENTER #{actionInvoked.ActionId} [{actionInvoked.ActionType}]");
+ break;
+
+ case DeclarativeActionCompleteEvent actionComplete:
+ Debug.WriteLine($"ACTION EXIT #{actionComplete.ActionId} [{actionComplete.ActionType}]");
+ break;
+
+ case ExecutorFailureEvent executorFailure:
+ Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
+ break;
+
+ case SuperStepCompletedEvent checkpointCompleted:
+ this.LastCheckpoint = checkpointCompleted.CompletionInfo?.Checkpoint;
+ Debug.WriteLine($"CHECKPOINT x{checkpointCompleted.StepNumber} [{this.LastCheckpoint?.CheckpointId ?? "(none)"}]");
+ break;
+
+ case RequestInfoEvent requestInfo:
+ Debug.WriteLine($"REQUEST #{requestInfo.Request.RequestId}");
+ if (response is not null)
{
- Console.ForegroundColor = ConsoleColor.Cyan;
- Console.WriteLine("ACTIVITY:");
- Console.ForegroundColor = ConsoleColor.Yellow;
- Console.WriteLine(messageEvent.Response?.Text.Trim());
+ ExternalResponse requestResponse = requestInfo.Request.CreateResponse(response);
+ await run.Run.SendResponseAsync(requestResponse).ConfigureAwait(false);
+ response = null;
}
else
{
+ return requestInfo.Request;
+ }
+ break;
+
+ case ConversationUpdateEvent invokeEvent:
+ Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
+ break;
+
+ case MessageActivityEvent activityEvent:
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("\nACTIVITY:");
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine(activityEvent.Message.Trim());
+ break;
+
+ case AgentRunUpdateEvent streamEvent:
+ if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal))
+ {
+ messageId = streamEvent.Update.MessageId;
+
+ if (messageId is not null)
+ {
+ string? agentId = streamEvent.Update.AuthorName;
+ if (agentId is not null)
+ {
+ if (!NameCache.TryGetValue(agentId, out string? realName))
+ {
+ PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId);
+ NameCache[agentId] = agent.Name;
+ realName = agent.Name;
+ }
+ agentId = realName;
+ }
+ agentId ??= nameof(ChatRole.Assistant);
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.Write($"\n{agentId.ToUpperInvariant()}:");
+ Console.ForegroundColor = ConsoleColor.DarkGray;
+ Console.WriteLine($" [{messageId}]");
+ }
+ }
+
+ ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate;
+ switch (chatUpdate?.RawRepresentation)
+ {
+ case MessageContentUpdate messageUpdate:
+ string? fileId = messageUpdate.ImageFileId ?? messageUpdate.TextAnnotation?.OutputFileId;
+ if (fileId is not null && FileCache.Add(fileId))
+ {
+ BinaryData content = await this.FoundryClient.Files.GetFileContentAsync(fileId);
+ await DownloadFileContentAsync(Path.GetFileName(messageUpdate.TextAnnotation?.TextToReplace ?? "response.png"), content);
+ }
+ break;
+ }
+ try
+ {
+ Console.ResetColor();
+ Console.Write(streamEvent.Data);
+ }
+ finally
+ {
+ Console.ResetColor();
+ }
+ break;
+
+ case AgentRunResponseEvent messageEvent:
+ try
+ {
+ Console.WriteLine();
if (messageEvent.Response.Usage is not null)
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]");
}
}
- }
- finally
- {
- Console.ResetColor();
- }
+ finally
+ {
+ Console.ResetColor();
+ }
+ break;
}
}
+
+ return default;
+ }
+ private static InputResponse HandleExternalRequest(ExternalRequest request)
+ {
+ InputRequest? message = request.Data as InputRequest;
+ string? userInput = null;
+ do
+ {
+ Console.ForegroundColor = ConsoleColor.DarkGreen;
+ Console.Write($"\n{message?.Prompt ?? "INPUT:"} ");
+ Console.ForegroundColor = ConsoleColor.White;
+ userInput = Console.ReadLine();
+ }
+ while (string.IsNullOrWhiteSpace(userInput));
+
+ return new InputResponse(userInput);
}
private static string ParseWorkflowFile(string[] args)
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs
new file mode 100644
index 0000000000..f88b7fa239
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs
@@ -0,0 +1,181 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Agents.Persistent;
+using Azure.Core;
+using Azure.Core.Pipeline;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.AI.Agents;
+
+namespace Microsoft.Agents.Workflows.Declarative;
+
+///
+/// Provides functionality to interact with Foundry agents within a specified project context.
+///
+/// This class is used to retrieve and manage AI agents associated with a Foundry project. It requires a
+/// project endpoint and credentials to authenticate requests.
+/// The endpoint URL of the Foundry project. This must be a valid, non-null URI pointing to the project.
+/// The credentials used to authenticate with the Foundry project. This must be a valid instance of .
+/// An optional instance to be used for making HTTP requests. If not provided, a default client will be used.
+public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential projectCredentials, HttpClient? httpClient = null) : WorkflowAgentProvider
+{
+ private static readonly Dictionary s_roleMap =
+ new()
+ {
+ [ChatRole.User.Value.ToUpperInvariant()] = MessageRole.User,
+ [ChatRole.Assistant.Value.ToUpperInvariant()] = MessageRole.Agent,
+ [ChatRole.System.Value.ToUpperInvariant()] = new MessageRole(ChatRole.System.Value),
+ [ChatRole.Tool.Value.ToUpperInvariant()] = new MessageRole(ChatRole.Tool.Value),
+ };
+
+ private PersistentAgentsClient? _agentsClient;
+
+ ///
+ public override async Task CreateConversationAsync(CancellationToken cancellationToken = default)
+ {
+ PersistentAgentThread conversation = await this.GetAgentsClient().Threads.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ return conversation.Id;
+ }
+
+ ///
+ public override async Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default)
+ {
+ await this.GetAgentsClient().Messages.CreateMessageAsync(
+ conversationId,
+ role: s_roleMap[conversationMessage.Role.Value.ToUpperInvariant()],
+ // TODO: PersistentAgent bug blocks supporting multiple content types:
+ // https://github.com/Azure/azure-sdk-for-net/issues/52571
+ //contentBlocks: GetContent(),
+ content: conversationMessage.Text,
+ attachments: null,
+ metadata: GetMetadata(),
+ cancellationToken).ConfigureAwait(false);
+
+ Dictionary? GetMetadata()
+ {
+ if (conversationMessage.AdditionalProperties is null)
+ {
+ return null;
+ }
+
+ return conversationMessage.AdditionalProperties.ToDictionary(prop => prop.Key, prop => prop.Value?.ToString() ?? string.Empty);
+ }
+
+ // TODO: PersistentAgent bug blocks supporting multiple content types:
+ // https://github.com/Azure/azure-sdk-for-net/issues/52571
+ //IEnumerable GetContent()
+ //{
+ // foreach (AIContent content in conversationMessage.Contents)
+ // {
+ // MessageInputContentBlock? contentBlock =
+ // content switch
+ // {
+ // TextContent textContent => new MessageInputTextBlock(textContent.Text),
+ // HostedFileContent fileContent => new MessageInputImageFileBlock(new MessageImageFileParam(fileContent.FileId)),
+ // UriContent uriContent when uriContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(uriContent.Uri.ToString())),
+ // _ => null // Unsupported content type
+ // };
+
+ // if (contentBlock is not null)
+ // {
+ // yield return contentBlock;
+ // }
+ // }
+ //}
+ }
+
+ ///
+ public override async Task GetAgentAsync(string agentId, CancellationToken cancellationToken = default)
+ {
+ AIAgent agent = await this.GetAgentsClient().GetAIAgentAsync(agentId, chatOptions: null, cancellationToken).ConfigureAwait(false);
+
+ return agent;
+ }
+
+ ///
+ public override async Task GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default)
+ {
+ PersistentThreadMessage message = await this.GetAgentsClient().Messages.GetMessageAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false);
+ return ToChatMessage(message);
+ }
+
+ ///
+ public override async IAsyncEnumerable GetMessagesAsync(
+ string conversationId,
+ int? limit = null,
+ string? after = null,
+ string? before = null,
+ bool newestFirst = false,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ ListSortOrder order = newestFirst ? ListSortOrder.Ascending : ListSortOrder.Descending;
+ await foreach (PersistentThreadMessage message in this.GetAgentsClient().Messages.GetMessagesAsync(conversationId, runId: null, limit, order, after, before, cancellationToken).ConfigureAwait(false))
+ {
+ yield return ToChatMessage(message);
+ }
+ }
+
+ private PersistentAgentsClient GetAgentsClient()
+ {
+ if (this._agentsClient is null)
+ {
+ PersistentAgentsAdministrationClientOptions clientOptions = new();
+
+ if (httpClient is not null)
+ {
+ clientOptions.Transport = new HttpClientTransport(httpClient);
+ }
+
+ PersistentAgentsClient newClient = new(projectEndpoint, projectCredentials, clientOptions);
+
+ Interlocked.CompareExchange(ref this._agentsClient, newClient, null);
+ }
+
+ return this._agentsClient;
+ }
+
+ private static ChatMessage ToChatMessage(PersistentThreadMessage message)
+ {
+ return
+ new ChatMessage(new ChatRole(message.Role.ToString()), [.. GetContent()])
+ {
+ MessageId = message.Id,
+ CreatedAt = message.CreatedAt,
+ AdditionalProperties = GetMetadata()
+ };
+
+ IEnumerable GetContent()
+ {
+ foreach (MessageContent contentItem in message.ContentItems)
+ {
+ AIContent? content =
+ contentItem switch
+ {
+ MessageTextContent textContent => new TextContent(textContent.Text),
+ MessageImageFileContent imageContent => new HostedFileContent(imageContent.FileId),
+ _ => null // Unsupported content type
+ };
+
+ if (content is not null)
+ {
+ yield return content;
+ }
+ }
+ }
+
+ AdditionalPropertiesDictionary? GetMetadata()
+ {
+ if (message.Metadata is null)
+ {
+ return null;
+ }
+
+ return new AdditionalPropertiesDictionary(message.Metadata.Select(m => new KeyValuePair(m.Key, m.Value)));
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
index 97a7d93328..a8f0eaad05 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
@@ -34,6 +34,7 @@ public static class DeclarativeWorkflowBuilder
using StreamReader yamlReader = File.OpenText(workflowFile);
return Build(yamlReader, options, inputTransform);
}
+
///
/// Builds a process from the provided YAML definition of a CPS Topic ObjectModel.
///
@@ -56,7 +57,7 @@ public static class DeclarativeWorkflowBuilder
throw new DeclarativeModelException($"Unsupported root element: {rootElement.GetType().Name}. Expected an {nameof(AdaptiveDialog)}.");
}
- string rootId = WorkflowActionVisitor.RootId(workflowElement.BeginDialog?.Id.Value ?? "workflow");
+ string rootId = WorkflowActionVisitor.Steps.Root(workflowElement.BeginDialog?.Id.Value);
WorkflowScopes scopes = new();
scopes.Initialize(WrapWithBot(workflowElement), options.Configuration);
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowEvents.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowEvents.cs
deleted file mode 100644
index 74970f62f1..0000000000
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowEvents.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.Agents.Workflows.Declarative.Extensions;
-using Microsoft.Bot.ObjectModel;
-
-namespace Microsoft.Agents.Workflows.Declarative;
-
-///
-/// Event that broadcasts the conversation identifier.
-///
-public class ConversationUpdateEvent(string executorid, string conversationId) : ExecutorEvent(executorid, conversationId)
-{
- ///
- /// The conversation ID associated with the workflow.
- ///
- public string ConversationId { get; } = conversationId;
-}
-
-///
-/// Event that indicates a declarative action has been invoked.
-///
-public class DeclarativeActionInvokeEvent(string actionId, DialogAction action, string? priorActionId) : WorkflowEvent(action)
-{
- ///
- /// The declarative action id.
- ///
- public string ActionId => actionId;
-
- ///
- /// The declarative action type name.
- ///
- public string ActionType => action.GetType().Name;
-
- ///
- /// Identifier of the parent action.
- ///
- public string? ParentActionId => action.GetParentId();
-
- ///
- /// Identifier of the previous action.
- ///
- public string? PriorActionId => priorActionId;
-}
-
-///
-/// Event that indicates a declarative action has completed.
-///
-public class DeclarativeActionCompleteEvent(string actionId, DialogAction action) : WorkflowEvent(action)
-{
- ///
- /// The declarative action identifier.
- ///
- public string ActionId => actionId;
-
- ///
- /// The declarative action type name.
- ///
- public string ActionType => action.GetType().Name;
-
- ///
- /// Identifier of the parent action.
- ///
- public string? ParentActionId => action.GetParentId();
-}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractionResult.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractionResult.cs
new file mode 100644
index 0000000000..96029a7e23
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractionResult.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.Agents.Workflows.Declarative.Entities;
+
+internal sealed record class EntityExtractionResult
+{
+ public EntityExtractionResult(FormulaValue? value)
+ {
+ this.Value = value;
+ this.ErrorMessage = null;
+ }
+
+ public EntityExtractionResult(string errorMessage)
+ {
+ this.Value = null;
+ this.ErrorMessage = errorMessage;
+ }
+
+ public FormulaValue? Value { get; }
+ public string? ErrorMessage { get; }
+ public bool IsValid => this.Value is not null;
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractor.cs
new file mode 100644
index 0000000000..7ba0ddac0e
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Entities/EntityExtractor.cs
@@ -0,0 +1,173 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Net.Mail;
+using System.Text.RegularExpressions;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.Agents.Workflows.Declarative.Entities;
+
+internal static partial class EntityExtractor
+{
+ private const string NumberUnitRegExExpression = @"(?[-+]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?|\d*\.\d+)";
+
+#if NET
+ [GeneratedRegex(NumberUnitRegExExpression, RegexOptions.IgnoreCase)]
+ private static partial Regex NumberUnitRegex();
+#else
+ private static Regex NumberUnitRegex() => s_numberUnitRegex;
+ private static readonly Regex s_numberUnitRegex = new(NumberUnitRegExExpression, RegexOptions.IgnoreCase | RegexOptions.Compiled);
+#endif
+
+ public static EntityExtractionResult Parse(EntityReference? entity, string value) =>
+ entity switch
+ {
+ null => UndefinedEntity(value),
+ AgePrebuiltEntity => TryParseNumberUnit(value, "age"),
+ BooleanPrebuiltEntity => TryParseBoolean(value),
+ CityPrebuiltEntity => TryParseString(value),
+ ColorPrebuiltEntity => TryParseString(value),
+ ContinentPrebuiltEntity => TryParseString(value),
+ CountryOrRegionPrebuiltEntity => TryParseString(value),
+ DatePrebuiltEntity => TryParseDate(value),
+ DateTimeNoTimeZonePrebuiltEntity => TryParseDateTimeNoTimeZone(value),
+ DateTimePrebuiltEntity => TryParseDateTime(value),
+ DurationPrebuiltEntity => TryParseDuration(value),
+ EmailPrebuiltEntity => TryParseEmail(value),
+ EventPrebuiltEntity => TryParseString(value),
+ LanguagePrebuiltEntity => TryParseString(value),
+ MoneyPrebuiltEntity => TryParseNumberUnit(value, "money"),
+ NumberPrebuiltEntity => TryParseNumber(value),
+ PercentagePrebuiltEntity => TryParseNumberUnit(value, "percentage"),
+ PhoneNumberPrebuiltEntity => TryParseString(value),
+ PointOfInterestPrebuiltEntity => TryParseString(value),
+ SpeedPrebuiltEntity => TryParseNumberUnit(value, "speed"),
+ StatePrebuiltEntity => TryParseString(value),
+ StreetAddressPrebuiltEntity => TryParseString(value),
+ StringPrebuiltEntity => TryParseString(value),
+ TemperaturePrebuiltEntity => TryParseNumberUnit(value, "temperature"),
+ URLPrebuiltEntity => TryParseURL(value),
+ WeightPrebuiltEntity => TryParseNumberUnit(value, "weight"),
+ _ => UnsupportedEntity(entity),
+ };
+
+ private static EntityExtractionResult TryParseBoolean(string value)
+ {
+ if (bool.TryParse(value, out bool parsedValue))
+ {
+ return new EntityExtractionResult(FormulaValue.New(parsedValue));
+ }
+
+ return new EntityExtractionResult($"Invalid boolean value: {value}");
+ }
+
+ private static EntityExtractionResult TryParseDate(string value)
+ {
+ if (DateTime.TryParse(value, out DateTime parsedValue))
+ {
+ return new EntityExtractionResult(FormulaValue.New(parsedValue.Date));
+ }
+
+ return new EntityExtractionResult($"Invalid date value: {value}");
+ }
+
+ private static EntityExtractionResult TryParseDateTimeNoTimeZone(string value)
+ {
+ if (DateTime.TryParse(value, out DateTime parsedValue))
+ {
+ return new EntityExtractionResult(
+ FormulaValue.New(
+ DateTime.SpecifyKind(parsedValue, DateTimeKind.Unspecified)));
+ }
+
+ return new EntityExtractionResult($"Invalid date value: {value}");
+ }
+
+ private static EntityExtractionResult TryParseDateTime(string value)
+ {
+ if (DateTime.TryParse(value, out DateTime parsedValue))
+ {
+ return new EntityExtractionResult(FormulaValue.New(parsedValue));
+ }
+
+ return new EntityExtractionResult($"Invalid date-time value: {value}");
+ }
+
+ private static EntityExtractionResult TryParseDuration(string value)
+ {
+ if (TimeSpan.TryParse(value, out TimeSpan parsedValue))
+ {
+ return new EntityExtractionResult(FormulaValue.New(parsedValue));
+ }
+
+ return new EntityExtractionResult($"Invalid duration value: {value}");
+ }
+
+ private static EntityExtractionResult TryParseEmail(string value)
+ {
+ try
+ {
+ MailAddress parsedValue = new(value);
+ return new EntityExtractionResult(FormulaValue.New(parsedValue.Address));
+ }
+ catch
+ {
+ return new EntityExtractionResult($"Invalid email value: {value}");
+ }
+ }
+
+ private static EntityExtractionResult TryParseNumberUnit(string value, string type)
+ {
+ Match m = NumberUnitRegex().Match(value);
+ if (m.Success)
+ {
+ return new EntityExtractionResult(FormulaValue.New(m.Groups[0].Value));
+ }
+
+ return new EntityExtractionResult($"Invalid {type} value: {value}");
+ }
+
+ private static EntityExtractionResult TryParseNumber(string value)
+ {
+ if (double.TryParse(value, out double parsedValue))
+ {
+ return new EntityExtractionResult(FormulaValue.New(parsedValue));
+ }
+
+ return new EntityExtractionResult($"Invalid double value: {value}");
+ }
+
+ private static EntityExtractionResult TryParseString(string value)
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ return new EntityExtractionResult(FormulaValue.New(value));
+ }
+
+ return new EntityExtractionResult("Empty value");
+ }
+
+ private static EntityExtractionResult TryParseURL(string value)
+ {
+ if (Uri.TryCreate(value, UriKind.Absolute, out Uri? uriResult))
+ {
+ return new EntityExtractionResult(FormulaValue.New(uriResult.AbsoluteUri));
+ }
+
+ return new EntityExtractionResult($"Invalid double value: {value}");
+ }
+
+ private static EntityExtractionResult UndefinedEntity(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return new EntityExtractionResult(FormulaValue.NewBlank());
+ }
+
+ return new EntityExtractionResult(FormulaValue.New(value));
+ }
+
+ private static EntityExtractionResult UnsupportedEntity(EntityReference entity) =>
+ new($"Unsupported entity: {entity.GetType().Name}");
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/ConversationUpdateEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/ConversationUpdateEvent.cs
new file mode 100644
index 0000000000..9fa9a12ccc
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/ConversationUpdateEvent.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.Workflows.Declarative;
+
+///
+/// Event that broadcasts the conversation identifier.
+///
+public sealed class ConversationUpdateEvent : WorkflowEvent
+{
+ ///
+ /// The conversation ID associated with the workflow.
+ ///
+ public string ConversationId { get; }
+
+ internal ConversationUpdateEvent(string conversationId)
+ : base(conversationId)
+ {
+ this.ConversationId = conversationId;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompleteEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompleteEvent.cs
new file mode 100644
index 0000000000..b574be1621
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompleteEvent.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.Workflows.Declarative.Extensions;
+using Microsoft.Bot.ObjectModel;
+
+namespace Microsoft.Agents.Workflows.Declarative;
+
+///
+/// Event that indicates a declarative action has been invoked.
+///
+public sealed class DeclarativeActionInvokeEvent : WorkflowEvent
+{
+ ///
+ /// The declarative action id.
+ ///
+ public string ActionId { get; }
+
+ ///
+ /// The declarative action type name.
+ ///
+ public string ActionType { get; }
+
+ ///
+ /// Identifier of the parent action.
+ ///
+ public string? ParentActionId { get; }
+
+ ///
+ /// Identifier of the previous action.
+ ///
+ public string? PriorActionId { get; }
+
+ internal DeclarativeActionInvokeEvent(DialogAction action, string? priorActionId) : base(action)
+ {
+ this.ActionId = action.GetId();
+ this.ActionType = action.GetType().Name;
+ this.ParentActionId = action.GetParentId();
+ this.PriorActionId = priorActionId;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokeEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokeEvent.cs
new file mode 100644
index 0000000000..bb9f676635
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokeEvent.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.Workflows.Declarative.Extensions;
+using Microsoft.Bot.ObjectModel;
+
+namespace Microsoft.Agents.Workflows.Declarative;
+
+///
+/// Event that indicates a declarative action has completed.
+///
+public sealed class DeclarativeActionCompleteEvent : WorkflowEvent
+{
+ ///
+ /// The declarative action identifier.
+ ///
+ public string ActionId { get; }
+
+ ///
+ /// The declarative action type name.
+ ///
+ public string ActionType { get; }
+
+ ///
+ /// Identifier of the parent action.
+ ///
+ public string? ParentActionId { get; }
+
+ internal DeclarativeActionCompleteEvent(DialogAction action) : base(action)
+ {
+ this.ActionId = action.GetId();
+ this.ActionType = action.GetType().Name;
+ this.ParentActionId = action.GetParentId();
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputRequest.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputRequest.cs
new file mode 100644
index 0000000000..c2ae30797f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputRequest.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.Workflows.Declarative.Events;
+
+///
+/// Represents a request for user input.
+///
+public sealed class InputRequest
+{
+ ///
+ /// The prompt message to display to the user.
+ ///
+ public string Prompt { get; }
+
+ internal InputRequest(string prompt)
+ {
+ this.Prompt = prompt;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputResponse.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputResponse.cs
new file mode 100644
index 0000000000..e72a74b583
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/InputResponse.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.Workflows.Declarative.Events;
+
+///
+/// Represents a user input response.
+///
+public sealed class InputResponse
+{
+ ///
+ /// The response value.
+ ///
+ public string Value { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The response value.
+ public InputResponse(string value)
+ {
+ this.Value = value;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/MessageActivityEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/MessageActivityEvent.cs
new file mode 100644
index 0000000000..1fb786b19f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/MessageActivityEvent.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.Workflows.Declarative;
+
+///
+/// Event that broadcasts the conversation identifier.
+///
+public sealed class MessageActivityEvent : WorkflowEvent
+{
+ ///
+ /// The conversation ID associated with the workflow.
+ ///
+ public string Message { get; }
+
+ internal MessageActivityEvent(string message) : base(message)
+ {
+ this.Message = message;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeModelException.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeModelException.cs
index f6b6adfb11..3f5563cb68 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeModelException.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeModelException.cs
@@ -7,7 +7,7 @@ namespace Microsoft.Agents.Workflows.Declarative;
///
/// Represents an exception that occurs when the declarative model is not supported.
///
-public class DeclarativeModelException : DeclarativeWorkflowException
+public sealed class DeclarativeModelException : DeclarativeWorkflowException
{
///
/// Initializes a new instance of the class.
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs
index 89a65c86b2..32de8a66a6 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Agents.Workflows.Declarative.PowerFx.Functions;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
@@ -9,15 +12,234 @@ namespace Microsoft.Agents.Workflows.Declarative.Extensions;
internal static class ChatMessageExtensions
{
- // ISSUE #485 - Align with message type updated OM is available.
public static RecordValue ToRecord(this ChatMessage message) =>
RecordValue.NewRecordFromFields(message.GetMessageFields());
+ public static TableValue ToTable(this IEnumerable messages) =>
+ FormulaValue.NewTable(s_messageRecordType, messages.Select(message => message.ToRecord()));
+
+ public static IEnumerable ToChatMessages(this DataValue messages)
+ {
+ if (messages is TableDataValue table)
+ {
+ return table.ToChatMessages();
+ }
+
+ if (messages is RecordDataValue record)
+ {
+ return [record.ToChatMessage()];
+ }
+
+ if (messages is StringDataValue text)
+ {
+ return [text.ToChatMessage()];
+ }
+
+ return [];
+ }
+
+ public static IEnumerable ToChatMessages(this TableDataValue messages)
+ {
+ foreach (DataValue message in messages.Values)
+ {
+ if (message is RecordDataValue record)
+ {
+ if (record.Properties.Count == 1 && record.Properties.TryGetValue("Value", out DataValue? singleColumn))
+ {
+ record = singleColumn as RecordDataValue ?? record;
+ }
+ ChatMessage? convertedMessage = record.ToChatMessage();
+ if (convertedMessage is not null)
+ {
+ yield return convertedMessage;
+ }
+ }
+ else if (message is StringDataValue text)
+ {
+ yield return ToChatMessage(text);
+ }
+ }
+ }
+
+ public static ChatMessage? ToChatMessage(this DataValue message)
+ {
+ if (message is RecordDataValue record)
+ {
+ return record.ToChatMessage();
+ }
+
+ if (message is StringDataValue text)
+ {
+ return text.ToChatMessage();
+ }
+
+ if (message is BlankDataValue)
+ {
+ return null;
+ }
+
+ throw new DeclarativeActionException($"Unable to convert {message.GetDataType()} to {nameof(ChatMessage)}.");
+ }
+
+ public static ChatMessage ToChatMessage(this RecordDataValue message) =>
+ new(message.GetRole(), [.. message.GetContent()])
+ {
+ AdditionalProperties = message.GetProperty("metadata").ToMetadata()
+ };
+
+ public static ChatMessage ToChatMessage(this StringDataValue message) => new(ChatRole.User, message.Value);
+
+ public static AdditionalPropertiesDictionary? ToMetadata(this RecordDataValue? metadata)
+ {
+ if (metadata is null)
+ {
+ return null;
+ }
+
+ AdditionalPropertiesDictionary properties = [];
+
+ foreach (KeyValuePair property in metadata.Properties)
+ {
+ properties[property.Key] = property.Value.ToObject();
+ }
+
+ return properties;
+ }
+
+ public static ChatRole ToChatRole(this AgentMessageRole role) =>
+ role switch
+ {
+ AgentMessageRole.Agent => ChatRole.Assistant,
+ AgentMessageRole.User => ChatRole.User,
+ _ => ChatRole.User
+ };
+
+ public static ChatRole ToChatRole(this AgentMessageRole? role) => role?.ToChatRole() ?? ChatRole.User;
+
+ public static AIContent? ToContent(this AgentMessageContentType contentType, string? contentValue)
+ {
+ if (string.IsNullOrEmpty(contentValue))
+ {
+ return null;
+ }
+
+ return
+ contentType switch
+ {
+ AgentMessageContentType.ImageUrl => new UriContent(contentValue, "image/*"),
+ AgentMessageContentType.ImageFile => new HostedFileContent(contentValue),
+ _ => new TextContent(contentValue)
+ };
+ }
+
+ private static ChatRole GetRole(this RecordDataValue message)
+ {
+ StringDataValue? roleValue = message.GetProperty(TypeSchema.Message.Fields.Role);
+ if (roleValue is null || string.IsNullOrWhiteSpace(roleValue.Value))
+ {
+ return ChatRole.User;
+ }
+
+ AgentMessageRole? role = null;
+ if (Enum.TryParse(roleValue.Value, out AgentMessageRole parsedRole))
+ {
+ role = parsedRole;
+ }
+
+ return role.ToChatRole();
+ }
+
+ private static IEnumerable GetContent(this RecordDataValue message)
+ {
+ TableDataValue? content = message.GetProperty(TypeSchema.Message.Fields.Content);
+ if (content is not null)
+ {
+ foreach (RecordDataValue contentItem in content.Values)
+ {
+ StringDataValue? contentValue = contentItem?.GetProperty(TypeSchema.Message.Fields.ContentValue);
+ if (contentValue is null || string.IsNullOrWhiteSpace(contentValue.Value))
+ {
+ continue;
+ }
+ yield return
+ contentItem?.GetProperty(TypeSchema.Message.Fields.ContentType)?.Value switch
+ {
+ TypeSchema.Message.ContentTypes.ImageUrl => new UriContent(contentValue.Value, "image/*"),
+ TypeSchema.Message.ContentTypes.ImageFile => new HostedFileContent(contentValue.Value),
+ _ => new TextContent(contentValue.Value)
+ };
+ }
+ }
+ }
+
+ private static TValue? GetProperty(this RecordDataValue record, string name)
+ where TValue : DataValue
+ {
+ if (record.Properties.TryGetValue(name, out DataValue? value) && value is TValue dataValue)
+ {
+ return dataValue;
+ }
+
+ return null;
+ }
+
private static IEnumerable GetMessageFields(this ChatMessage message)
{
- yield return new NamedValue(nameof(DialogAction.Id), message.MessageId.ToFormulaValue());
- yield return new NamedValue(nameof(ChatMessage.Role), FormulaValue.New(message.Role.Value));
- yield return new NamedValue(nameof(ChatMessage.AuthorName), message.AuthorName.ToFormulaValue());
- yield return new NamedValue(nameof(ChatMessage.Text), message.Text.ToFormulaValue());
+ yield return new NamedValue(TypeSchema.Message.Fields.Id, message.MessageId.ToFormula());
+ yield return new NamedValue(TypeSchema.Message.Fields.Role, message.Role.Value.ToFormula());
+ yield return new NamedValue(TypeSchema.Message.Fields.Author, message.AuthorName.ToFormula());
+ yield return new NamedValue(TypeSchema.Message.Fields.Content, TableValue.NewTable(s_contentRecordType, message.GetContentRecords()));
+ yield return new NamedValue(TypeSchema.Message.Fields.Text, message.Text.ToFormula());
+ yield return new NamedValue(TypeSchema.Message.Fields.Metadata, message.AdditionalProperties.ToRecord());
}
+
+ private static IEnumerable GetContentRecords(this ChatMessage message) =>
+ message.Contents.Select(content => RecordValue.NewRecordFromFields(content.GetContentFields()));
+
+ private static IEnumerable GetContentFields(this AIContent content)
+ {
+ return
+ content switch
+ {
+ UriContent uriContent => CreateContentRecord(TypeSchema.Message.ContentTypes.ImageUrl, uriContent.Uri.ToString()),
+ HostedFileContent fileContent => CreateContentRecord(TypeSchema.Message.ContentTypes.ImageFile, fileContent.FileId),
+ TextContent textContent => CreateContentRecord(TypeSchema.Message.ContentTypes.Text, textContent.Text),
+ _ => []
+ };
+
+ static IEnumerable CreateContentRecord(string type, string value)
+ {
+ yield return new NamedValue(TypeSchema.Message.Fields.ContentType, type.ToFormula());
+ yield return new NamedValue(TypeSchema.Message.Fields.ContentValue, value.ToFormula());
+ }
+ }
+ private static RecordValue ToRecord(this AdditionalPropertiesDictionary? value)
+ {
+ return FormulaValue.NewRecordFromFields(GetFields());
+
+ IEnumerable GetFields()
+ {
+ if (value is not null)
+ {
+ foreach (string key in value.Keys)
+ {
+ yield return new NamedValue(key, value[key].ToFormula());
+ }
+ }
+ }
+ }
+
+ private static readonly RecordType s_contentRecordType =
+ RecordType.Empty()
+ .Add(TypeSchema.Message.Fields.ContentType, FormulaType.String)
+ .Add(TypeSchema.Message.Fields.ContentValue, FormulaType.String);
+
+ private static readonly RecordType s_messageRecordType =
+ RecordType.Empty()
+ .Add(TypeSchema.Message.Fields.Id, FormulaType.String)
+ .Add(TypeSchema.Message.Fields.Role, FormulaType.String)
+ .Add(TypeSchema.Message.Fields.Author, FormulaType.String)
+ .Add(TypeSchema.Message.Fields.Content, s_contentRecordType.ToTable())
+ .Add(TypeSchema.Message.Fields.Text, FormulaType.String)
+ .Add(TypeSchema.Message.Fields.Metadata, RecordType.Empty());
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
index 81666b6013..687cc302f2 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
@@ -9,11 +9,11 @@ namespace Microsoft.Agents.Workflows.Declarative.Extensions;
internal static class DataValueExtensions
{
- public static FormulaValue ToFormulaValue(this DataValue? value) =>
+ public static FormulaValue ToFormula(this DataValue? value) =>
value switch
{
null => FormulaValue.NewBlank(),
- BlankDataValue blankValue => BlankValue.NewBlank(),
+ BlankDataValue => BlankValue.NewBlank(),
BooleanDataValue boolValue => FormulaValue.New(boolValue.Value),
NumberDataValue numberValue => FormulaValue.New(numberValue.Value),
FloatDataValue floatValue => FormulaValue.New(floatValue.Value),
@@ -53,12 +53,30 @@ internal static class DataValueExtensions
_ => FormulaType.Unknown,
};
+ public static object? ToObject(this DataValue? value) =>
+ value switch
+ {
+ null => null,
+ BlankDataValue => null,
+ BooleanDataValue boolValue => boolValue.Value,
+ NumberDataValue numberValue => (numberValue.Value),
+ FloatDataValue floatValue => (floatValue.Value),
+ StringDataValue stringValue => (stringValue.Value),
+ DateTimeDataValue dateTimeValue => (dateTimeValue.Value.DateTime),
+ DateDataValue dateValue => dateValue.Value,
+ TimeDataValue timeValue => timeValue.Value,
+ TableDataValue tableValue => tableValue.Values.Select(value => value.ToRecordValue()).ToArray(),
+ RecordDataValue recordValue => recordValue.ToDictionary(),
+ OptionDataValue optionValue => optionValue.Value.Value,
+ _ => throw new DeclarativeModelException($"Unsupported {nameof(DataValue)} type: {value.GetType().Name}"),
+ };
+
public static FormulaValue NewBlank(this DataType? type) => FormulaValue.NewBlank(type?.ToFormulaType() ?? FormulaType.Blank);
public static RecordValue ToRecordValue(this RecordDataValue recordDataValue) =>
FormulaValue.NewRecordFromFields(
recordDataValue.Properties.Select(
- property => new NamedValue(property.Key, property.Value.ToFormulaValue())));
+ property => new NamedValue(property.Key, property.Value.ToFormula())));
public static RecordType ToRecordType(this RecordDataType record)
{
@@ -79,4 +97,14 @@ internal static class DataValueExtensions
}
return recordType;
}
+
+ private static Dictionary ToDictionary(this RecordDataValue record)
+ {
+ Dictionary result = [];
+ foreach (KeyValuePair property in record.Properties)
+ {
+ result[property.Key] = property.Value.ToObject();
+ }
+ return result;
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
index 568f3ba104..278a6b0ba9 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
@@ -20,7 +20,7 @@ internal static class FormulaValueExtensions
public static FormulaValue NewBlank(this FormulaType? type) => FormulaValue.NewBlank(type ?? FormulaType.Blank);
- public static FormulaValue ToFormulaValue(this object? value) =>
+ public static FormulaValue ToFormula(this object? value) =>
value switch
{
null => FormulaValue.NewBlank(),
@@ -35,8 +35,9 @@ internal static class FormulaValueExtensions
DateTime dateonlyValue when dateonlyValue.TimeOfDay == TimeSpan.Zero => FormulaValue.NewDateOnly(dateonlyValue),
DateTime datetimeValue => FormulaValue.New(datetimeValue),
TimeSpan timeValue => FormulaValue.New(timeValue),
- object when value is IEnumerable tableValue => tableValue.ToTable(),
ExpandoObject expandoValue => expandoValue.ToRecord(),
+ object when value is IDictionary dictionaryValue => dictionaryValue.ToRecord(),
+ object when value is IEnumerable tableValue => tableValue.ToTable(),
_ => throw new DeclarativeModelException($"Unsupported variable type: {value.GetType().Name}"),
};
@@ -143,6 +144,19 @@ internal static class FormulaValueExtensions
public static RecordDataValue ToRecord(this RecordValue value) =>
RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()).ToImmutableArray());
+ private static RecordValue ToRecord(this IDictionary value)
+ {
+ return FormulaValue.NewRecordFromFields(GetFields());
+
+ IEnumerable GetFields()
+ {
+ foreach (string key in value.Keys)
+ {
+ yield return new NamedValue(key, value[key].ToFormula());
+ }
+ }
+ }
+
private static RecordDataType ToDataType(this RecordType record)
{
RecordDataType recordType = new();
@@ -176,48 +190,27 @@ internal static class FormulaValueExtensions
private static RecordValue ToRecord(this ExpandoObject value) =>
FormulaValue.NewRecordFromFields(
value.Select(
- property => new NamedValue(property.Key, property.Value.ToFormulaValue())));
+ property => new NamedValue(property.Key, property.Value.ToFormula())));
private static TableType ToTableType(this IEnumerable value)
{
- Type valueType = value.GetType();
- Type? elementType = valueType.GetElementType() ?? valueType.GetGenericArguments().FirstOrDefault();
-
- if (elementType is not null)
+ foreach (object? element in value)
{
- if (elementType != typeof(ExpandoObject))
+ if (element is not ExpandoObject expandoElement)
{
- throw new DeclarativeModelException($"Invalid table element: {elementType.Name}");
+ throw new DeclarativeModelException($"Invalid table element: {element.GetType().Name}");
}
- foreach (ExpandoObject element in value)
- {
- return element.ToRecordType().ToTable();
- }
+ return expandoElement.ToRecordType().ToTable(); // Return first element
}
return TableType.Empty();
}
- private static TableValue ToTable(this IEnumerable value)
- {
- Type valueType = value.GetType();
- Type? elementType = valueType.GetElementType() ?? valueType.GetGenericArguments().FirstOrDefault();
-
- if (elementType is null)
- {
- return FormulaValue.NewTable(RecordType.EmptySealed());
- }
-
- if (elementType != typeof(ExpandoObject))
- {
- throw new DeclarativeModelException($"Invalid table element: {elementType.Name}");
- }
-
- List records = [.. value.OfType().Select(element => element.ToRecord())];
-
- return FormulaValue.NewTable(value.ToTableType().ToRecord(), records);
- }
+ private static TableValue ToTable(this IEnumerable value) =>
+ FormulaValue.NewTable(
+ value.ToTableType().ToRecord(),
+ [.. value.OfType().Select(element => element.ToRecord())]);
private static KeyValuePair GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.ToDataValue());
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
new file mode 100644
index 0000000000..fb4312ec42
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows.Declarative.Interpreter;
+using Microsoft.Bot.ObjectModel;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class IWorkflowContextExtensions
+{
+ public static ValueTask RaiseInvocationEventAsync(this IWorkflowContext context, DialogAction action, string? priorEventId = null) =>
+ context.AddEventAsync(new DeclarativeActionInvokeEvent(action, priorEventId));
+
+ public static ValueTask RaiseCompletionEventAsync(this IWorkflowContext context, DialogAction action) =>
+ context.AddEventAsync(new DeclarativeActionCompleteEvent(action));
+
+ public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result = null, CancellationToken cancellationToken = default) =>
+ context.SendMessageAsync(new ExecutorResultMessage(id, result));
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/StringExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/StringExtensions.cs
index 0f3a078d2c..83099fddee 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/StringExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/StringExtensions.cs
@@ -20,6 +20,6 @@ internal static class StringExtensions
return value.Trim();
}
- public static FormulaValue ToFormulaValue(this string? value) =>
+ public static FormulaValue ToFormula(this string? value) =>
string.IsNullOrWhiteSpace(value) ? FormulaValue.NewBlank() : FormulaValue.New(value);
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/TemplateExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/TemplateExtensions.cs
index 9a41554a18..bf13587a38 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/TemplateExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/TemplateExtensions.cs
@@ -10,21 +10,21 @@ namespace Microsoft.Agents.Workflows.Declarative.Extensions;
internal static class TemplateExtensions
{
- public static string? Format(this RecalcEngine engine, IEnumerable template)
+ public static string Format(this RecalcEngine engine, IEnumerable template)
{
return string.Concat(template.Select(line => engine.Format(line)));
}
- public static string? Format(this RecalcEngine engine, TemplateLine? line)
+ public static string Format(this RecalcEngine engine, TemplateLine? line)
{
return string.Concat(line?.Segments.Select(segment => engine.Format(segment)) ?? [string.Empty]);
}
- public static string? Format(this RecalcEngine engine, TemplateSegment segment)
+ public static string Format(this RecalcEngine engine, TemplateSegment segment)
{
if (segment is TextSegment textSegment)
{
- return textSegment.Value;
+ return textSegment.Value ?? string.Empty;
}
if (segment is ExpressionSegment expressionSegment)
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
index 2fa157bcca..e8978347fd 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
@@ -15,19 +15,15 @@ using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
-internal sealed record class DeclarativeExecutorResult(string ExecutorId, object? Result = null);
-
internal abstract class DeclarativeActionExecutor(TAction model, DeclarativeWorkflowState state) :
- WorkflowActionExecutor(model, state)
+ DeclarativeActionExecutor(model, state)
where TAction : DialogAction
{
public new TAction Model => (TAction)base.Model;
}
-internal abstract class WorkflowActionExecutor : Executor
+internal abstract class DeclarativeActionExecutor : Executor
{
- public const string RootActionId = "(root)";
-
private static readonly ImmutableHashSet s_mutableScopes =
new HashSet
{
@@ -37,7 +33,7 @@ internal abstract class WorkflowActionExecutor : Executor this._parentId ??= this.Model.GetParentId() ?? RootActionId;
+ public string ParentId => this._parentId ??= this.Model.GetParentId() ?? WorkflowActionVisitor.Steps.Root();
- internal ILogger Logger { get; set; } = NullLogger.Instance;
+ internal ILogger Logger { get; set; } = NullLogger.Instance;
protected DeclarativeWorkflowState State { get; }
protected virtual bool IsDiscreteAction => true;
+ protected virtual bool EmitResultEvent => true;
+
///
- public override async ValueTask HandleAsync(DeclarativeExecutorResult message, IWorkflowContext context)
+ public override async ValueTask HandleAsync(ExecutorResultMessage message, IWorkflowContext context)
{
if (this.Model.Disabled)
{
@@ -68,15 +66,18 @@ internal abstract class WorkflowActionExecutor : Executor ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default);
- protected async ValueTask AssignAsync(PropertyPath targetPath, FormulaValue result, IWorkflowContext context)
+ ///
+ /// Restore the state of the executor from a checkpoint.
+ /// This must be overridden to restore any state that was saved during checkpointing.
+ ///
+ protected override ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
+ this.State.RestoreAsync(context, cancellation);
+
+ protected async ValueTask AssignAsync(PropertyPath? targetPath, FormulaValue result, IWorkflowContext context)
{
+ if (targetPath is null)
+ {
+ return;
+ }
+
if (!s_mutableScopes.Contains(Throw.IfNull(targetPath.VariableScopeName)))
{
throw new DeclarativeModelException($"Invalid scope: {targetPath.VariableScopeName}");
@@ -125,8 +138,4 @@ internal abstract class WorkflowActionExecutor : Executor context.AddEventAsync(new DeclarativeActionInvokeEvent(this.Id, this.Model, priorEventId));
-
- protected ValueTask RaiseCompletionEventAsync(IWorkflowContext context) => context.AddEventAsync(new DeclarativeActionCompleteEvent(this.Id, this.Model));
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs
index f65bb7f5f6..9e6a39d50f 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs
@@ -22,6 +22,6 @@ internal sealed class DeclarativeWorkflowExecutor(
ChatMessage input = inputTransform.Invoke(message);
await state.SetLastMessageAsync(context, input).ConfigureAwait(false);
- await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false);
+ await context.SendMessageAsync(new ExecutorResultMessage(this.Id)).ConfigureAwait(false);
}
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs
index 1d8f511f55..3bff3ad5db 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs
@@ -43,7 +43,19 @@ internal sealed class DeclarativeWorkflowModel
throw new DeclarativeModelException($"Unresolved parent for {executor.Id}: {parentId}.");
}
- ModelNode stepNode = this.DefineNode(executor, parentNode, executor.GetType(), completionHandler);
+ ModelNode stepNode = this.DefineNode(executor, parentNode, completionHandler);
+
+ parentNode.Children.Add(stepNode);
+ }
+
+ public void AddPort(InputPort port, string parentId)
+ {
+ if (!this.Nodes.TryGetValue(parentId, out ModelNode? parentNode))
+ {
+ throw new DeclarativeModelException($"Unresolved parent for {port.Id}: {parentId}.");
+ }
+
+ ModelNode stepNode = this.DefineNode(port, parentNode);
parentNode.Children.Add(stepNode);
}
@@ -96,13 +108,32 @@ internal sealed class DeclarativeWorkflowModel
Debug.WriteLine($"> CONNECT: {link.Source.Id} => {link.TargetId}{(link.Condition is null ? string.Empty : " (?)")}");
- workflowBuilder.AddEdge(link.Source.Executor, targetNode.Executor, link.Condition);
+ workflowBuilder.AddEdge(GetExecutorIsh(link.Source), GetExecutorIsh(targetNode), link.Condition);
+ }
+
+ ExecutorIsh GetExecutorIsh(ModelNode node)
+ {
+ if (node.Port is not null)
+ {
+ return node.Port;
+ }
+
+ return node.Executor;
}
}
- private ModelNode DefineNode(Executor executor, ModelNode? parentNode = null, Type? executorType = null, Action? completionHandler = null)
+ private ModelNode DefineNode(Executor executor, ModelNode? parentNode = null, Action? completionHandler = null)
{
- ModelNode stepNode = new(executor, parentNode, executorType, completionHandler);
+ ModelNode stepNode = new(executor, port: null, parentNode, completionHandler);
+
+ this.Nodes.Add(stepNode.Id, stepNode);
+
+ return stepNode;
+ }
+
+ private ModelNode DefineNode(InputPort port, ModelNode? parentNode = null)
+ {
+ ModelNode stepNode = new(executor: null!, port, parentNode);
this.Nodes.Add(stepNode.Id, stepNode);
@@ -134,13 +165,15 @@ internal sealed class DeclarativeWorkflowModel
return null;
}
- private sealed class ModelNode(Executor executor, ModelNode? parent = null, Type? executorType = null, Action? completionHandler = null)
+ private sealed class ModelNode(Executor executor, InputPort? port, ModelNode? parent = null, Action? completionHandler = null)
{
- public string Id => executor.Id;
+ public string Id => port?.Id ?? executor.Id;
public Executor Executor => executor;
- public Type? ExecutorType => executorType;
+ public InputPort? Port => port;
+
+ public Type? ExecutorType => this.Port?.GetType() ?? this.Executor.GetType();
public ModelNode? Parent { get; } = parent;
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowState.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowState.cs
index fbbc77c48d..adcfba8798 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowState.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowState.cs
@@ -77,9 +77,9 @@ internal sealed class DeclarativeWorkflowState
await context.QueueStateUpdateAsync(varName, value.ToObject(), scopeName).ConfigureAwait(false);
}
- public string? Format(IEnumerable template) => this._engine.Format(template);
+ public string Format(IEnumerable template) => this._engine.Format(template);
- public string? Format(TemplateLine? line) => this._engine.Format(line);
+ public string Format(TemplateLine? line) => this._engine.Format(line);
public async ValueTask RestoreAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
@@ -96,7 +96,7 @@ internal sealed class DeclarativeWorkflowState
foreach (string key in keys)
{
object? value = await context.ReadStateAsync
/// The unique identifier of the AI agent to retrieve. Cannot be null or empty.
/// A cancellation token that can be used to cancel the operation.
- /// A task that represents the asynchronous operation. The task result contains the associated
- /// with the specified . Returns if no agent is found.
+ /// The task result contains the associated.
public abstract Task GetAgentAsync(string agentId, CancellationToken cancellationToken = default);
-}
-
-///
-/// Provides functionality to interact with Foundry agents within a specified project context.
-///
-/// This class is used to retrieve and manage AI agents associated with a Foundry project. It requires a
-/// project endpoint and credentials to authenticate requests.
-/// The endpoint URL of the Foundry project. This must be a valid, non-null URI pointing to the project.
-/// The credentials used to authenticate with the Foundry project. This must be a valid instance of .
-/// An optional instance to be used for making HTTP requests. If not provided, a default client will be used.
-public sealed class FoundryAgentProvider(string projectEndpoint, TokenCredential projectCredentials, HttpClient? httpClient = null) : WorkflowAgentProvider
-{
- private PersistentAgentsClient? _agentsClient;
-
- ///
- public override async Task GetAgentAsync(string agentId, CancellationToken cancellationToken = default)
- {
- AIAgent agent = await this.GetAgentsClient().GetAIAgentAsync(agentId, chatOptions: null, cancellationToken).ConfigureAwait(false);
-
- return agent;
- }
-
- private PersistentAgentsClient GetAgentsClient()
- {
- if (this._agentsClient is null)
- {
- PersistentAgentsAdministrationClientOptions clientOptions = new();
-
- if (httpClient is not null)
- {
- clientOptions.Transport = new HttpClientTransport(httpClient);
- }
-
- PersistentAgentsClient newClient = new(projectEndpoint, projectCredentials, clientOptions);
-
- Interlocked.CompareExchange(ref this._agentsClient, newClient, null);
- }
-
- return this._agentsClient;
- }
+
+ ///
+ /// Asynchronously creates a new conversation and returns its unique identifier.
+ ///
+ /// A cancellation token that can be used to cancel the operation.
+ /// The conversation identifier
+ public abstract Task CreateConversationAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Creates a new message in the specified conversation.
+ ///
+ /// The identifier of the target conversation.
+ /// The message being added.
+ /// A cancellation token that can be used to cancel the operation.
+ public abstract Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default);
+
+ ///
+ /// Retrieves a specific message from a conversation.
+ ///
+ /// The identifier of the target conversation.
+ /// The identifier of the target message.
+ /// A cancellation token that can be used to cancel the operation.
+ /// The requested message
+ public abstract Task GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Retrieves a set of messages from a conversation.
+ ///
+ /// The identifier of the target conversation.
+ /// A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.
+ /// A cursor for use in pagination. after is an object ID that defines your place in the list.
+ /// A cursor for use in pagination. before is an object ID that defines your place in the list.
+ /// Provide records in descending order when true.
+ /// A cancellation token that can be used to cancel the operation.
+ /// The requested messages
+ public abstract IAsyncEnumerable GetMessagesAsync(
+ string conversationId,
+ int? limit = null,
+ string? after = null,
+ string? before = null,
+ bool newestFirst = false,
+ CancellationToken cancellationToken = default);
}
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/BasicAgent.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/BasicAgent.yaml
new file mode 100644
index 0000000000..2d41b2c5dd
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Agents/BasicAgent.yaml
@@ -0,0 +1,5 @@
+type: foundry_agent
+name: BasicAgent
+description: Basic agent for integration tests
+model:
+ id: ${AzureAI:ModelDeployment}
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs
new file mode 100644
index 0000000000..ad58b2b8d2
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs
@@ -0,0 +1,114 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+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;
+using Azure.Identity;
+using Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using Shared.IntegrationTests;
+using Xunit.Abstractions;
+
+namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests;
+
+///
+/// Tests execution of workflow created by .
+///
+[Collection("Global")]
+public sealed class DeclarativeWorkflowTest(ITestOutputHelper output, AgentFixture agentFixture) : WorkflowTest(output), IClassFixture
+{
+ [Theory]
+ [InlineData("SendActivity.yaml", "SendActivity.json")]
+ [InlineData("InvokeAgent.yaml", "InvokeAgent.json")]
+ public Task Validate(string workflowFileName, string testcaseFileName) =>
+ this.RunWorkflow(workflowFileName, testcaseFileName);
+
+ private Task RunWorkflow(string workflowFileName, string testcaseFileName)
+ {
+ this.Output.WriteLine($"WORKFLOW: {workflowFileName}");
+ this.Output.WriteLine($"TESTCASE: {testcaseFileName}");
+
+ Testcase testcase = ReadTestcase(testcaseFileName);
+ IConfiguration configuration = InitializeConfig();
+ string workflowPath = Path.Combine("Workflows", workflowFileName);
+
+ this.Output.WriteLine($" {testcase.Description}");
+
+ return
+ testcase.Setup.Input.Type switch
+ {
+ nameof(ChatMessage) => this.RunWorkflow(testcase, workflowPath, configuration),
+ nameof(String) => this.RunWorkflow(testcase, workflowPath, configuration),
+ _ => throw new NotSupportedException($"Input type '{testcase.Setup.Input.Type}' is not supported."),
+ };
+ }
+
+ private async Task RunWorkflow(
+ Testcase testcase,
+ string workflowPath,
+ IConfiguration configuration) where TInput : notnull
+ {
+ this.Output.WriteLine($"INPUT: {testcase.Setup.Input.Value}");
+
+ AzureAIConfiguration? foundryConfig = configuration.GetSection("AzureAI").Get();
+ Assert.NotNull(foundryConfig);
+
+ IDictionary agentMap = await agentFixture.GetAgentsAsync(foundryConfig);
+
+ IConfiguration workflowConfig =
+ new ConfigurationBuilder()
+ .AddInMemoryCollection(agentMap)
+ .Build();
+
+ DeclarativeWorkflowOptions workflowOptions =
+ new(new AzureAgentProvider(foundryConfig.Endpoint, new AzureCliCredential()))
+ {
+ Configuration = workflowConfig,
+ LoggerFactory = this.Output
+ };
+ Workflow workflow = DeclarativeWorkflowBuilder.Build(workflowPath, workflowOptions);
+
+ WorkflowEvents workflowEvents = await WorkflowHarness.RunAsync(workflow, (TInput)GetInput(testcase));
+ foreach (DeclarativeActionInvokeEvent actionInvokeEvent in workflowEvents.ActionInvokeEvents)
+ {
+ this.Output.WriteLine($"ACTION: {actionInvokeEvent.ActionId} [{actionInvokeEvent.ActionType}]");
+ }
+
+ Assert.Equal(testcase.Validation.ActionCount, workflowEvents.ActionInvokeEvents.Count);
+ Assert.Equal(testcase.Validation.ActionCount, workflowEvents.ActionCompleteEvents.Count);
+ }
+
+ private static object GetInput(Testcase testcase) where TInput : notnull =>
+ testcase.Setup.Input.Type switch
+ {
+ nameof(ChatMessage) => new ChatMessage(ChatRole.User, testcase.Setup.Input.Value),
+ nameof(String) => testcase.Setup.Input.Value,
+ _ => throw new NotSupportedException($"Input type '{testcase.Setup.Input.Type}' is not supported."),
+ };
+
+ private static Testcase ReadTestcase(string testcaseFileName)
+ {
+ using Stream testcaseStream = File.Open(Path.Combine("Testcases", testcaseFileName), FileMode.Open);
+ Testcase? testcase = JsonSerializer.Deserialize(testcaseStream, s_jsonSerializerOptions);
+ Assert.NotNull(testcase);
+ return testcase;
+ }
+
+ private static IConfigurationRoot InitializeConfig() =>
+ new ConfigurationBuilder()
+ .AddUserSecrets(Assembly.GetExecutingAssembly())
+ .AddEnvironmentVariables()
+ .Build();
+
+ private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
+ WriteIndented = true,
+ };
+}
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs
new file mode 100644
index 0000000000..f88d4c1f35
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFactory.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Agents.Persistent;
+using Azure.Identity;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.AzureAI;
+using Shared.IntegrationTests;
+
+namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
+
+#pragma warning disable SKEXP0110 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+
+internal static class AgentFactory
+{
+ public static async Task> CreateAsync(string agentsDirectory, AzureAIConfiguration config, CancellationToken cancellationToken)
+ {
+ PersistentAgentsClient clientAgents = new(config.Endpoint, new AzureCliCredential());
+
+ IKernelBuilder kernelBuilder = Kernel.CreateBuilder();
+ kernelBuilder.Services.AddSingleton(clientAgents);
+ Kernel kernel = kernelBuilder.Build();
+
+ AzureAIAgentFactory factory = new();
+
+ Dictionary agentMap = [];
+
+ foreach (string file in Directory.GetFiles(agentsDirectory, "*.yaml"))
+ {
+ Debug.WriteLine($"TEST AGENT: Creating - {file}");
+ string agentText = File.ReadAllText(file);
+
+ Agent? agent = await factory.CreateAgentFromYamlAsync(agentText, new AgentCreationOptions() { Kernel = kernel }, configuration: null, cancellationToken);
+
+ Assert.NotNull(agent?.Name);
+
+ Debug.WriteLine($"TEST AGENT: {agent.Name} => {agent.Id}");
+ agentMap[agent.Name] = agent.Id;
+ }
+
+ return agentMap.ToImmutableDictionary();
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs
new file mode 100644
index 0000000000..fb38bd1279
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using Shared.IntegrationTests;
+
+namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
+
+public sealed class AgentFixture : IDisposable
+{
+ private static ImmutableDictionary? s_agentMap;
+
+ internal async Task> GetAgentsAsync(AzureAIConfiguration config, CancellationToken cancellationToken = default)
+ {
+ s_agentMap ??= await AgentFactory.CreateAsync("Agents", config, cancellationToken);
+
+ return s_agentMap;
+ }
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/TestOutputAdapter.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/TestOutputAdapter.cs
new file mode 100644
index 0000000000..5887c185ea
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/TestOutputAdapter.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Microsoft.Extensions.Logging;
+using Xunit.Abstractions;
+
+namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
+
+public sealed class TestOutputAdapter(ITestOutputHelper output) : TextWriter, ILogger, ILoggerFactory
+{
+ private readonly Stack _scopes = [];
+
+ public override Encoding Encoding { get; } = Encoding.UTF8;
+
+ public void AddProvider(ILoggerProvider provider) => throw new NotSupportedException();
+
+ public ILogger CreateLogger(string categoryName) => this;
+
+ public bool IsEnabled(LogLevel logLevel) => true;
+
+ public override void WriteLine(object? value = null) => this.SafeWrite($"{value}");
+
+ public override void WriteLine(string? format, params object?[] arg) => this.SafeWrite(string.Format(format ?? string.Empty, arg));
+
+ public override void WriteLine(string? value) => this.SafeWrite(value ?? string.Empty);
+
+ public override void Write(object? value = null) => this.SafeWrite($"{value}");
+
+ public override void Write(char[]? buffer) => this.SafeWrite(new string(buffer));
+
+ public IDisposable BeginScope(TState state) where TState : notnull
+ {
+ this._scopes.Push($"{state}");
+ return new LoggerScope(() => this._scopes.Pop());
+ }
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ string message = formatter(state, exception);
+ string scope = this._scopes.Count > 0 ? $"[{this._scopes.Peek()}] " : string.Empty;
+ output.WriteLine($"{scope}{message}");
+ }
+
+ private void SafeWrite(string value)
+ {
+ try
+ {
+ output.WriteLine(value ?? string.Empty);
+ }
+ catch (InvalidOperationException exception) when (exception.Message == "There is no currently active test.")
+ {
+ // This exception is thrown when the test output is accessed outside of a test context.
+ // We can ignore it since we are not in a test context.
+ }
+ }
+
+ private sealed class LoggerScope(Action action) : IDisposable
+ {
+ private bool _disposed;
+
+ public void Dispose()
+ {
+ if (!this._disposed)
+ {
+ action.Invoke();
+ this._disposed = true;
+ }
+ }
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/Testcase.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/Testcase.cs
new file mode 100644
index 0000000000..aa35ddf65c
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/Testcase.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
+
+public sealed class Testcase
+{
+ [JsonConstructor]
+ public Testcase(
+ string description,
+ TestcaseSetup setup,
+ TestcaseValidation validation)
+ {
+ this.Description = description;
+ this.Setup = setup;
+ this.Validation = validation;
+ }
+
+ public string Description { get; }
+
+ public TestcaseSetup Setup { get; }
+
+ public TestcaseValidation Validation { get; }
+}
+
+public sealed class TestcaseSetup
+{
+ [JsonConstructor]
+ public TestcaseSetup(TestcaseInput input)
+ {
+ this.Input = input;
+ }
+ public TestcaseInput Input { get; }
+}
+
+public sealed class TestcaseInput
+{
+ [JsonConstructor]
+ public TestcaseInput(string type, string value)
+ {
+ this.Type = type;
+ this.Value = value;
+ }
+
+ public string Type { get; }
+ public string Value { get; }
+}
+
+public sealed class TestcaseValidation
+{
+ [JsonConstructor]
+ public TestcaseValidation(int actionCount)
+ {
+ this.ActionCount = actionCount;
+ }
+
+ public int ActionCount { get; }
+}
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs
new file mode 100644
index 0000000000..163eb19fdf
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowEvents.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
+
+internal sealed class WorkflowEvents
+{
+ public WorkflowEvents(ImmutableList workflowEvents)
+ {
+ this.Events = workflowEvents;
+ this.EventCounts = workflowEvents.GroupBy(e => e.GetType()).ToImmutableDictionary(e => e.Key, e => e.Count());
+ this.ActionInvokeEvents = workflowEvents.OfType().ToImmutableList();
+ this.ActionCompleteEvents = workflowEvents.OfType().ToImmutableList();
+ }
+
+ public ImmutableList Events { get; }
+ public IImmutableDictionary EventCounts { get; }
+ public ImmutableList ActionInvokeEvents { get; }
+ public ImmutableList ActionCompleteEvents { get; private set; }
+}
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs
new file mode 100644
index 0000000000..c1b43a76e9
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowHarness.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
+
+internal static class WorkflowHarness
+{
+ public static async Task RunAsync(Workflow workflow, TInput input) where TInput : notnull
+ {
+ StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
+ ImmutableList workflowEvents = run.WatchStreamAsync().ToEnumerable().ToImmutableList();
+ return new WorkflowEvents(workflowEvents);
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs
new file mode 100644
index 0000000000..afc70f3603
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Microsoft.Bot.ObjectModel;
+using Xunit.Abstractions;
+
+namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework;
+
+///
+/// Base class for workflow tests.
+///
+public abstract class WorkflowTest : IDisposable
+{
+ public TestOutputAdapter Output { get; }
+
+ protected WorkflowTest(ITestOutputHelper output)
+ {
+ this.Output = new TestOutputAdapter(output);
+ Console.SetOut(this.Output);
+ }
+
+ public void Dispose()
+ {
+ this.Dispose(isDisposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool isDisposing)
+ {
+ if (isDisposing)
+ {
+ this.Output.Dispose();
+ }
+ }
+
+ internal static string FormatVariablePath(string variableName, string? scope = null) => $"{scope ?? VariableScopeNames.Topic}.{variableName}";
+}
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Microsoft.Agents.Workflows.Declarative.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Microsoft.Agents.Workflows.Declarative.IntegrationTests.csproj
new file mode 100644
index 0000000000..785992170e
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Microsoft.Agents.Workflows.Declarative.IntegrationTests.csproj
@@ -0,0 +1,37 @@
+
+
+
+ $(ProjectsTargetFrameworks)
+ $(NoWarn);IDE1006
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
+
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json
new file mode 100644
index 0000000000..a48cc30b79
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/InvokeAgent.json
@@ -0,0 +1,12 @@
+{
+ "description": "Produce a single response from an agent.",
+ "setup": {
+ "input": {
+ "type": "String",
+ "value": "Why is the sky blue?"
+ }
+ },
+ "validation": {
+ "action_count": 1
+ }
+}
\ No newline at end of file
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/SendActivity.json b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/SendActivity.json
new file mode 100644
index 0000000000..a0f6b74752
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Testcases/SendActivity.json
@@ -0,0 +1,12 @@
+{
+ "description": "Send an activity message .",
+ "setup": {
+ "input": {
+ "type": "String",
+ "value": "Why is the sky blue?"
+ }
+ },
+ "validation": {
+ "action_count": 3
+ }
+}
\ No newline at end of file
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/AddMessages.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/AddMessages.yaml
new file mode 100644
index 0000000000..5a754c9be3
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/AddMessages.yaml
@@ -0,0 +1,49 @@
+#
+# This workflow demonstrates a conversation and message manipulation.
+#
+# Any Foundry Agent may be used to provide the response.
+# See: ./setup/QuestionAgent.yaml
+#
+kind: AdaptiveDialog
+beginDialog:
+
+ kind: OnActivity
+ id: workflow_test
+ actions:
+
+ - kind: CreateConversation
+ id: conversation_create1
+ conversationId: Topic.FirstConversationId
+
+ - kind: CreateConversation
+ id: conversation_create2
+ conversationId: Topic.SecondConversationId
+
+ - kind: SendActivity
+ id: sendActivity_conversation
+ activity: |-
+ Conversation 1: {Topic.FirstConversationId}
+ Conversation 2: {Topic.SecondConversationId}
+
+ - kind: AddConversationMessage
+ id: add_message
+ message: Topic.MyMessage1
+ role: User
+ conversationId: =Topic.FirstConversationId
+ content:
+ - type: Text
+ value: {System.LastMessage.Text}
+
+ - kind: SendActivity
+ id: sendActivity_message
+ activity: |-
+ Messsage 1: {Topic.MyMessage1}
+
+ - kind: CopyConversationMessages
+ id: copy_messages
+ conversationId: =Topic.SecondConversationId
+ messages: =[Topic.MyMessage1]
+
+ - kind: SendActivity
+ id: sendActivity_copy
+ activity: Done!
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessage.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessage.yaml
new file mode 100644
index 0000000000..94e8ba4d73
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessage.yaml
@@ -0,0 +1,23 @@
+#
+# This workflow demonstrates a conversation and message manipulation.
+#
+# Any Foundry Agent may be used to provide the response.
+# See: ./setup/QuestionAgent.yaml
+#
+kind: AdaptiveDialog
+beginDialog:
+
+ kind: OnActivity
+ id: workflow_test
+ actions:
+
+ - kind: RetrieveConversationMessage
+ id: get_message
+ message: Topic.MyMessage
+ conversationId: thread_T8xIzNrNcPkUkoCEGzxg80Vt
+ messageId: msg_J4x6YZTDUUWNs60FOUAucldy
+
+ - kind: SendActivity
+ id: sendActivity_message
+ activity: |-
+ {Topic.MyMessage}
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessages.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessages.yaml
new file mode 100644
index 0000000000..faaabfb1d1
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/GetMessages.yaml
@@ -0,0 +1,22 @@
+#
+# This workflow demonstrates a conversation and message manipulation.
+#
+# Any Foundry Agent may be used to provide the response.
+# See: ./setup/QuestionAgent.yaml
+#
+kind: AdaptiveDialog
+beginDialog:
+
+ kind: OnActivity
+ id: workflow_test
+ actions:
+
+ - kind: RetrieveConversationMessages
+ id: get_message
+ messages: Topic.MyMessages
+ conversationId: thread_T8xIzNrNcPkUkoCEGzxg80Vt
+
+ - kind: SendActivity
+ id: sendActivity_message
+ activity: |-
+ {Topic.MyMessages}
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml
new file mode 100644
index 0000000000..ad9a2c3d57
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/InvokeAgent.yaml
@@ -0,0 +1,15 @@
+kind: AdaptiveDialog
+beginDialog:
+
+ kind: OnActivity
+ id: workflow_test
+ actions:
+
+ - kind: InvokeAzureAgent
+ id: invoke_agent
+ agent:
+ name: =Env.BasicAgent
+ input:
+ messages: =[UserMessage(System.LastMessageText)]
+ output:
+ messages: Topic.Answer
diff --git a/workflows/HelloWorld.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml
similarity index 86%
rename from workflows/HelloWorld.yaml
rename to dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml
index 8fc434ed60..9095fe9655 100644
--- a/workflows/HelloWorld.yaml
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml
@@ -7,14 +7,14 @@ kind: AdaptiveDialog
beginDialog:
kind: OnActivity
- id: workflow_demo
+ id: workflow_test
actions:
# Capture input
- kind: SetVariable
id: setvar_userinput
variable: Topic.UserInput
- value: =System.LastMessageText
+ value: =System.LastMessage.Text
# Capture environment variable
- kind: SetVariable
@@ -27,4 +27,4 @@ beginDialog:
id: sendActivity_demo
activity: |-
Hello {Global.UserName},
- You said, "{Topic.UserInput}"
\ No newline at end of file
+ You said, "{Topic.UserInput}"
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs
index fc7ee92cbf..ddfa6f089e 100644
--- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs
@@ -168,7 +168,6 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
[InlineData(typeof(InvokeSkillAction.Builder))]
[InlineData(typeof(LogCustomTelemetryEvent.Builder))]
[InlineData(typeof(OAuthInput.Builder))]
- [InlineData(typeof(Question.Builder))]
[InlineData(typeof(RecognizeIntent.Builder))]
[InlineData(typeof(RepeatDialog.Builder))]
[InlineData(typeof(ReplaceDialog.Builder))]
@@ -192,7 +191,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
BeginDialog =
new OnActivity.Builder()
{
- Id = "workflow",
+ Id = "anything",
Actions = [unsupportedAction]
}
};
@@ -226,7 +225,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
private void AssertMessage(string message)
{
- Assert.Contains(this.WorkflowEvents.OfType(), e => string.Equals(e.Response.Messages[0].Text.Trim(), message, StringComparison.Ordinal));
+ Assert.Contains(this.WorkflowEvents.OfType(), e => string.Equals(e.Message.Trim(), message, StringComparison.Ordinal));
}
private Task RunWorkflow(string workflowPath) => this.RunWorkflow(workflowPath, string.Empty);
@@ -246,7 +245,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
{
if (workflowEvent is ExecutorInvokedEvent invokeEvent)
{
- DeclarativeExecutorResult? message = invokeEvent.Data as DeclarativeExecutorResult;
+ ExecutorResultMessage? message = invokeEvent.Data as ExecutorResultMessage;
this.Output.WriteLine($"EXEC: {invokeEvent.ExecutorId} << {message?.ExecutorId ?? "?"} [{message?.Result ?? "-"}]");
}
else if (workflowEvent is AgentRunResponseEvent messageEvent)
@@ -258,7 +257,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
}
private sealed class RootExecutor() :
- ReflectingExecutor(WorkflowActionVisitor.RootId("workflow")),
+ ReflectingExecutor(WorkflowActionVisitor.Steps.Root("anything")),
IMessageHandler
{
public async ValueTask HandleAsync(string message, IWorkflowContext context)
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Extensions/FormulaValueExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Extensions/FormulaValueExtensionsTests.cs
index bf450b41a2..a2ac6295dc 100644
--- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Extensions/FormulaValueExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Extensions/FormulaValueExtensionsTests.cs
@@ -19,7 +19,7 @@ public class FormulaValueExtensionsTests
BooleanDataValue typedValue = Assert.IsType(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
- BooleanValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue());
+ BooleanValue formulaCopy = Assert.IsType(dataValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal(bool.TrueString, formulaValue.Format());
@@ -35,7 +35,7 @@ public class FormulaValueExtensionsTests
StringDataValue typedValue = Assert.IsType(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
- StringValue formulaCopy = Assert.IsType(typedValue.ToFormulaValue());
+ StringValue formulaCopy = Assert.IsType(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal(formulaValue.Value, formulaValue.Format());
@@ -51,7 +51,7 @@ public class FormulaValueExtensionsTests
NumberDataValue typedValue = Assert.IsType(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
- DecimalValue formulaCopy = Assert.IsType(typedValue.ToFormulaValue());
+ DecimalValue formulaCopy = Assert.IsType(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal("45.3", formulaValue.Format());
@@ -67,7 +67,7 @@ public class FormulaValueExtensionsTests
FloatDataValue typedValue = Assert.IsType(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
- NumberValue formulaCopy = Assert.IsType(typedValue.ToFormulaValue());
+ NumberValue formulaCopy = Assert.IsType(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal("3.1415926535897", formulaValue.Format());
@@ -103,7 +103,7 @@ public class FormulaValueExtensionsTests
DateDataValue typedValue = Assert.IsType(dataValue);
Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), typedValue.Value);
- DateValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue());
+ DateValue formulaCopy = Assert.IsType(dataValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc));
Assert.Equal($"{timestamp}", formulaValue.Format());
@@ -120,7 +120,7 @@ public class FormulaValueExtensionsTests
DateTimeDataValue typedValue = Assert.IsType(dataValue);
Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), typedValue.Value);
- DateTimeValue formulaCopy = Assert.IsType(typedValue.ToFormulaValue());
+ DateTimeValue formulaCopy = Assert.IsType(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc));
Assert.Equal($"{timestamp}", formulaValue.Format());
@@ -136,7 +136,7 @@ public class FormulaValueExtensionsTests
TimeDataValue typedValue = Assert.IsType(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
- TimeValue formulaCopy = Assert.IsType(typedValue.ToFormulaValue());
+ TimeValue formulaCopy = Assert.IsType(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal("10:35:00", formulaValue.Format());
@@ -158,7 +158,7 @@ public class FormulaValueExtensionsTests
Assert.Contains(property.Key, formulaValue.Fields.Select(field => field.Name));
}
- RecordValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue(), exactMatch: false);
+ RecordValue formulaCopy = Assert.IsType(dataValue.ToFormula(), exactMatch: false);
Assert.Equal(formulaCopy.Fields.Count(), dataValue.Properties.Count);
foreach (NamedValue field in formulaCopy.Fields)
{
@@ -174,6 +174,16 @@ public class FormulaValueExtensionsTests
}
""",
formulaValue.Format().Replace(Environment.NewLine, "\n"));
+
+ Dictionary source =
+ new()
+ {
+ ["FieldA"] = 1,
+ ["FieldB"] = 2,
+ ["FieldC"] = 3
+ };
+ FormulaValue formula = source.ToFormula();
+ Assert.IsType(formula, exactMatch: false);
}
[Fact]
@@ -188,7 +198,7 @@ public class FormulaValueExtensionsTests
TableDataValue dataValue = formulaValue.ToTable();
Assert.Equal(formulaValue.Rows.Count(), dataValue.Values.Length);
- TableValue formulaCopy = Assert.IsType(dataValue.ToFormulaValue(), exactMatch: false);
+ TableValue formulaCopy = Assert.IsType(dataValue.ToFormula(), exactMatch: false);
Assert.Equal(formulaCopy.Rows.Count(), dataValue.Values.Length);
Assert.Equal(
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs
index 4337ef2952..5ef54a47a3 100644
--- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs
@@ -27,7 +27,7 @@ public sealed class SendActivityExecutorTest(ITestOutputHelper output) : Workflo
// Assert
this.VerifyModel(model, action);
- Assert.Contains(events, e => e is AgentRunResponseEvent);
+ Assert.Contains(events, e => e is MessageActivityEvent);
}
private SendActivity CreateModel(string displayName, string activityMessage, string? summary = null)
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs
index 8d7393864b..b2149c1c92 100644
--- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs
+++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs
@@ -14,7 +14,7 @@ using Xunit.Abstractions;
namespace Microsoft.Agents.Workflows.Declarative.UnitTests.ObjectModel;
///
-/// Base test class for implementations.
+/// Base test class for implementations.
///
public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : WorkflowTest(output)
{
@@ -26,7 +26,7 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor
protected string FormatDisplayName(string name) => $"{this.GetType().Name}_{name}";
- internal async Task Execute(WorkflowActionExecutor executor)
+ internal async Task Execute(DeclarativeActionExecutor executor)
{
TestWorkflowExecutor workflowExecutor = new();
WorkflowBuilder workflowBuilder = new(workflowExecutor);
@@ -38,7 +38,7 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor
return events;
}
- internal void VerifyModel(DialogAction model, WorkflowActionExecutor action)
+ internal void VerifyModel(DialogAction model, DeclarativeActionExecutor action)
{
Assert.Equal(model.Id, action.Id);
Assert.Equal(model, action.Model);
@@ -80,7 +80,7 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor
{
public async ValueTask HandleAsync(WorkflowScopes message, IWorkflowContext context)
{
- await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false);
+ await context.SendMessageAsync(new ExecutorResultMessage(this.Id)).ConfigureAwait(false);
}
}
}
diff --git a/workflows/DeepResearch.yaml b/workflows/DeepResearch.yaml
index d9482d5f6f..9612cc8464 100644
--- a/workflows/DeepResearch.yaml
+++ b/workflows/DeepResearch.yaml
@@ -81,54 +81,64 @@ beginDialog:
id: sendActivity_yFsbRy
activity: Analyzing facts...
- - kind: AnswerQuestionWithAI
+ - kind: CreateConversation
+ id: conversation_1a2b3c
+ conversationId: Topic.InternalConversationId
+
+ - kind: InvokeAzureAgent
id: question_UDoMUw
displayName: Get Facts
- autoSend: false
- variable: Topic.TaskFacts
- userInput: =Topic.InputTask
- additionalInstructions: |-
- {Env.FOUNDRY_AGENT_RESEARCHANALYST},
- In order to help begin addressing the user request, please answer the following pre-survey to the best of your ability.
- Keep in mind that you are Ken Jennings-level with trivia, and Mensa-level with puzzles, so there should be a deep well to draw from.
+ conversationId: =Topic.InternalConversationId
+ agent:
+ name: =Env.FOUNDRY_AGENT_RESEARCHANALYST
+ output:
+ messages: Topic.TaskFacts
+ input:
+ messages: =[UserMessage(Topic.InputTask)]
+ additionalInstructions: |-
+ In order to help begin addressing the user request, please answer the following pre-survey to the best of your ability.
+ Keep in mind that you are Ken Jennings-level with trivia, and Mensa-level with puzzles, so there should be a deep well to draw from.
- Here is the pre-survey:
+ Here is the pre-survey:
- 1. Please list any specific facts or figures that are GIVEN in the request itself. It is possible that there are none.
- 2. Please list any facts that may need to be looked up, and WHERE SPECIFICALLY they might be found. In some cases, authoritative sources are mentioned in the request itself.
- 3. Please list any facts that may need to be derived (e.g., via logical deduction, simulation, or computation)
- 4. Please list any facts that are recalled from memory, hunches, well-reasoned guesses, etc.
+ 1. Please list any specific facts or figures that are GIVEN in the request itself. It is possible that there are none.
+ 2. Please list any facts that may need to be looked up, and WHERE SPECIFICALLY they might be found. In some cases, authoritative sources are mentioned in the request itself.
+ 3. Please list any facts that may need to be derived (e.g., via logical deduction, simulation, or computation)
+ 4. Please list any facts that are recalled from memory, hunches, well-reasoned guesses, etc.
- When answering this survey, keep in mind that 'facts' will typically be specific names, dates, statistics, etc. Your answer must only use the headings:
+ When answering this survey, keep in mind that 'facts' will typically be specific names, dates, statistics, etc. Your answer must only use the headings:
- 1. GIVEN OR VERIFIED FACTS
- 2. FACTS TO LOOK UP
- 3. FACTS TO DERIVE
- 4. EDUCATED GUESSES
+ 1. GIVEN OR VERIFIED FACTS
+ 2. FACTS TO LOOK UP
+ 3. FACTS TO DERIVE
+ 4. EDUCATED GUESSES
- DO NOT include any other headings or sections in your response. DO NOT list next steps or plans until asked to do so.
+ DO NOT include any other headings or sections in your response. DO NOT list next steps or plans until asked to do so.
- kind: SendActivity
id: sendActivity_yFsbRz
activity: Creating a plan...
- - kind: AnswerQuestionWithAI
+ - kind: InvokeAzureAgent
id: question_DsBaJU
displayName: Create a Plan
- autoSend: false
- variable: Topic.Plan
- userInput: =""
- additionalInstructions: |-
- {Env.FOUNDRY_AGENT_RESEARCHMANAGER},
- Your only job is to devise an efficient plan that identifies (by name) how a team member may contribute to addressing the user request.
+ conversationId: =Topic.InternalConversationId
+ agent:
+ name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER
+ output:
+ messages: Topic.Plan
+ input:
+ messages: =[UserMessage(Topic.InputTask)]
+ additionalInstructions: |-
+ Your only job is to devise an efficient plan that identifies (by name) how a team member may contribute to addressing the user request.
- Only select the following team which is listed as "- [Name]: [Description]"
+ Only select the following team which is listed as "- [Name]: [Description]"
- {Topic.TeamDescription}
+ {Topic.TeamDescription}
- The plan must be a bullet point list must be in the form "- [AgentName]: [Specific action or task for that agent to perform]"
+ The plan must be a bullet point list must be in the form "- [AgentName]: [Specific action or task for that agent to perform]"
- Remember, there is no requirement to involve the entire team -- only select team member's whose particular expertise is required for this task.
+ Remember, there is no requirement to involve the entire team -- only select team member's whose particular expertise is required for this task.
- kind: SetVariable
id: setVariable_Kk2LDL
@@ -162,54 +172,57 @@ beginDialog:
id: sendActivity_bwNZiM
activity: {Topic.TaskInstructions}
- - kind: AnswerQuestionWithAI
+ - kind: InvokeAzureAgent
id: question_o3BQkf
displayName: Progress Ledger Prompt
- autoSend: false
- variable: Topic.ProgressLedgerUpdate
- userInput: =Topic.AgentResponseText
- additionalInstructions: |-
- {Env.FOUNDRY_AGENT_RESEARCHMANAGER},
- Recall we are working on the following request:
+ conversationId: =Topic.InternalConversationId
+ agent:
+ name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER
+ output:
+ messages: Topic.ProgressLedgerUpdate
+ input:
+ messages: =[UserMessage(Topic.AgentResponseText)]
+ additionalInstructions: |-
+ Recall we are working on the following request:
- {Topic.InputTask}
+ {Topic.InputTask}
- And we have assembled the following team:
+ And we have assembled the following team:
- {Topic.TeamDescription}
+ {Topic.TeamDescription}
- To make progress on the request, please answer the following questions, including necessary reasoning:
+ To make progress on the request, please answer the following questions, including necessary reasoning:
- - Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed)
- - Are we in a loop where we are repeating the same requests and / or getting the same responses from an agent multiple times? Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times.
- - Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success such as the inability to read from a required file)
- - Who should speak next? (select from: {Concat(Topic.AvailableAgents, name, ",")})
- - What instruction or question would you give this team member? (Phrase as if speaking directly to them, and include any specific information they may need)
+ - Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed)
+ - Are we in a loop where we are repeating the same requests and / or getting the same responses from an agent multiple times? Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times.
+ - Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success such as the inability to read from a required file)
+ - Who should speak next? (select from: {Concat(Topic.AvailableAgents, name, ",")})
+ - What instruction or question would you give this team member? (Phrase as if speaking directly to them, and include any specific information they may need)
- Please output an answer in pure JSON format according to the following schema. The JSON object must be parsable as-is. DO NOT OUTPUT ANYTHING OTHER THAN JSON, AND DO NOT DEVIATE FROM THIS SCHEMA:
+ Please output an answer in pure JSON format according to the following schema. The JSON object must be parsable as-is. DO NOT OUTPUT ANYTHING OTHER THAN JSON, AND DO NOT DEVIATE FROM THIS SCHEMA:
- {{
- "is_request_satisfied": {{
- "reason": string,
- "answer": boolean
- }},
- "is_in_loop": {{
- "reason": string,
- "answer": boolean
- }},
- "is_progress_being_made": {{
- "reason": string,
- "answer": boolean
- }},
- "next_speaker": {{
- "reason": string,
- "answer": string (select from: {Concat(Topic.AvailableAgents, name, ",")})
- }},
- "instruction_or_question": {{
- "reason": string,
- "answer": string
- }}
- }}
+ {{
+ "is_request_satisfied": {{
+ "reason": string,
+ "answer": boolean
+ }},
+ "is_in_loop": {{
+ "reason": string,
+ "answer": boolean
+ }},
+ "is_progress_being_made": {{
+ "reason": string,
+ "answer": boolean
+ }},
+ "next_speaker": {{
+ "reason": string,
+ "answer": string (select from: {Concat(Topic.AvailableAgents, name, ",")})
+ }},
+ "instruction_or_question": {{
+ "reason": string,
+ "answer": string
+ }}
+ }}
- kind: ParseValue
id: parse_rNZtlV
@@ -266,16 +279,20 @@ beginDialog:
id: sendActivity_kdl3mC
activity: Completed! {Topic.TypedProgressLedger.is_request_satisfied.reason}
- - kind: AnswerQuestionWithAI
+ - kind: InvokeAzureAgent
id: question_Ke3l1d
displayName: Generate Response
- variable: Topic.FinalResponse
- userInput: =Topic.SeedTask
- additionalInstructions: |-
- {Env.FOUNDRY_AGENT_RESEARCHMANAGER},
- We have completed the task.
- Based only on the conversation and without adding any new information, synthesize the result of the conversation as a complete response to the user task.
- The user will only every see this last response and not the entire conversation, so please ensure it is complete and self-contained.
+ conversationId: =System.ConversationId
+ agent:
+ name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER
+ output:
+ messages: Topic.FinalResponse
+ input:
+ messages: =[UserMessage(Topic.SeedTask)]
+ additionalInstructions: |-
+ We have completed the task.
+ Based only on the conversation and without adding any new information, synthesize the result of the conversation as a complete response to the user task.
+ The user will only every see this last response and not the entire conversation, so please ensure it is complete and self-contained.
- kind: EndConversation
id: end_SVoNSV
@@ -342,45 +359,53 @@ beginDialog:
id: sendActivity_cwNZiM
activity: Re-analyzing facts...
- - kind: AnswerQuestionWithAI
+ - kind: InvokeAzureAgent
id: question_wFJ123
displayName: Get New Facts Prompt
- autoSend: false
- variable: Topic.TaskFacts
- userInput: |-
- ="As a reminder, we are working to solve the following task:
+ conversationId: =Topic.InternalConversationId
+ agent:
+ name: =Env.FOUNDRY_AGENT_RESEARCHANALYST
+ output:
+ messages: Topic.TaskFacts
+ input:
+ messages: |-
+ =[
+ UserMessage(
+ "As a reminder, we are working to solve the following task:
- " & Topic.InputTask
- additionalInstructions: |-
- {Env.FOUNDRY_AGENT_RESEARCHANALYST},
- It's clear we aren't making as much progress as we would like, but we may have learned something new.
- Please rewrite the following fact sheet, updating it to include anything new we have learned that may be helpful.
- Example edits can include (but are not limited to) adding new guesses, moving educated guesses to verified facts if appropriate, etc.
- Updates may be made to any section of the fact sheet, and more than one section of the fact sheet can be edited.
- This is an especially good time to update educated guesses, so please at least add or update one educated guess or hunch, and explain your reasoning.
+ " & Topic.InputTask)
+ ]
+ additionalInstructions: |-
+ It's clear we aren't making as much progress as we would like, but we may have learned something new.
+ Please rewrite the following fact sheet, updating it to include anything new we have learned that may be helpful.
+ Example edits can include (but are not limited to) adding new guesses, moving educated guesses to verified facts if appropriate, etc.
+ Updates may be made to any section of the fact sheet, and more than one section of the fact sheet can be edited.
+ This is an especially good time to update educated guesses, so please at least add or update one educated guess or hunch, and explain your reasoning.
- Here is the old fact sheet:
+ Here is the old fact sheet:
- {Topic.TaskFacts}
+ {Topic.TaskFacts}
- kind: SendActivity
id: sendActivity_dsBaJU
activity: Re-analyzing plan...
-
- - kind: AnswerQuestionWithAI
+
+ - kind: InvokeAzureAgent
id: question_uEJ456
displayName: Create new Plan Prompt
- autoSend: false
- variable: Topic.Plan
- userInput: =""
- additionalInstructions: |-
- {Env.FOUNDRY_AGENT_RESEARCHMANAGER},
- Please briefly explain what went wrong on this last run (the root cause of the failure),
- and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes.
- As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition
- (do not involve any other outside people since we cannot contact anyone else):
+ conversationId: =Topic.InternalConversationId
+ agent:
+ name: =Env.FOUNDRY_AGENT_RESEARCHMANAGER
+ output:
+ messages: Topic.Plan
+ input:
+ additionalInstructions: |-
+ Please briefly explain what went wrong on this last run (the root cause of the failure),
+ and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes.
+ As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition
+ (do not involve any other outside people since we cannot contact anyone else):
- {Topic.TeamDescription}
+ {Topic.TeamDescription}
- kind: SetVariable
id: setVariable_jW7tmM
@@ -452,13 +477,18 @@ beginDialog:
displayName: If next Agent tool Exists
actions:
- - kind: AnswerQuestionWithAI
+ - kind: InvokeAzureAgent
id: question_orsBf06
- variable: Topic.AgentResponse
- userInput: =Topic.SeedTask
- additionalInstructions: |-
- {First(Topic.NextSpeaker).agentid},
- {Topic.TypedProgressLedger.instruction_or_question.answer}
+ displayName: Progress Ledger Prompt
+ conversationId: =System.ConversationId
+ agent:
+ name: =First(Topic.NextSpeaker).agentid
+ output:
+ messages: Topic.AgentResponse
+ input:
+ messages: =[UserMessage(Topic.SeedTask)]
+ additionalInstructions: |-
+ {Topic.TypedProgressLedger.instruction_or_question.answer}
- kind: SetVariable
id: setVariable_XzNrdM
diff --git a/workflows/HumanInLoop.yaml b/workflows/HumanInLoop.yaml
new file mode 100644
index 0000000000..997dc558df
--- /dev/null
+++ b/workflows/HumanInLoop.yaml
@@ -0,0 +1,44 @@
+#
+# This workflow demonstrates a single agent interaction based on user input.
+#
+# Any Foundry Agent may be used to provide the response.
+# See: ./setup/QuestionAgent.yaml
+#
+kind: AdaptiveDialog
+beginDialog:
+
+ kind: OnActivity
+ id: workflow_demo
+ actions:
+
+ # Capture original input
+ - kind: SetVariable
+ id: set_project
+ variable: Topic.OriginalInput
+ value: =System.LastMessage.Text
+
+ # Request input from user
+ - kind: Question
+ id: question_confirm
+ alwaysPrompt: false
+ property: Topic.ConfirmedInput
+ prompt:
+ kind: Message
+ text:
+ - "CONFIRM:"
+ entity:
+ kind: StringPrebuiltEntity
+
+ # Respond with input
+ - kind: SendActivity
+ id: sendActivity_input
+ activity: |-
+ You entered:
+ {Topic.OriginalInput}
+
+ Confirmed input:
+ {Topic.ConfirmedInput}
+
+
+
+
diff --git a/workflows/Marketing.yaml b/workflows/Marketing.yaml
new file mode 100644
index 0000000000..6293a3b616
--- /dev/null
+++ b/workflows/Marketing.yaml
@@ -0,0 +1,46 @@
+#
+# This workflow demonstrates a single agent interaction based on user input.
+#
+# Any Foundry Agent may be used to provide the response.
+# See: ./setup/QuestionAgent.yaml
+#
+kind: AdaptiveDialog
+beginDialog:
+
+ kind: OnActivity
+ id: workflow_demo
+ actions:
+
+ - 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
+ - Target audience
+ - Unique selling points
+
+ - kind: InvokeAzureAgent
+ id: invoke_writer
+ conversationId: =System.ConversationId
+ agent:
+ name: =Env.FOUNDRY_AGENT_ANSWER
+ input:
+ additionalInstructions: |-
+ You are a marketing copywriter. Given a block of text describing features, audience, and USPs,
+ compose a compelling marketing copy (like a newsletter section) that highlights these points.
+ Output should be short (around 150 words), output just the copy as a single text block.
+
+ - kind: InvokeAzureAgent
+ id: invoke_editor
+ conversationId: =System.ConversationId
+ agent:
+ name: =Env.FOUNDRY_AGENT_ANSWER
+ input:
+ additionalInstructions: |-
+ You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone,
+ give format and make it polished. Output the final improved copy as a single text block.
\ No newline at end of file
diff --git a/workflows/MathChat.yaml b/workflows/MathChat.yaml
index 117a241d02..3ddb611e21 100644
--- a/workflows/MathChat.yaml
+++ b/workflows/MathChat.yaml
@@ -34,21 +34,27 @@ beginDialog:
- kind: SetVariable
id: set_project
variable: Topic.Project
- value: =System.LastMessage.Text
+ value: =System.LastMessageText
- - kind: AnswerQuestionWithAI
+ - kind: InvokeAzureAgent
id: question_student
- userInput: =Topic.Project
- additionalInstructions: {Env.FOUNDRY_AGENT_STUDENT}
+ conversationId: =System.ConversationId
+ agent:
+ name: =Env.FOUNDRY_AGENT_STUDENT
+ input:
+ messages: =[UserMessage(Topic.Project)]
+ output:
+ messages: Topic.Answer
- kind: ResetVariable
id: reset_project
variable: Topic.Project
- - kind: AnswerQuestionWithAI
+ - kind: InvokeAzureAgent
id: question_teacher
- userInput: =""
- additionalInstructions: {Env.FOUNDRY_AGENT_TEACHER}
+ conversationId: =System.ConversationId
+ agent:
+ name: =Env.FOUNDRY_AGENT_TEACHER
- kind: SetVariable
id: set_count_increment
@@ -59,7 +65,7 @@ beginDialog:
id: check_completion
conditions:
- - condition: =!IsBlank(Find("congratulations", Lower(System.LastMessage.Text)))
+ - condition: =!IsBlank(Find("congratulations", Lower(System.LastMessageText)))
id: check_turn_done
actions:
diff --git a/workflows/Question.yaml b/workflows/Question.yaml
deleted file mode 100644
index 22a9d7b6d4..0000000000
--- a/workflows/Question.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-#
-# This workflow demonstrates a single agent interaction based on user input.
-#
-# Any Foundry Agent may be used to provide the response.
-# See: ./setup/QuestionAgent.yaml
-#
-kind: AdaptiveDialog
-beginDialog:
-
- kind: OnActivity
- id: workflow_demo
- actions:
-
- # Respond with input
- - kind: SendActivity
- id: sendActivity_demo
- activity: "Working..."
-
- # Use AI to answer the question
- - kind: AnswerQuestionWithAI
- id: question_demo
- variable: Topic.Answer
- userInput: =System.LastMessage.Text
- additionalInstructions: {Env.FOUNDRY_AGENT_ANSWER}