Conditional edge routing sample.

This commit is contained in:
Shyju Krishnankutty
2026-01-22 21:27:53 -08:00
parent 29fb22f7d8
commit 04fdf25019
12 changed files with 513 additions and 9 deletions
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
@@ -14,6 +14,10 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<None Include="local.settings.json" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
@@ -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,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,24 @@
// 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);
builder.AddEdge(orderEnrich, notifyFraud, condition: OrderRouteConditions.WhenBlocked());
builder.AddEdge(orderEnrich, paymentProcessor, condition: OrderRouteConditions.WhenNotBlocked());
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
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"
}
}
}
}