// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics; using Azure.Monitor.OpenTelemetry.Exporter; using Microsoft.Agents.AI.Workflows; using OpenTelemetry; using OpenTelemetry.Resources; using OpenTelemetry.Trace; namespace WorkflowObservabilitySample; /// /// This sample shows how to enable observability in a workflow and send the traces /// to be visualized in Application Insights. /// /// In this example, we create a simple text processing pipeline that: /// 1. Takes input text and converts it to uppercase using an UppercaseExecutor /// 2. Takes the uppercase text and reverses it using a ReverseTextExecutor /// /// The executors are connected sequentially, so data flows from one to the next in order. /// For input "Hello, World!", the workflow produces "!DLROW ,OLLEH". /// public static class Program { private const string SourceName = "Workflow.ApplicationInsightsSample"; private static readonly ActivitySource s_activitySource = new(SourceName); private static async Task Main() { var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING") ?? throw new InvalidOperationException("APPLICATIONINSIGHTS_CONNECTION_STRING is not set."); var resourceBuilder = ResourceBuilder .CreateDefault() .AddService("WorkflowSample"); using var traceProvider = Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(resourceBuilder) .AddSource(SourceName) // The following source is only required if not specifying // the `activitySource` in the WithOpenTelemetry call below .AddSource("Microsoft.Agents.AI.Workflows*") .AddAzureMonitorTraceExporter(options => options.ConnectionString = applicationInsightsConnectionString) .Build(); // Start a root activity for the application using var activity = s_activitySource.StartActivity("main"); Console.WriteLine($"Operation/Trace ID: {Activity.Current?.TraceId}"); // Create the executors UppercaseExecutor uppercase = new(); ReverseTextExecutor reverse = new(); // Build the workflow by connecting executors sequentially var workflow = new WorkflowBuilder(uppercase) .AddEdge(uppercase, reverse) .WithOpenTelemetry( // Set `EnableSensitiveData` to true to include message content in traces configure: cfg => cfg.EnableSensitiveData = true, activitySource: s_activitySource) .Build(); // Execute the workflow with input data Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!"); foreach (WorkflowEvent evt in run.NewEvents) { if (evt is ExecutorCompletedEvent executorComplete) { Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}"); } else if (evt is WorkflowErrorEvent workflowError) { Console.ForegroundColor = ConsoleColor.Red; Console.Error.WriteLine(workflowError.Exception?.ToString() ?? "Unknown workflow error occurred."); Console.ResetColor(); } else if (evt is ExecutorFailedEvent executorFailed) { Console.ForegroundColor = ConsoleColor.Red; Console.Error.WriteLine($"Executor '{executorFailed.ExecutorId}' failed with {(executorFailed.Data == null ? "unknown error" : $"exception {executorFailed.Data}")}."); Console.ResetColor(); } } } } /// /// First executor: converts input text to uppercase. /// internal sealed class UppercaseExecutor() : Executor("UppercaseExecutor") { /// /// Processes the input message by converting it to uppercase. /// /// The input text to convert /// Workflow context for accessing workflow services and adding events /// The to monitor for cancellation requests. /// The default is . /// The input text converted to uppercase public override async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) => message.ToUpperInvariant(); // The return value will be sent as a message along an edge to subsequent executors } /// /// Second executor: reverses the input text and completes the workflow. /// internal sealed class ReverseTextExecutor() : Executor("ReverseTextExecutor") { /// /// Processes the input message by reversing the text. /// /// The input text to reverse /// Workflow context for accessing workflow services and adding events /// The to monitor for cancellation requests. /// The default is . /// The input text reversed public override async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) => new(message.Reverse().ToArray()); }