// 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());
}