diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props
index 7b4dc5155b..a1bb7cf599 100644
--- a/dotnet/Directory.Build.props
+++ b/dotnet/Directory.Build.props
@@ -6,7 +6,7 @@
AllEnabledByDefault
latest
true
- 12
+ 13
enable
disable
$(NoWarn);IDE0290;IDE0079;NU5128;CS8002
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 907a58ece8..cc9ddcbdf4 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -37,6 +37,7 @@
+
@@ -86,14 +87,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/AgentOpenTelemetry/AgentOpenTelemetry.csproj b/dotnet/samples/GettingStarted/AgentOpenTelemetry/AgentOpenTelemetry.csproj
index d552be2c50..a58fa132d2 100644
--- a/dotnet/samples/GettingStarted/AgentOpenTelemetry/AgentOpenTelemetry.csproj
+++ b/dotnet/samples/GettingStarted/AgentOpenTelemetry/AgentOpenTelemetry.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
enable
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj
index fd2e0b7b80..1604ac6c73 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureFoundry/Agent_With_AzureFoundry.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureFoundry/Agent_With_AzureFoundry.csproj
index 9da22b3573..b93b40b7dc 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureFoundry/Agent_With_AzureFoundry.csproj
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureFoundry/Agent_With_AzureFoundry.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj
index 3c80c1527e..68b0f8d36b 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj
index 3c80c1527e..68b0f8d36b 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj
index 4fa7edf3fc..f46e9bd63c 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj
index 69320f79c0..6c4004765f 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj
index 44603cc24c..5eee1d6e72 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj
index bbce14eb4b..27d3e4077f 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj
index bbce14eb4b..27d3e4077f 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj
index 718aa6650b..9d84e4babb 100644
--- a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj
+++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step01_Running/Agent_Step01_Running.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step01_Running/Agent_Step01_Running.csproj
index 3c80c1527e..68b0f8d36b 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step01_Running/Agent_Step01_Running.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step01_Running/Agent_Step01_Running.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Agent_Step02_MultiturnConversation.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Agent_Step02_MultiturnConversation.csproj
index 3c80c1527e..68b0f8d36b 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Agent_Step02_MultiturnConversation.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Agent_Step02_MultiturnConversation.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj
index 3c80c1527e..68b0f8d36b 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj
index d83018dd29..e6ffdc2794 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj
index 3c80c1527e..68b0f8d36b 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj
index 3c80c1527e..68b0f8d36b 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Agent_Step07_3rdPartyThreadStorage.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Agent_Step07_3rdPartyThreadStorage.csproj
index 68c0002c72..b027b80775 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Agent_Step07_3rdPartyThreadStorage.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Agent_Step07_3rdPartyThreadStorage.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step08_Telemetry/Agent_Step08_Telemetry.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step08_Telemetry/Agent_Step08_Telemetry.csproj
index 96e2dfb566..fe7185c7ee 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step08_Telemetry/Agent_Step08_Telemetry.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step08_Telemetry/Agent_Step08_Telemetry.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj
index eaf68b3e73..c9ff416496 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj
index cdb063750c..009d39f4cf 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj
index a438862616..017711102f 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj
index 84ef628db4..3261ff1ebf 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj b/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj
index 759eaae572..dce21d16a9 100644
--- a/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj
+++ b/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj b/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj
index d26676a576..e410b2e29a 100644
--- a/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj
+++ b/dotnet/samples/GettingStarted/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj
index b1f90ee600..267e25180e 100644
--- a/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj
index 564e214dac..eac3c76d93 100644
--- a/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj
index b1f90ee600..267e25180e 100644
--- a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj
index bbee070910..897d1d4fd4 100644
--- a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/CheckpointAndRehydrate.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj
index 1025cb45c9..bcca8af014 100644
--- a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/CheckpointAndResume.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj
index bbee070910..897d1d4fd4 100644
--- a/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/CheckpointWithHumanInTheLoop.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent.csproj b/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent.csproj
index a9b76d86ce..694e8a1ea8 100644
--- a/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj
index dcee9dcd7f..492fe95fa6 100644
--- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj
index dcee9dcd7f..492fe95fa6 100644
--- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj
index dcee9dcd7f..492fe95fa6 100644
--- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
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/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj b/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj
index bbee070910..897d1d4fd4 100644
--- a/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/Loop/Loop.csproj b/dotnet/samples/GettingStarted/Workflows/Loop/Loop.csproj
index 75b48d7b65..7a4ee108ed 100644
--- a/dotnet/samples/GettingStarted/Workflows/Loop/Loop.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Loop/Loop.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj b/dotnet/samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj
index d96f028c4b..e21730816c 100644
--- a/dotnet/samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/SharedStates/SharedStates.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj
index bbee070910..897d1d4fd4 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj
index bbee070910..897d1d4fd4 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/02_Streaming/02_Streaming.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj
index b1f90ee600..267e25180e 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj
@@ -3,7 +3,6 @@
Exe
net9.0
- 12
enable
disable
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..2225921310 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
@@ -2,7 +2,6 @@
using System;
using System.IO;
-using System.Linq;
using Microsoft.Agents.Workflows.Declarative.Extensions;
using Microsoft.Agents.Workflows.Declarative.Interpreter;
using Microsoft.Agents.Workflows.Declarative.PowerFx;
@@ -22,7 +21,7 @@ public static class DeclarativeWorkflowBuilder
///
/// The type of the input message
/// The path to the workflow.
- /// The execution context for the workflow.
+ /// Configuration options for workflow execution.
/// An optional function to transform the input message into a .
///
public static Workflow Build(
@@ -34,12 +33,13 @@ 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.
///
/// The type of the input message
/// The reader that provides the workflow object model YAML.
- /// The execution context for the workflow.
+ /// Configuration options for workflow execution.
/// An optional function to transform the input message into a .
/// The that corresponds with the YAML object model.
public static Workflow Build(
@@ -56,20 +56,20 @@ 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);
- DeclarativeWorkflowState state = new(options.CreateRecalcEngine(), scopes);
+ WorkflowFormulaState state = new(options.CreateRecalcEngine());
+ state.Initialize(workflowElement.WrapWithBot(), options.Configuration);
DeclarativeWorkflowExecutor rootExecutor =
new(rootId,
state,
message => inputTransform?.Invoke(message) ?? DefaultTransform(message));
WorkflowActionVisitor visitor = new(rootExecutor, state, options);
- WorkflowElementWalker walker = new(rootElement, visitor);
+ WorkflowElementWalker walker = new(visitor);
+ walker.Visit(rootElement);
- return walker.GetWorkflow();
+ return visitor.Complete();
}
private static ChatMessage DefaultTransform(object message) =>
@@ -79,23 +79,4 @@ public static class DeclarativeWorkflowBuilder
string stringMessage => new ChatMessage(ChatRole.User, stringMessage),
_ => new(ChatRole.User, $"{message}")
};
-
- // Wrap with bot to ensure schema is set.
- private static AdaptiveDialog WrapWithBot(AdaptiveDialog dialog)
- {
- BotDefinition bot
- = new BotDefinition.Builder
- {
- Components =
- {
- new DialogComponent.Builder
- {
- SchemaName = dialog.HasSchemaName ? dialog.SchemaName : "default-schema",
- Dialog = new AdaptiveDialog.Builder(dialog),
- }
- }
- }.Build();
-
- return bot.Descendants().OfType().First();
- }
}
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..34e7efdb80 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, FormulaValue.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 => FormulaValue.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..7fa82066e1 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
+using System.Dynamic;
using System.Linq;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
@@ -9,11 +10,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 +54,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.ToObject()).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)
{
@@ -70,6 +89,19 @@ internal static class DataValueExtensions
return recordType;
}
+ public static ExpandoObject ToObject(this RecordDataValue recordDataValue)
+ {
+ ExpandoObject expandoObject = new();
+
+ IDictionary dictionary = expandoObject;
+ foreach (KeyValuePair field in recordDataValue.Properties)
+ {
+ dictionary[field.Key] = field.Value?.ToObject();
+ }
+
+ return expandoObject;
+ }
+
private static RecordType ParseRecordType(this RecordDataValue record)
{
RecordType recordType = RecordType.Empty();
@@ -79,4 +111,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/DialogBaseExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DialogBaseExtensions.cs
new file mode 100644
index 0000000000..97b07068e7
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DialogBaseExtensions.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Linq;
+using Microsoft.Bot.ObjectModel;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class DialogBaseExtensions
+{
+ public static TDialog WrapWithBot(this TDialog dialog) where TDialog : DialogBase
+ {
+ BotDefinition bot
+ = new BotDefinition.Builder
+ {
+ Components =
+ {
+ new DialogComponent.Builder
+ {
+ SchemaName = dialog.HasSchemaName ? dialog.SchemaName : "default-schema",
+ Dialog = dialog.ToBuilder(),
+ }
+ }
+ }.Build();
+
+ return bot.Descendants().OfType().First();
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
index 568f3ba104..f815486686 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
@@ -8,6 +8,7 @@ using System.Dynamic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
+using Microsoft.Agents.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using BlankType = Microsoft.PowerFx.Types.BlankType;
@@ -20,10 +21,11 @@ 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(),
+ UnassignedValue => FormulaValue.NewBlank(),
FormulaValue formulaValue => formulaValue,
bool booleanValue => FormulaValue.New(booleanValue),
int decimalValue => FormulaValue.New(decimalValue),
@@ -35,8 +37,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}"),
};
@@ -141,7 +144,20 @@ internal static class FormulaValueExtensions
TableDataValue.TableFromRecords(value.Rows.Select(row => row.Value.ToRecord()).ToImmutableArray());
public static RecordDataValue ToRecord(this RecordValue value) =>
- RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()).ToImmutableArray());
+ RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()));
+
+ 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)
{
@@ -176,48 +192,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..aa6fc18a98
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows.Declarative.Interpreter;
+using Microsoft.Agents.Workflows.Declarative.PowerFx;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.Shared.Diagnostics;
+
+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));
+
+ public static ValueTask QueueStateUpdateAsync(this IWorkflowContext context, PropertyPath variablePath, TValue? value) =>
+ context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.VariableScopeName));
+
+ public static async Task GetStateAsync(this IWorkflowContext context, CancellationToken cancellationToken)
+ {
+ if (context is DeclarativeWorkflowContext declarativeContext)
+ {
+ return declarativeContext.State;
+ }
+
+ WorkflowFormulaState state = new(RecalcEngineFactory.Create());
+
+ await state.RestoreAsync(context, cancellationToken).ConfigureAwait(false);
+
+ return state;
+ }
+}
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..44e845e63c 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
@@ -1,12 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
+using System.Collections.Frozen;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows.Declarative.Extensions;
+using Microsoft.Agents.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -15,29 +15,24 @@ 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)
+internal abstract class DeclarativeActionExecutor(TAction model, WorkflowFormulaState 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
- {
- VariableScopeNames.Topic,
- VariableScopeNames.Global,
- }.ToImmutableHashSet();
+ private static readonly FrozenSet s_mutableScopes =
+ [
+ VariableScopeNames.Topic,
+ VariableScopeNames.Global
+ ];
private string? _parentId;
- protected WorkflowActionExecutor(DialogAction model, DeclarativeWorkflowState state)
+ protected DeclarativeActionExecutor(DialogAction model, WorkflowFormulaState state)
: base(model.Id.Value)
{
if (!model.HasRequiredProperties)
@@ -51,16 +46,18 @@ 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 WorkflowFormulaState 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 +65,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}");
}
- await this.State.SetAsync(targetPath, result, context).ConfigureAwait(false);
+ await context.QueueStateUpdateAsync(targetPath, result).ConfigureAwait(false);
#if DEBUG
string? resultValue = result.Format();
@@ -114,7 +126,7 @@ 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/DeclarativeWorkflowContext.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs
new file mode 100644
index 0000000000..6c55282e22
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows.Declarative.Extensions;
+using Microsoft.Agents.Workflows.Declarative.PowerFx;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
+
+internal sealed class DeclarativeWorkflowContext : IWorkflowContext
+{
+ public DeclarativeWorkflowContext(IWorkflowContext source, WorkflowFormulaState state)
+ {
+ this.Source = source;
+ this.State = state;
+ }
+
+ private IWorkflowContext Source { get; }
+ public WorkflowFormulaState State { get; }
+
+ ///
+ public ValueTask AddEventAsync(WorkflowEvent workflowEvent) => this.Source.AddEventAsync(workflowEvent);
+
+ ///
+ public ValueTask QueueClearScopeAsync(string? scopeName = null)
+ {
+ this.State.ResetAll(scopeName);
+ return this.Source.QueueClearScopeAsync(scopeName);
+ }
+
+ ///
+ public async ValueTask QueueStateUpdateAsync(string key, T? value, string? scopeName = null)
+ {
+ ValueTask task = value switch
+ {
+ null => QueueEmptyStateAsync(),
+ FormulaValue formulaValue => QueueFormulaStateAsync(formulaValue),
+ DataValue dataValue => QueueDataValueStateAsync(dataValue),
+ _ => QueueNativeStateAsync(value),
+ };
+
+ await task.ConfigureAwait(false);
+
+ ValueTask QueueEmptyStateAsync()
+ {
+ this.State.Set(key, FormulaValue.NewBlank(), scopeName);
+ return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName);
+ }
+
+ ValueTask QueueFormulaStateAsync(FormulaValue formulaValue)
+ {
+ this.State.Set(key, formulaValue, scopeName);
+ return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
+ }
+
+ ValueTask QueueDataValueStateAsync(DataValue dataValue)
+ {
+ FormulaValue formulaValue = dataValue.ToFormula();
+ this.State.Set(key, formulaValue, scopeName);
+ return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
+ }
+
+ ValueTask QueueNativeStateAsync(object? rawValue)
+ {
+ FormulaValue formulaValue = rawValue.ToFormula();
+ this.State.Set(key, formulaValue, scopeName);
+ return this.Source.QueueStateUpdateAsync(key, rawValue, scopeName);
+ }
+ }
+
+ ///
+ public ValueTask ReadStateAsync(string key, string? scopeName = null) => this.Source.ReadStateAsync(key, scopeName);
+
+ ///
+ public ValueTask> ReadStateKeysAsync(string? scopeName = null) => this.Source.ReadStateKeysAsync(scopeName);
+
+ ///
+ public ValueTask SendMessageAsync(object message, string? targetId = null) => this.Source.SendMessageAsync(message, targetId);
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs
index f65bb7f5f6..a912258157 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowExecutor.cs
@@ -12,16 +12,19 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
///
internal sealed class DeclarativeWorkflowExecutor(
string workflowId,
- DeclarativeWorkflowState state,
+ WorkflowFormulaState state,
Func inputTransform) :
Executor(workflowId)
where TInput : notnull
{
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context)
{
- ChatMessage input = inputTransform.Invoke(message);
- await state.SetLastMessageAsync(context, input).ConfigureAwait(false);
+ // No state to restore if we're starting from the beginning.
+ state.SetInitialized();
- await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id)).ConfigureAwait(false);
+ ChatMessage input = inputTransform.Invoke(message);
+ state.SetLastMessage(input);
+
+ 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..b78c935020 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowModel.cs
@@ -2,8 +2,8 @@
using System;
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Diagnostics;
+using System.Linq;
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
@@ -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);
}
@@ -77,7 +89,8 @@ internal sealed class DeclarativeWorkflowModel
public void ConnectNodes(WorkflowBuilder workflowBuilder)
{
- foreach (ModelNode node in this.Nodes.Values.ToImmutableArray())
+ // Push `Values` into array to avoid modification during iteration.
+ foreach (ModelNode node in this.Nodes.Values.ToArray())
{
if (node.CompletionHandler is not null)
{
@@ -96,13 +109,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 +166,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
deleted file mode 100644
index fbbc77c48d..0000000000
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowState.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Agents.Workflows.Declarative.Extensions;
-using Microsoft.Agents.Workflows.Declarative.PowerFx;
-using Microsoft.Bot.ObjectModel;
-using Microsoft.PowerFx;
-using Microsoft.PowerFx.Types;
-using Microsoft.Shared.Diagnostics;
-
-namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
-
-internal sealed class DeclarativeWorkflowState
-{
- private static readonly ImmutableHashSet s_mutableScopes =
- new HashSet
- {
- VariableScopeNames.Topic,
- VariableScopeNames.Global,
- VariableScopeNames.System,
- }.ToImmutableHashSet();
-
- private readonly RecalcEngine _engine;
- private readonly WorkflowScopes _scopes;
- private WorkflowExpressionEngine? _expressionEngine;
- private int _isInitialized;
-
- public DeclarativeWorkflowState(RecalcEngine engine, WorkflowScopes? scopes = null)
- {
- this._scopes = scopes ?? new WorkflowScopes();
- this._engine = engine;
- this._scopes.Bind(this._engine);
- }
-
- public WorkflowExpressionEngine ExpressionEngine => this._expressionEngine ??= new WorkflowExpressionEngine(this._engine);
-
- public void Reset(PropertyPath variablePath) =>
- this.Reset(Throw.IfNull(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName));
-
- public void Reset(string scopeName, string? varName = null)
- {
- if (string.IsNullOrWhiteSpace(varName))
- {
- this._scopes.Clear(scopeName);
- }
- else
- {
- this._scopes.Reset(varName, scopeName);
- }
-
- this._scopes.Bind(this._engine, scopeName);
- }
-
- public FormulaValue Get(PropertyPath variablePath) =>
- this.Get(Throw.IfNull(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName));
-
- public FormulaValue Get(string scope, string varName) =>
- this._scopes.Get(varName, scope);
-
- public ValueTask SetAsync(PropertyPath variablePath, FormulaValue value, IWorkflowContext context) =>
- this.SetAsync(Throw.IfNull(variablePath.VariableScopeName), Throw.IfNull(variablePath.VariableName), value, context);
-
- public async ValueTask SetAsync(string scopeName, string varName, FormulaValue value, IWorkflowContext context)
- {
- if (!s_mutableScopes.Contains(scopeName))
- {
- throw new DeclarativeModelException($"Invalid scope: {scopeName}");
- }
-
- this._scopes.Set(varName, value, scopeName);
- this._scopes.Bind(this._engine, scopeName);
-
- await context.QueueStateUpdateAsync(varName, value.ToObject(), scopeName).ConfigureAwait(false);
- }
-
- public string? Format(IEnumerable template) => this._engine.Format(template);
-
- public string? Format(TemplateLine? line) => this._engine.Format(line);
-
- public async ValueTask RestoreAsync(IWorkflowContext context, CancellationToken cancellationToken)
- {
- if (Interlocked.CompareExchange(ref this._isInitialized, 1, 0) == 1)
- {
- return;
- }
-
- await Task.WhenAll(s_mutableScopes.Select(scopeName => ReadScopeAsync(scopeName).AsTask())).ConfigureAwait(false);
-
- async ValueTask ReadScopeAsync(string scopeName)
- {
- HashSet keys = await context.ReadStateKeysAsync(scopeName).ConfigureAwait(false);
- foreach (string key in keys)
- {
- object? value = await context.ReadStateAsync