Shared state support using durable entity.

This commit is contained in:
Shyju Krishnankutty
2026-01-20 08:56:12 -08:00
parent 5ddb4cd546
commit 8c3182e4e8
15 changed files with 884 additions and 101 deletions
@@ -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,64 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
namespace SingleAgent;
public static class MyOrchFunction
{
[Function(nameof(MyOrchFunction))]
public static async Task<List<string>> RunOrchestratorAsync(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchFunction));
logger.LogInformation("Saying hello.");
var outputs = new List<string>();
// Replace name and input with values relevant for your Durable Functions Activity
outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo"));
outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Seattle"));
outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "London"));
// returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
return outputs;
}
[Function(nameof(SayHello))]
public static string SayHello([ActivityTrigger] string name,
TaskActivityContext taskActivityContext,
FunctionContext functionContext)
{
ILogger logger = functionContext.GetLogger("SayHello");
if (logger.IsEnabled(LogLevel.Information))
{
var instanceId = (string)functionContext.BindingContext.BindingData["instanceId"]!;
logger.LogInformation("Saying hello to {Name} for instanceId:{InstanceId}", name, instanceId);
}
return $"Hello {name}!";
}
[Function("MyOrchFunction_HttpStart")]
public static async Task<HttpResponseData> HttpStartAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext)
{
ILogger logger = executionContext.GetLogger("MyOrchFunction_HttpStart");
// Function input comes from the request content.
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
nameof(MyOrchFunction));
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Started orchestration with ID = '{InstanceId}'.", instanceId);
}
// Returns an HTTP 202 response with an instance management payload.
// See https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-http-api#start-orchestration
return await client.CreateCheckStatusResponseAsync(req, instanceId);
}
}
@@ -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,29 @@
// 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);
// Optional - Configure AI agents
// options.Agents.AddAIAgent(agent);
})
.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"
}
}
}
}