chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,135 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
using Azure.AI.Projects;
using Azure.Identity;
using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
namespace WorkflowAsAnAgentObservabilitySample;
/// <summary>
/// This sample shows how to enable OpenTelemetry observability for workflows when
/// using them as <see cref="AIAgent"/>s.
///
/// In this example, we create a workflow that uses two language agents to process
/// input concurrently, one that responds in French and another that responds in English.
///
/// You will interact with the workflow in an interactive loop, sending messages and receiving
/// streaming responses from the workflow as if it were an agent who responds in both languages.
///
/// OpenTelemetry observability is enabled at multiple levels:
/// 1. At the chat client level, capturing telemetry for interactions with the Azure OpenAI service.
/// 2. At the agent level, capturing telemetry for agent operations.
/// 3. At the workflow level, capturing telemetry for workflow execution.
///
/// Traces will be sent to an Aspire dashboard via an OTLP endpoint, and optionally to
/// Azure Monitor if an Application Insights connection string is provided.
///
/// Learn how to set up an Aspire dashboard here:
/// https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone?tabs=bash
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - Foundational samples should be completed first.
/// - This sample uses concurrent processing.
/// - An Azure OpenAI endpoint and deployment name.
/// - An Application Insights resource for telemetry (optional).
/// </remarks>
public static class Program
{
private const string SourceName = "Workflow.ApplicationInsightsSample";
private static readonly ActivitySource s_activitySource = new(SourceName);
private static async Task Main()
{
// Set up observability
var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
var otlpEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") ?? "http://localhost:4317";
var resourceBuilder = ResourceBuilder
.CreateDefault()
.AddService("WorkflowSample");
var traceProviderBuilder = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(resourceBuilder)
.AddSource("Microsoft.Agents.AI.*") // Agent Framework telemetry
.AddSource("Microsoft.Extensions.AI.*") // Extensions AI telemetry
.AddSource(SourceName);
traceProviderBuilder.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint));
if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString))
{
traceProviderBuilder.AddAzureMonitorTraceExporter(options => options.ConnectionString = applicationInsightsConnectionString);
}
using var traceProvider = traceProviderBuilder.Build();
// Set up the Azure AI Foundry client
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Start a root activity for the application
using var activity = s_activitySource.StartActivity("main");
Console.WriteLine($"Operation/Trace ID: {Activity.Current?.TraceId}");
// Create the workflow and turn it into an agent with OpenTelemetry instrumentation
var workflow = WorkflowHelper.GetWorkflow(aiProjectClient, deploymentName, SourceName);
var agent = new OpenTelemetryAgent(workflow.AsAIAgent("workflow-agent", "Workflow Agent"), SourceName)
{
EnableSensitiveData = true // enable sensitive data at the agent level such as prompts and responses
};
var session = await agent.CreateSessionAsync();
// Start an interactive loop to interact with the workflow as if it were an agent
while (true)
{
Console.WriteLine();
Console.Write("User (or 'exit' to quit): ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
break;
}
await ProcessInputAsync(agent, session, input);
}
// Helper method to process user input and display streaming responses. To display
// multiple interleaved responses correctly, we buffer updates by message ID and
// re-render all messages on each update.
static async Task ProcessInputAsync(AIAgent agent, AgentSession? session, string input)
{
Dictionary<string, List<AgentResponseUpdate>> buffer = [];
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(input, session))
{
if (update.MessageId is null || string.IsNullOrEmpty(update.Text))
{
// skip updates that don't have a message ID or text
continue;
}
Console.Clear();
if (!buffer.TryGetValue(update.MessageId, out List<AgentResponseUpdate>? value))
{
value = [];
buffer[update.MessageId] = value;
}
value.Add(update);
foreach (var (messageId, segments) in buffer)
{
string combinedText = string.Concat(segments);
Console.WriteLine($"{segments[0].AuthorName}: {combinedText}");
Console.WriteLine();
}
}
}
}
}
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows.Generators\Microsoft.Agents.AI.Workflows.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,113 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace WorkflowAsAnAgentObservabilitySample;
internal static partial class WorkflowHelper
{
/// <summary>
/// Creates a workflow that uses two language agents to process input concurrently.
/// </summary>
/// <param name="client">The AI project client to use for the agents</param>
/// <param name="model">The model deployment name</param>
/// <param name="sourceName">The source name for OpenTelemetry instrumentation</param>
/// <returns>A workflow that processes input using two language agents</returns>
internal static Workflow GetWorkflow(AIProjectClient client, string model, string sourceName)
{
// Create executors
var startExecutor = new ConcurrentStartExecutor();
var aggregationExecutor = new ConcurrentAggregationExecutor();
AIAgent frenchAgent = GetLanguageAgent("French", client, model, sourceName);
AIAgent englishAgent = GetLanguageAgent("English", client, model, sourceName);
// Build the workflow by adding executors and connecting them
return new WorkflowBuilder(startExecutor)
.AddFanOutEdge(startExecutor, [frenchAgent, englishAgent])
.AddFanInBarrierEdge([frenchAgent, englishAgent], aggregationExecutor)
.WithOutputFrom(aggregationExecutor)
.Build();
}
/// <summary>
/// Creates a language agent for the specified target language.
/// </summary>
/// <param name="targetLanguage">The target language for translation</param>
/// <param name="client">The AI project client to use for the agent</param>
/// <param name="model">The model deployment name</param>
/// <param name="sourceName">The source name for OpenTelemetry instrumentation</param>
/// <returns>An AIAgent configured for the specified language</returns>
private static AIAgent GetLanguageAgent(string targetLanguage, AIProjectClient client, string model, string sourceName) =>
client.AsAIAgent(
model: model,
instructions: $"You're a helpful assistant who always responds in {targetLanguage}.",
name: $"{targetLanguage}Agent",
clientFactory: c => c.AsBuilder()
.UseOpenTelemetry(sourceName: sourceName, configure: cfg => cfg.EnableSensitiveData = true)
.Build()
)
.AsBuilder()
.UseOpenTelemetry(sourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
.Build();
/// <summary>
/// Executor that starts the concurrent processing by sending messages to the agents.
/// </summary>
[SendsMessage(typeof(List<ChatMessage>))]
[SendsMessage(typeof(TurnToken))]
private sealed partial class ConcurrentStartExecutor()
: Executor("ConcurrentStartExecutor", declareCrossRunShareable: true), IResettableExecutor
{
[MessageHandler]
internal ValueTask RouteMessages(IEnumerable<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
{
List<ChatMessage> payload = messages as List<ChatMessage> ?? messages.ToList();
return context.SendMessageAsync(payload, cancellationToken: cancellationToken);
}
[MessageHandler]
internal ValueTask RouteTurnTokenAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken)
{
return context.SendMessageAsync(token, cancellationToken: cancellationToken);
}
public ValueTask ResetAsync() => default;
}
/// <summary>
/// Executor that aggregates the results from the concurrent agents.
/// </summary>
[YieldsOutput(typeof(string))]
private sealed partial class ConcurrentAggregationExecutor() :
Executor<List<ChatMessage>>("ConcurrentAggregationExecutor"), IResettableExecutor
{
private readonly List<ChatMessage> _messages = [];
/// <summary>
/// Handles incoming messages from the agents and aggregates their responses.
/// </summary>
/// <param name="message">The message from the agent</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
public override async ValueTask HandleAsync(List<ChatMessage> message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._messages.AddRange(message);
if (this._messages.Count == 2)
{
var formattedMessages = string.Join(Environment.NewLine, this._messages.Select(m => $"{m.Text}"));
await context.YieldOutputAsync(formattedMessages, cancellationToken);
}
}
public ValueTask ResetAsync()
{
this._messages.Clear();
return default;
}
}
}