mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Minor cleanups
This commit is contained in:
@@ -36,7 +36,7 @@
|
||||
<Project Path="samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
|
||||
<Project Path="samples/AzureFunctions/09_Workflow/09_Workflow.csproj" />
|
||||
<Project Path="samples/AzureFunctions/10_WorkflowConcurrent/10_WorkflowConcurrent.csproj" />
|
||||
<Project Path="samples/AzureFunctions/11_WorkflowWithDifferentTypes/11_WorkflowWithDifferentTypes.csproj" />
|
||||
<Project Path="samples/AzureFunctions/11_WorkflowSharedState/11_WorkflowSharedState.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/DurableWorkflows/">
|
||||
<Project Path="samples/DurableWorkflows/01_ExecutorsAndEdges/01_ExecutorsAndEdges.csproj" />
|
||||
|
||||
@@ -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
|
||||
|
||||
-48
@@ -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>
|
||||
-215
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
-82
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.DurableTask;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a durable workflow orchestration execution.
|
||||
/// </summary>
|
||||
public sealed class DurableWorkflowRunResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DurableWorkflowRunResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="workflowName">The name of the workflow that was executed.</param>
|
||||
/// <param name="output">The output from the workflow execution.</param>
|
||||
public DurableWorkflowRunResult(string workflowName, string output)
|
||||
{
|
||||
this.WorkflowName = workflowName;
|
||||
this.Output = output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the workflow that was executed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("workflowName")]
|
||||
public string WorkflowName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the output from the workflow execution.
|
||||
/// </summary>
|
||||
[JsonPropertyName("output")]
|
||||
public string Output { get; }
|
||||
}
|
||||
@@ -43,29 +43,32 @@ public class DurableWorkflowRunner
|
||||
/// Runs a workflow orchestration.
|
||||
/// </summary>
|
||||
/// <param name="context">The task orchestration context.</param>
|
||||
/// <param name="request">The workflow run request containing workflow name and input.</param>
|
||||
/// <param name="input">The workflow run input containing workflow name and input.</param>
|
||||
/// <param name="logger">The replay-safe logger for orchestration logging.</param>
|
||||
/// <returns>A list containing the workflow execution result.</returns>
|
||||
public async Task<List<string>> RunWorkflowOrchestrationAsync(
|
||||
/// <returns>The result of the workflow execution.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the specified workflow is not found.</exception>
|
||||
public async Task<string> RunWorkflowOrchestrationAsync(
|
||||
TaskOrchestrationContext context,
|
||||
DurableWorkflowRunRequest request,
|
||||
string input,
|
||||
ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(input);
|
||||
|
||||
if (!this.Options.Workflows.TryGetValue(request.WorkflowName, out Workflow? workflow))
|
||||
string orchestrationName = context.Name;
|
||||
string workflowName = WorkflowNamingHelper.ToWorkflowName(orchestrationName);
|
||||
if (!this.Options.Workflows.TryGetValue(workflowName, out Workflow? workflow))
|
||||
{
|
||||
throw new InvalidOperationException($"Workflow '{request.WorkflowName}' not found.");
|
||||
throw new InvalidOperationException($"Workflow '{workflowName}' not found.");
|
||||
}
|
||||
|
||||
logger.LogRunningWorkflow(workflow.Name);
|
||||
|
||||
string result = await this.ExecuteWorkflowLevelsAsync(context, workflow, request.Input, logger).ConfigureAwait(true);
|
||||
string result = await this.ExecuteWorkflowLevelsAsync(context, workflow, input, logger).ConfigureAwait(true);
|
||||
|
||||
await CleanupWorkflowStateAsync(context).ConfigureAwait(true);
|
||||
|
||||
return [result];
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -87,36 +90,23 @@ public class DurableWorkflowRunner
|
||||
/// <returns>The extracted executor name.</returns>
|
||||
protected static string ParseExecutorName(string activityFunctionName)
|
||||
{
|
||||
const string Prefix = "dafx-";
|
||||
|
||||
if (!activityFunctionName.StartsWith(Prefix, StringComparison.Ordinal))
|
||||
if (!activityFunctionName.StartsWith(WorkflowNamingHelper.OrchestrationFunctionPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Activity function name '{activityFunctionName}' does not start with '{Prefix}' prefix.");
|
||||
$"Activity function name '{activityFunctionName}' does not start with '{WorkflowNamingHelper.OrchestrationFunctionPrefix}' prefix.");
|
||||
}
|
||||
|
||||
string executorName = activityFunctionName[Prefix.Length..];
|
||||
string executorName = activityFunctionName[WorkflowNamingHelper.OrchestrationFunctionPrefix.Length..];
|
||||
|
||||
if (string.IsNullOrEmpty(executorName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Activity function name '{activityFunctionName}' is not in the expected format '{Prefix}{{executorName}}'.");
|
||||
$"Activity function name '{activityFunctionName}' is not in the expected format '{WorkflowNamingHelper.OrchestrationFunctionPrefix}{{executorName}}'.");
|
||||
}
|
||||
|
||||
return executorName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base name from an executor ID by removing any GUID suffix.
|
||||
/// </summary>
|
||||
/// <param name="executorId">The executor ID.</param>
|
||||
/// <returns>The base name without the GUID suffix.</returns>
|
||||
protected static string GetBaseName(string executorId)
|
||||
{
|
||||
int underscoreIndex = executorId.IndexOf('_');
|
||||
return underscoreIndex > 0 ? executorId[..underscoreIndex] : executorId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a list of strings to JSON.
|
||||
/// </summary>
|
||||
@@ -240,7 +230,8 @@ public class DurableWorkflowRunner
|
||||
{
|
||||
if (!executorInfo.IsAgenticExecutor)
|
||||
{
|
||||
string triggerName = $"dafx-{GetBaseName(executorInfo.ExecutorId)}";
|
||||
string executorName = WorkflowNamingHelper.GetExecutorName(executorInfo.ExecutorId);
|
||||
string triggerName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName);
|
||||
return await context.CallActivityAsync<string>(triggerName, input).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
@@ -253,7 +244,7 @@ public class DurableWorkflowRunner
|
||||
string input,
|
||||
ILogger logger)
|
||||
{
|
||||
string agentName = GetBaseName(executorInfo.ExecutorId);
|
||||
string agentName = WorkflowNamingHelper.GetExecutorName(executorInfo.ExecutorId);
|
||||
DurableAIAgent agent = context.GetAgent(agentName);
|
||||
|
||||
if (agent is null)
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.AI.DurableTask;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for workflow naming conventions used in durable orchestrations.
|
||||
/// </summary>
|
||||
public static class WorkflowNamingHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The prefix used for durable workflow orchestration function names.
|
||||
/// </summary>
|
||||
public const string OrchestrationFunctionPrefix = "dafx-";
|
||||
|
||||
/// <summary>
|
||||
/// Converts a workflow name to its corresponding orchestration function name.
|
||||
/// </summary>
|
||||
/// <param name="workflowName">The workflow name.</param>
|
||||
/// <returns>The orchestration function name.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the workflow name is null or empty.</exception>
|
||||
public static string ToOrchestrationFunctionName(string workflowName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(workflowName);
|
||||
return $"{OrchestrationFunctionPrefix}{workflowName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an orchestration function name back to its workflow name.
|
||||
/// </summary>
|
||||
/// <param name="orchestrationFunctionName">The orchestration function name.</param>
|
||||
/// <returns>The workflow name.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the orchestration function name is null, empty, or doesn't have the expected prefix.</exception>
|
||||
public static string ToWorkflowName(string orchestrationFunctionName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(orchestrationFunctionName);
|
||||
|
||||
if (!orchestrationFunctionName.StartsWith(OrchestrationFunctionPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Orchestration function name '{orchestrationFunctionName}' does not start with the expected '{OrchestrationFunctionPrefix}' prefix.",
|
||||
nameof(orchestrationFunctionName));
|
||||
}
|
||||
|
||||
string workflowName = orchestrationFunctionName[OrchestrationFunctionPrefix.Length..];
|
||||
|
||||
if (string.IsNullOrEmpty(workflowName))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Orchestration function name '{orchestrationFunctionName}' does not contain a workflow name after the prefix.",
|
||||
nameof(orchestrationFunctionName));
|
||||
}
|
||||
|
||||
return workflowName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to convert an orchestration function name back to its workflow name.
|
||||
/// </summary>
|
||||
/// <param name="orchestrationFunctionName">The orchestration function name.</param>
|
||||
/// <param name="workflowName">When this method returns, contains the workflow name if the conversion succeeded, or null if it failed.</param>
|
||||
/// <returns><c>true</c> if the conversion succeeded; otherwise, <c>false</c>.</returns>
|
||||
public static bool TryGetWorkflowName(string? orchestrationFunctionName, out string? workflowName)
|
||||
{
|
||||
workflowName = null;
|
||||
|
||||
if (string.IsNullOrEmpty(orchestrationFunctionName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!orchestrationFunctionName.StartsWith(OrchestrationFunctionPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
workflowName = orchestrationFunctionName[OrchestrationFunctionPrefix.Length..];
|
||||
return !string.IsNullOrEmpty(workflowName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The suffix separator used when the workflow builder appends a GUID to executor IDs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For agentic executors, the workflow builder appends a GUID suffix to ensure uniqueness.
|
||||
/// For example: "Physicist_8884e71021334ce49517fa2b17b1695b".
|
||||
/// </remarks>
|
||||
private const char ExecutorIdSuffixSeparator = '_';
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the executor name from an executor ID.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For non-agentic executors, the executor ID is the same as the executor name (e.g., "OrderParser").
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For agentic executors, the workflow builder appends a GUID suffix separated by an underscore
|
||||
/// (e.g., "Physicist_8884e71021334ce49517fa2b17b1695b"). This method extracts just the name portion.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="executorId">The executor ID, which may contain a GUID suffix.</param>
|
||||
/// <returns>The executor name without any GUID suffix.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the executor ID is null or empty.</exception>
|
||||
public static string GetExecutorName(string executorId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(executorId);
|
||||
|
||||
int separatorIndex = executorId.IndexOf(ExecutorIdSuffixSeparator);
|
||||
return separatorIndex > 0 ? executorId[..separatorIndex] : executorId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the executor ID contains a GUID suffix.
|
||||
/// </summary>
|
||||
/// <param name="executorId">The executor ID to check.</param>
|
||||
/// <returns><c>true</c> if the executor ID contains a suffix; otherwise, <c>false</c>.</returns>
|
||||
public static bool HasExecutorIdSuffix(string? executorId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(executorId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int separatorIndex = executorId.IndexOf(ExecutorIdSuffixSeparator);
|
||||
return separatorIndex > 0 && separatorIndex < executorId.Length - 1;
|
||||
}
|
||||
}
|
||||
@@ -75,10 +75,10 @@ internal static class BuiltInFunctions
|
||||
[DurableClient] DurableTaskClient client,
|
||||
FunctionContext context)
|
||||
{
|
||||
var workflowName = context.FunctionDefinition.Name.Replace("http-", "");
|
||||
var orchestrationFunctionName = $"dafx-{workflowName}";
|
||||
var workflowName = context.FunctionDefinition.Name.Replace(HttpPrefix, string.Empty);
|
||||
var orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName);
|
||||
var inputMessage = await req.ReadAsStringAsync();
|
||||
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(orchestrationFunctionName, new DurableWorkflowRunRequest { WorkflowName = workflowName, Input = inputMessage! }); //OrchFunction"); // dafx-MyTestWorkflow");
|
||||
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(orchestrationFunctionName, inputMessage);
|
||||
|
||||
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
|
||||
await response.WriteStringAsync($"InvokeWorkflowOrechstrtationAsync is invoked for {workflowName}. Orchestration instanceId: {instanceId}");
|
||||
@@ -221,7 +221,7 @@ internal static class BuiltInFunctions
|
||||
}
|
||||
|
||||
#pragma warning disable DURTASK001 // Durable analyzer complained
|
||||
public static Task<List<string>> WorkflowRunnerOrchestrationAsync(TaskOrchestrationContext context, DurableWorkflowRunRequest input)
|
||||
public static Task<DurableWorkflowRunResult> WorkflowRunnerOrchestrationAsync(TaskOrchestrationContext context, DurableWorkflowRunRequest input)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
@@ -236,7 +236,7 @@ internal static class BuiltInFunctions
|
||||
|
||||
var workFlowName = input.WorkflowName;
|
||||
|
||||
return Task.FromResult(new List<string>() { workFlowName });
|
||||
return Task.FromResult(new DurableWorkflowRunResult(workFlowName, workFlowName));
|
||||
}
|
||||
#pragma warning restore DURTASK001
|
||||
|
||||
|
||||
+6
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.DurableTask;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -39,7 +40,12 @@ internal static class CoreAgentConfigurationExtensions
|
||||
/// <returns>The functions application builder for method chaining.</returns>
|
||||
internal static FunctionsApplicationBuilder RegisterWorkflowServices(this FunctionsApplicationBuilder builder)
|
||||
{
|
||||
// Register FunctionsWorkflowRunner as a singleton
|
||||
builder.Services.TryAddSingleton<FunctionsWorkflowRunner>();
|
||||
|
||||
// Also register it as DurableWorkflowRunner so orchestrations can resolve it by base type
|
||||
builder.Services.TryAddSingleton<DurableWorkflowRunner>(sp => sp.GetRequiredService<FunctionsWorkflowRunner>());
|
||||
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IFunctionMetadataTransformer, DurableWorkflowFunctionMetadataTransformer>());
|
||||
|
||||
return builder;
|
||||
|
||||
@@ -81,25 +81,24 @@ public static class DurableOptionsExtensions
|
||||
|
||||
private static void ConfigureWorkflowOrchestrations(FunctionsApplicationBuilder builder, DurableWorkflowOptions workflows)
|
||||
{
|
||||
// Registering orchestration functions and the workflow state entity.
|
||||
|
||||
builder.ConfigureDurableWorker().AddTasks(tasks =>
|
||||
{
|
||||
// Register the workflow state entity for durable state management
|
||||
// Each orchestration instance gets its own entity keyed by instance ID
|
||||
// Register the workflow state entity for shared state management within workflows.
|
||||
tasks.AddEntity<WorkflowSharedStateEntity>(WorkflowSharedStateEntity.EntityName);
|
||||
|
||||
foreach (string workflowName in workflows.Workflows.Select(kp => kp.Key))
|
||||
{
|
||||
tasks.AddOrchestratorFunc<DurableWorkflowRunRequest, List<string>>(
|
||||
$"dafx-{workflowName}",
|
||||
string orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName);
|
||||
|
||||
tasks.AddOrchestratorFunc<string, string>(
|
||||
orchestrationFunctionName,
|
||||
async (orchestrationContext, request) =>
|
||||
{
|
||||
FunctionContext functionContext = orchestrationContext.GetFunctionContext()
|
||||
?? throw new InvalidOperationException("FunctionContext is not available in the orchestration context.");
|
||||
|
||||
DurableWorkflowRunner runner = functionContext.InstanceServices.GetRequiredService<DurableWorkflowRunner>();
|
||||
ILogger logger = orchestrationContext.CreateReplaySafeLogger($"dafx-orchestration-{workflowName}");
|
||||
ILogger logger = orchestrationContext.CreateReplaySafeLogger(orchestrationFunctionName);
|
||||
|
||||
return await runner.RunWorkflowOrchestrationAsync(orchestrationContext, request, logger).ConfigureAwait(true);
|
||||
});
|
||||
|
||||
+2
-1
@@ -61,7 +61,8 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
|
||||
{
|
||||
if (executorInfos.TryGetValue(executorId, out ExecutorInfo? executorInfo))
|
||||
{
|
||||
string functionName = $"dafx-{executorId.Split("_")[0]}";
|
||||
string executorName = WorkflowNamingHelper.GetExecutorName(executorId);
|
||||
string functionName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName);
|
||||
|
||||
// Check if the executor type is an agent-related type
|
||||
if (WorkflowHelper.IsAgentExecutorType(executorInfo.ExecutorType))
|
||||
|
||||
Reference in New Issue
Block a user