diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 47f1feaf1e..eafabd7c28 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -36,7 +36,7 @@ - + diff --git a/dotnet/samples/AzureFunctions/09_Workflow/OrderProcessingExecutors.cs b/dotnet/samples/AzureFunctions/09_Workflow/OrderProcessingExecutors.cs new file mode 100644 index 0000000000..193735f192 --- /dev/null +++ b/dotnet/samples/AzureFunctions/09_Workflow/OrderProcessingExecutors.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.Workflows; + +namespace SingleAgent; + +/// +/// Parses an Order ID from a string input and returns an Order object populated. +/// +internal sealed class OrderLookupExecutor() : Executor("OrderLookup") +{ + public override async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) + { + // Populate Order information from OrderId. + return new Order(message, 100.0m); + } +} + +/// +/// Enriches an Order object with additional information. +/// +internal sealed class OrderEnricherExecutor() : Executor("EnrichOrder") +{ + public override async ValueTask 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("ProcessPayment") +{ + public override async ValueTask 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); diff --git a/dotnet/samples/AzureFunctions/09_Workflow/Program.cs b/dotnet/samples/AzureFunctions/09_Workflow/Program.cs index a4b8b4effb..db166d184d 100644 --- a/dotnet/samples/AzureFunctions/09_Workflow/Program.cs +++ b/dotnet/samples/AzureFunctions/09_Workflow/Program.cs @@ -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 responseHandlingExecutorFunc = input => input switch +Func 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(); diff --git a/dotnet/samples/AzureFunctions/09_Workflow/SurveyResponseParserExecutor.cs b/dotnet/samples/AzureFunctions/09_Workflow/SurveyResponseParserExecutor.cs deleted file mode 100644 index 0697dd4edf..0000000000 --- a/dotnet/samples/AzureFunctions/09_Workflow/SurveyResponseParserExecutor.cs +++ /dev/null @@ -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; - -/// -/// This executor parses survey responses and produces structured output. -/// Example input: "Rating: 8. The app is good but checkout process is confusing." -/// -[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by workflow framework")] -internal sealed partial class SurveyResponseParserExecutor() : Executor("SurveyResponseParserExecutor") -{ - private static readonly JsonSerializerOptions s_jsonOptions = new() - { - WriteIndented = true - }; - - [GeneratedRegex(@"Rating:\s*(\d+)", RegexOptions.IgnoreCase)] - private static partial Regex RatingRegex(); - - public override ValueTask 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; - } -} diff --git a/dotnet/samples/AzureFunctions/09_Workflow/demo.http b/dotnet/samples/AzureFunctions/09_Workflow/demo.http index a3f7d87235..0c23d3c7ca 100644 --- a/dotnet/samples/AzureFunctions/09_Workflow/demo.http +++ b/dotnet/samples/AzureFunctions/09_Workflow/demo.http @@ -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. \ No newline at end of file +12345 diff --git a/dotnet/samples/AzureFunctions/12_WorkflowSharedState/12_WorkflowSharedState.csproj b/dotnet/samples/AzureFunctions/11_WorkflowSharedState/11_WorkflowSharedState.csproj similarity index 100% rename from dotnet/samples/AzureFunctions/12_WorkflowSharedState/12_WorkflowSharedState.csproj rename to dotnet/samples/AzureFunctions/11_WorkflowSharedState/11_WorkflowSharedState.csproj diff --git a/dotnet/samples/AzureFunctions/12_WorkflowSharedState/MyOrchFunction.cs b/dotnet/samples/AzureFunctions/11_WorkflowSharedState/MyOrchFunction.cs similarity index 100% rename from dotnet/samples/AzureFunctions/12_WorkflowSharedState/MyOrchFunction.cs rename to dotnet/samples/AzureFunctions/11_WorkflowSharedState/MyOrchFunction.cs diff --git a/dotnet/samples/AzureFunctions/12_WorkflowSharedState/OrderIdParserExecutor.cs b/dotnet/samples/AzureFunctions/11_WorkflowSharedState/OrderIdParserExecutor.cs similarity index 100% rename from dotnet/samples/AzureFunctions/12_WorkflowSharedState/OrderIdParserExecutor.cs rename to dotnet/samples/AzureFunctions/11_WorkflowSharedState/OrderIdParserExecutor.cs diff --git a/dotnet/samples/AzureFunctions/12_WorkflowSharedState/Program.cs b/dotnet/samples/AzureFunctions/11_WorkflowSharedState/Program.cs similarity index 100% rename from dotnet/samples/AzureFunctions/12_WorkflowSharedState/Program.cs rename to dotnet/samples/AzureFunctions/11_WorkflowSharedState/Program.cs diff --git a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/README.md b/dotnet/samples/AzureFunctions/11_WorkflowSharedState/README.md similarity index 100% rename from dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/README.md rename to dotnet/samples/AzureFunctions/11_WorkflowSharedState/README.md diff --git a/dotnet/samples/AzureFunctions/12_WorkflowSharedState/demo.http b/dotnet/samples/AzureFunctions/11_WorkflowSharedState/demo.http similarity index 100% rename from dotnet/samples/AzureFunctions/12_WorkflowSharedState/demo.http rename to dotnet/samples/AzureFunctions/11_WorkflowSharedState/demo.http diff --git a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/host.json b/dotnet/samples/AzureFunctions/11_WorkflowSharedState/host.json similarity index 100% rename from dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/host.json rename to dotnet/samples/AzureFunctions/11_WorkflowSharedState/host.json diff --git a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/11_WorkflowWithDifferentTypes.csproj b/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/11_WorkflowWithDifferentTypes.csproj deleted file mode 100644 index 4872f05930..0000000000 --- a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/11_WorkflowWithDifferentTypes.csproj +++ /dev/null @@ -1,48 +0,0 @@ - - - net10.0 - v4 - Exe - enable - enable - - SingleAgent - SingleAgent - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/OrderProcessingExecutors.cs b/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/OrderProcessingExecutors.cs deleted file mode 100644 index cc0dfef6b8..0000000000 --- a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/OrderProcessingExecutors.cs +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI.Workflows; - -namespace SingleAgent; - -/// -/// Represents the details of a customer order. -/// -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 Items { get; set; } = []; - - public decimal TotalAmount { get; set; } - - public OrderStatus Status { get; set; } - - public DateTime OrderDate { get; set; } - - public DateTime? EstimatedDelivery { get; set; } -} - -/// -/// Represents an item in an order. -/// -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; -} - -/// -/// Represents the status of an order. -/// -public enum OrderStatus -{ - Pending, - Processing, - Shipped, - Delivered, - Cancelled -} - -/// -/// Executor that looks up order details by order ID. -/// Input: int (orderId) -/// Output: OrderDetails -/// -internal sealed class OrderLookupExecutor() : Executor("OrderLookupExecutor") -{ - // Simulated order database - private static readonly Dictionary 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 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 - }); - } -} - -/// -/// Executor that generates a human-readable summary from order details. -/// Input: OrderDetails -/// Output: string -/// -internal sealed class OrderSummaryExecutor() : Executor("OrderSummaryExecutor") -{ - public override ValueTask 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); - } -} - -/// -/// Executor that parses a string input to extract an order ID. -/// Input: string (e.g., "Check order 1001" or just "1001") -/// Output: int (orderId) -/// -internal sealed class OrderIdParserExecutor() : Executor("OrderIdParserExecutor") -{ - public override ValueTask 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); - } -} diff --git a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/Program.cs b/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/Program.cs deleted file mode 100644 index a2a70a2a20..0000000000 --- a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/Program.cs +++ /dev/null @@ -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(); diff --git a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/ResponseRouterExecutor.cs b/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/ResponseRouterExecutor.cs deleted file mode 100644 index fdf1090ec2..0000000000 --- a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/ResponseRouterExecutor.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI.Workflows; - -namespace SingleAgent; - -/// -/// Routes survey responses to appropriate teams based on rating and category. -/// -public sealed class ResponseRouterExecutor() : Executor("ResponseRouterExecutor") -{ - public override ValueTask 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"); - } - } -} diff --git a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/SurveyResponseParserExecutor.cs b/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/SurveyResponseParserExecutor.cs deleted file mode 100644 index f6a66c9038..0000000000 --- a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/SurveyResponseParserExecutor.cs +++ /dev/null @@ -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; - -/// -/// This executor parses survey responses and produces structured output. -/// Example input: "Rating: 8. The app is good but checkout process is confusing." -/// -[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by workflow framework")] -internal sealed partial class SurveyResponseParserExecutor() : Executor("SurveyResponseParserExecutor") -{ - private static readonly JsonSerializerOptions s_jsonOptions = new() - { - WriteIndented = true - }; - - [GeneratedRegex(@"Rating:\s*(\d+)", RegexOptions.IgnoreCase)] - private static partial Regex RatingRegex(); - - public override ValueTask 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; - } -} diff --git a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/demo.http b/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/demo.http deleted file mode 100644 index e6275a79da..0000000000 --- a/dotnet/samples/AzureFunctions/11_WorkflowWithDifferentTypes/demo.http +++ /dev/null @@ -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 diff --git a/dotnet/samples/AzureFunctions/12_WorkflowSharedState/README.md b/dotnet/samples/AzureFunctions/12_WorkflowSharedState/README.md deleted file mode 100644 index d4ac968978..0000000000 --- a/dotnet/samples/AzureFunctions/12_WorkflowSharedState/README.md +++ /dev/null @@ -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 - } - } -} -``` diff --git a/dotnet/samples/AzureFunctions/12_WorkflowSharedState/host.json b/dotnet/samples/AzureFunctions/12_WorkflowSharedState/host.json deleted file mode 100644 index 9384a0a583..0000000000 --- a/dotnet/samples/AzureFunctions/12_WorkflowSharedState/host.json +++ /dev/null @@ -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" - } - } - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowRunResult.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowRunResult.cs new file mode 100644 index 0000000000..77d7bdcce9 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowRunResult.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.DurableTask; + +/// +/// Represents the result of a durable workflow orchestration execution. +/// +public sealed class DurableWorkflowRunResult +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the workflow that was executed. + /// The output from the workflow execution. + public DurableWorkflowRunResult(string workflowName, string output) + { + this.WorkflowName = workflowName; + this.Output = output; + } + + /// + /// Gets the name of the workflow that was executed. + /// + [JsonPropertyName("workflowName")] + public string WorkflowName { get; } + + /// + /// Gets the output from the workflow execution. + /// + [JsonPropertyName("output")] + public string Output { get; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowRunner.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowRunner.cs index becdf348fa..5a966b4688 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowRunner.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowRunner.cs @@ -43,29 +43,32 @@ public class DurableWorkflowRunner /// Runs a workflow orchestration. /// /// The task orchestration context. - /// The workflow run request containing workflow name and input. + /// The workflow run input containing workflow name and input. /// The replay-safe logger for orchestration logging. - /// A list containing the workflow execution result. - public async Task> RunWorkflowOrchestrationAsync( + /// The result of the workflow execution. + /// Thrown when the specified workflow is not found. + public async Task 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; } /// @@ -87,36 +90,23 @@ public class DurableWorkflowRunner /// The extracted executor name. 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; } - /// - /// Gets the base name from an executor ID by removing any GUID suffix. - /// - /// The executor ID. - /// The base name without the GUID suffix. - protected static string GetBaseName(string executorId) - { - int underscoreIndex = executorId.IndexOf('_'); - return underscoreIndex > 0 ? executorId[..underscoreIndex] : executorId; - } - /// /// Serializes a list of strings to JSON. /// @@ -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(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) diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/WorkflowNamingHelper.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/WorkflowNamingHelper.cs new file mode 100644 index 0000000000..6c53fd5b36 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/WorkflowNamingHelper.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.AI.DurableTask; + +/// +/// Provides helper methods for workflow naming conventions used in durable orchestrations. +/// +public static class WorkflowNamingHelper +{ + /// + /// The prefix used for durable workflow orchestration function names. + /// + public const string OrchestrationFunctionPrefix = "dafx-"; + + /// + /// Converts a workflow name to its corresponding orchestration function name. + /// + /// The workflow name. + /// The orchestration function name. + /// Thrown when the workflow name is null or empty. + public static string ToOrchestrationFunctionName(string workflowName) + { + ArgumentException.ThrowIfNullOrEmpty(workflowName); + return $"{OrchestrationFunctionPrefix}{workflowName}"; + } + + /// + /// Converts an orchestration function name back to its workflow name. + /// + /// The orchestration function name. + /// The workflow name. + /// Thrown when the orchestration function name is null, empty, or doesn't have the expected prefix. + 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; + } + + /// + /// Tries to convert an orchestration function name back to its workflow name. + /// + /// The orchestration function name. + /// When this method returns, contains the workflow name if the conversion succeeded, or null if it failed. + /// true if the conversion succeeded; otherwise, false. + 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); + } + + /// + /// The suffix separator used when the workflow builder appends a GUID to executor IDs. + /// + /// + /// For agentic executors, the workflow builder appends a GUID suffix to ensure uniqueness. + /// For example: "Physicist_8884e71021334ce49517fa2b17b1695b". + /// + private const char ExecutorIdSuffixSeparator = '_'; + + /// + /// Extracts the executor name from an executor ID. + /// + /// + /// + /// For non-agentic executors, the executor ID is the same as the executor name (e.g., "OrderParser"). + /// + /// + /// 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. + /// + /// + /// The executor ID, which may contain a GUID suffix. + /// The executor name without any GUID suffix. + /// Thrown when the executor ID is null or empty. + public static string GetExecutorName(string executorId) + { + ArgumentException.ThrowIfNullOrEmpty(executorId); + + int separatorIndex = executorId.IndexOf(ExecutorIdSuffixSeparator); + return separatorIndex > 0 ? executorId[..separatorIndex] : executorId; + } + + /// + /// Determines whether the executor ID contains a GUID suffix. + /// + /// The executor ID to check. + /// true if the executor ID contains a suffix; otherwise, false. + public static bool HasExecutorIdSuffix(string? executorId) + { + if (string.IsNullOrEmpty(executorId)) + { + return false; + } + + int separatorIndex = executorId.IndexOf(ExecutorIdSuffixSeparator); + return separatorIndex > 0 && separatorIndex < executorId.Length - 1; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs index bb442471fe..21601406fa 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs @@ -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> WorkflowRunnerOrchestrationAsync(TaskOrchestrationContext context, DurableWorkflowRunRequest input) + public static Task 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() { workFlowName }); + return Task.FromResult(new DurableWorkflowRunResult(workFlowName, workFlowName)); } #pragma warning restore DURTASK001 diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CoreAgentConfigurationExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CoreAgentConfigurationExtensions.cs index 16ae42662c..481d146f52 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CoreAgentConfigurationExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CoreAgentConfigurationExtensions.cs @@ -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 /// The functions application builder for method chaining. internal static FunctionsApplicationBuilder RegisterWorkflowServices(this FunctionsApplicationBuilder builder) { + // Register FunctionsWorkflowRunner as a singleton builder.Services.TryAddSingleton(); + + // Also register it as DurableWorkflowRunner so orchestrations can resolve it by base type + builder.Services.TryAddSingleton(sp => sp.GetRequiredService()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); return builder; diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs index 6ec90142e6..30b7a4dc55 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableOptionsExtensions.cs @@ -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.EntityName); foreach (string workflowName in workflows.Workflows.Select(kp => kp.Key)) { - tasks.AddOrchestratorFunc>( - $"dafx-{workflowName}", + string orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflowName); + + tasks.AddOrchestratorFunc( + orchestrationFunctionName, async (orchestrationContext, request) => { FunctionContext functionContext = orchestrationContext.GetFunctionContext() ?? throw new InvalidOperationException("FunctionContext is not available in the orchestration context."); DurableWorkflowRunner runner = functionContext.InstanceServices.GetRequiredService(); - ILogger logger = orchestrationContext.CreateReplaySafeLogger($"dafx-orchestration-{workflowName}"); + ILogger logger = orchestrationContext.CreateReplaySafeLogger(orchestrationFunctionName); return await runner.RunWorkflowOrchestrationAsync(orchestrationContext, request, logger).ConfigureAwait(true); }); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs index abdcd2b020..d231e2550c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs @@ -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))