mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Conditional edge routing sample.
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user