Skip to content

Commit 0f36eec

Browse files
.NET: Durable Agent samples and automated validation for non-Azure Functions (#3042)
* Durable Agent samples and automated validation for non-Azure Functions * Update test projects * fix file encoding * Remove AgentThreadMetadata usage * Absorb breaking change from #3152 * Absorb newer breaking changes (AgentRunResponse --> AgentResponse) * Absorb more breaking changes (see #3222) --------- Co-authored-by: Mark Wallace <[email protected]>
1 parent b773830 commit 0f36eec

File tree

32 files changed

+3920
-7
lines changed

32 files changed

+3920
-7
lines changed

dotnet/agent-framework-dotnet.slnx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@
3535
<Project Path="samples/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
3636
<Project Path="samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
3737
</Folder>
38+
<Folder Name="/Samples/DurableAgents/">
39+
<File Path="samples/DurableAgents/ConsoleApps/README.md" />
40+
</Folder>
41+
<Folder Name="/Samples/DurableAgents/ConsoleApps/">
42+
<Project Path="samples/DurableAgents/ConsoleApps/01_SingleAgent/01_SingleAgent.csproj" />
43+
<Project Path="samples/DurableAgents/ConsoleApps/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
44+
<Project Path="samples/DurableAgents/ConsoleApps/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
45+
<Project Path="samples/DurableAgents/ConsoleApps/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
46+
<Project Path="samples/DurableAgents/ConsoleApps/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
47+
<Project Path="samples/DurableAgents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
48+
<Project Path="samples/DurableAgents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
49+
</Folder>
3850
<Folder Name="/Samples/GettingStarted/">
3951
<File Path="samples/GettingStarted/README.md" />
4052
</Folder>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>net10.0</TargetFrameworks>
4+
<OutputType>Exe</OutputType>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<AssemblyName>SingleAgent</AssemblyName>
8+
<RootNamespace>SingleAgent</RootNamespace>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Azure.AI.OpenAI" />
13+
<PackageReference Include="Azure.Identity" />
14+
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" />
15+
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" />
16+
<PackageReference Include="Microsoft.Extensions.Hosting" />
17+
</ItemGroup>
18+
19+
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
20+
<!--
21+
<ItemGroup>
22+
<PackageReference Include="Microsoft.Agents.AI.DurableTask" />
23+
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
24+
</ItemGroup>
25+
-->
26+
<ItemGroup>
27+
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
28+
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
29+
</ItemGroup>
30+
</Project>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Azure;
4+
using Azure.AI.OpenAI;
5+
using Azure.Identity;
6+
using Microsoft.Agents.AI;
7+
using Microsoft.Agents.AI.DurableTask;
8+
using Microsoft.DurableTask.Client.AzureManaged;
9+
using Microsoft.DurableTask.Worker.AzureManaged;
10+
using Microsoft.Extensions.AI;
11+
using Microsoft.Extensions.DependencyInjection;
12+
using Microsoft.Extensions.Hosting;
13+
using Microsoft.Extensions.Logging;
14+
using OpenAI.Chat;
15+
16+
// Get the Azure OpenAI endpoint and deployment name from environment variables.
17+
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
18+
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
19+
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
20+
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
21+
22+
// Get DTS connection string from environment variable
23+
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
24+
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
25+
26+
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
27+
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
28+
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
29+
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
30+
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
31+
32+
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
33+
const string JokerName = "Joker";
34+
const string JokerInstructions = "You are good at telling jokes.";
35+
36+
AIAgent agent = client.GetChatClient(deploymentName).AsAIAgent(JokerInstructions, JokerName);
37+
38+
// Configure the console app to host the AI agent.
39+
IHost host = Host.CreateDefaultBuilder(args)
40+
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Warning))
41+
.ConfigureServices(services =>
42+
{
43+
services.ConfigureDurableAgents(
44+
options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)),
45+
workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
46+
clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
47+
})
48+
.Build();
49+
50+
await host.StartAsync();
51+
52+
// Get the agent proxy from services
53+
IServiceProvider services = host.Services;
54+
AIAgent agentProxy = services.GetRequiredKeyedService<AIAgent>(JokerName);
55+
56+
// Console colors for better UX
57+
Console.ForegroundColor = ConsoleColor.Cyan;
58+
Console.WriteLine("=== Single Agent Console Sample ===");
59+
Console.ResetColor();
60+
Console.WriteLine("Enter a message for the Joker agent (or 'exit' to quit):");
61+
Console.WriteLine();
62+
63+
// Create a thread for the conversation
64+
AgentThread thread = await agentProxy.GetNewThreadAsync();
65+
66+
while (true)
67+
{
68+
// Read input from stdin
69+
Console.ForegroundColor = ConsoleColor.Yellow;
70+
Console.Write("You: ");
71+
Console.ResetColor();
72+
73+
string? input = Console.ReadLine();
74+
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
75+
{
76+
break;
77+
}
78+
79+
// Run the agent
80+
Console.ForegroundColor = ConsoleColor.Green;
81+
Console.Write("Joker: ");
82+
Console.ResetColor();
83+
84+
try
85+
{
86+
AgentResponse agentResponse = await agentProxy.RunAsync(
87+
message: input,
88+
thread: thread,
89+
cancellationToken: CancellationToken.None);
90+
91+
Console.WriteLine(agentResponse.Text);
92+
Console.WriteLine();
93+
}
94+
catch (Exception ex)
95+
{
96+
Console.ForegroundColor = ConsoleColor.Red;
97+
Console.Error.WriteLine($"Error: {ex.Message}");
98+
Console.ResetColor();
99+
Console.WriteLine();
100+
}
101+
}
102+
103+
await host.StopAsync();
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Single Agent Sample
2+
3+
This sample demonstrates how to use the durable agents extension to create a simple console app that hosts a single AI agent and provides interactive conversation via stdin/stdout.
4+
5+
## Key Concepts Demonstrated
6+
7+
- Using the Microsoft Agent Framework to define a simple AI agent with a name and instructions.
8+
- Registering durable agents with the console app and running them interactively.
9+
- Conversation management (via threads) for isolated interactions.
10+
11+
## Environment Setup
12+
13+
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.
14+
15+
## Running the Sample
16+
17+
With the environment setup, you can run the sample:
18+
19+
```bash
20+
cd dotnet/samples/DurableAgents/ConsoleApps/01_SingleAgent
21+
dotnet run --framework net10.0
22+
```
23+
24+
The app will prompt you for input. You can interact with the Joker agent:
25+
26+
```text
27+
=== Single Agent Console Sample ===
28+
Enter a message for the Joker agent (or 'exit' to quit):
29+
30+
You: Tell me a joke about a pirate.
31+
Joker: Why don't pirates ever learn the alphabet? Because they always get stuck at "C"!
32+
33+
You: Now explain the joke.
34+
Joker: The joke plays on the word "sea" (C), which pirates are famously associated with...
35+
36+
You: exit
37+
```
38+
39+
## Scriptable Usage
40+
41+
You can also pipe input to the app for scriptable usage:
42+
43+
```bash
44+
echo "Tell me a joke about a pirate." | dotnet run
45+
```
46+
47+
The app will read from stdin, process the input, and write the response to stdout.
48+
49+
## Viewing Agent State
50+
51+
You can view the state of the agent in the Durable Task Scheduler dashboard:
52+
53+
1. Open your browser and navigate to `http://localhost:8082`
54+
2. In the dashboard, you can view the state of the Joker agent, including its conversation history and current state
55+
56+
The agent maintains conversation state across multiple interactions, and you can inspect this state in the dashboard to understand how the durable agents extension manages conversation context.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>net10.0</TargetFrameworks>
4+
<OutputType>Exe</OutputType>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<AssemblyName>AgentOrchestration_Chaining</AssemblyName>
8+
<RootNamespace>AgentOrchestration_Chaining</RootNamespace>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Azure.AI.OpenAI" />
13+
<PackageReference Include="Azure.Identity" />
14+
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" />
15+
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" />
16+
<PackageReference Include="Microsoft.Extensions.Hosting" />
17+
</ItemGroup>
18+
19+
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
20+
<!--
21+
<ItemGroup>
22+
<PackageReference Include="Microsoft.Agents.AI.DurableTask" />
23+
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
24+
</ItemGroup>
25+
-->
26+
<ItemGroup>
27+
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
28+
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
29+
</ItemGroup>
30+
</Project>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
namespace AgentOrchestration_Chaining;
4+
5+
// Response model
6+
public sealed record TextResponse(string Text);
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using AgentOrchestration_Chaining;
4+
using Azure;
5+
using Azure.AI.OpenAI;
6+
using Azure.Identity;
7+
using Microsoft.Agents.AI;
8+
using Microsoft.Agents.AI.DurableTask;
9+
using Microsoft.DurableTask;
10+
using Microsoft.DurableTask.Client;
11+
using Microsoft.DurableTask.Client.AzureManaged;
12+
using Microsoft.DurableTask.Worker;
13+
using Microsoft.DurableTask.Worker.AzureManaged;
14+
using Microsoft.Extensions.DependencyInjection;
15+
using Microsoft.Extensions.Hosting;
16+
using Microsoft.Extensions.Logging;
17+
using OpenAI.Chat;
18+
using Environment = System.Environment;
19+
20+
// Get the Azure OpenAI endpoint and deployment name from environment variables.
21+
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
22+
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
23+
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
24+
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
25+
26+
// Get DTS connection string from environment variable
27+
string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
28+
?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
29+
30+
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
31+
string? azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
32+
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
33+
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
34+
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
35+
36+
// Single agent used by the orchestration to demonstrate sequential calls on the same thread.
37+
const string WriterName = "WriterAgent";
38+
const string WriterInstructions =
39+
"""
40+
You refine short pieces of text. When given an initial sentence you enhance it;
41+
when given an improved sentence you polish it further.
42+
""";
43+
44+
AIAgent writerAgent = client.GetChatClient(deploymentName).AsAIAgent(WriterInstructions, WriterName);
45+
46+
// Orchestrator function
47+
static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context)
48+
{
49+
DurableAIAgent writer = context.GetAgent("WriterAgent");
50+
AgentThread writerThread = await writer.GetNewThreadAsync();
51+
52+
AgentResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
53+
message: "Write a concise inspirational sentence about learning.",
54+
thread: writerThread);
55+
56+
AgentResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
57+
message: $"Improve this further while keeping it under 25 words: {initial.Result.Text}",
58+
thread: writerThread);
59+
60+
return refined.Result.Text;
61+
}
62+
63+
// Configure the console app to host the AI agent.
64+
IHost host = Host.CreateDefaultBuilder(args)
65+
.ConfigureLogging(loggingBuilder => loggingBuilder.SetMinimumLevel(LogLevel.Warning))
66+
.ConfigureServices(services =>
67+
{
68+
services.ConfigureDurableAgents(
69+
options => options.AddAIAgent(writerAgent),
70+
workerBuilder: builder =>
71+
{
72+
builder.UseDurableTaskScheduler(dtsConnectionString);
73+
builder.AddTasks(registry => registry.AddOrchestratorFunc(nameof(RunOrchestratorAsync), RunOrchestratorAsync));
74+
},
75+
clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
76+
})
77+
.Build();
78+
79+
await host.StartAsync();
80+
81+
DurableTaskClient durableClient = host.Services.GetRequiredService<DurableTaskClient>();
82+
83+
// Console colors for better UX
84+
Console.ForegroundColor = ConsoleColor.Cyan;
85+
Console.WriteLine("=== Single Agent Orchestration Chaining Sample ===");
86+
Console.ResetColor();
87+
Console.WriteLine("Starting orchestration...");
88+
Console.WriteLine();
89+
90+
try
91+
{
92+
// Start the orchestration
93+
string instanceId = await durableClient.ScheduleNewOrchestrationInstanceAsync(
94+
orchestratorName: nameof(RunOrchestratorAsync));
95+
96+
Console.ForegroundColor = ConsoleColor.Gray;
97+
Console.WriteLine($"Orchestration started with instance ID: {instanceId}");
98+
Console.WriteLine("Waiting for completion...");
99+
Console.ResetColor();
100+
101+
// Wait for orchestration to complete
102+
OrchestrationMetadata status = await durableClient.WaitForInstanceCompletionAsync(
103+
instanceId,
104+
getInputsAndOutputs: true,
105+
CancellationToken.None);
106+
107+
Console.WriteLine();
108+
109+
if (status.RuntimeStatus == OrchestrationRuntimeStatus.Completed)
110+
{
111+
Console.ForegroundColor = ConsoleColor.Green;
112+
Console.WriteLine("✓ Orchestration completed successfully!");
113+
Console.ResetColor();
114+
Console.WriteLine();
115+
Console.ForegroundColor = ConsoleColor.Yellow;
116+
Console.Write("Result: ");
117+
Console.ResetColor();
118+
Console.WriteLine(status.ReadOutputAs<string>());
119+
}
120+
else if (status.RuntimeStatus == OrchestrationRuntimeStatus.Failed)
121+
{
122+
Console.ForegroundColor = ConsoleColor.Red;
123+
Console.WriteLine("✗ Orchestration failed!");
124+
Console.ResetColor();
125+
if (status.FailureDetails != null)
126+
{
127+
Console.WriteLine($"Error: {status.FailureDetails.ErrorMessage}");
128+
}
129+
Environment.Exit(1);
130+
}
131+
else
132+
{
133+
Console.ForegroundColor = ConsoleColor.Yellow;
134+
Console.WriteLine($"Orchestration status: {status.RuntimeStatus}");
135+
Console.ResetColor();
136+
}
137+
}
138+
catch (Exception ex)
139+
{
140+
Console.ForegroundColor = ConsoleColor.Red;
141+
Console.Error.WriteLine($"Error: {ex.Message}");
142+
Console.ResetColor();
143+
Environment.Exit(1);
144+
}
145+
finally
146+
{
147+
await host.StopAsync();
148+
}

0 commit comments

Comments
 (0)