Minor cleanups

This commit is contained in:
Shyju Krishnankutty
2026-01-20 20:20:15 -08:00
parent 530f8b389a
commit 00650f2525
27 changed files with 295 additions and 751 deletions
@@ -0,0 +1,59 @@
// 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 OrderLookupExecutor() : 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 OrderEnricherExecutor() : 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 PaymentProcessorExecutor() : 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 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);
@@ -1,87 +1,41 @@
// 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.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());
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
const string FeedbackAnalysisInstructions = """
You are a Customer Feedback Analyzer. Your task is to analyze customer survey responses and categorize them accurately.
INPUT: You will receive customer feedback text that may include a rating and comments.
OUTPUT: Return ONLY ONE category from this list:
- Bug Report
- General Feedback
- Billing Question
- Support Incident Status
CATEGORIZATION RULES:
- "Bug Report": Technical issues, errors, crashes, features not working as expected
- "General Feedback": Suggestions, compliments, general comments about the product or service
- "Billing Question": Payment issues, subscription inquiries, pricing questions, refund requests
- "Support Incident Status": Follow-ups on existing tickets, status inquiries about previous issues
RESPONSE FORMAT: Return only the category name exactly as shown above, with no additional text or explanation.
""";
AIAgent agent = client.GetChatClient(deploymentName).CreateAIAgent(JokerInstructions, JokerName);
// 3 Executors usd by the workflow.
// 1. Class based executor to parse survey response.
SurveyResponseParserExecutor surveyResponseParserExecutor = new();
// 2. AI Agent to analyze feedback and categorize it.
AIAgent surveyFeedbackAgent = client.GetChatClient(deploymentName).CreateAIAgent(FeedbackAnalysisInstructions, "FeedbackAnalyzerAgent");
// 3. Function delegate executor to route response based on category.
Func<string, string> responseHandlingExecutorFunc = input => input switch
Func<string, string> orderParserFunc = input =>
{
var s when s.Contains("billing", StringComparison.OrdinalIgnoreCase)
=> "Will notify Billing Team",
var s when s.Contains("Bug Report", StringComparison.OrdinalIgnoreCase)
=> "Will notify Technical Support Team",
_ => "Will notify General Support Team"
};
var responseRouterExecutor = responseHandlingExecutorFunc.BindAsExecutor("ResponseRouterExecutor");
WorkflowBuilder builder = new(surveyResponseParserExecutor);
builder.AddEdge(surveyResponseParserExecutor, surveyFeedbackAgent);
builder.AddEdge(surveyFeedbackAgent, responseRouterExecutor).WithOutputFrom(responseRouterExecutor);
var workflow = builder.WithName("HandleSurveyResponse").Build();
FunctionsApplication.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(agent))
.ConfigureDurableOptions(options =>
// 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)
{
// Configure workflows
options.Workflows.AddWorkflow(workflow);
return input[^OrderIdPartLength..];
}
// Optional - Configure AI agents
// options.Agents.AddAIAgent(agent);
})
.Build().Run();
return input;
};
var orderParserExecutor = orderParserFunc.BindAsExecutor("OrderParser");
OrderLookupExecutor orderLookupExecutor = new();
OrderEnricherExecutor orderEnricherExeecutor = new();
PaymentProcessorExecutor paymentProcessorExecutor = new();
Workflow workflow = 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)
.WithOutputFrom(paymentProcessorExecutor)
.Build();
var host = FunctionsApplication.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableOptions(options => options.Workflows.AddWorkflow(workflow))
.Build();
host.Run();
@@ -1,82 +0,0 @@
// 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 = ParseSurveyResponse(message);
string jsonResult = JsonSerializer.Serialize(response, s_jsonOptions);
return ValueTask.FromResult(jsonResult);
}
private static 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;
}
}
@@ -1,14 +1,14 @@
# Default endpoint address for local testing
@authority=http://localhost:7071
### Start the workflow
POST {{authority}}/api/workflows/HandleSurveyResponse/run
### Look up a long order reference id
POST {{authority}}/api/workflows/FulfillOrder/run
Content-Type: text/plain
Rating: 10. Why was I charged $99 when my plan should be $49? I need a refund for the overcharge
QWERTY80853
### Start the workflow with another bug report input.
POST {{authority}}/api/workflows/HandleSurveyResponse/run
### Look up a short order id
POST {{authority}}/api/workflows/FulfillOrder/run
Content-Type: text/plain
Rating: 3. The app throws an InvalidOperationException when starting up.
12345
@@ -1,48 +0,0 @@
<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>
@@ -1,215 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows;
namespace SingleAgent;
/// <summary>
/// Represents the details of a customer order.
/// </summary>
public sealed class OrderDetails
{
public int OrderId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string CustomerEmail { get; set; } = string.Empty;
public List<OrderItem> Items { get; set; } = [];
public decimal TotalAmount { get; set; }
public OrderStatus Status { get; set; }
public DateTime OrderDate { get; set; }
public DateTime? EstimatedDelivery { get; set; }
}
/// <summary>
/// Represents an item in an order.
/// </summary>
public sealed class OrderItem
{
public string ProductName { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Total => this.Quantity * this.UnitPrice;
}
/// <summary>
/// Represents the status of an order.
/// </summary>
public enum OrderStatus
{
Pending,
Processing,
Shipped,
Delivered,
Cancelled
}
/// <summary>
/// Executor that looks up order details by order ID.
/// Input: int (orderId)
/// Output: OrderDetails
/// </summary>
internal sealed class OrderLookupExecutor() : Executor<int, OrderDetails>("OrderLookupExecutor")
{
// Simulated order database
private static readonly Dictionary<int, OrderDetails> s_orders = new()
{
[1001] = new OrderDetails
{
OrderId = 1001,
CustomerName = "Alice Johnson",
CustomerEmail = "alice@example.com",
Items =
[
new OrderItem { ProductName = "Wireless Headphones", Quantity = 1, UnitPrice = 79.99m },
new OrderItem { ProductName = "Phone Case", Quantity = 2, UnitPrice = 15.99m }
],
TotalAmount = 111.97m,
Status = OrderStatus.Shipped,
OrderDate = DateTime.UtcNow.AddDays(-3),
EstimatedDelivery = DateTime.UtcNow.AddDays(2)
},
[1002] = new OrderDetails
{
OrderId = 1002,
CustomerName = "Bob Smith",
CustomerEmail = "bob@example.com",
Items =
[
new OrderItem { ProductName = "Laptop Stand", Quantity = 1, UnitPrice = 49.99m }
],
TotalAmount = 49.99m,
Status = OrderStatus.Processing,
OrderDate = DateTime.UtcNow.AddDays(-1),
EstimatedDelivery = DateTime.UtcNow.AddDays(5)
},
[1003] = new OrderDetails
{
OrderId = 1003,
CustomerName = "Carol Davis",
CustomerEmail = "carol@example.com",
Items =
[
new OrderItem { ProductName = "USB-C Hub", Quantity = 1, UnitPrice = 35.00m },
new OrderItem { ProductName = "HDMI Cable", Quantity = 3, UnitPrice = 12.99m },
new OrderItem { ProductName = "Webcam", Quantity = 1, UnitPrice = 89.99m }
],
TotalAmount = 163.96m,
Status = OrderStatus.Delivered,
OrderDate = DateTime.UtcNow.AddDays(-7),
EstimatedDelivery = null
}
};
public override ValueTask<OrderDetails> HandleAsync(int message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (s_orders.TryGetValue(message, out OrderDetails? order))
{
return ValueTask.FromResult(order);
}
// Return a "not found" order
return ValueTask.FromResult(new OrderDetails
{
OrderId = message,
CustomerName = "Unknown",
Status = OrderStatus.Cancelled,
OrderDate = DateTime.UtcNow
});
}
}
/// <summary>
/// Executor that generates a human-readable summary from order details.
/// Input: OrderDetails
/// Output: string
/// </summary>
internal sealed class OrderSummaryExecutor() : Executor<OrderDetails, string>("OrderSummaryExecutor")
{
public override ValueTask<string> HandleAsync(OrderDetails message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (message.CustomerName == "Unknown")
{
return ValueTask.FromResult($"❌ Order #{message.OrderId} was not found in our system.");
}
string statusEmoji = message.Status switch
{
OrderStatus.Pending => "⏳",
OrderStatus.Processing => "🔄",
OrderStatus.Shipped => "📦",
OrderStatus.Delivered => "✅",
OrderStatus.Cancelled => "❌",
_ => "❓"
};
string itemsList = string.Join("\n", message.Items.Select(i =>
$" • {i.ProductName} (x{i.Quantity}) - ${i.Total:F2}"));
string deliveryInfo = message.Status == OrderStatus.Delivered
? "Delivered!"
: message.EstimatedDelivery.HasValue
? $"Expected: {message.EstimatedDelivery.Value:MMM dd, yyyy}"
: "Calculating...";
string summary = $"""
═══════════════════════════════════════
📋 ORDER SUMMARY - #{message.OrderId}
═══════════════════════════════════════
👤 Customer: {message.CustomerName}
📧 Email: {message.CustomerEmail}
📅 Order Date: {message.OrderDate:MMM dd, yyyy}
{statusEmoji} Status: {message.Status}
🚚 Delivery: {deliveryInfo}
─────────────────────────────────────────
📦 ITEMS:
{itemsList}
─────────────────────────────────────────
💰 TOTAL: ${message.TotalAmount:F2}
═══════════════════════════════════════
""";
return ValueTask.FromResult(summary);
}
}
/// <summary>
/// Executor that parses a string input to extract an order ID.
/// Input: string (e.g., "Check order 1001" or just "1001")
/// Output: int (orderId)
/// </summary>
internal sealed class OrderIdParserExecutor() : Executor<string, int>("OrderIdParserExecutor")
{
public override ValueTask<int> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Try to extract order ID from the message
// Handles formats like: "1001", "order 1001", "Check order #1001", etc.
string cleanedInput = message
.Replace("order", "", StringComparison.OrdinalIgnoreCase)
.Replace("#", "")
.Trim();
// Find the first number in the string
string numberStr = new(cleanedInput.Where(char.IsDigit).ToArray());
if (int.TryParse(numberStr, out int orderId))
{
return ValueTask.FromResult(orderId);
}
// Default to an invalid order ID if parsing fails
return ValueTask.FromResult(-1);
}
}
@@ -1,38 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates a workflow with different input/output types:
// - OrderIdParserExecutor: string → int
// - OrderLookupExecutor: int → OrderDetails (custom POCO)
// - OrderSummaryExecutor: OrderDetails → string
//
// Workflow: HTTP Request (string) → Parse Order ID → Lookup Order → Generate Summary
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using SingleAgent;
// Create the executors with different input/output types
OrderIdParserExecutor orderIdParser = new(); // string → int
OrderLookupExecutor orderLookup = new(); // int → OrderDetails POCO
OrderSummaryExecutor orderSummary = new(); // OrderDetails → string
// Build the workflow: Parse → Lookup → Summarize
Workflow workflow = new WorkflowBuilder(orderIdParser)
.WithName("OrderLookupWorkflow")
.WithDescription("Looks up an order by ID and returns a formatted summary")
.AddEdge(orderIdParser, orderLookup) // string → int → OrderDetails
.AddEdge(orderLookup, orderSummary) // OrderDetails → string
.WithOutputFrom(orderSummary)
.Build();
// Configure the function app to host workflows.
// This will automatically generate HTTP API endpoints for the workflow.
FunctionsApplicationBuilder functionBuilder = FunctionsApplication.CreateBuilder(args);
functionBuilder.ConfigureFunctionsWebApplication().ConfigureDurableOptions(options =>
{
// Register the workflow
options.Workflows.AddWorkflow(workflow);
});
functionBuilder.Build().Run();
@@ -1,27 +0,0 @@
// 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");
}
}
}
@@ -1,82 +0,0 @@
// 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;
}
}
@@ -1,26 +0,0 @@
# Default endpoint address for local testing
@authority=http://localhost:7071
### Look up Order #1001 (Shipped order with multiple items)
POST {{authority}}/api/workflows/OrderLookupWorkflow/run
Content-Type: text/plain
Check order 1001
### Look up Order #1002 (Processing order)
POST {{authority}}/api/workflows/OrderLookupWorkflow/run
Content-Type: text/plain
order 1002
### Look up Order #1003 (Delivered order)
POST {{authority}}/api/workflows/OrderLookupWorkflow/run
Content-Type: text/plain
1003
### Look up non-existent order
POST {{authority}}/api/workflows/OrderLookupWorkflow/run
Content-Type: text/plain
order #9999
@@ -1,89 +0,0 @@
# 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
}
}
}
```
@@ -1,20 +0,0 @@
{
"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"
}
}
}
}