// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI; /// /// Provides a delegating implementation that implements the OpenTelemetry Semantic Conventions for Generative AI systems. /// /// /// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.37, defined at . /// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. /// public sealed class OpenTelemetryAgent : DelegatingAIAgent, IDisposable { // IMPLEMENTATION NOTE: The OpenTelemetryChatClient from Microsoft.Extensions.AI provides a full and up-to-date // implementationof the OpenTelemetry Semantic Conventions for Generative AI systems, specifically for the client // metrics and the chat span. But the chat span is almost identical to the invoke_agent span, just with invoke_agent // have a different value for the operation name and a few additional tags. To avoid needing to reimplement the // convention, then, and keep it up-to-date as the convention evolves, for now this implementation just delegates // to OpenTelemetryChatClient for the actual telemetry work. For RunAsync and RunStreamingAsync, it delegates to the // inner agent not directly but rather via OpenTelemetryChatClient, which wraps a ForwardingChatClient that in turn // calls back into the inner agent. /// The providing the bulk of the telemetry. private readonly OpenTelemetryChatClient _otelClient; /// The provider name extracted from . private readonly string? _providerName; /// Initializes a new instance of the class. /// The underlying to be augmented with telemetry capabilities. /// /// An optional source name that will be used to identify telemetry data from this agent. /// If not provided, a default source name will be used for telemetry identification. /// /// is . /// /// The constructor automatically extracts provider metadata from the inner agent and configures /// telemetry collection according to OpenTelemetry semantic conventions for AI systems. /// public OpenTelemetryAgent(AIAgent innerAgent, string? sourceName = null) : base(innerAgent) { this._providerName = innerAgent.GetService()?.ProviderName; this._otelClient = new OpenTelemetryChatClient( new ForwardingChatClient(this), sourceName: string.IsNullOrEmpty(sourceName) ? OpenTelemetryConsts.DefaultSourceName : sourceName!); } /// public void Dispose() => this._otelClient.Dispose(); /// /// Gets or sets a value indicating whether potentially sensitive information should be included in telemetry. /// /// /// if potentially sensitive information should be included in telemetry; /// if telemetry shouldn't include raw inputs and outputs. /// The default value is , unless the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT /// environment variable is set to "true" (case-insensitive). /// /// /// By default, telemetry includes metadata, such as token counts, but not raw inputs /// and outputs, such as message content, function call arguments, and function call results. /// The default value can be overridden by setting the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT /// environment variable to "true". Explicitly setting this property will override the environment variable. /// public bool EnableSensitiveData { get => this._otelClient.EnableSensitiveData; set => this._otelClient.EnableSensitiveData = value; } /// public override async Task RunAsync( IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ChatOptions co = new ForwardedOptions(options, thread, Activity.Current); var response = await this._otelClient.GetResponseAsync(messages, co, cancellationToken).ConfigureAwait(false); return response.RawRepresentation as AgentRunResponse ?? new AgentRunResponse(response); } /// public override async IAsyncEnumerable RunStreamingAsync( IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { ChatOptions co = new ForwardedOptions(options, thread, Activity.Current); await foreach (var update in this._otelClient.GetStreamingResponseAsync(messages, co, cancellationToken).ConfigureAwait(false)) { yield return update.RawRepresentation as AgentRunResponseUpdate ?? new AgentRunResponseUpdate(update); } } /// Augments the current activity created by the with agent-specific information. /// The that was current prior to the 's invocation. private void UpdateCurrentActivity(Activity? previousActivity) { // If there isn't a current activity to augment, or it's the same one that was current when the agent was invoked (meaning // the OpenTelemetryChatClient didn't create one), then there's nothing to do. if (Activity.Current is not { } activity || ReferenceEquals(activity, previousActivity)) { return; } // Override information set by OpenTelemetryChatClient to make it specific to invoke_agent. activity.DisplayName = $"invoke_agent {this.DisplayName}"; if (!string.IsNullOrWhiteSpace(this._providerName)) { _ = activity.SetTag(OpenTelemetryConsts.GenAI.Provider.Name, this._providerName); } // Further augment the activity with agent-specific tags. _ = activity.SetTag(OpenTelemetryConsts.GenAI.Agent.Id, this.Id); if (this.Name is { } name && !string.IsNullOrWhiteSpace(name)) { _ = activity.SetTag(OpenTelemetryConsts.GenAI.Agent.Name, this.Name); } if (this.Description is { } description && !string.IsNullOrWhiteSpace(description)) { _ = activity.SetTag(OpenTelemetryConsts.GenAI.Agent.Description, description); } } /// State passed from this instance into the inner agent, circumventing the intermediate . private sealed class ForwardedOptions : ChatOptions { public ForwardedOptions(AgentRunOptions? options, AgentThread? thread, Activity? currentActivity) { this.Options = options; this.Thread = thread; this.CurrentActivity = currentActivity; if (options is ChatClientAgentRunOptions { ChatOptions: { } chatClientOptions }) { // Keep this faux copy ctor in sync with public properties on ChatOptions. this.AdditionalProperties = chatClientOptions.AdditionalProperties; this.AllowMultipleToolCalls = chatClientOptions.AllowMultipleToolCalls; this.ConversationId = chatClientOptions.ConversationId; this.FrequencyPenalty = chatClientOptions.FrequencyPenalty; this.Instructions = chatClientOptions.Instructions; this.MaxOutputTokens = chatClientOptions.MaxOutputTokens; this.ModelId = chatClientOptions.ModelId; this.PresencePenalty = chatClientOptions.PresencePenalty; this.RawRepresentationFactory = chatClientOptions.RawRepresentationFactory; this.ResponseFormat = chatClientOptions.ResponseFormat; this.Seed = chatClientOptions.Seed; this.StopSequences = chatClientOptions.StopSequences; this.Temperature = chatClientOptions.Temperature; this.Tools = chatClientOptions.Tools; this.ToolMode = chatClientOptions.ToolMode; this.TopK = chatClientOptions.TopK; this.TopP = chatClientOptions.TopP; } } public AgentRunOptions? Options { get; } public AgentThread? Thread { get; } public Activity? CurrentActivity { get; } } /// The stub used to delegate from the into the inner . /// private sealed class ForwardingChatClient(OpenTelemetryAgent parentAgent) : IChatClient { public async Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { ForwardedOptions? fo = options as ForwardedOptions; // Update the current activity to reflect the agent invocation. parentAgent.UpdateCurrentActivity(fo?.CurrentActivity); // Invoke the inner agent. var response = await parentAgent.InnerAgent.RunAsync(messages, fo?.Thread, fo?.Options, cancellationToken).ConfigureAwait(false); // Wrap the response in a ChatResponse so we can pass it back through OpenTelemetryChatClient. return response.AsChatResponse(); } public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { ForwardedOptions? fo = options as ForwardedOptions; // Update the current activity to reflect the agent invocation. parentAgent.UpdateCurrentActivity(fo?.CurrentActivity); // Invoke the inner agent. await foreach (var update in parentAgent.InnerAgent.RunStreamingAsync(messages, fo?.Thread, fo?.Options, cancellationToken).ConfigureAwait(false)) { // Wrap the response updates in ChatResponseUpdates so we can pass them back through OpenTelemetryChatClient. yield return update.AsChatResponseUpdate(); } } public object? GetService(Type serviceType, object? serviceKey = null) => // Delegate any inquiries made by the OpenTelemetryChatClient back to the parent agent. parentAgent.GetService(serviceType, serviceKey); public void Dispose() { } } }