mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4a49c60cf | ||
|
|
5519533c60 | ||
|
|
d8a1a3977f | ||
|
|
d0ad92af7a | ||
|
|
45050a2b08 | ||
|
|
e64fb4a435 | ||
|
|
c82bab4d0e | ||
|
|
8add9748ef | ||
|
|
6b55de8438 | ||
|
|
06b921cc47 | ||
|
|
35caf28239 | ||
|
|
420e7b6038 | ||
|
|
cf6e0070b4 | ||
|
|
d98520d0ca | ||
|
|
ffb2468945 | ||
|
|
6224bce853 | ||
|
|
774a83cf53 | ||
|
|
d4d03def47 | ||
|
|
eec89baaec | ||
|
|
63a2283033 | ||
|
|
61e37cdc6e | ||
|
|
97ac897dff | ||
|
|
ce96daf05a | ||
|
|
261f050bad | ||
|
|
ebeeeeb421 | ||
|
|
fcd7aa0b77 | ||
|
|
56314f96f5 | ||
|
|
0fb395f5a1 | ||
|
|
1b3c67df96 | ||
|
|
5dfcfedf1e | ||
|
|
cf1c61760d | ||
|
|
6f69299f4a | ||
|
|
8b21180cf2 | ||
|
|
2fd9a5916a | ||
|
|
8b7aef63a6 | ||
|
|
5090ec393e | ||
|
|
536364131d |
@@ -1,15 +1,11 @@
|
||||
{
|
||||
"name": "C# (.NET)",
|
||||
//"image": "mcr.microsoft.com/devcontainers/dotnet",
|
||||
// Workaround for https://github.com/devcontainers/images/issues/1752
|
||||
"build": {
|
||||
"dockerfile": "dotnet.Dockerfile"
|
||||
},
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:10.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/dotnet:2.4.2": {},
|
||||
"ghcr.io/devcontainers/features/dotnet:2.4.0": {},
|
||||
"ghcr.io/devcontainers/features/powershell:1.5.1": {},
|
||||
"ghcr.io/devcontainers/features/azure-cli:1.2.9": {},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2.14.0": {}
|
||||
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2.12.4": {}
|
||||
},
|
||||
"workspaceFolder": "/workspaces/agent-framework/dotnet/",
|
||||
"customizations": {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
FROM mcr.microsoft.com/devcontainers/dotnet
|
||||
|
||||
# Remove Yarn repository with expired GPG key to prevent apt-get update failures
|
||||
# Tracking issue: https://github.com/devcontainers/images/issues/1752
|
||||
RUN rm -f /etc/apt/sources.list.d/yarn.list
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
dotnet
|
||||
python
|
||||
workflow-samples
|
||||
|
||||
|
||||
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
|
||||
- name: Start Azure Cosmos DB Emulator
|
||||
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
popd
|
||||
popd
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
|
||||
- name: Run Unit Tests
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
for project in $UT_PROJECTS; do
|
||||
# Query the project's target frameworks using MSBuild with the current configuration
|
||||
target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
|
||||
|
||||
|
||||
# Check if the project supports the target framework
|
||||
if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
|
||||
if [[ "${{ matrix.targetFramework }}" == "${{ env.COVERAGE_FRAMEWORK }}" ]]; then
|
||||
@@ -165,8 +165,8 @@ jobs:
|
||||
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
|
||||
|
||||
- name: Log event name and matrix integration-tests
|
||||
shell: bash
|
||||
run: echo "github.event_name:${{ github.event_name }} matrix.integration-tests:${{ matrix.integration-tests }} github.event.action:${{ github.event.action }} github.event.pull_request.merged:${{ github.event.pull_request.merged }}"
|
||||
shell: bash
|
||||
run: echo "github.event_name:${{ github.event_name }} matrix.integration-tests:${{ matrix.integration-tests }} github.event.action:${{ github.event.action }} github.event.pull_request.merged:${{ github.event.pull_request.merged }}"
|
||||
|
||||
- name: Azure CLI Login
|
||||
if: github.event_name != 'pull_request' && matrix.integration-tests
|
||||
@@ -192,10 +192,10 @@ jobs:
|
||||
for project in $INTEGRATION_TEST_PROJECTS; do
|
||||
# Query the project's target frameworks using MSBuild with the current configuration
|
||||
target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
|
||||
|
||||
|
||||
# Check if the project supports the target framework
|
||||
if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
|
||||
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --filter "Category!=IntegrationDisabled"
|
||||
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx
|
||||
else
|
||||
echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
|
||||
fi
|
||||
|
||||
+5
-6
@@ -209,14 +209,13 @@ WARP.md
|
||||
**/tmpclaude*
|
||||
|
||||
# Azurite storage emulator files
|
||||
*/__azurite_db_blob__.json*
|
||||
*/__azurite_db_blob_extent__.json*
|
||||
*/__azurite_db_queue__.json*
|
||||
*/__azurite_db_queue_extent__.json*
|
||||
*/__azurite_db_table__.json*
|
||||
*/__azurite_db_blob__.json
|
||||
*/__azurite_db_blob_extent__.json
|
||||
*/__azurite_db_queue__.json
|
||||
*/__azurite_db_queue_extent__.json
|
||||
*/__azurite_db_table__.json
|
||||
*/__blobstorage__/
|
||||
*/__queuestorage__/
|
||||
*/AzuriteConfig
|
||||
|
||||
# Azure Functions local settings
|
||||
local.settings.json
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
|
||||
<!-- Agent SDKs -->
|
||||
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.18" />
|
||||
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
|
||||
<!-- M365 Agents SDK -->
|
||||
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
|
||||
@@ -113,14 +112,14 @@
|
||||
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1106.1" />
|
||||
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
|
||||
<!-- Durable Task -->
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.18.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.19.1" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.19.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.19.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.19.0" />
|
||||
<!-- Azure Functions -->
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.11.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.13.1" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.1" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
||||
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
<Project Path="samples/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
|
||||
<Project Path="samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
|
||||
<Project Path="samples/AzureFunctions/09_Workflow/09_Workflow.csproj" />
|
||||
<Project Path="samples/AzureFunctions/10_WorkflowConcurrent/10_WorkflowConcurrent.csproj" />
|
||||
<Project Path="samples/AzureFunctions/11_WorkflowSharedState/11_WorkflowSharedState.csproj" />
|
||||
<Project Path="samples/AzureFunctions/12_ConditionalEdges/12_ConditionalEdges.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/DurableAgents/">
|
||||
<File Path="samples/DurableAgents/ConsoleApps/README.md" />
|
||||
@@ -46,6 +50,12 @@
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/08_SingleWorkflow/08_SingleWorkflow.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/09_SubWorkflows/09_SubWorkflows.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/09_Workflow_Concurrency/09_Workflow_Concurrency.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/10_Workflow_HITL/10_Workflow_HITL.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/11_WorkflowEvents/11_WorkflowEvents.csproj" />
|
||||
<Project Path="samples/DurableAgents/ConsoleApps/12_WorkflowLoop/12_WorkflowLoop.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/">
|
||||
<File Path="samples/GettingStarted/README.md" />
|
||||
@@ -65,7 +75,6 @@
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
|
||||
@@ -230,7 +239,6 @@
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Agents/">
|
||||
<Project Path="samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/GroupChatToolApproval.csproj" />
|
||||
<Project Path="samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/Workflows/Checkpoint/">
|
||||
@@ -397,7 +405,6 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.GithubCopilot/Microsoft.Agents.AI.GithubCopilot.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
||||
@@ -416,8 +423,8 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Tests/" />
|
||||
@@ -427,7 +434,6 @@
|
||||
<Project Path="tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
|
||||
@@ -442,7 +448,6 @@
|
||||
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/Microsoft.Agents.AI.GithubCopilot.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"src\\Microsoft.Agents.AI.Abstractions\\Microsoft.Agents.AI.Abstractions.csproj",
|
||||
"src\\Microsoft.Agents.AI.AGUI\\Microsoft.Agents.AI.AGUI.csproj",
|
||||
"src\\Microsoft.Agents.AI.Anthropic\\Microsoft.Agents.AI.Anthropic.csproj",
|
||||
"src\\Microsoft.Agents.AI.GithubCopilot\\Microsoft.Agents.AI.GithubCopilot.csproj",
|
||||
"src\\Microsoft.Agents.AI.AzureAI.Persistent\\Microsoft.Agents.AI.AzureAI.Persistent.csproj",
|
||||
"src\\Microsoft.Agents.AI.AzureAI\\Microsoft.Agents.AI.AzureAI.csproj",
|
||||
"src\\Microsoft.Agents.AI.CopilotStudio\\Microsoft.Agents.AI.CopilotStudio.csproj",
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="LocalNugetSource" value="C:\LocalNugetSource" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="LocalNugetSource">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
@@ -2,9 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260127.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260127.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.260127.1</GitTag>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260121.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260121.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.260121.1</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -42,7 +42,7 @@ public static class Program
|
||||
// Create the Host agent
|
||||
var hostAgent = new HostClientAgent(loggerFactory);
|
||||
await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(";"));
|
||||
AgentSession session = await hostAgent.Agent!.GetNewSessionAsync(cancellationToken);
|
||||
AgentThread thread = await hostAgent.Agent!.GetNewThreadAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
@@ -61,7 +61,7 @@ public static class Program
|
||||
break;
|
||||
}
|
||||
|
||||
var agentResponse = await hostAgent.Agent!.RunAsync(message, session, cancellationToken: cancellationToken);
|
||||
var agentResponse = await hostAgent.Agent!.RunAsync(message, thread, cancellationToken: cancellationToken);
|
||||
foreach (var chatMessage in agentResponse.Messages)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
|
||||
@@ -88,7 +88,7 @@ public static class Program
|
||||
description: "AG-UI Client Agent",
|
||||
tools: [changeBackground, readClientClimateSensors]);
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync(cancellationToken);
|
||||
AgentThread thread = await agent.GetNewThreadAsync(cancellationToken);
|
||||
List<ChatMessage> messages = [new(ChatRole.System, "You are a helpful assistant.")];
|
||||
try
|
||||
{
|
||||
@@ -112,23 +112,23 @@ public static class Program
|
||||
|
||||
// Call RunStreamingAsync to get streaming updates
|
||||
bool isFirstUpdate = true;
|
||||
string? sessionId = null;
|
||||
string? threadId = null;
|
||||
var updates = new List<ChatResponseUpdate>();
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session, cancellationToken: cancellationToken))
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: cancellationToken))
|
||||
{
|
||||
// Use AsChatResponseUpdate to access ChatResponseUpdate properties
|
||||
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
|
||||
updates.Add(chatUpdate);
|
||||
if (chatUpdate.ConversationId != null)
|
||||
{
|
||||
sessionId = chatUpdate.ConversationId;
|
||||
threadId = chatUpdate.ConversationId;
|
||||
}
|
||||
|
||||
// Display run started information from the first update
|
||||
if (isFirstUpdate && sessionId != null && update.ResponseId != null)
|
||||
if (isFirstUpdate && threadId != null && update.ResponseId != null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"\n[Run Started - Session: {sessionId}, Run: {update.ResponseId}]");
|
||||
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {update.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
@@ -177,7 +177,7 @@ public static class Program
|
||||
var lastUpdate = updates[^1];
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"[Run Ended - Session: {sessionId}, Run: {lastUpdate.ResponseId}]");
|
||||
Console.WriteLine($"[Run Ended - Thread: {threadId}, Run: {lastUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
messages.Clear();
|
||||
|
||||
@@ -19,21 +19,21 @@ internal sealed class AgenticUIAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Track function calls that should trigger state events
|
||||
var trackedFunctionCalls = new Dictionary<string, FunctionCallContent>();
|
||||
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// Process contents: track function calls and emit state events for results
|
||||
List<AIContent> stateEventsToEmit = new();
|
||||
|
||||
+4
-4
@@ -20,21 +20,21 @@ internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Track the last emitted document state to avoid duplicates
|
||||
string? lastEmittedDocument = null;
|
||||
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// Check if we're seeing a write_document tool call and emit predictive state
|
||||
bool hasToolCall = false;
|
||||
|
||||
@@ -19,21 +19,21 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions ||
|
||||
!properties.TryGetValue("ag_ui_state", out JsonElement state))
|
||||
{
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
@@ -64,7 +64,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
var firstRunMessages = messages.Append(stateUpdateMessage);
|
||||
|
||||
var allUpdates = new List<AgentResponseUpdate>();
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, session, firstRunOptions, cancellationToken).ConfigureAwait(false))
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
allUpdates.Add(update);
|
||||
|
||||
@@ -98,7 +98,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
ChatRole.System,
|
||||
[new TextContent("Please provide a concise summary of the state changes in at most two sentences.")]));
|
||||
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, thread, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ var pirateAgentBuilder = builder.AddAIAgent(
|
||||
chatClientServiceKey: "chat-model")
|
||||
.WithAITool(new CustomAITool())
|
||||
.WithAITool(new CustomFunctionTool())
|
||||
.WithInMemorySessionStore();
|
||||
.WithInMemoryThreadStore();
|
||||
|
||||
var knightsKnavesAgentBuilder = builder.AddAIAgent("knights-and-knaves", (sp, key) =>
|
||||
{
|
||||
|
||||
@@ -28,13 +28,13 @@ internal sealed class A2AAgentClient : AgentClientBase
|
||||
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
string agentName,
|
||||
IList<ChatMessage> messages,
|
||||
string? sessionId = null,
|
||||
string? threadId = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._logger.LogInformation("Running agent {AgentName} with {MessageCount} messages via A2A", agentName, messages.Count);
|
||||
|
||||
var (a2aClient, _) = this.ResolveClient(agentName);
|
||||
var contextId = sessionId ?? Guid.NewGuid().ToString("N");
|
||||
var contextId = threadId ?? Guid.NewGuid().ToString("N");
|
||||
|
||||
// Convert and send messages via A2A without try-catch in yield method
|
||||
var results = new List<AgentResponseUpdate>();
|
||||
|
||||
@@ -16,13 +16,13 @@ internal abstract class AgentClientBase
|
||||
/// </summary>
|
||||
/// <param name="agentName">The name of the agent to run.</param>
|
||||
/// <param name="messages">The messages to send to the agent.</param>
|
||||
/// <param name="sessionId">Optional session identifier for conversation continuity.</param>
|
||||
/// <param name="threadId">Optional thread identifier for conversation continuity.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>An asynchronous enumerable of agent response updates.</returns>
|
||||
public abstract IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
string agentName,
|
||||
IList<ChatMessage> messages,
|
||||
string? sessionId = null,
|
||||
string? threadId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -34,3 +34,16 @@ internal abstract class AgentClientBase
|
||||
public virtual Task<AgentCard?> GetAgentCardAsync(string agentName, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<AgentCard?>(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to create a thread-like wrapper for agent clients.
|
||||
/// </summary>
|
||||
public class AgentClientThread
|
||||
{
|
||||
public string ThreadId { get; }
|
||||
|
||||
public AgentClientThread(string? threadId = null)
|
||||
{
|
||||
this.ThreadId = threadId ?? Guid.NewGuid().ToString("N");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ internal sealed class OpenAIChatCompletionsAgentClient(HttpClient httpClient) :
|
||||
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
string agentName,
|
||||
IList<ChatMessage> messages,
|
||||
string? sessionId = null,
|
||||
string? threadId = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
OpenAIClientOptions options = new()
|
||||
|
||||
@@ -18,7 +18,7 @@ internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentC
|
||||
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
string agentName,
|
||||
IList<ChatMessage> messages,
|
||||
string? sessionId = null,
|
||||
string? threadId = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
OpenAIClientOptions options = new()
|
||||
@@ -30,7 +30,7 @@ internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentC
|
||||
var openAiClient = new ResponsesClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
|
||||
var chatOptions = new ChatOptions()
|
||||
{
|
||||
ConversationId = sessionId
|
||||
ConversationId = threadId
|
||||
};
|
||||
|
||||
await foreach (var update in openAiClient.GetStreamingResponseAsync(messages, chatOptions, cancellationToken: cancellationToken))
|
||||
|
||||
@@ -8,3 +8,6 @@ dotnet_diagnostic.DURABLE0003.severity = none
|
||||
dotnet_diagnostic.DURABLE0004.severity = none
|
||||
dotnet_diagnostic.DURABLE0005.severity = none
|
||||
dotnet_diagnostic.DURABLE0006.severity = none
|
||||
|
||||
# CA1812: Internal classes are instantiated via dependency injection or reflection in samples
|
||||
dotnet_diagnostic.CA1812.severity = none
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>SingleAgent</AssemblyName>
|
||||
<RootNamespace>SingleAgent</RootNamespace>
|
||||
<AssemblyName>Workflow</AssemblyName>
|
||||
<RootNamespace>Workflow</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
POST {{authority}}/api/agents/Joker/run
|
||||
Content-Type: text/plain
|
||||
|
||||
Tell me a joke about a pirate.
|
||||
Hello world
|
||||
|
||||
@@ -19,15 +19,15 @@ public static class FunctionTriggers
|
||||
public static async Task<string> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
|
||||
{
|
||||
DurableAIAgent writer = context.GetAgent("WriterAgent");
|
||||
AgentSession writerSession = await writer.GetNewSessionAsync();
|
||||
AgentThread writerThread = await writer.GetNewThreadAsync();
|
||||
|
||||
AgentResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
|
||||
message: "Write a concise inspirational sentence about learning.",
|
||||
session: writerSession);
|
||||
thread: writerThread);
|
||||
|
||||
AgentResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
|
||||
message: $"Improve this further while keeping it under 25 words: {initial.Result.Text}",
|
||||
session: writerSession);
|
||||
thread: writerThread);
|
||||
|
||||
return refined.Result.Text;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
|
||||
// Single agent used by the orchestration to demonstrate sequential calls on the same session.
|
||||
// Single agent used by the orchestration to demonstrate sequential calls on the same thread.
|
||||
const string WriterName = "WriterAgent";
|
||||
const string WriterInstructions =
|
||||
"""
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Single Agent Orchestration Sample
|
||||
|
||||
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that orchestrates sequential calls to a single AI agent using the same session for context continuity.
|
||||
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that orchestrates sequential calls to a single AI agent using the same conversation thread for context continuity.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Orchestrating multiple interactions with the same agent in a deterministic order
|
||||
- Using the same `AgentSession` across multiple calls to maintain conversational context
|
||||
- Using the same `AgentThread` across multiple calls to maintain conversational context
|
||||
- Durable orchestration with automatic checkpointing and resumption from failures
|
||||
- HTTP API integration for starting and monitoring orchestrations
|
||||
|
||||
|
||||
+4
-4
@@ -21,7 +21,7 @@ public static class FunctionTriggers
|
||||
|
||||
// Get the spam detection agent
|
||||
DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent");
|
||||
AgentSession spamSession = await spamDetectionAgent.GetNewSessionAsync();
|
||||
AgentThread spamThread = await spamDetectionAgent.GetNewThreadAsync();
|
||||
|
||||
// Step 1: Check if the email is spam
|
||||
AgentResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
|
||||
@@ -31,7 +31,7 @@ public static class FunctionTriggers
|
||||
Email ID: {email.EmailId}
|
||||
Content: {email.EmailContent}
|
||||
""",
|
||||
session: spamSession);
|
||||
thread: spamThread);
|
||||
DetectionResult result = spamDetectionResponse.Result;
|
||||
|
||||
// Step 2: Conditional logic based on spam detection result
|
||||
@@ -43,7 +43,7 @@ public static class FunctionTriggers
|
||||
|
||||
// Generate and send response for legitimate email
|
||||
DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent");
|
||||
AgentSession emailSession = await emailAssistantAgent.GetNewSessionAsync();
|
||||
AgentThread emailThread = await emailAssistantAgent.GetNewThreadAsync();
|
||||
|
||||
AgentResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
|
||||
message:
|
||||
@@ -53,7 +53,7 @@ public static class FunctionTriggers
|
||||
Email ID: {email.EmailId}
|
||||
Content: {email.EmailContent}
|
||||
""",
|
||||
session: emailSession);
|
||||
thread: emailThread);
|
||||
|
||||
EmailResponse emailResponse = emailAssistantResponse.Result;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public static class FunctionTriggers
|
||||
|
||||
// Get the writer agent
|
||||
DurableAIAgent writerAgent = context.GetAgent("WriterAgent");
|
||||
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
|
||||
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
|
||||
|
||||
// Set initial status
|
||||
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
|
||||
@@ -32,7 +32,7 @@ public static class FunctionTriggers
|
||||
// Step 1: Generate initial content
|
||||
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
|
||||
message: $"Write a short article about '{input.Topic}'.",
|
||||
session: writerSession);
|
||||
thread: writerThread);
|
||||
GeneratedContent content = writerResponse.Result;
|
||||
|
||||
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
|
||||
@@ -81,7 +81,7 @@ public static class FunctionTriggers
|
||||
|
||||
Human Feedback: {humanResponse.Feedback}
|
||||
""",
|
||||
session: writerSession);
|
||||
thread: writerThread);
|
||||
|
||||
content = writerResponse.Result;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public static class FunctionTriggers
|
||||
|
||||
// Get the writer agent
|
||||
DurableAIAgent writerAgent = context.GetAgent("Writer");
|
||||
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
|
||||
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
|
||||
|
||||
// Set initial status
|
||||
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
|
||||
@@ -28,7 +28,7 @@ public static class FunctionTriggers
|
||||
// Step 1: Generate initial content
|
||||
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
|
||||
message: $"Write a short article about '{input.Topic}'.",
|
||||
session: writerSession);
|
||||
thread: writerThread);
|
||||
GeneratedContent content = writerResponse.Result;
|
||||
|
||||
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
|
||||
@@ -102,7 +102,7 @@ public static class FunctionTriggers
|
||||
|
||||
Human Feedback: {humanResponse.Feedback}
|
||||
""",
|
||||
session: writerSession);
|
||||
thread: writerThread);
|
||||
|
||||
content = writerResponse.Result;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ internal sealed class Tools(ILogger<Tools> logger)
|
||||
{
|
||||
this._logger.LogInformation("Getting status for workflow instance: {InstanceId}", instanceId);
|
||||
|
||||
// Get the current agent context using the session-static property
|
||||
// Get the current agent context using the thread-static property
|
||||
OrchestrationMetadata? status = await DurableAgentContext.Current.GetOrchestrationStatusAsync(
|
||||
instanceId,
|
||||
includeDetails);
|
||||
|
||||
@@ -94,15 +94,15 @@ public sealed class FunctionTriggers
|
||||
|
||||
AIAgent agentProxy = durableClient.AsDurableAgentProxy(context, "TravelPlanner");
|
||||
|
||||
// Create a new agent session
|
||||
AgentSession session = await agentProxy.GetNewSessionAsync(cancellationToken);
|
||||
string agentSessionId = session.GetService<AgentSessionId>().ToString();
|
||||
// Create a new agent thread
|
||||
AgentThread thread = await agentProxy.GetNewThreadAsync(cancellationToken);
|
||||
string agentSessionId = thread.GetService<AgentSessionId>().ToString();
|
||||
|
||||
this._logger.LogInformation("Creating new agent session: {AgentSessionId}", agentSessionId);
|
||||
|
||||
// Run the agent in the background (fire-and-forget)
|
||||
DurableAgentRunOptions options = new() { IsFireAndForget = true };
|
||||
await agentProxy.RunAsync(prompt, session, options, cancellationToken);
|
||||
await agentProxy.RunAsync(prompt, thread, options, cancellationToken);
|
||||
|
||||
this._logger.LogInformation("Agent run started for session: {AgentSessionId}", agentSessionId);
|
||||
|
||||
|
||||
@@ -65,9 +65,9 @@ public sealed class RedisStreamResponseHandler : IAgentResponseHandler
|
||||
"DurableAgentContext.Current is not set. This handler must be used within a durable agent context.");
|
||||
}
|
||||
|
||||
// Get session ID from the current session context, which is only available in the context of
|
||||
// Get session ID from the current thread context, which is only available in the context of
|
||||
// a durable agent execution.
|
||||
string agentSessionId = context.CurrentSession.GetService<AgentSessionId>().ToString();
|
||||
string agentSessionId = context.CurrentThread.GetService<AgentSessionId>().ToString();
|
||||
string streamKey = GetStreamKey(agentSessionId);
|
||||
|
||||
IDatabase db = this._redis.GetDatabase();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>SingleAgent</AssemblyName>
|
||||
<RootNamespace>SingleAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Azure Functions packages -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Parses an Order ID from a string input and returns an Order object populated.
|
||||
/// </summary>
|
||||
internal sealed class OrderLookup() : Executor<string, Order>("OrderLookup")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Populate Order information from OrderId.
|
||||
return new Order(message, 100.0m);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enriches an Order object with additional information.
|
||||
/// </summary>
|
||||
internal sealed class OrderEnrich() : Executor<Order, Order>("EnrichOrder")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.Customer is null)
|
||||
{
|
||||
// populate customer information for the order from database.
|
||||
message.Customer = new Customer(1, "Jerry");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PaymentProcessor() : Executor<Order, Order>("ProcessPayment")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
message.PaymentReferenceNumber = Guid.NewGuid().ToString()[^4..];
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class OrderCancel() : Executor<Order, string>("OrderCancel")
|
||||
{
|
||||
public override async ValueTask<string> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return $"Order {message.Id} cancelled at {DateTime.UtcNow:g} UTC.";
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class Order
|
||||
{
|
||||
public Order(string id, decimal amount)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Amount = amount;
|
||||
}
|
||||
public string Id { get; }
|
||||
public decimal Amount { get; }
|
||||
public Customer? Customer { get; set; }
|
||||
public string? PaymentReferenceNumber { get; set; }
|
||||
}
|
||||
|
||||
public sealed record Customer(int Id, string Name);
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using SingleAgent;
|
||||
|
||||
Func<string, string> orderParserFunc = input =>
|
||||
{
|
||||
// We accept both short ordereId(Ex:12345) and long order reference number(MSFT12345)
|
||||
// OrderId is the last 5 digigs of order reference number.
|
||||
const int OrderIdPartLength = 5;
|
||||
if (input.Length > OrderIdPartLength)
|
||||
{
|
||||
return input[^OrderIdPartLength..];
|
||||
}
|
||||
|
||||
return input;
|
||||
};
|
||||
var orderParserExecutor = orderParserFunc.BindAsExecutor("ParseOrderId");
|
||||
|
||||
OrderLookup orderLookupExecutor = new();
|
||||
OrderEnrich orderEnricherExeecutor = new();
|
||||
PaymentProcessor paymentProcessorExecutor = new();
|
||||
|
||||
Workflow fulfillOrder = new WorkflowBuilder(orderParserExecutor)
|
||||
.WithName("FulfillOrder")
|
||||
.WithDescription("Looks up an order by ID and run payment processing")
|
||||
.AddEdge(orderParserExecutor, orderLookupExecutor)
|
||||
.AddEdge(orderLookupExecutor, orderEnricherExeecutor)
|
||||
.AddEdge(orderEnricherExeecutor, paymentProcessorExecutor)
|
||||
.Build();
|
||||
|
||||
//OrderCancel orderArchiverExecutor = new();
|
||||
//Workflow cancelOrder = new WorkflowBuilder(orderParserExecutor)
|
||||
// .WithName("CancelOrder")
|
||||
// .WithDescription("Cancel an order")
|
||||
// .AddEdge(orderParserExecutor, orderLookupExecutor)
|
||||
// .AddEdge(orderLookupExecutor, orderArchiverExecutor)
|
||||
// .Build();
|
||||
|
||||
var host = FunctionsApplication.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableOptions(options => options.Workflows.AddWorkflow(fulfillOrder))
|
||||
.Build();
|
||||
|
||||
host.Run();
|
||||
@@ -0,0 +1,89 @@
|
||||
# Single Agent Sample
|
||||
|
||||
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that hosts a single AI agent and provides direct HTTP API access for interactive conversations.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Using the Microsoft Agent Framework to define a simple AI agent with a name and instructions.
|
||||
- Registering agents with the Function app and running them using HTTP.
|
||||
- Conversation management (via session IDs) for isolated interactions.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
With the environment setup and function app running, you can test the sample by sending an HTTP request to the agent endpoint.
|
||||
|
||||
You can use the `demo.http` file to send a message to the agent, or a command line tool like `curl` as shown below:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/agents/Joker/run \
|
||||
-H "Content-Type: text/plain" \
|
||||
-d "Tell me a joke about a pirate."
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/agents/Joker/run `
|
||||
-ContentType text/plain `
|
||||
-Body "Tell me a joke about a pirate."
|
||||
```
|
||||
|
||||
You can also send JSON requests:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/agents/Joker/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d '{"message": "Tell me a joke about a pirate."}'
|
||||
```
|
||||
|
||||
To continue a conversation, include the `thread_id` in the query string or JSON body:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:7071/api/agents/Joker/run?thread_id=your-thread-id" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d '{"message": "Tell me another one."}'
|
||||
```
|
||||
|
||||
The response from the agent will be displayed in the terminal where you ran `func start`. The expected `text/plain` output will look something like:
|
||||
|
||||
```text
|
||||
Why don't pirates ever learn the alphabet? Because they always get stuck at "C"!
|
||||
```
|
||||
|
||||
The expected `application/json` output will look something like:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"thread_id": "ee6e47a0-f24b-40b1-ade8-16fcebb9eb40",
|
||||
"response": {
|
||||
"Messages": [
|
||||
{
|
||||
"AuthorName": "Joker",
|
||||
"CreatedAt": "2025-11-11T12:00:00.0000000Z",
|
||||
"Role": "assistant",
|
||||
"Contents": [
|
||||
{
|
||||
"Type": "text",
|
||||
"Text": "Why don't pirates ever learn the alphabet? Because they always get stuck at 'C'!"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Usage": {
|
||||
"InputTokenCount": 78,
|
||||
"OutputTokenCount": 36,
|
||||
"TotalTokenCount": 114
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
# Default endpoint address for local testing
|
||||
@authority=http://localhost:7071
|
||||
|
||||
### Look up a long order reference id
|
||||
POST {{authority}}/api/workflows/FulfillOrder/run
|
||||
Content-Type: text/plain
|
||||
|
||||
QWERTY80853
|
||||
|
||||
### Look up a short order id
|
||||
POST {{authority}}/api/workflows/CancelOrder/run
|
||||
Content-Type: text/plain
|
||||
|
||||
12345
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>SingleAgent</AssemblyName>
|
||||
<RootNamespace>SingleAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="local.settings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Azure Functions packages -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
internal sealed class ConcurrentStartExecutor() : Executor<string, string>("ConcurrentStartExecutor")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// do some initial parsing and validation of the message.
|
||||
// Return a polished version ith additional metadta.
|
||||
if (!message.StartsWith("Query for the agent:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
message = "Query for the agent: " + message;
|
||||
}
|
||||
|
||||
return ValueTask.FromResult(message);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ResultAggregationExecutor() : Executor<string[], string>("ResultAggregationExecutor")
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles incoming messages from the agents and aggregates their responses.
|
||||
/// </summary>
|
||||
/// <param name="message">The messages from the parallel agents.</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public override ValueTask<string> HandleAsync(string[] message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Aggregate all responses from parallel executors
|
||||
string aggregatedResponse = string.Join("\n---\n", message);
|
||||
return ValueTask.FromResult($"Aggregated {message.Length} responses:\n{aggregatedResponse}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenAI.Chat;
|
||||
using SingleAgent;
|
||||
|
||||
// Get the Azure OpenAI endpoint and deployment name from environment variables.
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
|
||||
|
||||
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
|
||||
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
|
||||
AIAgent physicist = client.GetChatClient(deploymentName).AsAIAgent("You are an expert in physics. You answer questions from a physics perspective.", "Physicist");
|
||||
AIAgent chemist = client.GetChatClient(deploymentName).AsAIAgent("You are an expert in chemistry. You answer questions from a chemistry perspective.", "Chemist");
|
||||
|
||||
var startExecutor = new ConcurrentStartExecutor();
|
||||
var aggregationExecutor = new ResultAggregationExecutor();
|
||||
|
||||
var workflow = new WorkflowBuilder(startExecutor)
|
||||
.WithName("ExpertReview")
|
||||
.AddFanOutEdge(startExecutor, [physicist, chemist])
|
||||
.AddFanInEdge([physicist, chemist], aggregationExecutor)
|
||||
.Build();
|
||||
|
||||
var host = FunctionsApplication.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableOptions(options =>
|
||||
{
|
||||
// Configure workflows
|
||||
options.Workflows.AddWorkflow(workflow);
|
||||
})
|
||||
.Build();
|
||||
|
||||
host.Run();
|
||||
@@ -0,0 +1,89 @@
|
||||
# Single Agent Sample
|
||||
|
||||
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that hosts a single AI agent and provides direct HTTP API access for interactive conversations.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Using the Microsoft Agent Framework to define a simple AI agent with a name and instructions.
|
||||
- Registering agents with the Function app and running them using HTTP.
|
||||
- Conversation management (via session IDs) for isolated interactions.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
With the environment setup and function app running, you can test the sample by sending an HTTP request to the agent endpoint.
|
||||
|
||||
You can use the `demo.http` file to send a message to the agent, or a command line tool like `curl` as shown below:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/agents/Joker/run \
|
||||
-H "Content-Type: text/plain" \
|
||||
-d "Tell me a joke about a pirate."
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/agents/Joker/run `
|
||||
-ContentType text/plain `
|
||||
-Body "Tell me a joke about a pirate."
|
||||
```
|
||||
|
||||
You can also send JSON requests:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/agents/Joker/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d '{"message": "Tell me a joke about a pirate."}'
|
||||
```
|
||||
|
||||
To continue a conversation, include the `thread_id` in the query string or JSON body:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:7071/api/agents/Joker/run?thread_id=your-thread-id" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d '{"message": "Tell me another one."}'
|
||||
```
|
||||
|
||||
The response from the agent will be displayed in the terminal where you ran `func start`. The expected `text/plain` output will look something like:
|
||||
|
||||
```text
|
||||
Why don't pirates ever learn the alphabet? Because they always get stuck at "C"!
|
||||
```
|
||||
|
||||
The expected `application/json` output will look something like:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"thread_id": "ee6e47a0-f24b-40b1-ade8-16fcebb9eb40",
|
||||
"response": {
|
||||
"Messages": [
|
||||
{
|
||||
"AuthorName": "Joker",
|
||||
"CreatedAt": "2025-11-11T12:00:00.0000000Z",
|
||||
"Role": "assistant",
|
||||
"Contents": [
|
||||
{
|
||||
"Type": "text",
|
||||
"Text": "Why don't pirates ever learn the alphabet? Because they always get stuck at 'C'!"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Usage": {
|
||||
"InputTokenCount": 78,
|
||||
"OutputTokenCount": 36,
|
||||
"TotalTokenCount": 114
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
//// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
//using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
//namespace SingleAgent;
|
||||
|
||||
///// <summary>
|
||||
///// Routes survey responses to appropriate teams based on rating and category.
|
||||
///// </summary>
|
||||
//public sealed class ResponseRouterExecutor() : Executor<string, string>("ResponseRouterExecutor")
|
||||
//{
|
||||
// public override ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
// {
|
||||
// if (message.Contains("billing", StringComparison.OrdinalIgnoreCase))
|
||||
// {
|
||||
// return ValueTask.FromResult("Routed to Billing Team");
|
||||
// }
|
||||
// else if (message.Contains("technical", StringComparison.OrdinalIgnoreCase))
|
||||
// {
|
||||
// return ValueTask.FromResult("Routed to Technical Support Team");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// return ValueTask.FromResult("Routed to General Support Team");
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,82 @@
|
||||
//// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
//using System.Text.Json;
|
||||
//using System.Text.RegularExpressions;
|
||||
//using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
//namespace SingleAgent;
|
||||
|
||||
///// <summary>
|
||||
///// This executor parses survey responses and produces structured output.
|
||||
///// Example input: "Rating: 8. The app is good but checkout process is confusing."
|
||||
///// </summary>
|
||||
//[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by workflow framework")]
|
||||
//internal sealed partial class SurveyResponseParserExecutor() : Executor<string, string>("SurveyResponseParserExecutor")
|
||||
//{
|
||||
// private static readonly JsonSerializerOptions s_jsonOptions = new()
|
||||
// {
|
||||
// WriteIndented = true
|
||||
// };
|
||||
|
||||
// [GeneratedRegex(@"Rating:\s*(\d+)", RegexOptions.IgnoreCase)]
|
||||
// private static partial Regex RatingRegex();
|
||||
|
||||
// public override ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
// {
|
||||
// SurveyResponse response = this.ParseSurveyResponse(message);
|
||||
// string jsonResult = JsonSerializer.Serialize(response, s_jsonOptions);
|
||||
// return ValueTask.FromResult(jsonResult);
|
||||
// }
|
||||
|
||||
// private SurveyResponse ParseSurveyResponse(string message)
|
||||
// {
|
||||
// // Parse the message to extract rating and comment
|
||||
// int? rating = null;
|
||||
// string comment = message;
|
||||
|
||||
// // Try to extract rating using pattern "Rating: {number}"
|
||||
// Match ratingMatch = RatingRegex().Match(message);
|
||||
// if (ratingMatch.Success && int.TryParse(ratingMatch.Groups[1].Value, out int parsedRating))
|
||||
// {
|
||||
// rating = parsedRating;
|
||||
|
||||
// // Remove the rating part from the message to get the comment
|
||||
// // Find the position after the rating number
|
||||
// int ratingEndIndex = ratingMatch.Index + ratingMatch.Length;
|
||||
|
||||
// // Skip any separators (period, comma, dash, etc.) and whitespace
|
||||
// while (ratingEndIndex < message.Length &&
|
||||
// (char.IsWhiteSpace(message[ratingEndIndex]) ||
|
||||
// message[ratingEndIndex] == '.' ||
|
||||
// message[ratingEndIndex] == ',' ||
|
||||
// message[ratingEndIndex] == '-'))
|
||||
// {
|
||||
// ratingEndIndex++;
|
||||
// }
|
||||
|
||||
// if (ratingEndIndex < message.Length)
|
||||
// {
|
||||
// comment = message[ratingEndIndex..].Trim();
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// comment = string.Empty;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Create and return the structured response
|
||||
// return new SurveyResponse
|
||||
// {
|
||||
// Rating = rating,
|
||||
// Comment = comment,
|
||||
// OriginalMessage = message
|
||||
// };
|
||||
// }
|
||||
|
||||
// private sealed class SurveyResponse
|
||||
// {
|
||||
// public int? Rating { get; set; }
|
||||
// public string Comment { get; set; } = string.Empty;
|
||||
// public string OriginalMessage { get; set; } = string.Empty;
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Default endpoint address for local testing
|
||||
@authority=http://localhost:7071
|
||||
|
||||
### Start the workflow
|
||||
POST {{authority}}/api/workflows/ExpertReview/run
|
||||
Content-Type: text/plain
|
||||
|
||||
What is temperature?
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>SingleAgent</AssemblyName>
|
||||
<RootNamespace>SingleAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="local.settings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Azure Functions packages -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use durable state management in Azure Functions workflows.
|
||||
// The OrderIdParserExecutor writes a value to shared state, and the EmailSenderExecutor reads it back.
|
||||
// The state is persisted durably using Durable Entities behind the scenes.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Constants for shared state scopes used across executors.
|
||||
/// </summary>
|
||||
internal static class SharedStateConstants
|
||||
{
|
||||
public const string MessageScope = "MessageState";
|
||||
public const string ProcessedMessageKey = "ProcessedMessage";
|
||||
}
|
||||
|
||||
public sealed class Order
|
||||
{
|
||||
public Order(string id, decimal amount)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Amount = amount;
|
||||
}
|
||||
public string Id { get; }
|
||||
public decimal Amount { get; }
|
||||
public string? PaymentReferenceNumber { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// First executor that processes a message and stores the result in shared state.
|
||||
/// </summary>
|
||||
internal sealed class OrderIdParserExecutor() : Executor<string, Order>("OrderIdParserExecutor")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Process the message
|
||||
string processedMessage = $"Processed: {message}";
|
||||
|
||||
// Store the processed message in shared state for the next executor
|
||||
await context.QueueStateUpdateAsync(
|
||||
SharedStateConstants.ProcessedMessageKey,
|
||||
processedMessage,
|
||||
SharedStateConstants.MessageScope,
|
||||
cancellationToken);
|
||||
|
||||
return GetOrder(message);
|
||||
}
|
||||
|
||||
private static Order GetOrder(string id)
|
||||
{
|
||||
// Simulate fetching order details
|
||||
return new Order(id, 100.0m);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Second executor that reads the shared state and appends to the message.
|
||||
/// </summary>
|
||||
internal sealed class EmailSenderExecutor() : Executor<Order, string>("EmailSenderExecutor")
|
||||
{
|
||||
public override async ValueTask<string> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Read the processed message from shared state (written by OrderIdParserExecutor)
|
||||
string? storedMessage = await context.ReadStateAsync<string>(
|
||||
SharedStateConstants.ProcessedMessageKey,
|
||||
SharedStateConstants.MessageScope,
|
||||
cancellationToken);
|
||||
|
||||
// Combine with the input message
|
||||
return storedMessage is not null
|
||||
? $"From state: [{storedMessage}] | Input: [{message.Id}]"
|
||||
: $"No state found | Input: [{message.Id}]";
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PaymentProcesserExecutor() : Executor<Order, Order>("PaymentProcesserExecutor")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Call payment gateway.
|
||||
message.PaymentReferenceNumber = Guid.NewGuid().ToString().Substring(0, 4);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using SingleAgent;
|
||||
|
||||
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
|
||||
|
||||
OrderIdParserExecutor orderParser = new();
|
||||
PaymentProcesserExecutor paymentProcessor = new();
|
||||
EmailSenderExecutor emailSender = new();
|
||||
|
||||
WorkflowBuilder builder = new(orderParser);
|
||||
builder.AddEdge(orderParser, paymentProcessor);
|
||||
builder.AddEdge(paymentProcessor, emailSender).WithOutputFrom(emailSender);
|
||||
var workflow = builder.WithName("ProcessOrder").Build();
|
||||
|
||||
FunctionsApplication.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableOptions(options => options.Workflows.AddWorkflow(workflow))
|
||||
.Build().Run();
|
||||
@@ -0,0 +1,89 @@
|
||||
# Single Agent Sample
|
||||
|
||||
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that hosts a single AI agent and provides direct HTTP API access for interactive conversations.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Using the Microsoft Agent Framework to define a simple AI agent with a name and instructions.
|
||||
- Registering agents with the Function app and running them using HTTP.
|
||||
- Conversation management (via session IDs) for isolated interactions.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
With the environment setup and function app running, you can test the sample by sending an HTTP request to the agent endpoint.
|
||||
|
||||
You can use the `demo.http` file to send a message to the agent, or a command line tool like `curl` as shown below:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/agents/Joker/run \
|
||||
-H "Content-Type: text/plain" \
|
||||
-d "Tell me a joke about a pirate."
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/agents/Joker/run `
|
||||
-ContentType text/plain `
|
||||
-Body "Tell me a joke about a pirate."
|
||||
```
|
||||
|
||||
You can also send JSON requests:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/agents/Joker/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d '{"message": "Tell me a joke about a pirate."}'
|
||||
```
|
||||
|
||||
To continue a conversation, include the `thread_id` in the query string or JSON body:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:7071/api/agents/Joker/run?thread_id=your-thread-id" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d '{"message": "Tell me another one."}'
|
||||
```
|
||||
|
||||
The response from the agent will be displayed in the terminal where you ran `func start`. The expected `text/plain` output will look something like:
|
||||
|
||||
```text
|
||||
Why don't pirates ever learn the alphabet? Because they always get stuck at "C"!
|
||||
```
|
||||
|
||||
The expected `application/json` output will look something like:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"thread_id": "ee6e47a0-f24b-40b1-ade8-16fcebb9eb40",
|
||||
"response": {
|
||||
"Messages": [
|
||||
{
|
||||
"AuthorName": "Joker",
|
||||
"CreatedAt": "2025-11-11T12:00:00.0000000Z",
|
||||
"Role": "assistant",
|
||||
"Contents": [
|
||||
{
|
||||
"Type": "text",
|
||||
"Text": "Why don't pirates ever learn the alphabet? Because they always get stuck at 'C'!"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Usage": {
|
||||
"InputTokenCount": 78,
|
||||
"OutputTokenCount": 36,
|
||||
"TotalTokenCount": 114
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
# Default endpoint address for local testing
|
||||
@authority=http://localhost:7071
|
||||
|
||||
### Start the workflow
|
||||
POST {{authority}}/api/workflows/ProcessOrder/run
|
||||
Content-Type: text/plain
|
||||
|
||||
123
|
||||
|
||||
### Start second workflow
|
||||
POST {{authority}}/api/workflows/ProcessOrder/run
|
||||
Content-Type: text/plain
|
||||
|
||||
456
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>SingleAgent</AssemblyName>
|
||||
<RootNamespace>SingleAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="local.settings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Azure Functions packages -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use durable state management in Azure Functions workflows.
|
||||
// The OrderIdParserExecutor writes a value to shared state, and the FraudValidation reads it back.
|
||||
// The state is persisted durably using Durable Entities behind the scenes.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Constants for shared state scopes used across executors.
|
||||
/// </summary>
|
||||
internal static class SharedStateConstants
|
||||
{
|
||||
public const string MessageScope = "MessageState";
|
||||
public const string ProcessedMessageKey = "ProcessedMessage";
|
||||
}
|
||||
|
||||
internal sealed class Order
|
||||
{
|
||||
public Order(string id, decimal amount)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Amount = amount;
|
||||
}
|
||||
public string Id { get; }
|
||||
public decimal Amount { get; }
|
||||
public Customer? Customer { get; set; }
|
||||
public string? PaymentReferenceNumber { get; set; }
|
||||
}
|
||||
|
||||
public sealed record Customer(int Id, string Name, bool IsBlocked);
|
||||
|
||||
internal sealed class OrderIdParserExecutor() : Executor<string, Order>("OrderIdParserExecutor")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return GetOrder(message);
|
||||
}
|
||||
|
||||
private static Order GetOrder(string id)
|
||||
{
|
||||
// Simulate fetching order details
|
||||
return new Order(id, 100.0m);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class OrderEnrich() : Executor<Order, Order>("EnrichOrder")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
message.Customer = GetCustomerForOrder(message.Id);
|
||||
return message;
|
||||
}
|
||||
|
||||
private static Customer GetCustomerForOrder(string orderId)
|
||||
{
|
||||
if (orderId.Contains('B'))
|
||||
{
|
||||
return new Customer(101, "George", true);
|
||||
}
|
||||
|
||||
return new Customer(201, "Jerry", false);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PaymentProcesserExecutor() : Executor<Order, Order>("PaymentProcesserExecutor")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Call payment gateway.
|
||||
message.PaymentReferenceNumber = Guid.NewGuid().ToString().Substring(0, 4);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class NotifyFraudExecutor() : Executor<Order, string>("NotifyFraud")
|
||||
{
|
||||
public override async ValueTask<string> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Notify fraud team.
|
||||
return $"Order {message.Id} flagged as fraudulent for customer {message.Customer?.Name}.";
|
||||
}
|
||||
}
|
||||
|
||||
internal static class OrderRouteConditions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a condition that evaluates to true when the customer is blocked.
|
||||
/// </summary>
|
||||
internal static Func<Order?, bool> WhenBlocked() => order => order?.Customer?.IsBlocked == true;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a condition that evaluates to true when the customer is not blocked.
|
||||
/// </summary>
|
||||
internal static Func<Order?, bool> WhenNotBlocked() => order => order?.Customer?.IsBlocked == false;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using SingleAgent;
|
||||
|
||||
OrderIdParserExecutor orderParser = new();
|
||||
OrderEnrich orderEnrich = new();
|
||||
PaymentProcesserExecutor paymentProcessor = new();
|
||||
NotifyFraudExecutor notifyFraud = new();
|
||||
|
||||
WorkflowBuilder builder = new(orderParser);
|
||||
builder
|
||||
.AddEdge(orderParser, orderEnrich)
|
||||
.AddEdge(orderEnrich, notifyFraud, condition: OrderRouteConditions.WhenBlocked())
|
||||
.AddEdge(orderEnrich, paymentProcessor, condition: OrderRouteConditions.WhenNotBlocked());
|
||||
|
||||
var workflow = builder.WithName("AuditOrder").Build();
|
||||
|
||||
FunctionsApplication.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableOptions(options => options.Workflows.AddWorkflow(workflow))
|
||||
.Build()
|
||||
.Run();
|
||||
@@ -0,0 +1,89 @@
|
||||
# Single Agent Sample
|
||||
|
||||
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that hosts a single AI agent and provides direct HTTP API access for interactive conversations.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Using the Microsoft Agent Framework to define a simple AI agent with a name and instructions.
|
||||
- Registering agents with the Function app and running them using HTTP.
|
||||
- Conversation management (via session IDs) for isolated interactions.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
With the environment setup and function app running, you can test the sample by sending an HTTP request to the agent endpoint.
|
||||
|
||||
You can use the `demo.http` file to send a message to the agent, or a command line tool like `curl` as shown below:
|
||||
|
||||
Bash (Linux/macOS/WSL):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/agents/Joker/run \
|
||||
-H "Content-Type: text/plain" \
|
||||
-d "Tell me a joke about a pirate."
|
||||
```
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod -Method Post `
|
||||
-Uri http://localhost:7071/api/agents/Joker/run `
|
||||
-ContentType text/plain `
|
||||
-Body "Tell me a joke about a pirate."
|
||||
```
|
||||
|
||||
You can also send JSON requests:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7071/api/agents/Joker/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d '{"message": "Tell me a joke about a pirate."}'
|
||||
```
|
||||
|
||||
To continue a conversation, include the `thread_id` in the query string or JSON body:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:7071/api/agents/Joker/run?thread_id=your-thread-id" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d '{"message": "Tell me another one."}'
|
||||
```
|
||||
|
||||
The response from the agent will be displayed in the terminal where you ran `func start`. The expected `text/plain` output will look something like:
|
||||
|
||||
```text
|
||||
Why don't pirates ever learn the alphabet? Because they always get stuck at "C"!
|
||||
```
|
||||
|
||||
The expected `application/json` output will look something like:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"thread_id": "ee6e47a0-f24b-40b1-ade8-16fcebb9eb40",
|
||||
"response": {
|
||||
"Messages": [
|
||||
{
|
||||
"AuthorName": "Joker",
|
||||
"CreatedAt": "2025-11-11T12:00:00.0000000Z",
|
||||
"Role": "assistant",
|
||||
"Contents": [
|
||||
{
|
||||
"Type": "text",
|
||||
"Text": "Why don't pirates ever learn the alphabet? Because they always get stuck at 'C'!"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Usage": {
|
||||
"InputTokenCount": 78,
|
||||
"OutputTokenCount": 36,
|
||||
"TotalTokenCount": 114
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
# Default endpoint address for local testing
|
||||
@authority=http://localhost:7071
|
||||
|
||||
### Start the workflow
|
||||
POST {{authority}}/api/workflows/ProcessOrder/run
|
||||
Content-Type: text/plain
|
||||
|
||||
B123
|
||||
|
||||
### Start second workflow
|
||||
POST {{authority}}/api/workflows/ProcessOrder/run
|
||||
Content-Type: text/plain
|
||||
|
||||
456
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"Microsoft.Agents.AI.DurableTask": "Information",
|
||||
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
|
||||
"DurableTask": "Information",
|
||||
"Microsoft.DurableTask": "Information"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"durableTask": {
|
||||
"hubName": "default",
|
||||
"storageProvider": {
|
||||
"type": "AzureManaged",
|
||||
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,8 @@ Console.ResetColor();
|
||||
Console.WriteLine("Enter a message for the Joker agent (or 'exit' to quit):");
|
||||
Console.WriteLine();
|
||||
|
||||
// Create a session for the conversation
|
||||
AgentSession session = await agentProxy.GetNewSessionAsync();
|
||||
// Create a thread for the conversation
|
||||
AgentThread thread = await agentProxy.GetNewThreadAsync();
|
||||
|
||||
while (true)
|
||||
{
|
||||
@@ -85,7 +85,7 @@ while (true)
|
||||
{
|
||||
AgentResponse agentResponse = await agentProxy.RunAsync(
|
||||
message: input,
|
||||
session: session,
|
||||
thread: thread,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
Console.WriteLine(agentResponse.Text);
|
||||
|
||||
@@ -33,7 +33,7 @@ AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
|
||||
// Single agent used by the orchestration to demonstrate sequential calls on the same session.
|
||||
// Single agent used by the orchestration to demonstrate sequential calls on the same thread.
|
||||
const string WriterName = "WriterAgent";
|
||||
const string WriterInstructions =
|
||||
"""
|
||||
@@ -47,15 +47,15 @@ AIAgent writerAgent = client.GetChatClient(deploymentName).AsAIAgent(WriterInstr
|
||||
static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context)
|
||||
{
|
||||
DurableAIAgent writer = context.GetAgent("WriterAgent");
|
||||
AgentSession writerSession = await writer.GetNewSessionAsync();
|
||||
AgentThread writerThread = await writer.GetNewThreadAsync();
|
||||
|
||||
AgentResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
|
||||
message: "Write a concise inspirational sentence about learning.",
|
||||
session: writerSession);
|
||||
thread: writerThread);
|
||||
|
||||
AgentResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
|
||||
message: $"Improve this further while keeping it under 25 words: {initial.Result.Text}",
|
||||
session: writerSession);
|
||||
thread: writerThread);
|
||||
|
||||
return refined.Result.Text;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Single Agent Orchestration Sample
|
||||
|
||||
This sample demonstrates how to use the durable agents extension to create a simple console app that orchestrates sequential calls to a single AI agent using the same session for context continuity.
|
||||
This sample demonstrates how to use the durable agents extension to create a simple console app that orchestrates sequential calls to a single AI agent using the same conversation thread for context continuity.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- Orchestrating multiple interactions with the same agent in a deterministic order
|
||||
- Using the same `AgentSession` across multiple calls to maintain conversational context
|
||||
- Using the same `AgentThread` across multiple calls to maintain conversational context
|
||||
- Durable orchestration with automatic checkpointing and resumption from failures
|
||||
- Waiting for orchestration completion using `WaitForInstanceCompletionAsync`
|
||||
|
||||
|
||||
+4
-4
@@ -56,7 +56,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
{
|
||||
// Get the spam detection agent
|
||||
DurableAIAgent spamDetectionAgent = context.GetAgent(SpamDetectionAgentName);
|
||||
AgentSession spamSession = await spamDetectionAgent.GetNewSessionAsync();
|
||||
AgentThread spamThread = await spamDetectionAgent.GetNewThreadAsync();
|
||||
|
||||
// Step 1: Check if the email is spam
|
||||
AgentResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
|
||||
@@ -66,7 +66,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
Email ID: {email.EmailId}
|
||||
Content: {email.EmailContent}
|
||||
""",
|
||||
session: spamSession);
|
||||
thread: spamThread);
|
||||
DetectionResult result = spamDetectionResponse.Result;
|
||||
|
||||
// Step 2: Conditional logic based on spam detection result
|
||||
@@ -78,7 +78,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
|
||||
// Generate and send response for legitimate email
|
||||
DurableAIAgent emailAssistantAgent = context.GetAgent(EmailAssistantAgentName);
|
||||
AgentSession emailSession = await emailAssistantAgent.GetNewSessionAsync();
|
||||
AgentThread emailThread = await emailAssistantAgent.GetNewThreadAsync();
|
||||
|
||||
AgentResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
|
||||
message:
|
||||
@@ -88,7 +88,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
Email ID: {email.EmailId}
|
||||
Content: {email.EmailContent}
|
||||
""",
|
||||
session: emailSession);
|
||||
thread: emailThread);
|
||||
|
||||
EmailResponse emailResponse = emailAssistantResponse.Result;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
{
|
||||
// Get the writer agent
|
||||
DurableAIAgent writerAgent = context.GetAgent("WriterAgent");
|
||||
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
|
||||
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
|
||||
|
||||
// Set initial status
|
||||
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
|
||||
@@ -56,7 +56,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
// Step 1: Generate initial content
|
||||
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
|
||||
message: $"Write a short article about '{input.Topic}' in less than 300 words.",
|
||||
session: writerSession);
|
||||
thread: writerThread);
|
||||
GeneratedContent content = writerResponse.Result;
|
||||
|
||||
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
|
||||
@@ -105,7 +105,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
|
||||
Human Feedback: {humanResponse.Feedback}
|
||||
""",
|
||||
session: writerSession);
|
||||
thread: writerThread);
|
||||
|
||||
content = writerResponse.Result;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
{
|
||||
// Get the writer agent
|
||||
DurableAIAgent writerAgent = context.GetAgent(WriterAgentName);
|
||||
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
|
||||
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
|
||||
|
||||
// Set initial status
|
||||
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
|
||||
@@ -67,7 +67,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
// Step 1: Generate initial content
|
||||
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
|
||||
message: $"Write a short article about '{input.Topic}'.",
|
||||
session: writerSession);
|
||||
thread: writerThread);
|
||||
GeneratedContent content = writerResponse.Result;
|
||||
|
||||
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
|
||||
@@ -141,7 +141,7 @@ static async Task<object> RunOrchestratorAsync(TaskOrchestrationContext context,
|
||||
|
||||
Human Feedback: {humanResponse.Feedback}
|
||||
""",
|
||||
session: writerSession);
|
||||
thread: writerThread);
|
||||
|
||||
content = writerResponse.Result;
|
||||
}
|
||||
@@ -203,7 +203,7 @@ static async Task<object> GetWorkflowStatusAsync(
|
||||
[Description("The instance ID of the workflow to check")] string instanceId,
|
||||
[Description("Whether to include detailed information")] bool includeDetails = true)
|
||||
{
|
||||
// Get the current agent context using the session-static property
|
||||
// Get the current agent context using the thread-static property
|
||||
OrchestrationMetadata? status = await DurableAgentContext.Current.GetOrchestrationStatusAsync(
|
||||
instanceId,
|
||||
includeDetails);
|
||||
@@ -298,8 +298,8 @@ Console.ResetColor();
|
||||
Console.WriteLine("Enter a topic for the Publisher agent to write about (or 'exit' to quit):");
|
||||
Console.WriteLine();
|
||||
|
||||
// Create a session for the conversation
|
||||
AgentSession session = await agentProxy.GetNewSessionAsync();
|
||||
// Create a thread for the conversation
|
||||
AgentThread thread = await agentProxy.GetNewThreadAsync();
|
||||
|
||||
using CancellationTokenSource cts = new();
|
||||
Console.CancelKeyPress += (sender, e) =>
|
||||
@@ -330,7 +330,7 @@ while (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
AgentResponse agentResponse = await agentProxy.RunAsync(
|
||||
message: input,
|
||||
session: session,
|
||||
thread: thread,
|
||||
cancellationToken: cts.Token);
|
||||
|
||||
Console.WriteLine(agentResponse.Text);
|
||||
|
||||
@@ -304,9 +304,9 @@ if (string.IsNullOrWhiteSpace(prompt) || prompt.Equals("exit", StringComparison.
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new agent session
|
||||
AgentSession session = await agentProxy.GetNewSessionAsync();
|
||||
AgentSessionId sessionId = session.GetService<AgentSessionId>();
|
||||
// Create a new agent thread
|
||||
AgentThread thread = await agentProxy.GetNewThreadAsync();
|
||||
AgentSessionId sessionId = thread.GetService<AgentSessionId>();
|
||||
string conversationId = sessionId.ToString();
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
@@ -316,7 +316,7 @@ Console.ResetColor();
|
||||
|
||||
// Run the agent in the background
|
||||
DurableAgentRunOptions options = new() { IsFireAndForget = true };
|
||||
await agentProxy.RunAsync(prompt, session, options, CancellationToken.None);
|
||||
await agentProxy.RunAsync(prompt, thread, options, CancellationToken.None);
|
||||
|
||||
bool streamCompleted = false;
|
||||
while (!streamCompleted)
|
||||
|
||||
+3
-3
@@ -61,12 +61,12 @@ public sealed class RedisStreamResponseHandler : IAgentResponseHandler
|
||||
DurableAgentContext context = DurableAgentContext.Current
|
||||
?? throw new InvalidOperationException("DurableAgentContext.Current is not set. This handler must be used within a durable agent context.");
|
||||
|
||||
// Get conversation ID from the current session context, which is only available in the context of
|
||||
// Get conversation ID from the current thread context, which is only available in the context of
|
||||
// a durable agent execution.
|
||||
string conversationId = context.CurrentSession.GetService<AgentSessionId>().ToString();
|
||||
string conversationId = context.CurrentThread.GetService<AgentSessionId>().ToString();
|
||||
if (string.IsNullOrEmpty(conversationId))
|
||||
{
|
||||
throw new InvalidOperationException("Unable to determine conversation ID from the current session.");
|
||||
throw new InvalidOperationException("Unable to determine conversation ID from the current thread.");
|
||||
}
|
||||
|
||||
string streamKey = GetStreamKey(conversationId);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>SingleWorkflow</AssemblyName>
|
||||
<RootNamespace>SingleAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an order in the system.
|
||||
/// </summary>
|
||||
internal sealed class Order
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public DateTime OrderDate { get; set; }
|
||||
public bool IsCancelled { get; set; }
|
||||
public required Customer Customer { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a customer associated with an order.
|
||||
/// </summary>
|
||||
internal sealed class Customer
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up an order by its ID.
|
||||
/// This activity simulates a database lookup with a 2 second delay.
|
||||
/// </summary>
|
||||
internal sealed class OrderLookup() : Executor<string, Order>("OrderLookup")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Log that this activity is executing (not replaying)
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine($"│ [Activity] OrderLookup: Starting lookup for order '{message}'");
|
||||
Console.ResetColor();
|
||||
|
||||
// Simulate database lookup with delay
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
Order order = new()
|
||||
{
|
||||
Id = message,
|
||||
OrderDate = DateTime.UtcNow.AddDays(-1),
|
||||
IsCancelled = false,
|
||||
Customer = new Customer { Name = "Jerry", Email = "jerry@example.com" }
|
||||
};
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine($"│ [Activity] OrderLookup: Found order '{message}' for customer '{order.Customer.Name}'");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return order;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels an order.
|
||||
/// This activity simulates a slow cancellation process with a 5 second delay.
|
||||
/// Try pressing Ctrl+C during this activity to see durability in action!
|
||||
/// </summary>
|
||||
internal sealed class OrderCancel() : Executor<Order, Order>("OrderCancel")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Log that this activity is executing (not replaying)
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine($"│ [Activity] OrderCancel: Starting cancellation for order '{message.Id}'");
|
||||
Console.WriteLine("│ [Activity] OrderCancel: ⚠️ This takes 5 seconds - try Ctrl+C!");
|
||||
Console.ResetColor();
|
||||
|
||||
// Simulate a slow cancellation process (e.g., calling external payment system)
|
||||
// This is where you can kill the process to test durability
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||
Console.WriteLine($"│ [Activity] OrderCancel: Processing... {i}/10 seconds");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
// Mark the order as cancelled
|
||||
message.IsCancelled = true;
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"│ [Activity] OrderCancel: ✓ Order '{message.Id}' has been cancelled");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a cancellation confirmation email to the customer.
|
||||
/// This activity simulates sending an email with a 1 second delay.
|
||||
/// </summary>
|
||||
internal sealed class SendEmail() : Executor<Order, string>("SendEmail")
|
||||
{
|
||||
public override async ValueTask<string> HandleAsync(Order message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Log that this activity is executing (not replaying)
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine($"│ [Activity] SendEmail: Sending email to '{message.Customer.Email}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
// Simulate email sending delay
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
string result = $"Cancellation email sent to {message.Customer.Email} for order {message.Id}.";
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("│ [Activity] SendEmail: ✓ Email sent successfully!");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to run a workflow as a durable orchestration from a console application.
|
||||
// The workflow consists of three executors: OrderLookup -> OrderCancel -> SendEmail.
|
||||
// It uses the DurableExecutionEnvironment which is injected via DI.
|
||||
//
|
||||
// DURABILITY DEMONSTRATION:
|
||||
// - Each activity has artificial delays to simulate real-world operations
|
||||
// - Stop the app (Ctrl+C or stop debugging) during the OrderCancel activity (5 seconds)
|
||||
// - Restart the application - the workflow will automatically resume!
|
||||
// - The Durable Task Framework will skip already-completed activities (OrderLookup)
|
||||
// and continue from where it left off (OrderCancel)
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.DurableTask.Client.AzureManaged;
|
||||
using Microsoft.DurableTask.Worker.AzureManaged;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SingleAgent;
|
||||
|
||||
// Get DTS connection string from environment variable
|
||||
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
|
||||
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
|
||||
|
||||
// Define executors for the workflow
|
||||
OrderLookup orderLookup = new();
|
||||
OrderCancel orderCancel = new();
|
||||
SendEmail sendEmail = new();
|
||||
|
||||
// Build the CancelOrder workflow: OrderLookup -> OrderCancel -> SendEmail
|
||||
Workflow cancelOrder = new WorkflowBuilder(orderLookup)
|
||||
.WithName("CancelOrder")
|
||||
.WithDescription("Cancel an order and notify the customer")
|
||||
.AddEdge(orderLookup, orderCancel)
|
||||
.AddEdge(orderCancel, sendEmail)
|
||||
.Build();
|
||||
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.ConfigureDurableWorkflows(
|
||||
options => options.Workflows.AddWorkflow(cancelOrder),
|
||||
workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
|
||||
clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.StartAsync();
|
||||
|
||||
// Get the IWorkflowClient from DI - no need to manually resolve DurableTaskClient
|
||||
IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
|
||||
|
||||
Console.WriteLine("Durable Workflow Sample");
|
||||
Console.WriteLine("Workflow: OrderLookup (2s) -> OrderCancel (5s) -> SendEmail (1s)");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("TIP: Stop the app during OrderCancel to test durability.");
|
||||
Console.WriteLine(" Restart - it will resume from where it left off.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Checking for pending workflows...");
|
||||
await Task.Delay(TimeSpan.FromSeconds(2));
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Enter an order ID (or 'exit'):");
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.Write("> ");
|
||||
string? input = Console.ReadLine();
|
||||
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await StartNewWorkflowAsync(input, cancelOrder, workflowClient);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
await host.StopAsync();
|
||||
|
||||
// Start a new workflow using IWorkflowClient (no DurableTaskClient needed)
|
||||
async Task StartNewWorkflowAsync(string orderId, Workflow workflow, IWorkflowClient client)
|
||||
{
|
||||
Console.WriteLine($"Starting workflow for order '{orderId}'...");
|
||||
|
||||
// RunAsync returns IRun, cast to DurableRun for durable-specific features like WaitForCompletionAsync
|
||||
await using DurableRun run = (DurableRun)await client.RunAsync(workflow, orderId);
|
||||
Console.WriteLine($"Instance ID: {run.InstanceId}");
|
||||
|
||||
try
|
||||
{
|
||||
string? result = await run.WaitForCompletionAsync();
|
||||
Console.WriteLine($"Completed: {result}");
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Console.WriteLine($"Failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
# Single Workflow Console Sample
|
||||
|
||||
This sample demonstrates how to run a workflow as a durable orchestration from a console application using the Durable Task Framework. It showcases the **durability** aspect - if the process crashes mid-execution, the workflow can be resumed without re-executing completed activities.
|
||||
|
||||
## Overview
|
||||
|
||||
The sample implements an order cancellation workflow with three executors, each with artificial delays to simulate real-world operations:
|
||||
|
||||
1. **OrderLookup** (2 seconds) - Looks up an order by its ID
|
||||
2. **OrderCancel** (5 seconds) - Marks the order as cancelled
|
||||
3. **SendEmail** (1 second) - Sends a cancellation confirmation email
|
||||
|
||||
## Durability Demonstration
|
||||
|
||||
The key feature of Durable Task Framework is **durability**:
|
||||
|
||||
|
||||
- **Activity results are persisted**: When an activity completes, its result is saved
|
||||
- **Orchestrations are replayed**: On restart, the orchestration replays from the beginning
|
||||
- **Completed activities are skipped**: The framework uses cached results for completed activities
|
||||
- **Failed activities are retried**: If an activity was interrupted, it runs again
|
||||
- **Automatic resume**: When the worker starts, it automatically picks up any pending work!
|
||||
|
||||
### Try It Yourself
|
||||
|
||||
1. Start the application and enter an order ID (e.g., `12345`)
|
||||
2. Stop the app (Ctrl+C or stop debugging) during the `OrderCancel` activity (5 seconds)
|
||||
3. Restart the application
|
||||
4. **Watch for automatic resume!** The worker automatically picks up the interrupted workflow
|
||||
5. Observe that `OrderLookup` is NOT re-executed (its result was cached)
|
||||
6. `OrderCancel` restarts from the beginning (it didn't complete)
|
||||
7. `SendEmail` runs after `OrderCancel` completes
|
||||
|
||||
The durability is completely automatic - no manual intervention needed!
|
||||
|
||||
## Workflow Flow
|
||||
|
||||
```
|
||||
User Input (Order ID)
|
||||
?
|
||||
?
|
||||
???????????????????
|
||||
? OrderLookup ? ? 2 second delay (database lookup)
|
||||
? (2 seconds) ?
|
||||
???????????????????
|
||||
?
|
||||
?
|
||||
???????????????????
|
||||
? OrderCancel ? ? 5 second delay - TRY INTERRUPTING HERE!
|
||||
? (5 seconds) ?
|
||||
???????????????????
|
||||
?
|
||||
?
|
||||
???????????????????
|
||||
? SendEmail ? ? 1 second delay (email sending)
|
||||
? (1 second) ?
|
||||
???????????????????
|
||||
?
|
||||
?
|
||||
Result
|
||||
```
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- **ConfigureDurableWorkflows** - Simplified API for registering workflows
|
||||
- **DurableExecution.RunAsync** - Start a new workflow (similar to InProcessExecution)
|
||||
- **DurableRun** - Handle to monitor and interact with a running workflow
|
||||
- **Automatic Resume** - Interrupted workflows continue automatically on restart
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
With the environment setup, you can run the sample:
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/DurableAgents/ConsoleApps/08_SingleWorkflow
|
||||
dotnet run --framework net10.0
|
||||
```
|
||||
|
||||
### Sample Session
|
||||
|
||||
```text
|
||||
??????????????????????????????????????????????????????????????????????
|
||||
? Durable Workflow Console Sample ?
|
||||
??????????????????????????????????????????????????????????????????????
|
||||
? This sample demonstrates durability in workflows. ?
|
||||
? Workflow: OrderLookup (2s) -> OrderCancel (5s) -> SendEmail (1s) ?
|
||||
??????????????????????????????????????????????????????????????????????
|
||||
|
||||
?? TIP: Stop the app during OrderCancel (5 seconds) to test durability!
|
||||
Restart the app - it will automatically resume from where it left off.
|
||||
|
||||
? Checking for pending workflows...
|
||||
|
||||
Enter an order ID to start a new workflow (or 'exit' to quit):
|
||||
|
||||
Order ID: 12345
|
||||
|
||||
Starting workflow for order '12345'...
|
||||
Instance ID: abc123-def456-...
|
||||
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? [Activity] OrderLookup: Starting lookup for order '12345'
|
||||
? [Activity] OrderLookup: Found order '12345' for customer 'Jerry'
|
||||
???????????????????????????????????????????????????????????????????
|
||||
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? [Activity] OrderCancel: Starting cancellation for order '12345'
|
||||
? [Activity] OrderCancel: ?? This takes 5 seconds - try Ctrl+C!
|
||||
? [Activity] OrderCancel: Processing... 1/5 seconds
|
||||
? [Activity] OrderCancel: Processing... 2/5 seconds
|
||||
^C <-- User stops the app here
|
||||
|
||||
[After restart...]
|
||||
|
||||
? Checking for pending workflows...
|
||||
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? [Activity] OrderCancel: Starting cancellation for order '12345' <-- Auto-resumed!
|
||||
? [Activity] OrderCancel: ?? This takes 5 seconds - try Ctrl+C!
|
||||
? [Activity] OrderCancel: Processing... 1/5 seconds
|
||||
...
|
||||
? [Activity] OrderCancel: ? Order '12345' has been cancelled
|
||||
???????????????????????????????????????????????????????????????????
|
||||
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? [Activity] SendEmail: Sending email to 'jerry@example.com'...
|
||||
? [Activity] SendEmail: ? Email sent successfully!
|
||||
???????????????????????????????????????????????????????????????????
|
||||
|
||||
Enter an order ID to start a new workflow (or 'exit' to quit):
|
||||
|
||||
Order ID: _
|
||||
```
|
||||
|
||||
Notice that when resumed:
|
||||
- `OrderLookup` was **NOT re-executed** (result was cached by Durable Task)
|
||||
- `OrderCancel` **restarted automatically** (it was interrupted before completing)
|
||||
- `SendEmail` ran normally after `OrderCancel` completed
|
||||
|
||||
## Viewing Workflow State
|
||||
|
||||
You can view the state of the workflow in the Durable Task Scheduler dashboard:
|
||||
|
||||
1. Open your browser and navigate to `http://localhost:8082`
|
||||
2. In the dashboard, you can view the state of the orchestration, including activity history and current state
|
||||
|
||||
## Related Samples
|
||||
|
||||
- [01_SingleAgent](../01_SingleAgent) - Single agent console sample
|
||||
- [02_AgentOrchestration_Chaining](../02_AgentOrchestration_Chaining) - Agent chaining with durable orchestration
|
||||
- [05_AgentOrchestration_HITL](../05_AgentOrchestration_HITL) - Human-in-the-loop orchestration
|
||||
- [09_Workflow](../../AzureFunctions/09_Workflow) - Azure Functions version of workflow hosting
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>SubWorkflows</AssemblyName>
|
||||
<RootNamespace>SubWorkflows</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,266 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SubWorkflows;
|
||||
|
||||
// ============================================
|
||||
// Order Processing Models
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Represents an order being processed.
|
||||
/// </summary>
|
||||
internal sealed class OrderInfo
|
||||
{
|
||||
public required string OrderId { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string? PaymentTransactionId { get; set; }
|
||||
public string? InventoryReservationId { get; set; }
|
||||
public string? TrackingNumber { get; set; }
|
||||
public string? Carrier { get; set; }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Main Workflow Executors
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Entry point executor that receives the order ID and creates an OrderInfo object.
|
||||
/// </summary>
|
||||
internal sealed class OrderReceived() : Executor<string, OrderInfo>("OrderReceived")
|
||||
{
|
||||
public override ValueTask<OrderInfo> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine($"[OrderReceived] Processing order '{message}'");
|
||||
Console.ResetColor();
|
||||
|
||||
OrderInfo order = new()
|
||||
{
|
||||
OrderId = message,
|
||||
Amount = 99.99m // Simulated order amount
|
||||
};
|
||||
|
||||
return ValueTask.FromResult(order);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Final executor that outputs the completed order summary.
|
||||
/// </summary>
|
||||
internal sealed class OrderCompleted() : Executor<OrderInfo, string>("OrderCompleted")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine($"│ [OrderCompleted] Order '{message.OrderId}' successfully processed!");
|
||||
Console.WriteLine($"│ Payment: {message.PaymentTransactionId}");
|
||||
Console.WriteLine($"│ Inventory: {message.InventoryReservationId}");
|
||||
Console.WriteLine($"│ Shipping: {message.Carrier} - {message.TrackingNumber}");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return ValueTask.FromResult($"Order {message.OrderId} completed. Tracking: {message.TrackingNumber}");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Payment Sub-Workflow Executors
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Validates payment information for an order.
|
||||
/// </summary>
|
||||
internal sealed class ValidatePayment() : Executor<OrderInfo, OrderInfo>("ValidatePayment")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($" [Payment/ValidatePayment] Validating payment for order '{message.OrderId}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($" [Payment/ValidatePayment] Payment validated for ${message.Amount}");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Fraud Check Sub-Sub-Workflow Executors (Level 2 nesting)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes transaction patterns for potential fraud.
|
||||
/// </summary>
|
||||
internal sealed class AnalyzePatterns() : Executor<OrderInfo, OrderInfo>("AnalyzePatterns")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||
Console.WriteLine($" [Payment/FraudCheck/AnalyzePatterns] Analyzing patterns for order '{message.OrderId}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||
Console.WriteLine(" [Payment/FraudCheck/AnalyzePatterns] ✓ Pattern analysis complete");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a risk score for the transaction.
|
||||
/// </summary>
|
||||
internal sealed class CalculateRiskScore() : Executor<OrderInfo, OrderInfo>("CalculateRiskScore")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||
Console.WriteLine($" [Payment/FraudCheck/CalculateRiskScore] Calculating risk score for order '{message.OrderId}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
int riskScore = new Random().Next(1, 100);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||
Console.WriteLine($" [Payment/FraudCheck/CalculateRiskScore] ✓ Risk score: {riskScore}/100 (Low risk)");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Charges the payment for an order.
|
||||
/// </summary>
|
||||
internal sealed class ChargePayment() : Executor<OrderInfo, OrderInfo>("ChargePayment")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($" [Payment/ChargePayment] Charging ${message.Amount} for order '{message.OrderId}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
message.PaymentTransactionId = $"TXN-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($" [Payment/ChargePayment] ✓ Payment processed: {message.PaymentTransactionId}");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Inventory Sub-Workflow Executors
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Checks inventory availability for an order.
|
||||
/// </summary>
|
||||
internal sealed class CheckInventory() : Executor<OrderInfo, OrderInfo>("CheckInventory")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine($" [Inventory/CheckInventory] Checking inventory for order '{message.OrderId}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine(" [Inventory/CheckInventory] ✓ Items available in stock");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reserves inventory for an order.
|
||||
/// </summary>
|
||||
internal sealed class ReserveInventory() : Executor<OrderInfo, OrderInfo>("ReserveInventory")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine($" [Inventory/ReserveInventory] Reserving items for order '{message.OrderId}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
message.InventoryReservationId = $"RES-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine($" [Inventory/ReserveInventory] ✓ Reserved: {message.InventoryReservationId}");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Shipping Sub-Workflow Executors
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Selects a shipping carrier for an order.
|
||||
/// </summary>
|
||||
internal sealed class SelectCarrier() : Executor<OrderInfo, OrderInfo>("SelectCarrier")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine($" [Shipping/SelectCarrier] Selecting carrier for order '{message.OrderId}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
message.Carrier = message.Amount > 50 ? "Express" : "Standard";
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine($" [Shipping/SelectCarrier] ✓ Selected carrier: {message.Carrier}");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates shipment and generates tracking number.
|
||||
/// </summary>
|
||||
internal sealed class CreateShipment() : Executor<OrderInfo, OrderInfo>("CreateShipment")
|
||||
{
|
||||
public override async ValueTask<OrderInfo> HandleAsync(OrderInfo message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine($" [Shipping/CreateShipment] Creating shipment for order '{message.OrderId}'...");
|
||||
Console.ResetColor();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
|
||||
message.TrackingNumber = $"TRACK-{Guid.NewGuid().ToString("N")[..10].ToUpperInvariant()}";
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine($" [Shipping/CreateShipment] ✓ Shipment created: {message.TrackingNumber}");
|
||||
Console.ResetColor();
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use sub-workflows within a durable orchestration.
|
||||
// Sub-workflows allow you to compose complex workflows from simpler, reusable components.
|
||||
//
|
||||
// The sample implements an order processing workflow with three sub-workflows:
|
||||
// 1. PaymentProcessing - Validates and processes payment
|
||||
// - Contains a nested FraudCheck sub-workflow (Level 2 nesting)
|
||||
// 2. InventoryManagement - Checks and reserves inventory
|
||||
// 3. ShippingArrangement - Arranges shipping and generates tracking
|
||||
//
|
||||
// The FraudCheck sub-workflow demonstrates Level 2 nesting (sub-workflow within sub-workflow):
|
||||
// - AnalyzePatterns - Analyzes transaction patterns
|
||||
// - CalculateRiskScore - Calculates fraud risk score
|
||||
//
|
||||
// Each sub-workflow runs as a separate orchestration instance, visible in the DTS dashboard.
|
||||
// This provides:
|
||||
// - Modular, reusable workflow components
|
||||
// - Independent checkpointing and replay
|
||||
// - Hierarchical visualization in the dashboard (including nested sub-workflows)
|
||||
// - Failure isolation between parent and child workflows
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.DurableTask.Client.AzureManaged;
|
||||
using Microsoft.DurableTask.Worker.AzureManaged;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SubWorkflows;
|
||||
|
||||
// Get DTS connection string from environment variable
|
||||
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
|
||||
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
|
||||
|
||||
// ============================================
|
||||
// Step 1: Build the Fraud Check sub-sub-workflow (Level 2 nesting)
|
||||
// ============================================
|
||||
AnalyzePatterns analyzePatterns = new();
|
||||
CalculateRiskScore calculateRiskScore = new();
|
||||
|
||||
Workflow fraudCheckWorkflow = new WorkflowBuilder(analyzePatterns)
|
||||
.WithName("SubFraudCheck")
|
||||
.WithDescription("Analyzes transaction patterns and calculates risk score")
|
||||
.AddEdge(analyzePatterns, calculateRiskScore)
|
||||
.Build();
|
||||
|
||||
// ============================================
|
||||
// Step 2: Build the Payment Processing sub-workflow (now with nested sub-workflow)
|
||||
// ============================================
|
||||
ValidatePayment validatePayment = new();
|
||||
ExecutorBinding fraudCheckExecutor = fraudCheckWorkflow.BindAsExecutor("FraudCheck");
|
||||
ChargePayment chargePayment = new();
|
||||
|
||||
Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
|
||||
.WithName("SubPaymentProcessing")
|
||||
.WithDescription("Validates and processes payment for an order")
|
||||
.AddEdge(validatePayment, fraudCheckExecutor)
|
||||
.AddEdge(fraudCheckExecutor, chargePayment)
|
||||
.Build();
|
||||
|
||||
// ============================================
|
||||
// Step 3: Build the Inventory Management sub-workflow
|
||||
// ============================================
|
||||
CheckInventory checkInventory = new();
|
||||
ReserveInventory reserveInventory = new();
|
||||
|
||||
Workflow inventoryWorkflow = new WorkflowBuilder(checkInventory)
|
||||
.WithName("SubInventoryManagement")
|
||||
.WithDescription("Checks availability and reserves inventory")
|
||||
.AddEdge(checkInventory, reserveInventory)
|
||||
.Build();
|
||||
|
||||
// ============================================
|
||||
// Step 4: Build the Shipping Arrangement sub-workflow
|
||||
// ============================================
|
||||
SelectCarrier selectCarrier = new();
|
||||
CreateShipment createShipment = new();
|
||||
|
||||
Workflow shippingWorkflow = new WorkflowBuilder(selectCarrier)
|
||||
.WithName("SubShippingArrangement")
|
||||
.WithDescription("Selects carrier and creates shipment")
|
||||
.AddEdge(selectCarrier, createShipment)
|
||||
.Build();
|
||||
|
||||
// ============================================
|
||||
// Step 5: Build the Main Order Processing workflow using sub-workflows
|
||||
// ============================================
|
||||
// Bind sub-workflows as executors for use in the main workflow
|
||||
ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
|
||||
ExecutorBinding inventoryExecutor = inventoryWorkflow.BindAsExecutor("Inventory");
|
||||
ExecutorBinding shippingExecutor = shippingWorkflow.BindAsExecutor("Shipping");
|
||||
|
||||
// Create entry and exit executors for the main workflow
|
||||
OrderReceived orderReceived = new();
|
||||
OrderCompleted orderCompleted = new();
|
||||
|
||||
// Build the main workflow: OrderReceived -> Payment -> Inventory -> Shipping -> OrderCompleted
|
||||
Workflow orderProcessingWorkflow = new WorkflowBuilder(orderReceived)
|
||||
.WithName("OrderProcessing")
|
||||
.WithDescription("Processes an order through payment, inventory, and shipping")
|
||||
.AddEdge(orderReceived, paymentExecutor)
|
||||
.AddEdge(paymentExecutor, inventoryExecutor)
|
||||
.AddEdge(inventoryExecutor, shippingExecutor)
|
||||
.AddEdge(shippingExecutor, orderCompleted)
|
||||
.Build();
|
||||
|
||||
// ============================================
|
||||
// Step 6: Configure and start the host
|
||||
// ============================================
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
// Register only the main workflow - sub-workflows are discovered automatically!
|
||||
services.ConfigureDurableWorkflows(
|
||||
options => options.Workflows.AddWorkflow(orderProcessingWorkflow),
|
||||
workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
|
||||
clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.StartAsync();
|
||||
|
||||
// Get the IWorkflowClient from DI
|
||||
IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
|
||||
|
||||
Console.WriteLine("╔══════════════════════════════════════════════════════════════════╗");
|
||||
Console.WriteLine("║ Durable Sub-Workflows Sample ║");
|
||||
Console.WriteLine("╠══════════════════════════════════════════════════════════════════╣");
|
||||
Console.WriteLine("║ Main Workflow: OrderProcessing ║");
|
||||
Console.WriteLine("║ ├── Payment (sub-workflow) ║");
|
||||
Console.WriteLine("║ │ ├── ValidatePayment (1s) ║");
|
||||
Console.WriteLine("║ │ ├── FraudCheck (sub-sub-workflow) ← Level 2 nesting! ║");
|
||||
Console.WriteLine("║ │ │ ├── AnalyzePatterns (1s) ║");
|
||||
Console.WriteLine("║ │ │ └── CalculateRiskScore (1s) ║");
|
||||
Console.WriteLine("║ │ └── ChargePayment (2s) ║");
|
||||
Console.WriteLine("║ ├── Inventory (sub-workflow) ║");
|
||||
Console.WriteLine("║ │ ├── CheckInventory (1s) ║");
|
||||
Console.WriteLine("║ │ └── ReserveInventory (2s) ║");
|
||||
Console.WriteLine("║ └── Shipping (sub-workflow) ║");
|
||||
Console.WriteLine("║ ├── SelectCarrier (1s) ║");
|
||||
Console.WriteLine("║ └── CreateShipment (2s) ║");
|
||||
Console.WriteLine("╚══════════════════════════════════════════════════════════════════╝");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Open the DTS dashboard at http://localhost:8080 to see the");
|
||||
Console.WriteLine("parent-child orchestration hierarchy in the Timeline view!");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Enter an order ID (or 'exit'):");
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.Write("> ");
|
||||
string? input = Console.ReadLine();
|
||||
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await StartNewWorkflowAsync(input, orderProcessingWorkflow, workflowClient);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
await host.StopAsync();
|
||||
|
||||
// Start a new workflow using IWorkflowClient
|
||||
async Task StartNewWorkflowAsync(string orderId, Workflow workflow, IWorkflowClient client)
|
||||
{
|
||||
Console.WriteLine($"\nStarting order processing for '{orderId}'...");
|
||||
|
||||
await using DurableRun run = (DurableRun)await client.RunAsync(workflow, orderId);
|
||||
Console.WriteLine($"Instance ID: {run.InstanceId}");
|
||||
Console.WriteLine("Check the DTS dashboard Timeline tab to see sub-orchestrations!");
|
||||
Console.WriteLine();
|
||||
|
||||
try
|
||||
{
|
||||
string? result = await run.WaitForCompletionAsync();
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"✓ Order completed: {result}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"✗ Failed: {ex.Message}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
# Sub-Workflows Console Sample
|
||||
|
||||
This sample demonstrates how to compose workflows hierarchically by using sub-workflows within a durable orchestration. Sub-workflows are executed as separate orchestration instances, providing modularity, reusability, and excellent visibility in the Durable Task dashboard.
|
||||
|
||||
## Overview
|
||||
|
||||
The sample implements an order processing system with three sub-workflows:
|
||||
|
||||
```
|
||||
OrderProcessing (Main Workflow)
|
||||
??? OrderReceived
|
||||
??? Payment (Sub-Workflow)
|
||||
? ??? ValidatePayment (1s)
|
||||
? ??? ChargePayment (2s)
|
||||
??? Inventory (Sub-Workflow)
|
||||
? ??? CheckInventory (1s)
|
||||
? ??? ReserveInventory (2s)
|
||||
??? Shipping (Sub-Workflow)
|
||||
? ??? SelectCarrier (1s)
|
||||
? ??? CreateShipment (2s)
|
||||
??? OrderCompleted
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Sub-Workflow Benefits
|
||||
|
||||
1. **Modularity**: Each sub-workflow encapsulates related logic (payment, inventory, shipping)
|
||||
2. **Reusability**: Sub-workflows can be used in multiple parent workflows
|
||||
3. **Independent Execution**: Each sub-workflow runs as a separate orchestration instance
|
||||
4. **Dashboard Visibility**: Sub-workflows appear in the Timeline view with parent-child relationships
|
||||
5. **Failure Isolation**: A failure in a sub-workflow doesn't corrupt the parent's state
|
||||
|
||||
### How Sub-Workflows Work
|
||||
|
||||
```csharp
|
||||
// Step 1: Build a sub-workflow
|
||||
Workflow paymentWorkflow = new WorkflowBuilder(validatePayment)
|
||||
.WithName("PaymentProcessing")
|
||||
.AddEdge(validatePayment, chargePayment)
|
||||
.Build();
|
||||
|
||||
// Step 2: Bind it as an executor for use in a parent workflow
|
||||
ExecutorBinding paymentExecutor = paymentWorkflow.BindAsExecutor("Payment");
|
||||
|
||||
// Step 3: Use the sub-workflow executor in the main workflow
|
||||
Workflow mainWorkflow = new WorkflowBuilder(orderReceived)
|
||||
.AddEdge(orderReceived, paymentExecutor) // Sub-workflow as an edge target
|
||||
.AddEdge(paymentExecutor, inventoryExecutor)
|
||||
.Build();
|
||||
|
||||
// Step 4: Register only the main workflow - sub-workflows are discovered automatically!
|
||||
services.ConfigureDurableWorkflows(
|
||||
options => options.Workflows.AddWorkflow(mainWorkflow),
|
||||
...);
|
||||
```
|
||||
|
||||
### Dashboard Visualization
|
||||
|
||||
Open the DTS dashboard at `http://localhost:8080` after running a workflow:
|
||||
|
||||
1. Click on the main orchestration instance
|
||||
2. Switch to the **Timeline** tab
|
||||
3. You'll see a hierarchical view showing:
|
||||
- `OrderProcessing` (parent orchestration)
|
||||
- `PaymentProcessing` (sub-orchestration)
|
||||
- `InventoryManagement` (sub-orchestration)
|
||||
- `ShippingArrangement` (sub-orchestration)
|
||||
|
||||
Each sub-orchestration has its own instance ID and can be inspected independently.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) file in the parent directory for information on:
|
||||
- Installing prerequisites (.NET 10+, Docker)
|
||||
- Starting the Durable Task Scheduler emulator
|
||||
- Configuring environment variables
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
# Start the DTS emulator (if not already running)
|
||||
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
|
||||
|
||||
# Run the sample
|
||||
cd dotnet/samples/DurableAgents/ConsoleApps/09_SubWorkflows
|
||||
dotnet run --framework net10.0
|
||||
```
|
||||
|
||||
### Sample Session
|
||||
|
||||
```text
|
||||
????????????????????????????????????????????????????????????????????
|
||||
? Durable Sub-Workflows Sample ?
|
||||
????????????????????????????????????????????????????????????????????
|
||||
? Main Workflow: OrderProcessing ?
|
||||
? ??? Payment (sub-workflow) ?
|
||||
? ? ??? ValidatePayment (1s) ?
|
||||
? ? ??? ChargePayment (2s) ?
|
||||
? ??? Inventory (sub-workflow) ?
|
||||
? ? ??? CheckInventory (1s) ?
|
||||
? ? ??? ReserveInventory (2s) ?
|
||||
? ??? Shipping (sub-workflow) ?
|
||||
? ??? SelectCarrier (1s) ?
|
||||
? ??? CreateShipment (2s) ?
|
||||
????????????????????????????????????????????????????????????????????
|
||||
|
||||
Open the DTS dashboard at http://localhost:8080 to see the
|
||||
parent-child orchestration hierarchy in the Timeline view!
|
||||
|
||||
Enter an order ID (or 'exit'):
|
||||
> ORD-12345
|
||||
|
||||
Starting order processing for 'ORD-12345'...
|
||||
Instance ID: abc123def456
|
||||
Check the DTS dashboard Timeline tab to see sub-orchestrations!
|
||||
|
||||
[OrderReceived] Processing order 'ORD-12345'
|
||||
[Payment/ValidatePayment] Validating payment for order 'ORD-12345'...
|
||||
[Payment/ValidatePayment] Payment validated for $99.99
|
||||
[Payment/ChargePayment] Charging $99.99 for order 'ORD-12345'...
|
||||
[Payment/ChargePayment] ? Payment processed: TXN-A1B2C3D4
|
||||
[Inventory/CheckInventory] Checking inventory for order 'ORD-12345'...
|
||||
[Inventory/CheckInventory] ? Items available in stock
|
||||
[Inventory/ReserveInventory] Reserving items for order 'ORD-12345'...
|
||||
[Inventory/ReserveInventory] ? Reserved: RES-E5F6G7H8
|
||||
[Shipping/SelectCarrier] Selecting carrier for order 'ORD-12345'...
|
||||
[Shipping/SelectCarrier] ? Selected carrier: Express
|
||||
[Shipping/CreateShipment] Creating shipment for order 'ORD-12345'...
|
||||
[Shipping/CreateShipment] ? Shipment created: TRACK-I9J0K1L2M3
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? [OrderCompleted] Order 'ORD-12345' successfully processed!
|
||||
? Payment: TXN-A1B2C3D4
|
||||
? Inventory: RES-E5F6G7H8
|
||||
? Shipping: Express - TRACK-I9J0K1L2M3
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? Order completed. Tracking: TRACK-I9J0K1L2M3
|
||||
```
|
||||
|
||||
## Comparison with In-Process Sub-Workflows
|
||||
|
||||
| Feature | In-Process | Durable |
|
||||
|---------|------------|---------|
|
||||
| Execution | Same process, synchronized supersteps | Separate orchestration instances |
|
||||
| Visibility | Single workflow view | Hierarchical dashboard view |
|
||||
| Checkpointing | Parent checkpoints include child state | Independent checkpoints per sub-workflow |
|
||||
| Failure Recovery | Parent must handle child failures | Automatic retry with state preservation |
|
||||
| Scalability | Single process | Can scale across workers |
|
||||
|
||||
## Related Samples
|
||||
|
||||
- [06_SubWorkflows (In-Process)](../../../GettingStarted/Workflows/_Foundational/06_SubWorkflows) - In-process sub-workflow execution
|
||||
- [08_SingleWorkflow](../08_SingleWorkflow) - Basic durable workflow without sub-workflows
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>SingleWorkflow</AssemblyName>
|
||||
<RootNamespace>SingleAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace WorkflowConcurrency;
|
||||
|
||||
/// <summary>
|
||||
/// Parses and validates the incoming question before sending to AI agents.
|
||||
/// </summary>
|
||||
internal sealed class ParseQuestionExecutor() : Executor<string, string>("ParseQuestion")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine("│ [ParseQuestion] Preparing question for AI agents...");
|
||||
|
||||
string formattedQuestion = message.Trim();
|
||||
if (!formattedQuestion.EndsWith('?'))
|
||||
{
|
||||
formattedQuestion += "?";
|
||||
}
|
||||
|
||||
Console.WriteLine($"│ [ParseQuestion] Question: \"{formattedQuestion}\"");
|
||||
Console.WriteLine("│ [ParseQuestion] → Sending to Physicist and Chemist in PARALLEL...");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
return ValueTask.FromResult(formattedQuestion);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates responses from all AI agents into a comprehensive answer.
|
||||
/// This is the Fan-in point where parallel results are collected.
|
||||
/// </summary>
|
||||
internal sealed class AggregatorExecutor() : Executor<string[], string>("Aggregator")
|
||||
{
|
||||
public override ValueTask<string> HandleAsync(string[] message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("┌─────────────────────────────────────────────────────────────────┐");
|
||||
Console.WriteLine($"│ [Aggregator] 📋 Received {message.Length} AI agent responses");
|
||||
Console.WriteLine("│ [Aggregator] Combining into comprehensive answer...");
|
||||
Console.WriteLine("│ [Aggregator] ✓ Aggregation complete!");
|
||||
Console.WriteLine("└─────────────────────────────────────────────────────────────────┘");
|
||||
Console.ResetColor();
|
||||
|
||||
string aggregatedResult = "═══════════════════════════════════════════════════════════════\n" +
|
||||
" AI EXPERT PANEL RESPONSES\n" +
|
||||
"═══════════════════════════════════════════════════════════════\n\n";
|
||||
|
||||
for (int i = 0; i < message.Length; i++)
|
||||
{
|
||||
string expertLabel = i == 0 ? "⚛️ PHYSICIST" : "🧪 CHEMIST";
|
||||
aggregatedResult += $"{expertLabel}:\n{message[i]}\n\n";
|
||||
}
|
||||
|
||||
aggregatedResult += "═══════════════════════════════════════════════════════════════\n" +
|
||||
$"Summary: Received perspectives from {message.Length} AI experts.\n" +
|
||||
"═══════════════════════════════════════════════════════════════";
|
||||
|
||||
return ValueTask.FromResult(aggregatedResult);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates the Fan-out/Fan-in pattern in a durable workflow.
|
||||
// The workflow uses 4 executors: 2 class-based executors and 2 AI agents.
|
||||
//
|
||||
// WORKFLOW PATTERN (4 Executors):
|
||||
//
|
||||
// ┌──────────────────┐
|
||||
// │ ParseQuestion │ ← Class-based Executor
|
||||
// └────────┬─────────┘
|
||||
// │
|
||||
// ┌────────┴────────┐
|
||||
// ▼ ▼
|
||||
// ┌──────────┐ ┌──────────┐
|
||||
// │ Physicist│ │ Chemist │ ← AI Agents (parallel)
|
||||
// └────┬─────┘ └────┬─────┘
|
||||
// │ │
|
||||
// └──────┬───────┘
|
||||
// ▼
|
||||
// ┌──────────────────┐
|
||||
// │ Aggregator │ ← Class-based Executor
|
||||
// └──────────────────┘
|
||||
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.DurableTask.Client.AzureManaged;
|
||||
using Microsoft.DurableTask.Worker.AzureManaged;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenAI.Chat;
|
||||
using WorkflowConcurrency;
|
||||
|
||||
// Configuration
|
||||
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
|
||||
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
|
||||
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
|
||||
|
||||
// Create Azure OpenAI client
|
||||
AzureOpenAIClient openAiClient = !string.IsNullOrEmpty(azureOpenAiKey)
|
||||
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
|
||||
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
|
||||
ChatClient chatClient = openAiClient.GetChatClient(deploymentName);
|
||||
|
||||
// Define the 4 executors for the workflow
|
||||
ParseQuestionExecutor parseQuestion = new(); // Executor 1: Class-based
|
||||
AIAgent physicist = chatClient.AsAIAgent("You are a physics expert. Be concise (2-3 sentences).", "Physicist"); // Executor 2: AI Agent
|
||||
AIAgent chemist = chatClient.AsAIAgent("You are a chemistry expert. Be concise (2-3 sentences).", "Chemist"); // Executor 3: AI Agent
|
||||
AggregatorExecutor aggregator = new(); // Executor 4: Class-based
|
||||
|
||||
// Build workflow: ParseQuestion → [Physicist, Chemist] (parallel) → Aggregator
|
||||
Workflow workflow = new WorkflowBuilder(parseQuestion)
|
||||
.WithName("ExpertReview")
|
||||
.AddFanOutEdge(parseQuestion, [physicist, chemist])
|
||||
.AddFanInEdge([physicist, chemist], aggregator)
|
||||
.Build();
|
||||
|
||||
// Configure and start the host
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.ConfigureDurableWorkflows(
|
||||
options => options.Workflows.AddWorkflow(workflow),
|
||||
workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
|
||||
clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.StartAsync();
|
||||
|
||||
// Get the IWorkflowClient from DI - no need to manually resolve DurableTaskClient
|
||||
IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
|
||||
|
||||
// Console UI
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine("╔═══════════════════════════════════════════════════════════════════════╗");
|
||||
Console.WriteLine("║ Fan-out/Fan-in Workflow Sample (4 Executors) ║");
|
||||
Console.WriteLine("║ ║");
|
||||
Console.WriteLine("║ ParseQuestion → [Physicist, Chemist] → Aggregator ║");
|
||||
Console.WriteLine("║ (class-based) (AI agents, parallel) (class-based) ║");
|
||||
Console.WriteLine("╚═══════════════════════════════════════════════════════════════════════╝");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine();
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2)); // Allow pending workflows to resume
|
||||
|
||||
Console.WriteLine("Enter a science question (or 'exit' to quit):");
|
||||
Console.WriteLine();
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.Write("Question: ");
|
||||
Console.ResetColor();
|
||||
|
||||
string? input = Console.ReadLine();
|
||||
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Cast to DurableRun for durable-specific features like InstanceId and WaitForCompletionAsync
|
||||
await using DurableRun run = (DurableRun)await workflowClient.RunAsync(workflow, input);
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
Console.WriteLine($"Instance: {run.InstanceId}");
|
||||
Console.ResetColor();
|
||||
|
||||
string? result = await run.WaitForCompletionAsync();
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine("\n✓ Workflow completed!\n");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"✗ Error: {ex.Message}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
await host.StopAsync();
|
||||
@@ -0,0 +1,160 @@
|
||||
# Fan-out/Fan-in Workflow with AI Agents
|
||||
|
||||
This sample demonstrates the **Fan-out/Fan-in pattern** using real AI agents in a durable workflow. A question is sent to multiple AI "expert" agents in parallel, and their responses are aggregated into a final result.
|
||||
|
||||
## Overview
|
||||
|
||||
The sample implements an expert consultation workflow using Azure OpenAI:
|
||||
- A question is parsed and prepared
|
||||
- The question is sent to **2 AI agents in parallel** (Fan-out)
|
||||
- AI agent responses are **collected and aggregated** (Fan-in)
|
||||
|
||||
### Components
|
||||
|
||||
| Component | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| ParseQuestion | Executor | Validates and formats the incoming question |
|
||||
| Physicist | AI Agent | Azure OpenAI agent with physics expertise |
|
||||
| Chemist | AI Agent | Azure OpenAI agent with chemistry expertise |
|
||||
| Aggregator | Executor | Combines all AI agent responses |
|
||||
|
||||
## Workflow Pattern
|
||||
|
||||
```
|
||||
???????????????????
|
||||
? ParseQuestion ?
|
||||
???????????????????
|
||||
?
|
||||
???????????????????????????????
|
||||
? ?
|
||||
? ?
|
||||
??????????????????? ???????????????????
|
||||
? Physicist ? ? Chemist ?
|
||||
? (AI Agent) ? ? (AI Agent) ?
|
||||
??????????????????? ???????????????????
|
||||
? ?
|
||||
? PARALLEL EXECUTION ?
|
||||
? ?
|
||||
???????????????????????????????
|
||||
?
|
||||
?
|
||||
???????????????????
|
||||
? Aggregator ?
|
||||
???????????????????
|
||||
```
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- **Fan-out (AddFanOutEdge)**: One executor sends to multiple AI agents in parallel
|
||||
- **Fan-in (AddFanInEdge)**: Multiple AI agent results are collected into an array
|
||||
- **AI Agents in Workflows**: Using Azure OpenAI agents as workflow executors
|
||||
- **Durability**: If interrupted, completed AI agent responses are preserved on restart
|
||||
|
||||
## Configuration
|
||||
|
||||
When using AI agents in durable workflows, `ConfigureDurableWorkflows` automatically registers any AI agents found in the workflow. You only need a single configuration call:
|
||||
|
||||
```csharp
|
||||
services.ConfigureDurableWorkflows(
|
||||
options => options.Workflows.AddWorkflow(expertReview),
|
||||
workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
|
||||
clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
|
||||
```
|
||||
|
||||
This is similar to how `ConfigureDurableOptions` works in Azure Functions samples.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
This sample requires:
|
||||
1. **Durable Task Scheduler** - See the [parent README](../README.md) for setup instructions
|
||||
2. **Azure OpenAI** - You need an Azure OpenAI resource with a deployed model
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` | Connection string for DTS (defaults to local emulator) |
|
||||
| `AZURE_OPENAI_ENDPOINT` | Your Azure OpenAI endpoint URL |
|
||||
| `AZURE_OPENAI_DEPLOYMENT` | Name of your deployed model (e.g., `gpt-4`) |
|
||||
| `AZURE_OPENAI_KEY` | (Optional) API key - if not set, uses Azure CLI credential |
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/DurableAgents/ConsoleApps/09_Workflow_Concurrency
|
||||
dotnet run --framework net10.0
|
||||
```
|
||||
|
||||
### Sample Session
|
||||
|
||||
```text
|
||||
??????????????????????????????????????????????????????????????????????
|
||||
? Fan-out/Fan-in Workflow with AI Agents ?
|
||||
??????????????????????????????????????????????????????????????????????
|
||||
? This sample demonstrates parallel AI agent consultation. ?
|
||||
? ?
|
||||
? Workflow: Question ? [Physicist, Chemist] ? Aggregator ?
|
||||
? (AI agents run in parallel) ?
|
||||
??????????????????????????????????????????????????????????????????????
|
||||
|
||||
?? TIP: Stop during execution to test durability - completed agent responses are preserved!
|
||||
|
||||
? Checking for pending workflows...
|
||||
|
||||
Enter a science question (or 'exit' to quit):
|
||||
Example: "What is water?"
|
||||
|
||||
Question: What is water
|
||||
|
||||
Starting expert review workflow...
|
||||
Instance ID: abc123-def456-...
|
||||
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? [ParseQuestion] Preparing question for AI agents...
|
||||
? [ParseQuestion] Question: "What is water?"
|
||||
? [ParseQuestion] ? Sending to Physicist and Chemist in PARALLEL...
|
||||
???????????????????????????????????????????????????????????????????
|
||||
|
||||
... (AI agents process in parallel)
|
||||
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? [Aggregator] ?? Received 2 AI agent responses
|
||||
? [Aggregator] Combining into comprehensive answer...
|
||||
? [Aggregator] ? Aggregation complete!
|
||||
???????????????????????????????????????????????????????????????????
|
||||
|
||||
??????????????????????????????????????????????????????????????????????
|
||||
? ? Expert review completed! ?
|
||||
??????????????????????????????????????????????????????????????????????
|
||||
|
||||
???????????????????????????????????????????????????????????????
|
||||
AI EXPERT PANEL RESPONSES
|
||||
???????????????????????????????????????????????????????????????
|
||||
|
||||
?? PHYSICIST:
|
||||
Water is H2O - two hydrogen atoms bonded to one oxygen atom. From a physics
|
||||
perspective, water exhibits unique properties like high specific heat capacity
|
||||
and surface tension due to hydrogen bonding.
|
||||
|
||||
?? CHEMIST:
|
||||
Water (H2O) is a polar molecule formed by covalent bonds. Its bent molecular
|
||||
geometry creates a dipole moment, making it an excellent solvent for ionic
|
||||
and polar compounds.
|
||||
|
||||
???????????????????????????????????????????????????????????????
|
||||
Summary: Received perspectives from 2 AI experts.
|
||||
???????????????????????????????????????????????????????????????
|
||||
```
|
||||
|
||||
## Durability with AI Agents
|
||||
|
||||
If you stop the application while one AI agent is processing:
|
||||
- The completed agent's response is **preserved** (cached by Durable Task)
|
||||
- On restart, only the incomplete agent **re-runs**
|
||||
- The aggregator waits for all agents to complete
|
||||
|
||||
## Related Samples
|
||||
|
||||
- [08_SingleWorkflow](../08_SingleWorkflow) - Sequential workflow with durability demonstration
|
||||
- [10_WorkflowConcurrent](../../AzureFunctions/10_WorkflowConcurrent) - Azure Functions version
|
||||
- [02_AgentOrchestration_Chaining](../02_AgentOrchestration_Chaining) - Agent chaining with durable orchestration
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>SingleWorkflow</AssemblyName>
|
||||
<RootNamespace>SingleAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
public record ApprovalRequest(string ExpenseId, decimal Amount, string EmployeeName);
|
||||
public record ApprovalResponse(bool Approved, string? Comments);
|
||||
|
||||
internal sealed class CreateApprovalRequest() : Executor<string, ApprovalRequest>("RetrieveRequest")
|
||||
{
|
||||
public override async ValueTask<ApprovalRequest> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Get request details from db.
|
||||
return new ApprovalRequest(message, 1500.00m, "Jerry");
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ExpenseReimburse() : Executor<ApprovalResponse, string>("Reimburse")
|
||||
{
|
||||
public override async ValueTask<string> HandleAsync(ApprovalResponse message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Simulate payment processing.
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
return $"Expense reimbursed at {DateTime.Now.ToUniversalTime()}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates a Human-in-the-Loop (HITL) workflow using Durable Tasks.
|
||||
// The workflow creates an expense approval request, waits for manager approval via an external event,
|
||||
// and then processes the expense reimbursement based on the approval response.
|
||||
// This sample mirrors the pattern used in the in-process HumanInTheLoopBasic sample.
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.DurableTask.Client.AzureManaged;
|
||||
using Microsoft.DurableTask.Worker.AzureManaged;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SingleAgent;
|
||||
|
||||
// Get DTS connection string from environment variable
|
||||
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
|
||||
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
|
||||
|
||||
// Define executors for the workflow
|
||||
CreateApprovalRequest createRequest = new();
|
||||
RequestPort<ApprovalRequest, ApprovalResponse> managerApproval = RequestPort.Create<ApprovalRequest, ApprovalResponse>("ManagerApproval");
|
||||
ExpenseReimburse reimburse = new();
|
||||
|
||||
Workflow expenseApproval = new WorkflowBuilder(createRequest)
|
||||
.WithName("ExpenseReImbursement")
|
||||
.WithDescription("Expense ReImbursement")
|
||||
.AddEdge(createRequest, managerApproval)
|
||||
.AddEdge(managerApproval, reimburse)
|
||||
.Build();
|
||||
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.ConfigureDurableWorkflows(
|
||||
options => options.Workflows.AddWorkflow(expenseApproval),
|
||||
workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
|
||||
clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.StartAsync();
|
||||
|
||||
// Get the IWorkflowClient from DI - no need to manually resolve DurableTaskClient
|
||||
IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
|
||||
|
||||
// Start the workflow with an expense ID as input
|
||||
string expenseId = "EXP-2025-001";
|
||||
Console.WriteLine($"Starting expense reimbursement workflow for expense: {expenseId}");
|
||||
|
||||
// Start the workflow and get a streaming handle
|
||||
// Cast to DurableStreamingRun for durable-specific features like InstanceId and SendResponseAsync
|
||||
await using DurableStreamingRun run = (DurableStreamingRun)await workflowClient.StreamAsync(expenseApproval, expenseId);
|
||||
|
||||
Console.WriteLine($"Workflow started with instance ID: {run.InstanceId}");
|
||||
Console.WriteLine("Watching for workflow events...\n");
|
||||
|
||||
// Watch for workflow events - similar pattern to InProcessExecution.StreamAsync
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case DurableRequestInfoEvent requestEvent:
|
||||
// Handle request for external input (human-in-the-loop)
|
||||
Console.WriteLine($"Workflow is waiting for input at RequestPort: {requestEvent.RequestPortId}");
|
||||
Console.WriteLine($" Input data: {requestEvent.Input}");
|
||||
Console.WriteLine($" Expected response type: {requestEvent.ResponseType}");
|
||||
|
||||
// Simulate manager approval
|
||||
ApprovalResponse response = HandleApprovalRequest(requestEvent);
|
||||
await run.SendResponseAsync(requestEvent, response);
|
||||
Console.WriteLine($" Response sent: Approved={response.Approved}\n");
|
||||
break;
|
||||
|
||||
case DurableWorkflowCompletedEvent completedEvent:
|
||||
// The workflow has completed
|
||||
Console.WriteLine($"Workflow completed with result: {completedEvent.Result}");
|
||||
break;
|
||||
|
||||
case DurableWorkflowFailedEvent failedEvent:
|
||||
// The workflow has failed
|
||||
Console.WriteLine($"Workflow failed: {failedEvent.ErrorMessage}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Console.ReadLine();
|
||||
await host.StopAsync();
|
||||
|
||||
// Handler for approval requests - similar to HandleExternalRequest in the in-process sample
|
||||
static ApprovalResponse HandleApprovalRequest(DurableRequestInfoEvent requestEvent)
|
||||
{
|
||||
// In a real scenario, this would involve human interaction (e.g., a web UI)
|
||||
// For this sample, we simulate automatic approval
|
||||
ApprovalRequest? request = requestEvent.GetInputAs<ApprovalRequest>();
|
||||
|
||||
if (request is not null)
|
||||
{
|
||||
Console.WriteLine($" Approval request for: {request.EmployeeName}, Amount: {request.Amount:C}");
|
||||
}
|
||||
|
||||
return new ApprovalResponse(Approved: true, Comments: "Approved by manager. Looks good!");
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
# Workflow Human-in-the-Loop (HITL) Sample
|
||||
|
||||
This sample demonstrates a **Human-in-the-Loop** pattern in durable workflows using `RequestPort`. The workflow pauses execution to wait for external input (e.g., manager approval) and resumes when the response is provided.
|
||||
|
||||
## Overview
|
||||
|
||||
The sample implements an expense approval workflow:
|
||||
|
||||
1. **CreateApprovalRequest** - Retrieves expense details and creates an approval request
|
||||
2. **ManagerApproval** (RequestPort) - Pauses workflow to wait for manager approval
|
||||
3. **ExpenseReimburse** - Processes the reimbursement based on approval response
|
||||
|
||||
## Workflow Flow
|
||||
|
||||
```
|
||||
User Input (Expense ID)
|
||||
|
|
||||
v
|
||||
+---------------------+
|
||||
| CreateApprovalRequest| Creates ApprovalRequest with expense details
|
||||
+---------------------+
|
||||
|
|
||||
v
|
||||
+---------------------+
|
||||
| ManagerApproval | RequestPort - PAUSES here waiting for external input
|
||||
| (RequestPort) | Workflow is durable while waiting
|
||||
+---------------------+
|
||||
|
|
||||
v (ApprovalResponse)
|
||||
+---------------------+
|
||||
| ExpenseReimburse | Processes reimbursement if approved
|
||||
+---------------------+
|
||||
|
|
||||
v
|
||||
Result
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **RequestPort** - A special executor that pauses the workflow and waits for external input
|
||||
- **DurableRequestInfoEvent** - Event emitted when the workflow reaches a RequestPort
|
||||
- **SendResponseAsync** - Method to provide the response and resume the workflow
|
||||
- **Durability** - The workflow can survive process restarts while waiting for human input
|
||||
|
||||
## Code Highlights
|
||||
|
||||
### Defining the RequestPort
|
||||
|
||||
```csharp
|
||||
RequestPort<ApprovalRequest, ApprovalResponse> managerApproval =
|
||||
RequestPort.Create<ApprovalRequest, ApprovalResponse>("ManagerApproval");
|
||||
```
|
||||
|
||||
### Handling the Request Event
|
||||
|
||||
```csharp
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case DurableRequestInfoEvent requestEvent:
|
||||
// Workflow is waiting for input
|
||||
ApprovalResponse response = HandleApprovalRequest(requestEvent);
|
||||
await run.SendResponseAsync(requestEvent, response);
|
||||
break;
|
||||
// ... other events
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) file in the parent directory for environment configuration.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/DurableAgents/ConsoleApps/10_Workflow_HITL
|
||||
dotnet run --framework net10.0
|
||||
```
|
||||
|
||||
### Sample Output
|
||||
|
||||
```
|
||||
Starting expense reimbursement workflow for expense: EXP-2025-001
|
||||
Workflow started with instance ID: abc123...
|
||||
Watching for workflow events...
|
||||
|
||||
Workflow is waiting for input at RequestPort: ManagerApproval
|
||||
Input data: {"ExpenseId":"EXP-2025-001","Amount":1500.00,"EmployeeName":"Jerry"}
|
||||
Expected response type: SingleAgent.ApprovalResponse
|
||||
Approval request for: Jerry, Amount: $1,500.00
|
||||
Response sent: Approved=True
|
||||
|
||||
Workflow completed with result: Expense reimbursed at 1/23/2025 5:30:00 PM
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>SingleWorkflow</AssemblyName>
|
||||
<RootNamespace>SingleAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Event emitted to report cancellation progress.
|
||||
/// </summary>
|
||||
public sealed class CancellationProgressEvent(string orderId, int percentComplete, string status)
|
||||
: WorkflowEvent($"Cancellation {percentComplete}%: {status}")
|
||||
{
|
||||
public string OrderId { get; } = orderId;
|
||||
public int PercentComplete { get; } = percentComplete;
|
||||
public string Status { get; } = status;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Event emitted when an email is sent.
|
||||
/// </summary>
|
||||
public sealed class EmailSentEvent(string email, string subject) : WorkflowEvent($"Email sent to {email}")
|
||||
{
|
||||
public string Email { get; } = email;
|
||||
public string Subject { get; } = subject;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use IWorkflowContext methods in your executors:
|
||||
// - AddEventAsync: Emit custom events that can be observed by the workflow caller
|
||||
// - YieldOutputAsync: Stream intermediate outputs during execution
|
||||
//
|
||||
// These features enable rich observability and control over workflow execution.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
#region Domain Models
|
||||
|
||||
/// <summary>
|
||||
/// Represents an order in the system.
|
||||
/// </summary>
|
||||
public sealed class Order
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public DateTime OrderDate { get; set; }
|
||||
public bool IsCancelled { get; set; }
|
||||
public required Customer Customer { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a customer associated with an order.
|
||||
/// </summary>
|
||||
public sealed class Customer
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region Custom Workflow Events
|
||||
|
||||
#endregion
|
||||
|
||||
#region Executors
|
||||
|
||||
/// <summary>
|
||||
/// Looks up an order by its ID. Demonstrates AddEventAsync for custom events.
|
||||
/// </summary>
|
||||
internal sealed class OrderLookup() : Executor<string, Order>("OrderLookup")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(
|
||||
string message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await context.AddEventAsync(new OrderLookupStartedEvent(message), cancellationToken);
|
||||
await Task.Delay(500, cancellationToken);
|
||||
|
||||
Order order = new()
|
||||
{
|
||||
Id = message,
|
||||
OrderDate = DateTime.UtcNow.AddDays(-3),
|
||||
IsCancelled = false,
|
||||
Customer = new Customer { Name = "Jerry", Email = "jerry@example.com" }
|
||||
};
|
||||
|
||||
await context.AddEventAsync(new OrderFoundEvent(order), cancellationToken);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels an order with progress reporting.
|
||||
/// Demonstrates AddEventAsync for progress events and YieldOutputAsync for streaming outputs.
|
||||
/// </summary>
|
||||
internal sealed class OrderCancel() : Executor<Order, Order>("OrderCancel")
|
||||
{
|
||||
public override async ValueTask<Order> HandleAsync(
|
||||
Order message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Simulate cancellation steps with progress events
|
||||
string[] steps = ["Validating", "Processing refund", "Finalizing"];
|
||||
for (int i = 0; i < steps.Length; i++)
|
||||
{
|
||||
await Task.Delay(500, cancellationToken);
|
||||
int percent = (i + 1) * 33;
|
||||
|
||||
// Emit progress event (callers can observe this in real-time)
|
||||
await context.AddEventAsync(new CancellationProgressEvent(message.Id, percent, steps[i]), cancellationToken);
|
||||
|
||||
// YieldOutputAsync streams intermediate results matching the executor's return type
|
||||
await context.YieldOutputAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
message.IsCancelled = true;
|
||||
await context.AddEventAsync(new OrderCancelledEvent(message.Id), cancellationToken);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a cancellation confirmation email. Demonstrates AddEventAsync for completion events.
|
||||
/// </summary>
|
||||
internal sealed class SendEmail() : Executor<Order, string>("SendEmail")
|
||||
{
|
||||
public override async ValueTask<string> HandleAsync(
|
||||
Order message,
|
||||
IWorkflowContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Task.Delay(500, cancellationToken);
|
||||
|
||||
string email = message.Customer.Email;
|
||||
await context.AddEventAsync(new EmailSentEvent(email, $"Order {message.Id} Cancelled"), cancellationToken);
|
||||
return $"Email sent to {email}";
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Event emitted when an order is successfully cancelled.
|
||||
/// </summary>
|
||||
public sealed class OrderCancelledEvent(string orderId) : WorkflowEvent($"Order {orderId} has been cancelled")
|
||||
{
|
||||
public string OrderId { get; } = orderId;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Event emitted when an order is found.
|
||||
/// </summary>
|
||||
public sealed class OrderFoundEvent(Order order) : WorkflowEvent($"Found order {order.Id} for {order.Customer.Name}")
|
||||
{
|
||||
public Order Order { get; } = order;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Event emitted when an order lookup starts.
|
||||
/// </summary>
|
||||
public sealed class OrderLookupStartedEvent(string orderId) : WorkflowEvent($"Looking up order {orderId}")
|
||||
{
|
||||
public string OrderId { get; } = orderId;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// SAMPLE: Workflow Events and IWorkflowContext Features
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
//
|
||||
// This sample demonstrates how to use IWorkflowContext methods in executors:
|
||||
//
|
||||
// 1. AddEventAsync - Emit custom events that callers can observe in real-time
|
||||
// 2. YieldOutputAsync - Stream intermediate outputs during long-running operations
|
||||
//
|
||||
// The sample uses DurableExecutionEnvironment.StreamAsync to observe events as they occur,
|
||||
// showing how callers can receive real-time updates from the workflow.
|
||||
//
|
||||
// Workflow: OrderLookup -> OrderCancel -> SendEmail
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.DurableTask.Client.AzureManaged;
|
||||
using Microsoft.DurableTask.Worker.AzureManaged;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SingleAgent;
|
||||
|
||||
// Get DTS connection string from environment variable
|
||||
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
|
||||
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
|
||||
|
||||
// Define executors and build workflow
|
||||
OrderLookup orderLookup = new();
|
||||
OrderCancel orderCancel = new();
|
||||
SendEmail sendEmail = new();
|
||||
|
||||
Workflow cancelOrder = new WorkflowBuilder(orderLookup)
|
||||
.WithName("CancelOrder")
|
||||
.WithDescription("Cancel an order and notify the customer")
|
||||
.AddEdge(orderLookup, orderCancel)
|
||||
.AddEdge(orderCancel, sendEmail)
|
||||
.Build();
|
||||
|
||||
// Configure host with durable workflow support
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.ConfigureDurableWorkflows(
|
||||
options => options.Workflows.AddWorkflow(cancelOrder),
|
||||
workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
|
||||
clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.StartAsync();
|
||||
|
||||
// Get the IWorkflowClient from DI - no need to manually resolve DurableTaskClient
|
||||
IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
|
||||
|
||||
Console.WriteLine("Workflow Events Demo - Enter order ID (or 'exit'):");
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.Write("> ");
|
||||
string? input = Console.ReadLine();
|
||||
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await RunWorkflowWithStreamingAsync(input, cancelOrder, workflowClient);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
await host.StopAsync();
|
||||
|
||||
// Runs a workflow and streams events as they occur
|
||||
async Task RunWorkflowWithStreamingAsync(string orderId, Workflow workflow, IWorkflowClient client)
|
||||
{
|
||||
// StreamAsync starts the workflow and returns a handle for observing events
|
||||
// Cast to DurableStreamingRun for durable-specific features like InstanceId
|
||||
await using DurableStreamingRun run = (DurableStreamingRun)await client.StreamAsync(workflow, orderId);
|
||||
Console.WriteLine($"Started: {run.InstanceId}");
|
||||
|
||||
// WatchStreamAsync yields events as they're emitted by executors
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
// Always print the event type name
|
||||
Console.WriteLine($" Event: {evt.GetType().Name}");
|
||||
|
||||
switch (evt)
|
||||
{
|
||||
// Custom domain events (emitted via AddEventAsync)
|
||||
case OrderLookupStartedEvent e:
|
||||
WriteColored($" [Lookup] Looking up order {e.OrderId}", ConsoleColor.Cyan);
|
||||
break;
|
||||
case OrderFoundEvent e:
|
||||
WriteColored($" [Lookup] Found: {e.Order.Customer.Name}", ConsoleColor.Cyan);
|
||||
break;
|
||||
case CancellationProgressEvent e:
|
||||
WriteColored($" [Cancel] {e.PercentComplete}% - {e.Status}", ConsoleColor.Yellow);
|
||||
break;
|
||||
case OrderCancelledEvent e:
|
||||
WriteColored(" [Cancel] Done", ConsoleColor.Yellow);
|
||||
break;
|
||||
case EmailSentEvent e:
|
||||
WriteColored($" [Email] Sent to {e.Email}", ConsoleColor.Magenta);
|
||||
break;
|
||||
|
||||
// Yielded outputs (emitted via YieldOutputAsync)
|
||||
case DurableYieldedOutputEvent e:
|
||||
WriteColored($" [Output] {e.ExecutorId}", ConsoleColor.DarkGray);
|
||||
break;
|
||||
|
||||
// Workflow completion
|
||||
case DurableWorkflowCompletedEvent e:
|
||||
WriteColored($" Completed: {e.Result}", ConsoleColor.Green);
|
||||
break;
|
||||
case DurableWorkflowFailedEvent e:
|
||||
WriteColored($" Failed: {e.ErrorMessage}", ConsoleColor.Red);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WriteColored(string message, ConsoleColor color)
|
||||
{
|
||||
Console.ForegroundColor = color;
|
||||
Console.WriteLine(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
# Single Workflow Console Sample
|
||||
|
||||
This sample demonstrates how to run a workflow as a durable orchestration from a console application using the Durable Task Framework. It showcases the **durability** aspect - if the process crashes mid-execution, the workflow can be resumed without re-executing completed activities.
|
||||
|
||||
## Overview
|
||||
|
||||
The sample implements an order cancellation workflow with three executors, each with artificial delays to simulate real-world operations:
|
||||
|
||||
1. **OrderLookup** (2 seconds) - Looks up an order by its ID
|
||||
2. **OrderCancel** (5 seconds) - Marks the order as cancelled
|
||||
3. **SendEmail** (1 second) - Sends a cancellation confirmation email
|
||||
|
||||
## Durability Demonstration
|
||||
|
||||
The key feature of Durable Task Framework is **durability**:
|
||||
|
||||
|
||||
- **Activity results are persisted**: When an activity completes, its result is saved
|
||||
- **Orchestrations are replayed**: On restart, the orchestration replays from the beginning
|
||||
- **Completed activities are skipped**: The framework uses cached results for completed activities
|
||||
- **Failed activities are retried**: If an activity was interrupted, it runs again
|
||||
- **Automatic resume**: When the worker starts, it automatically picks up any pending work!
|
||||
|
||||
### Try It Yourself
|
||||
|
||||
1. Start the application and enter an order ID (e.g., `12345`)
|
||||
2. Stop the app (Ctrl+C or stop debugging) during the `OrderCancel` activity (5 seconds)
|
||||
3. Restart the application
|
||||
4. **Watch for automatic resume!** The worker automatically picks up the interrupted workflow
|
||||
5. Observe that `OrderLookup` is NOT re-executed (its result was cached)
|
||||
6. `OrderCancel` restarts from the beginning (it didn't complete)
|
||||
7. `SendEmail` runs after `OrderCancel` completes
|
||||
|
||||
The durability is completely automatic - no manual intervention needed!
|
||||
|
||||
## Workflow Flow
|
||||
|
||||
```
|
||||
User Input (Order ID)
|
||||
?
|
||||
?
|
||||
???????????????????
|
||||
? OrderLookup ? ? 2 second delay (database lookup)
|
||||
? (2 seconds) ?
|
||||
???????????????????
|
||||
?
|
||||
?
|
||||
???????????????????
|
||||
? OrderCancel ? ? 5 second delay - TRY INTERRUPTING HERE!
|
||||
? (5 seconds) ?
|
||||
???????????????????
|
||||
?
|
||||
?
|
||||
???????????????????
|
||||
? SendEmail ? ? 1 second delay (email sending)
|
||||
? (1 second) ?
|
||||
???????????????????
|
||||
?
|
||||
?
|
||||
Result
|
||||
```
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- **ConfigureDurableWorkflows** - Simplified API for registering workflows
|
||||
- **DurableExecution.RunAsync** - Start a new workflow (similar to InProcessExecution)
|
||||
- **DurableRun** - Handle to monitor and interact with a running workflow
|
||||
- **Automatic Resume** - Interrupted workflows continue automatically on restart
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
With the environment setup, you can run the sample:
|
||||
|
||||
```bash
|
||||
cd dotnet/samples/DurableAgents/ConsoleApps/08_SingleWorkflow
|
||||
dotnet run --framework net10.0
|
||||
```
|
||||
|
||||
### Sample Session
|
||||
|
||||
```text
|
||||
??????????????????????????????????????????????????????????????????????
|
||||
? Durable Workflow Console Sample ?
|
||||
??????????????????????????????????????????????????????????????????????
|
||||
? This sample demonstrates durability in workflows. ?
|
||||
? Workflow: OrderLookup (2s) -> OrderCancel (5s) -> SendEmail (1s) ?
|
||||
??????????????????????????????????????????????????????????????????????
|
||||
|
||||
?? TIP: Stop the app during OrderCancel (5 seconds) to test durability!
|
||||
Restart the app - it will automatically resume from where it left off.
|
||||
|
||||
? Checking for pending workflows...
|
||||
|
||||
Enter an order ID to start a new workflow (or 'exit' to quit):
|
||||
|
||||
Order ID: 12345
|
||||
|
||||
Starting workflow for order '12345'...
|
||||
Instance ID: abc123-def456-...
|
||||
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? [Activity] OrderLookup: Starting lookup for order '12345'
|
||||
? [Activity] OrderLookup: Found order '12345' for customer 'Jerry'
|
||||
???????????????????????????????????????????????????????????????????
|
||||
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? [Activity] OrderCancel: Starting cancellation for order '12345'
|
||||
? [Activity] OrderCancel: ?? This takes 5 seconds - try Ctrl+C!
|
||||
? [Activity] OrderCancel: Processing... 1/5 seconds
|
||||
? [Activity] OrderCancel: Processing... 2/5 seconds
|
||||
^C <-- User stops the app here
|
||||
|
||||
[After restart...]
|
||||
|
||||
? Checking for pending workflows...
|
||||
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? [Activity] OrderCancel: Starting cancellation for order '12345' <-- Auto-resumed!
|
||||
? [Activity] OrderCancel: ?? This takes 5 seconds - try Ctrl+C!
|
||||
? [Activity] OrderCancel: Processing... 1/5 seconds
|
||||
...
|
||||
? [Activity] OrderCancel: ? Order '12345' has been cancelled
|
||||
???????????????????????????????????????????????????????????????????
|
||||
|
||||
???????????????????????????????????????????????????????????????????
|
||||
? [Activity] SendEmail: Sending email to 'jerry@example.com'...
|
||||
? [Activity] SendEmail: ? Email sent successfully!
|
||||
???????????????????????????????????????????????????????????????????
|
||||
|
||||
Enter an order ID to start a new workflow (or 'exit' to quit):
|
||||
|
||||
Order ID: _
|
||||
```
|
||||
|
||||
Notice that when resumed:
|
||||
- `OrderLookup` was **NOT re-executed** (result was cached by Durable Task)
|
||||
- `OrderCancel` **restarted automatically** (it was interrupted before completing)
|
||||
- `SendEmail` ran normally after `OrderCancel` completed
|
||||
|
||||
## Viewing Workflow State
|
||||
|
||||
You can view the state of the workflow in the Durable Task Scheduler dashboard:
|
||||
|
||||
1. Open your browser and navigate to `http://localhost:8082`
|
||||
2. In the dashboard, you can view the state of the orchestration, including activity history and current state
|
||||
|
||||
## Related Samples
|
||||
|
||||
- [01_SingleAgent](../01_SingleAgent) - Single agent console sample
|
||||
- [02_AgentOrchestration_Chaining](../02_AgentOrchestration_Chaining) - Agent chaining with durable orchestration
|
||||
- [05_AgentOrchestration_HITL](../05_AgentOrchestration_HITL) - Human-in-the-loop orchestration
|
||||
- [09_Workflow](../../AzureFunctions/09_Workflow) - Azure Functions version of workflow hosting
|
||||
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>SingleWorkflow</AssemblyName>
|
||||
<RootNamespace>SingleAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.AI.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
internal sealed class FeedbackExecutor : Executor<SloganResult>
|
||||
{
|
||||
private readonly AIAgent _agent;
|
||||
private AgentThread? _thread;
|
||||
|
||||
public int MinimumRating { get; init; } = 9;
|
||||
|
||||
public int MaxAttempts { get; init; } = 3;
|
||||
|
||||
private int _attempts;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FeedbackExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">A unique identifier for the executor.</param>
|
||||
/// <param name="chatClient">The chat client to use for the AI agent.</param>
|
||||
public FeedbackExecutor(string id, IChatClient chatClient) : base(id)
|
||||
{
|
||||
ChatClientAgentOptions agentOptions = new()
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a professional editor. You will be given a slogan and the task it is meant to accomplish.",
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<FeedbackResult>()
|
||||
}
|
||||
};
|
||||
|
||||
this._agent = new ChatClientAgent(chatClient, agentOptions);
|
||||
}
|
||||
|
||||
public override async ValueTask HandleAsync(SloganResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._thread ??= await this._agent.GetNewThreadAsync(cancellationToken);
|
||||
|
||||
var sloganMessage = $"""
|
||||
Here is a slogan for the task '{message.Task}':
|
||||
Slogan: {message.Slogan}
|
||||
Please provide feedback on this slogan, including comments, a rating from 1 to 10, and suggested actions for improvement.
|
||||
""";
|
||||
|
||||
var response = await this._agent.RunAsync(sloganMessage, this._thread, cancellationToken: cancellationToken);
|
||||
var feedback = JsonSerializer.Deserialize<FeedbackResult>(response.Text) ?? throw new InvalidOperationException("Failed to deserialize feedback.");
|
||||
|
||||
if (feedback.Rating >= this.MinimumRating)
|
||||
{
|
||||
await context.YieldOutputAsync($"The following slogan was accepted:\n\n{message.Slogan}", cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._attempts >= this.MaxAttempts)
|
||||
{
|
||||
await context.YieldOutputAsync($"The slogan was rejected after {this.MaxAttempts} attempts. Final slogan:\n\n{message.Slogan}", cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("Sending back for refining");
|
||||
await context.SendMessageAsync(feedback, cancellationToken: cancellationToken);
|
||||
this._attempts++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to run a CYCLIC WORKFLOW as a durable orchestration.
|
||||
// The workflow contains a loop: SloganWriter ⟷ FeedbackProvider
|
||||
//
|
||||
// WORKFLOW LOOP PATTERN:
|
||||
// 1. SloganWriter generates a slogan based on user input
|
||||
// 2. FeedbackProvider evaluates the slogan and provides feedback
|
||||
// 3. If the rating is below threshold, FeedbackProvider sends feedback back to SloganWriter
|
||||
// 4. SloganWriter improves the slogan based on feedback
|
||||
// 5. Loop continues until FeedbackProvider accepts the slogan (rating >= threshold)
|
||||
//
|
||||
// This demonstrates:
|
||||
// - Cyclic workflow support (back-edges in the graph)
|
||||
// - Multi-type executor handlers (SloganWriter handles both string and FeedbackResult)
|
||||
// - Message routing via SendMessageAsync for void-returning executors
|
||||
// - YieldOutputAsync for final output when the loop completes
|
||||
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.DurableTask.Client.AzureManaged;
|
||||
using Microsoft.DurableTask.Worker.AzureManaged;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SingleAgent;
|
||||
using Azure.AI.OpenAI;
|
||||
|
||||
// Get DTS connection string from environment variable
|
||||
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
|
||||
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o-mini";
|
||||
var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
|
||||
|
||||
// Define executors for the workflow
|
||||
var sloganWriter = new SloganWriterExecutor("SloganWriter", chatClient);
|
||||
var feedbackProvider = new FeedbackExecutor("FeedbackProvider", chatClient);
|
||||
|
||||
// Build the workflow by adding executors and connecting them
|
||||
var workflow = new WorkflowBuilder(sloganWriter)
|
||||
.WithName("SloganCreationWorkflow")
|
||||
.AddEdge(sloganWriter, feedbackProvider)
|
||||
.AddEdge(feedbackProvider, sloganWriter)
|
||||
.WithOutputFrom(feedbackProvider)
|
||||
.Build();
|
||||
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.ConfigureDurableWorkflows(
|
||||
options => options.Workflows.AddWorkflow(workflow),
|
||||
workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
|
||||
clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.StartAsync();
|
||||
|
||||
// Get the IWorkflowClient from DI - no need to manually resolve DurableTaskClient
|
||||
IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
|
||||
|
||||
Console.WriteLine("Workflow Events Demo - Enter input for slogan generation (or 'exit'):");
|
||||
|
||||
while (true)
|
||||
{
|
||||
Console.Write("> ");
|
||||
string? input = Console.ReadLine();
|
||||
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await RunWorkflowWithStreamingAsync(input, workflow, workflowClient);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
await host.StopAsync();
|
||||
|
||||
// Runs a workflow and streams events as they occur
|
||||
async Task RunWorkflowWithStreamingAsync(string orderId, Workflow workflow, IWorkflowClient client)
|
||||
{
|
||||
// StreamAsync starts the workflow and returns a handle for observing events
|
||||
// Cast to DurableStreamingRun for durable-specific features like InstanceId
|
||||
await using DurableStreamingRun run = (DurableStreamingRun)await client.StreamAsync(workflow, orderId);
|
||||
Console.WriteLine($"Started: {run.InstanceId}");
|
||||
|
||||
// WatchStreamAsync yields events as they're emitted by executors
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
// Always print the event type name
|
||||
WriteColored($" Event: {evt.GetType().Name}", ConsoleColor.Gray);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteColored(string message, ConsoleColor color)
|
||||
{
|
||||
Console.ForegroundColor = color;
|
||||
Console.WriteLine(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Workflow Loop Sample
|
||||
|
||||
This sample demonstrates how to run a cyclic workflow (containing loops) as a durable orchestration.
|
||||
|
||||
## Overview
|
||||
|
||||
The workflow iteratively improves a slogan based on AI feedback until it meets quality criteria.
|
||||
|
||||
### Executors
|
||||
|
||||
- **SloganWriter** - Generates slogans using AI (handles string and FeedbackResult)
|
||||
- **FeedbackProvider** - Evaluates slogans (calls YieldOutput to accept, SendMessage to loop)
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- Cyclic Workflow Support (back-edges)
|
||||
- Multi-Type Executor Handlers
|
||||
- Message Routing via SendMessageAsync
|
||||
- Workflow Termination via YieldOutputAsync
|
||||
|
||||
## Running
|
||||
|
||||
Set AZURE_OPENAI_ENDPOINT and run: dotnet run
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
public sealed class SloganResult
|
||||
{
|
||||
[JsonPropertyName("task")]
|
||||
public required string Task { get; set; }
|
||||
|
||||
[JsonPropertyName("slogan")]
|
||||
public required string Slogan { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FeedbackResult
|
||||
{
|
||||
[JsonPropertyName("comments")]
|
||||
public string Comments { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("rating")]
|
||||
public int Rating { get; set; }
|
||||
|
||||
[JsonPropertyName("actions")]
|
||||
public string Actions { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace SingleAgent;
|
||||
|
||||
internal sealed class SloganWriterExecutor : Executor
|
||||
{
|
||||
private readonly AIAgent _agent;
|
||||
private AgentThread? _thread;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SloganWriterExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">A unique identifier for the executor.</param>
|
||||
/// <param name="chatClient">The chat client to use for the AI agent.</param>
|
||||
public SloganWriterExecutor(string id, IChatClient chatClient) : base(id)
|
||||
{
|
||||
ChatClientAgentOptions agentOptions = new()
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a professional slogan writer. You will be given a task to create a slogan.",
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<SloganResult>()
|
||||
}
|
||||
};
|
||||
|
||||
this._agent = new ChatClientAgent(chatClient, agentOptions);
|
||||
}
|
||||
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
|
||||
routeBuilder.AddHandler<string, SloganResult>(this.HandleAsync)
|
||||
.AddHandler<FeedbackResult, SloganResult>(this.HandleAsync);
|
||||
|
||||
public async ValueTask<SloganResult> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._thread ??= await this._agent.GetNewThreadAsync(cancellationToken);
|
||||
|
||||
var result = await this._agent.RunAsync(message, this._thread, cancellationToken: cancellationToken);
|
||||
|
||||
return JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
|
||||
}
|
||||
|
||||
public async ValueTask<SloganResult> HandleAsync(FeedbackResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var feedbackMessage = $"""
|
||||
Here is the feedback on your previous slogan:
|
||||
Comments: {message.Comments}
|
||||
Rating: {message.Rating}
|
||||
Suggested Actions: {message.Actions}
|
||||
|
||||
Please use this feedback to improve your slogan.
|
||||
""";
|
||||
|
||||
var result = await this._agent.RunAsync(feedbackMessage, this._thread, cancellationToken: cancellationToken);
|
||||
return JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ This directory contains samples for console app hosting of durable agents. These
|
||||
- **[05_AgentOrchestration_HITL](05_AgentOrchestration_HITL)**: A sample that demonstrates how to implement a human-in-the-loop workflow using durable orchestration, including interactive approval prompts.
|
||||
- **[06_LongRunningTools](06_LongRunningTools)**: A sample that demonstrates how agents can start and interact with durable orchestrations from tool calls to enable long-running tool scenarios.
|
||||
- **[07_ReliableStreaming](07_ReliableStreaming)**: A sample that demonstrates how to implement reliable streaming for durable agents using Redis Streams, enabling clients to disconnect and reconnect without losing messages.
|
||||
- **[08_SingleWorkflow](08_SingleWorkflow)**: A sample that demonstrates how to run a simple workflow as a durable orchestration, showcasing activity durability and automatic resume on restart.
|
||||
- **[09_SubWorkflows](09_SubWorkflows)**: A sample that demonstrates how to compose workflows hierarchically using sub-workflows, which run as separate orchestration instances visible in the DTS dashboard.
|
||||
|
||||
## Running the Samples
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
|
||||
namespace WorkflowExecutorsAndEdgesSample;
|
||||
|
||||
/// <summary>
|
||||
/// This sample introduces the concepts of executors and edges in a workflow.
|
||||
///
|
||||
/// Workflows are built from executors (processing units) connected by edges (data flow paths).
|
||||
/// In this example, we create a simple text processing pipeline that:
|
||||
/// 1. Takes input text and converts it to uppercase using an UppercaseExecutor
|
||||
/// 2. Takes the uppercase text and reverses it using a ReverseTextExecutor
|
||||
///
|
||||
/// The executors are connected sequentially, so data flows from one to the next in order.
|
||||
/// For input "Hello, World!", the workflow produces "!DLROW ,OLLEH".
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Create the executors
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
ReverseTextExecutor reverse = new();
|
||||
|
||||
// Build the workflow by connecting executors sequentially
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
var workflow = builder.Build();
|
||||
|
||||
// Execute the workflow with input data
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!");
|
||||
foreach (WorkflowEvent evt in run.NewEvents)
|
||||
{
|
||||
if (evt is ExecutorCompletedEvent executorComplete)
|
||||
{
|
||||
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Second executor: reverses the input text and completes the workflow.
|
||||
/// </summary>
|
||||
internal sealed class ReverseTextExecutor() : Executor<string, string>("ReverseTextExecutor")
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes the input message by reversing the text.
|
||||
/// </summary>
|
||||
/// <param name="message">The input text to reverse</param>
|
||||
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The input text reversed</returns>
|
||||
public override ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Because we do not suppress it, the returned result will be yielded as an output from this executor.
|
||||
return ValueTask.FromResult(string.Concat(message.Reverse()));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user