// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace Microsoft.Agents.AI; /// /// A delegating AI agent that logs agent operations to an . /// /// /// /// The provided implementation of is thread-safe for concurrent use so long as the /// employed is also thread-safe for concurrent use. /// /// /// When the employed enables , the contents of /// messages, options, and responses are logged. These may contain sensitive application data. /// is disabled by default and should never be enabled in a production environment. /// Messages and options are not logged at other logging levels. /// /// public sealed partial class LoggingAgent : DelegatingAIAgent { /// An instance used for all logging. private readonly ILogger _logger; /// The to use for serialization of state written to the logger. private JsonSerializerOptions _jsonSerializerOptions; /// Initializes a new instance of the class. /// The underlying . /// An instance that will be used for all logging. /// or is . public LoggingAgent(AIAgent innerAgent, ILogger logger) : base(innerAgent) { this._logger = Throw.IfNull(logger); this._jsonSerializerOptions = AgentJsonUtilities.DefaultOptions; } /// Gets or sets JSON serialization options to use when serializing logging data. public JsonSerializerOptions JsonSerializerOptions { get => this._jsonSerializerOptions; set => this._jsonSerializerOptions = Throw.IfNull(value); } /// protected override async Task RunCoreAsync( IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { if (this._logger.IsEnabled(LogLevel.Debug)) { if (this._logger.IsEnabled(LogLevel.Trace)) { this.LogInvokedSensitive(nameof(RunAsync), this.AsJson(messages), this.AsJson(options), this.AsJson(this.GetService())); } else { this.LogInvoked(nameof(RunAsync)); } } try { AgentResponse response = await base.RunCoreAsync(messages, thread, options, cancellationToken).ConfigureAwait(false); if (this._logger.IsEnabled(LogLevel.Debug)) { if (this._logger.IsEnabled(LogLevel.Trace)) { this.LogCompletedSensitive(nameof(RunAsync), this.AsJson(response)); } else { this.LogCompleted(nameof(RunAsync)); } } return response; } catch (OperationCanceledException) { this.LogInvocationCanceled(nameof(RunAsync)); throw; } catch (Exception ex) { this.LogInvocationFailed(nameof(RunAsync), ex); throw; } } /// protected override async IAsyncEnumerable RunCoreStreamingAsync( IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (this._logger.IsEnabled(LogLevel.Debug)) { if (this._logger.IsEnabled(LogLevel.Trace)) { this.LogInvokedSensitive(nameof(RunStreamingAsync), this.AsJson(messages), this.AsJson(options), this.AsJson(this.GetService())); } else { this.LogInvoked(nameof(RunStreamingAsync)); } } IAsyncEnumerator e; try { e = base.RunCoreStreamingAsync(messages, thread, options, cancellationToken).GetAsyncEnumerator(cancellationToken); } catch (OperationCanceledException) { this.LogInvocationCanceled(nameof(RunStreamingAsync)); throw; } catch (Exception ex) { this.LogInvocationFailed(nameof(RunStreamingAsync), ex); throw; } try { AgentResponseUpdate? update = null; while (true) { try { if (!await e.MoveNextAsync().ConfigureAwait(false)) { break; } update = e.Current; } catch (OperationCanceledException) { this.LogInvocationCanceled(nameof(RunStreamingAsync)); throw; } catch (Exception ex) { this.LogInvocationFailed(nameof(RunStreamingAsync), ex); throw; } if (this._logger.IsEnabled(LogLevel.Trace)) { this.LogStreamingUpdateSensitive(this.AsJson(update)); } yield return update; } this.LogCompleted(nameof(RunStreamingAsync)); } finally { await e.DisposeAsync().ConfigureAwait(false); } } private string AsJson(T value) { try { return JsonSerializer.Serialize(value, this._jsonSerializerOptions.GetTypeInfo(typeof(T))); } catch { // If serialization fails, return a simple string representation return value?.ToString() ?? "null"; } } [LoggerMessage(LogLevel.Debug, "{MethodName} invoked.")] private partial void LogInvoked(string methodName); [LoggerMessage(LogLevel.Trace, "{MethodName} invoked: {Messages}. Options: {Options}. Metadata: {Metadata}.")] private partial void LogInvokedSensitive(string methodName, string messages, string options, string metadata); [LoggerMessage(LogLevel.Debug, "{MethodName} completed.")] private partial void LogCompleted(string methodName); [LoggerMessage(LogLevel.Trace, "{MethodName} completed: {Response}.")] private partial void LogCompletedSensitive(string methodName, string response); [LoggerMessage(LogLevel.Trace, "RunStreamingAsync received update: {Update}")] private partial void LogStreamingUpdateSensitive(string update); [LoggerMessage(LogLevel.Debug, "{MethodName} canceled.")] private partial void LogInvocationCanceled(string methodName); [LoggerMessage(LogLevel.Error, "{MethodName} failed.")] private partial void LogInvocationFailed(string methodName, Exception error); }