mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Update OpenTelemetryAgent to latest spec (#967)
* Update OpenTelemetryAgent to latest spec It was stale. Rather than try to keep it up-to-date manually, I've changed it to piggy back on OpenTelemetryChatClient, so that it inherits everything OpenTelemetryChatClient does and then augments it just with agent-specific tags. * Address feedback / merge
This commit is contained in:
committed by
GitHub
Unverified
parent
4fd61924fc
commit
0502f3ef91
@@ -103,7 +103,7 @@ using var instrumentedChatClient = new AzureOpenAIClient(new Uri(endpoint), new
|
||||
.AsIChatClient() // Converts a native OpenAI SDK ChatClient into a Microsoft.Extensions.AI.IChatClient
|
||||
.AsBuilder()
|
||||
.UseFunctionInvocation()
|
||||
.UseOpenTelemetry(loggerFactory: loggerFactory, sourceName: SourceName, (cfg) => cfg.EnableSensitiveData = true)
|
||||
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true)
|
||||
.Build();
|
||||
|
||||
appLogger.LogInformation("Creating Agent with OpenTelemetry instrumentation");
|
||||
@@ -112,7 +112,7 @@ using var agent = new ChatClientAgent(instrumentedChatClient,
|
||||
name: "OpenTelemetryDemoAgent",
|
||||
instructions: "You are a helpful assistant that provides concise and informative responses.",
|
||||
tools: [AIFunctionFactory.Create(GetWeatherAsync)])
|
||||
.WithOpenTelemetry(loggerFactory, SourceName); // Enable telemetry on the agent
|
||||
.WithOpenTelemetry(SourceName); // Enable telemetry on the agent
|
||||
|
||||
var thread = agent.GetNewThread();
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
@@ -18,15 +17,20 @@ public static class AgentExtensions
|
||||
/// Wraps the agent with OpenTelemetry instrumentation.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent to wrap.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILogger"/> to use for emitting events.</param>
|
||||
/// <param name="sourceName">An optional source name that will be used on the telemetry data.</param>
|
||||
/// <param name="enableSensitiveData">When <see langword="true"/> indicates whether potentially sensitive information should be included in telemetry. Default is <see langword="false"/></param>
|
||||
/// <returns>An <see cref="OpenTelemetryAgent"/> that wraps the original agent with telemetry.</returns>
|
||||
public static OpenTelemetryAgent WithOpenTelemetry(this AIAgent agent, ILoggerFactory? loggerFactory = null, string? sourceName = null, bool? enableSensitiveData = null) =>
|
||||
new(agent, loggerFactory?.CreateLogger(typeof(OpenTelemetryAgent)), sourceName)
|
||||
public static OpenTelemetryAgent WithOpenTelemetry(this AIAgent agent, string? sourceName = null, bool? enableSensitiveData = null)
|
||||
{
|
||||
OpenTelemetryAgent otel = new(agent, sourceName);
|
||||
|
||||
if (enableSensitiveData is not null)
|
||||
{
|
||||
EnableSensitiveData = enableSensitiveData ?? false
|
||||
};
|
||||
otel.EnableSensitiveData = enableSensitiveData.Value;
|
||||
}
|
||||
|
||||
return otel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="AIFunction"/> that will invoke the provided Agent.
|
||||
|
||||
@@ -64,7 +64,6 @@ internal sealed class FunctionInvocationDelegatingAgent : DelegatingAIAgent
|
||||
Arguments = arguments,
|
||||
Function = this.InnerFunction,
|
||||
CallContent = new(string.Empty, this.InnerFunction.Name, new Dictionary<string, object?>(arguments)),
|
||||
Iteration = 0, // Indicate this function was not invoked by a FICC and has no iteration flow.
|
||||
};
|
||||
|
||||
return await next(innerAgent, context, CoreLogicAsync, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
@@ -11,6 +11,7 @@
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectDiagnosticClassesOnLegacy>true</InjectDiagnosticClassesOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
@@ -19,20 +16,16 @@ public static class OpenTelemetryAIAgentBuilderExtensions
|
||||
/// The specification is still experimental and subject to change; as such, the telemetry output by this agent is also subject to change.
|
||||
/// </remarks>
|
||||
/// <param name="builder">The <see cref="AIAgentBuilder"/>.</param>
|
||||
/// <param name="loggerFactory">An optional <see cref="ILoggerFactory"/> to use to create a logger for logging events.</param>
|
||||
/// <param name="sourceName">An optional source name that will be used on the telemetry data.</param>
|
||||
/// <param name="configure">An optional callback that can be used to configure the <see cref="OpenTelemetryAgent"/> instance.</param>
|
||||
/// <returns>The <paramref name="builder"/>.</returns>
|
||||
public static AIAgentBuilder UseOpenTelemetry(
|
||||
this AIAgentBuilder builder,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
string? sourceName = null,
|
||||
Action<OpenTelemetryAgent>? configure = null) =>
|
||||
Throw.IfNull(builder).Use((innerAgent, services) =>
|
||||
{
|
||||
loggerFactory ??= services.GetService<ILoggerFactory>();
|
||||
|
||||
var agent = new OpenTelemetryAgent(innerAgent, loggerFactory?.CreateLogger(typeof(OpenTelemetryAgent)), sourceName);
|
||||
var agent = new OpenTelemetryAgent(innerAgent, sourceName);
|
||||
configure?.Invoke(agent);
|
||||
|
||||
return agent;
|
||||
|
||||
@@ -3,97 +3,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a delegating agent that implements OpenTelemetry instrumentation for agent operations.
|
||||
/// </summary>
|
||||
/// <summary>Represents a delegating <see cref="AIAgent"/> that implements the OpenTelemetry Semantic Conventions for Generative AI systems.</summary>
|
||||
/// <remarks>
|
||||
/// This class provides telemetry instrumentation for agent operations including activities, metrics, and logging.
|
||||
/// The telemetry output follows OpenTelemetry semantic conventions in <see href="https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/"/> and is subject to change as the conventions evolve.
|
||||
/// This class provides an implementation of the Semantic Conventions for Generative AI systems, defined at <see href="https://opentelemetry.io/docs/specs/semconv/gen-ai/" />.
|
||||
/// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change.
|
||||
/// </remarks>
|
||||
public sealed partial class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
|
||||
public sealed class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
|
||||
{
|
||||
private const LogLevel EventLogLevel = LogLevel.Information;
|
||||
private JsonSerializerOptions _jsonSerializerOptions;
|
||||
private readonly OpenTelemetryChatClient? _openTelemetryChatClient;
|
||||
private readonly string? _system;
|
||||
private readonly ActivitySource _activitySource;
|
||||
private readonly Meter _meter;
|
||||
private readonly Histogram<double> _operationDurationHistogram;
|
||||
private readonly Histogram<int> _tokenUsageHistogram;
|
||||
private readonly ILogger _logger;
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OpenTelemetryAgent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="innerAgent">The underlying agent to wrap with telemetry.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use for emitting events.</param>
|
||||
/// <summary>The <see cref="OpenTelemetryChatClient"/> providing the bulk of the telemetry.</summary>
|
||||
private readonly OpenTelemetryChatClient _otelClient;
|
||||
/// <summary>The provider name extracted from <see cref="AIAgentMetadata"/>.</summary>
|
||||
private readonly string? _providerName;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="OpenTelemetryAgent"/> class.</summary>
|
||||
/// <param name="innerAgent">The underlying <see cref="AIAgent"/>.</param>
|
||||
/// <param name="sourceName">An optional source name that will be used on the telemetry data.</param>
|
||||
public OpenTelemetryAgent(AIAgent innerAgent, ILogger? logger = null, string? sourceName = null)
|
||||
: base(innerAgent)
|
||||
/// <exception cref="ArgumentNullException"><paramref name="innerAgent"/> is <see langword="null"/>.</exception>
|
||||
public OpenTelemetryAgent(AIAgent innerAgent, string? sourceName = null) : base(innerAgent)
|
||||
{
|
||||
string name = string.IsNullOrEmpty(sourceName) ? OpenTelemetryConsts.DefaultSourceName : sourceName!;
|
||||
this._activitySource = new(name);
|
||||
this._meter = new(name);
|
||||
this._logger = logger ?? NullLogger.Instance;
|
||||
this._system = this.GetService<AIAgentMetadata>()?.ProviderName ?? OpenTelemetryConsts.GenAI.SystemNameValues.MicrosoftAgentsAI;
|
||||
this._providerName = innerAgent.GetService<AIAgentMetadata>()?.ProviderName;
|
||||
|
||||
// Attempt to get the open telemetry chat client if the inner agent is a ChatClientAgent.
|
||||
this._openTelemetryChatClient = (this.InnerAgent as ChatClientAgent)?.ChatClient.GetService<OpenTelemetryChatClient>();
|
||||
|
||||
// Inherit by default the EnableSensitiveData setting from the TelemetryChatClient if available.
|
||||
this.EnableSensitiveData = this._openTelemetryChatClient?.EnableSensitiveData ?? false;
|
||||
|
||||
this._operationDurationHistogram = this._meter.CreateHistogram<double>(
|
||||
OpenTelemetryConsts.GenAI.Client.OperationDuration.Name,
|
||||
OpenTelemetryConsts.SecondsUnit,
|
||||
OpenTelemetryConsts.GenAI.Client.OperationDuration.Description
|
||||
#if NET9_0_OR_GREATER
|
||||
, advice: new() { HistogramBucketBoundaries = OpenTelemetryConsts.GenAI.Client.OperationDuration.ExplicitBucketBoundaries }
|
||||
#endif
|
||||
);
|
||||
|
||||
this._tokenUsageHistogram = this._meter.CreateHistogram<int>(
|
||||
OpenTelemetryConsts.GenAI.Client.TokenUsage.Name,
|
||||
OpenTelemetryConsts.TokensUnit,
|
||||
OpenTelemetryConsts.GenAI.Client.TokenUsage.Description
|
||||
#if NET9_0_OR_GREATER
|
||||
, advice: new() { HistogramBucketBoundaries = OpenTelemetryConsts.GenAI.Client.TokenUsage.ExplicitBucketBoundaries }
|
||||
#endif
|
||||
);
|
||||
|
||||
this._jsonSerializerOptions = AIJsonUtilities.DefaultOptions;
|
||||
this._otelClient = new OpenTelemetryChatClient(
|
||||
new ForwardingChatClient(this),
|
||||
sourceName: string.IsNullOrEmpty(sourceName) ? OpenTelemetryConsts.DefaultSourceName : sourceName!);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets JSON serialization options to use when formatting chat data into telemetry strings.</summary>
|
||||
public JsonSerializerOptions JsonSerializerOptions
|
||||
{
|
||||
get => this._jsonSerializerOptions;
|
||||
set => this._jsonSerializerOptions = Throw.IfNull(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the telemetry resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this._activitySource.Dispose();
|
||||
this._meter.Dispose();
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public void Dispose() => this._otelClient.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether potentially sensitive information should be included in telemetry.
|
||||
@@ -101,434 +53,176 @@ public sealed partial class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
|
||||
/// <value>
|
||||
/// <see langword="true"/> if potentially sensitive information should be included in telemetry;
|
||||
/// <see langword="false"/> if telemetry shouldn't include raw inputs and outputs.
|
||||
/// The default value is <see langword="false"/>.
|
||||
/// The default value is <see langword="false"/>, unless the <c>OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT</c>
|
||||
/// environment variable is set to "true" (case-insensitive).
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// 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 <c>OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT</c>
|
||||
/// environment variable to "true". Explicitly setting this property will override the environment variable.
|
||||
/// </remarks>
|
||||
public bool EnableSensitiveData { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? GetService(Type serviceType, object? serviceKey = null)
|
||||
public bool EnableSensitiveData
|
||||
{
|
||||
// Handle ActivitySource requests directly - always return our own ActivitySource
|
||||
if (serviceType == typeof(ActivitySource))
|
||||
{
|
||||
return this._activitySource;
|
||||
}
|
||||
|
||||
// For other service types, use the base delegation logic
|
||||
return base.GetService(serviceType, serviceKey);
|
||||
get => this._otelClient.EnableSensitiveData;
|
||||
set => this._otelClient.EnableSensitiveData = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<AgentRunResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inputMessages = Throw.IfNull(messages) as IReadOnlyCollection<ChatMessage> ?? messages.ToList();
|
||||
ChatOptions co = new ForwardedOptions(options, thread, Activity.Current);
|
||||
|
||||
using Activity? activity = this.CreateAndConfigureActivity(OpenTelemetryConsts.GenAI.Operation.NameValues.InvokeAgent, thread);
|
||||
Stopwatch? stopwatch = this._operationDurationHistogram.Enabled ? Stopwatch.StartNew() : null;
|
||||
var response = await this._otelClient.GetResponseAsync(messages, co, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this.LogChatMessages(inputMessages);
|
||||
|
||||
AgentRunResponse? response = null;
|
||||
Exception? error = null;
|
||||
try
|
||||
{
|
||||
response = await base.RunAsync(inputMessages, thread, options, cancellationToken).ConfigureAwait(false);
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.TraceResponse(activity, response, error, stopwatch);
|
||||
}
|
||||
return (AgentRunResponse)response.RawRepresentation!;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inputMessages = Throw.IfNull(messages) as IReadOnlyCollection<ChatMessage> ?? messages.ToList();
|
||||
ChatOptions co = new ForwardedOptions(options, thread, Activity.Current);
|
||||
|
||||
using Activity? activity = this.CreateAndConfigureActivity(OpenTelemetryConsts.GenAI.Operation.NameValues.InvokeAgent, thread);
|
||||
Stopwatch? stopwatch = this._operationDurationHistogram.Enabled ? Stopwatch.StartNew() : null;
|
||||
|
||||
IAsyncEnumerable<AgentRunResponseUpdate> updates;
|
||||
try
|
||||
await foreach (var update in this._otelClient.GetStreamingResponseAsync(messages, co, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
updates = base.RunStreamingAsync(inputMessages, thread, options, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.TraceResponse(activity, response: null, ex, stopwatch);
|
||||
throw;
|
||||
}
|
||||
|
||||
var responseEnumerator = updates.GetAsyncEnumerator(cancellationToken);
|
||||
List<AgentRunResponseUpdate> trackedUpdates = [];
|
||||
Exception? error = null;
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
AgentRunResponseUpdate update;
|
||||
try
|
||||
{
|
||||
if (!await responseEnumerator.MoveNextAsync().ConfigureAwait(false))
|
||||
{
|
||||
break;
|
||||
}
|
||||
update = responseEnumerator.Current;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex;
|
||||
throw;
|
||||
}
|
||||
|
||||
trackedUpdates.Add(update);
|
||||
yield return update;
|
||||
Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.TraceResponse(activity, trackedUpdates.ToAgentRunResponse(), error, stopwatch);
|
||||
await responseEnumerator.DisposeAsync().ConfigureAwait(false);
|
||||
yield return (AgentRunResponseUpdate)update.RawRepresentation!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an activity for an agent request, or returns null if not enabled.
|
||||
/// </summary>
|
||||
private Activity? CreateAndConfigureActivity(string operationName, AgentThread? thread)
|
||||
/// <summary>Augments the current activity created by the <see cref="OpenTelemetryChatClient"/> with agent-specific information.</summary>
|
||||
/// <param name="previousActivity">The <see cref="Activity"/> that was current prior to the <see cref="OpenTelemetryChatClient"/>'s invocation.</param>
|
||||
private void UpdateCurrentActivity(Activity? previousActivity)
|
||||
{
|
||||
// Get the GenAI system name for telemetry
|
||||
var chatClientAgent = this.InnerAgent as ChatClientAgent;
|
||||
Activity? activity = null;
|
||||
if (this._activitySource.HasListeners())
|
||||
// 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))
|
||||
{
|
||||
string activityName = string.IsNullOrWhiteSpace(this.Name) ? operationName : $"{operationName} {this.Name}";
|
||||
activity = this._activitySource.StartActivity(activityName, ActivityKind.Client);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activity is not null)
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>State passed from this instance into the inner agent, circumventing the intermediate <see cref="OpenTelemetryChatClient"/>.</summary>
|
||||
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 })
|
||||
{
|
||||
_ = activity
|
||||
// Required attributes per OpenTelemetry semantic conventions
|
||||
.AddTag(OpenTelemetryConsts.GenAI.Operation.Name, operationName)
|
||||
.AddTag(OpenTelemetryConsts.GenAI.SystemName, this._system)
|
||||
// Agent-specific attributes
|
||||
.AddTag(OpenTelemetryConsts.GenAI.Agent.Id, this.Id);
|
||||
|
||||
// Add agent name if available (following gen_ai.agent.name convention - conditionally required when available)
|
||||
if (!string.IsNullOrWhiteSpace(this.Name))
|
||||
{
|
||||
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Agent.Name, this.Name);
|
||||
}
|
||||
|
||||
// Add description if available (following gen_ai.agent.description convention)
|
||||
if (!string.IsNullOrWhiteSpace(this.Description))
|
||||
{
|
||||
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Agent.Description, this.Description);
|
||||
}
|
||||
|
||||
// Add conversation ID if thread is available (following gen_ai.conversation.id convention)
|
||||
var metadata = thread?.GetService<AgentThreadMetadata>();
|
||||
if (!string.IsNullOrWhiteSpace(metadata?.ConversationId))
|
||||
{
|
||||
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Conversation.Id, metadata.ConversationId);
|
||||
}
|
||||
|
||||
// Add instructions if available (for ChatClientAgent)
|
||||
if (!string.IsNullOrWhiteSpace(chatClientAgent?.Instructions))
|
||||
{
|
||||
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.Instructions, chatClientAgent.Instructions);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
return activity;
|
||||
public AgentRunOptions? Options { get; }
|
||||
|
||||
public AgentThread? Thread { get; }
|
||||
|
||||
public Activity? CurrentActivity { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a tag to the tag list if the value is not null or whitespace.
|
||||
/// </summary>
|
||||
private static void AddIfNotWhiteSpace(ref TagList tags, string key, string? value)
|
||||
/// <summary>The stub <see cref="IChatClient"/> used to delegate from the <see cref="OpenTelemetryChatClient"/> into the inner <see cref="AIAgent"/>.</summary>
|
||||
/// <param name="parentAgent"></param>
|
||||
private sealed class ForwardingChatClient(OpenTelemetryAgent parentAgent) : IChatClient
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
public async Task<ChatResponse> GetResponseAsync(
|
||||
IEnumerable<ChatMessage> messages, ChatOptions? options, CancellationToken cancellationToken)
|
||||
{
|
||||
tags.Add(key, value);
|
||||
}
|
||||
}
|
||||
ForwardedOptions? fo = options as ForwardedOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Adds agent response information to the activity and records metrics.
|
||||
/// </summary>
|
||||
private void TraceResponse(
|
||||
Activity? activity,
|
||||
AgentRunResponse? response,
|
||||
Exception? error,
|
||||
Stopwatch? stopwatch)
|
||||
{
|
||||
// Record operation duration metric
|
||||
if (this._operationDurationHistogram.Enabled && stopwatch is not null)
|
||||
{
|
||||
TagList tags = new()
|
||||
// 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 new ChatResponse
|
||||
{
|
||||
{ OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Operation.NameValues.InvokeAgent }
|
||||
AdditionalProperties = response.AdditionalProperties,
|
||||
CreatedAt = response.CreatedAt,
|
||||
Messages = response.Messages,
|
||||
RawRepresentation = response,
|
||||
ResponseId = response.ResponseId,
|
||||
Usage = response.Usage,
|
||||
};
|
||||
|
||||
AddIfNotWhiteSpace(ref tags, OpenTelemetryConsts.GenAI.Agent.Name, this.DisplayName);
|
||||
|
||||
if (error is not null)
|
||||
{
|
||||
tags.Add(OpenTelemetryConsts.Error.Type, error.GetType().FullName);
|
||||
}
|
||||
|
||||
this._operationDurationHistogram.Record(stopwatch.Elapsed.TotalSeconds, tags);
|
||||
}
|
||||
|
||||
// Record token usage metrics
|
||||
if (this._tokenUsageHistogram.Enabled && response?.Usage is { } usage)
|
||||
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
|
||||
IEnumerable<ChatMessage> messages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
if (usage.InputTokenCount is long inputTokens)
|
||||
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))
|
||||
{
|
||||
TagList tags = new()
|
||||
// Wrap the response updates in ChatResponseUpdates so we can pass them back through OpenTelemetryChatClient.
|
||||
yield return new ChatResponseUpdate
|
||||
{
|
||||
{ OpenTelemetryConsts.GenAI.Token.Type, "input" }
|
||||
AdditionalProperties = update.AdditionalProperties,
|
||||
AuthorName = update.AuthorName,
|
||||
Contents = update.Contents,
|
||||
CreatedAt = update.CreatedAt,
|
||||
MessageId = update.MessageId,
|
||||
RawRepresentation = update,
|
||||
ResponseId = update.ResponseId,
|
||||
Role = update.Role,
|
||||
};
|
||||
|
||||
AddIfNotWhiteSpace(ref tags, OpenTelemetryConsts.GenAI.Agent.Name, this.Name);
|
||||
|
||||
this._tokenUsageHistogram.Record((int)inputTokens, tags);
|
||||
}
|
||||
|
||||
if (usage.OutputTokenCount is long outputTokens)
|
||||
{
|
||||
TagList tags = new()
|
||||
{
|
||||
{ OpenTelemetryConsts.GenAI.Token.Type, "output" }
|
||||
};
|
||||
|
||||
AddIfNotWhiteSpace(ref tags, OpenTelemetryConsts.GenAI.Agent.Name, this.Name);
|
||||
|
||||
this._tokenUsageHistogram.Record((int)outputTokens, tags);
|
||||
}
|
||||
}
|
||||
|
||||
// Add activity tags
|
||||
if (activity is not null)
|
||||
{
|
||||
if (error is not null)
|
||||
{
|
||||
_ = activity
|
||||
.AddTag(OpenTelemetryConsts.Error.Type, error.GetType().FullName)
|
||||
.SetStatus(ActivityStatusCode.Error, error.Message);
|
||||
}
|
||||
public object? GetService(Type serviceType, object? serviceKey = null) =>
|
||||
// Delegate any inquiries made by the OpenTelemetryChatClient back to the parent agent.
|
||||
parentAgent.GetService(serviceType, serviceKey);
|
||||
|
||||
if (response is not null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(response.ResponseId))
|
||||
{
|
||||
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Response.Id, response.ResponseId);
|
||||
}
|
||||
|
||||
if (response.Usage?.InputTokenCount is long inputTokens)
|
||||
{
|
||||
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Usage.InputTokens, (int)inputTokens);
|
||||
}
|
||||
|
||||
if (response.Usage?.OutputTokenCount is long outputTokens)
|
||||
{
|
||||
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Usage.OutputTokens, (int)outputTokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log the agent response for choice events
|
||||
if (response is not null)
|
||||
{
|
||||
this.LogAgentResponse(response);
|
||||
}
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
private void LogChatMessages(IEnumerable<ChatMessage> messages)
|
||||
{
|
||||
if (this._openTelemetryChatClient is not null)
|
||||
{
|
||||
// To avoid duplication of telemetry data the logging will be skipped if the agent is a ChatClientAgent and
|
||||
// its innerChatClient already has telemetry enabled,
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._logger.IsEnabled(EventLogLevel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ChatMessage message in messages)
|
||||
{
|
||||
if (message.Role == ChatRole.Assistant)
|
||||
{
|
||||
this.Log(new EventId(1, OpenTelemetryConsts.GenAI.Assistant.Message),
|
||||
JsonSerializer.Serialize(this.CreateAssistantEvent(message.Contents), OtelContext.Default.AssistantEvent));
|
||||
}
|
||||
else if (message.Role == ChatRole.Tool)
|
||||
{
|
||||
foreach (FunctionResultContent frc in message.Contents.OfType<FunctionResultContent>())
|
||||
{
|
||||
this.Log(new EventId(1, OpenTelemetryConsts.GenAI.Tool.Message),
|
||||
JsonSerializer.Serialize(new ToolEvent()
|
||||
{
|
||||
Id = frc.CallId,
|
||||
Content = this.EnableSensitiveData && frc.Result is object result ?
|
||||
JsonSerializer.SerializeToNode(result, this._jsonSerializerOptions.GetTypeInfo(result.GetType())) :
|
||||
null,
|
||||
}, OtelContext.Default.ToolEvent));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Log(new EventId(1, message.Role == ChatRole.System ? OpenTelemetryConsts.GenAI.System.Message : OpenTelemetryConsts.GenAI.User.Message),
|
||||
JsonSerializer.Serialize(new SystemOrUserEvent()
|
||||
{
|
||||
Role = message.Role != ChatRole.System && message.Role != ChatRole.User && !string.IsNullOrWhiteSpace(message.Role.Value) ? message.Role.Value : null,
|
||||
Content = this.GetMessageContent(message.Contents),
|
||||
}, OtelContext.Default.SystemOrUserEvent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LogAgentResponse(AgentRunResponse response)
|
||||
{
|
||||
if (this._openTelemetryChatClient is not null)
|
||||
{
|
||||
// To avoid duplication of telemetry data the logging will be skipped if the agent is a ChatClientAgent and
|
||||
// its innerChatClient already has telemetry enabled
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._logger.IsEnabled(EventLogLevel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EventId id = new(1, OpenTelemetryConsts.GenAI.Choice);
|
||||
this.Log(id, JsonSerializer.Serialize(new ChoiceEvent()
|
||||
{
|
||||
FinishReason = (response.RawRepresentation as ChatResponse)?.FinishReason?.Value ?? string.Empty,
|
||||
Index = 0,
|
||||
Message = this.CreateAssistantEvent(response.Messages is { Count: 1 } ? response.Messages[0].Contents : response.Messages.SelectMany(m => m.Contents)),
|
||||
}, OtelContext.Default.ChoiceEvent));
|
||||
}
|
||||
|
||||
private void Log(EventId id, string eventBodyJson)
|
||||
{
|
||||
// This is not the idiomatic way to log, but it's necessary for now in order to structure
|
||||
// the data in a way that the OpenTelemetry collector can work with it. The event body
|
||||
// can be very large and should not be logged as an attribute.
|
||||
|
||||
KeyValuePair<string, object?>[] tags =
|
||||
[
|
||||
new(OpenTelemetryConsts.Event.Name, id.Name),
|
||||
new(OpenTelemetryConsts.GenAI.SystemName, this._system),
|
||||
];
|
||||
|
||||
this._logger.Log(EventLogLevel, id, tags, null, (_, __) => eventBodyJson);
|
||||
}
|
||||
|
||||
private AssistantEvent CreateAssistantEvent(IEnumerable<AIContent> contents)
|
||||
{
|
||||
var toolCalls = contents.OfType<FunctionCallContent>().Select(fc => new ToolCall
|
||||
{
|
||||
Id = fc.CallId,
|
||||
Function = new()
|
||||
{
|
||||
Name = fc.Name,
|
||||
Arguments = this.EnableSensitiveData ?
|
||||
JsonSerializer.SerializeToNode(fc.Arguments, this._jsonSerializerOptions.GetTypeInfo(typeof(IDictionary<string, object?>))) :
|
||||
null,
|
||||
},
|
||||
}).ToArray();
|
||||
|
||||
return new()
|
||||
{
|
||||
Content = this.GetMessageContent(contents),
|
||||
ToolCalls = toolCalls.Length > 0 ? toolCalls : null,
|
||||
};
|
||||
}
|
||||
|
||||
private string? GetMessageContent(IEnumerable<AIContent> contents)
|
||||
{
|
||||
if (this.EnableSensitiveData)
|
||||
{
|
||||
string content = string.Concat(contents.OfType<TextContent>());
|
||||
if (content.Length > 0)
|
||||
{
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private sealed partial class SystemOrUserEvent
|
||||
{
|
||||
public string? Role { get; set; }
|
||||
public string? Content { get; set; }
|
||||
}
|
||||
|
||||
private sealed class AssistantEvent
|
||||
{
|
||||
public string? Content { get; set; }
|
||||
public ToolCall[]? ToolCalls { get; set; }
|
||||
}
|
||||
|
||||
private sealed partial class ToolEvent
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public JsonNode? Content { get; set; }
|
||||
}
|
||||
|
||||
private sealed partial class ChoiceEvent
|
||||
{
|
||||
public string? FinishReason { get; set; }
|
||||
public int Index { get; set; }
|
||||
public AssistantEvent? Message { get; set; }
|
||||
}
|
||||
|
||||
private sealed partial class ToolCall
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string? Type { get; set; } = "function";
|
||||
public ToolCallFunction? Function { get; set; }
|
||||
}
|
||||
|
||||
private sealed partial class ToolCallFunction
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public JsonNode? Arguments { get; set; }
|
||||
}
|
||||
|
||||
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonSerializable(typeof(SystemOrUserEvent))]
|
||||
[JsonSerializable(typeof(AssistantEvent))]
|
||||
[JsonSerializable(typeof(ToolEvent))]
|
||||
[JsonSerializable(typeof(ChoiceEvent))]
|
||||
[JsonSerializable(typeof(object))]
|
||||
private sealed partial class OtelContext : JsonSerializerContext;
|
||||
}
|
||||
|
||||
@@ -7,33 +7,9 @@ internal static class OpenTelemetryConsts
|
||||
{
|
||||
public const string DefaultSourceName = "Experimental.Microsoft.Agents.AI";
|
||||
|
||||
public const string SecondsUnit = "s";
|
||||
public const string TokensUnit = "token";
|
||||
|
||||
public static class Event
|
||||
{
|
||||
public const string Name = "event.name";
|
||||
}
|
||||
|
||||
public static class Error
|
||||
{
|
||||
public const string Type = "error.type";
|
||||
}
|
||||
|
||||
public static class GenAI
|
||||
{
|
||||
public const string Choice = "gen_ai.choice";
|
||||
|
||||
public const string SystemName = "gen_ai.system";
|
||||
|
||||
public static class SystemNameValues
|
||||
{
|
||||
public const string MicrosoftAgentsAI = "microsoft.agents.ai";
|
||||
}
|
||||
|
||||
public const string Chat = "chat";
|
||||
public const string Embeddings = "embeddings";
|
||||
public const string ExecuteTool = "execute_tool";
|
||||
public const string InvokeAgent = "invoke_agent";
|
||||
|
||||
public static class Agent
|
||||
{
|
||||
@@ -42,113 +18,9 @@ internal static class OpenTelemetryConsts
|
||||
public const string Description = "gen_ai.agent.description";
|
||||
}
|
||||
|
||||
public static class Assistant
|
||||
public static class Provider
|
||||
{
|
||||
public const string Message = "gen_ai.assistant.message";
|
||||
public const string Name = "gen_ai.provider.name";
|
||||
}
|
||||
|
||||
public static class Client
|
||||
{
|
||||
public static class OperationDuration
|
||||
{
|
||||
public const string Description = "Measures the duration of a GenAI operation";
|
||||
public const string Name = "gen_ai.client.operation.duration";
|
||||
public static readonly double[] ExplicitBucketBoundaries = [0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92];
|
||||
}
|
||||
|
||||
public static class TokenUsage
|
||||
{
|
||||
public const string Description = "Measures number of input and output tokens used";
|
||||
public const string Name = "gen_ai.client.token.usage";
|
||||
public static readonly int[] ExplicitBucketBoundaries = [1, 4, 16, 64, 256, 1_024, 4_096, 16_384, 65_536, 262_144, 1_048_576, 4_194_304, 16_777_216, 67_108_864];
|
||||
}
|
||||
}
|
||||
|
||||
public static class Conversation
|
||||
{
|
||||
public const string Id = "gen_ai.conversation.id";
|
||||
}
|
||||
|
||||
public static class Operation
|
||||
{
|
||||
public const string Name = "gen_ai.operation.name";
|
||||
|
||||
public static class NameValues
|
||||
{
|
||||
public const string InvokeAgent = "invoke_agent";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Output
|
||||
{
|
||||
public const string Type = "gen_ai.output.type";
|
||||
}
|
||||
|
||||
public static class Request
|
||||
{
|
||||
public const string EmbeddingDimensions = "gen_ai.request.embedding.dimensions";
|
||||
public const string FrequencyPenalty = "gen_ai.request.frequency_penalty";
|
||||
public const string Model = "gen_ai.request.model";
|
||||
public const string MaxTokens = "gen_ai.request.max_tokens";
|
||||
public const string PresencePenalty = "gen_ai.request.presence_penalty";
|
||||
public const string Seed = "gen_ai.request.seed";
|
||||
public const string StopSequences = "gen_ai.request.stop_sequences";
|
||||
public const string Temperature = "gen_ai.request.temperature";
|
||||
public const string TopK = "gen_ai.request.top_k";
|
||||
public const string TopP = "gen_ai.request.top_p";
|
||||
|
||||
// Not available in OTEL : Potential proposals
|
||||
public const string Instructions = "gen_ai.request.instructions";
|
||||
|
||||
public static string PerProvider(string providerName, string parameterName) => $"gen_ai.{providerName}.request.{parameterName}";
|
||||
}
|
||||
|
||||
public static class Response
|
||||
{
|
||||
public const string FinishReasons = "gen_ai.response.finish_reasons";
|
||||
public const string Id = "gen_ai.response.id";
|
||||
public const string Model = "gen_ai.response.model";
|
||||
|
||||
public static string PerProvider(string providerName, string parameterName) => $"gen_ai.{providerName}.response.{parameterName}";
|
||||
}
|
||||
|
||||
public static class System
|
||||
{
|
||||
public const string Message = "gen_ai.system.message";
|
||||
}
|
||||
|
||||
public static class Token
|
||||
{
|
||||
public const string Type = "gen_ai.token.type";
|
||||
}
|
||||
|
||||
public static class Tool
|
||||
{
|
||||
public const string Name = "gen_ai.tool.name";
|
||||
public const string Description = "gen_ai.tool.description";
|
||||
public const string Message = "gen_ai.tool.message";
|
||||
|
||||
public static class Call
|
||||
{
|
||||
public const string Id = "gen_ai.tool.call.id";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Usage
|
||||
{
|
||||
public const string InputTokens = "gen_ai.usage.input_tokens";
|
||||
public const string OutputTokens = "gen_ai.usage.output_tokens";
|
||||
}
|
||||
|
||||
public static class User
|
||||
{
|
||||
public const string Message = "gen_ai.user.message";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Server
|
||||
{
|
||||
public const string Address = "server.address";
|
||||
public const string Port = "server.port";
|
||||
}
|
||||
}
|
||||
|
||||
-19
@@ -39,24 +39,6 @@ public class OpenTelemetryAIAgentBuilderExtensionsTests
|
||||
Assert.IsType<OpenTelemetryAgent>(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that UseOpenTelemetry with logger factory works correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void UseOpenTelemetry_WithLoggerFactory_UsesProvidedLoggerFactory()
|
||||
{
|
||||
// Arrange
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
using var loggerFactory = LoggerFactory.Create(builder => { });
|
||||
var builder = new AIAgentBuilder(mockAgent.Object);
|
||||
|
||||
// Act
|
||||
var result = builder.UseOpenTelemetry(loggerFactory).Build();
|
||||
|
||||
// Assert
|
||||
Assert.IsType<OpenTelemetryAgent>(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that UseOpenTelemetry with source name works correctly.
|
||||
/// </summary>
|
||||
@@ -131,7 +113,6 @@ public class OpenTelemetryAIAgentBuilderExtensionsTests
|
||||
|
||||
// Act
|
||||
var result = builder.UseOpenTelemetry(
|
||||
loggerFactory: loggerFactory,
|
||||
sourceName: SourceName,
|
||||
configure: agent =>
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
internal sealed class TestAIAgent : AIAgent
|
||||
{
|
||||
public Func<string>? NameFunc;
|
||||
public Func<string>? DescriptionFunc;
|
||||
|
||||
public Func<JsonElement, JsonSerializerOptions?, AgentThread> DeserializeThreadFunc = delegate { throw new NotSupportedException(); };
|
||||
public Func<AgentThread> GetNewThreadFunc = delegate { throw new NotSupportedException(); };
|
||||
public Func<IEnumerable<ChatMessage>, AgentThread?, AgentRunOptions?, CancellationToken, Task<AgentRunResponse>> RunAsyncFunc = delegate { throw new NotSupportedException(); };
|
||||
public Func<IEnumerable<ChatMessage>, AgentThread?, AgentRunOptions?, CancellationToken, IAsyncEnumerable<AgentRunResponseUpdate>> RunStreamingAsyncFunc = delegate { throw new NotSupportedException(); };
|
||||
public Func<Type, object?, object?>? GetServiceFunc;
|
||||
|
||||
public override string? Name => this.NameFunc?.Invoke() ?? base.Name;
|
||||
|
||||
public override string? Description => this.DescriptionFunc?.Invoke() ?? base.Description;
|
||||
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) =>
|
||||
this.DeserializeThreadFunc(serializedThread, jsonSerializerOptions);
|
||||
|
||||
public override AgentThread GetNewThread() =>
|
||||
this.GetNewThreadFunc();
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
this.RunAsyncFunc(messages, thread, options, cancellationToken);
|
||||
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
this.RunStreamingAsyncFunc(messages, thread, options, cancellationToken);
|
||||
|
||||
public override object? GetService(Type serviceType, object? serviceKey = null) =>
|
||||
this.GetServiceFunc is { } func ? func(serviceType, serviceKey) :
|
||||
base.GetService(serviceType, serviceKey);
|
||||
}
|
||||
Reference in New Issue
Block a user