diff --git a/dotnet/demos/AgentOpenTelemetry/Program.cs b/dotnet/demos/AgentOpenTelemetry/Program.cs index cfc9434a6a..e42506b948 100644 --- a/dotnet/demos/AgentOpenTelemetry/Program.cs +++ b/dotnet/demos/AgentOpenTelemetry/Program.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.Metrics; using Azure.AI.OpenAI; @@ -8,13 +9,14 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using OpenAI; using OpenTelemetry; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; +#region Setup Telemetry + const string SourceName = "OpenTelemetryAspire.ConsoleApp"; const string ServiceName = "AgentOpenTelemetry"; @@ -24,7 +26,7 @@ AppContext.SetSwitch("Microsoft.Extensions.AI.Agents.EnableTelemetry", true); // Configure OpenTelemetry for Aspire dashboard var otlpEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") ?? "http://localhost:4318"; -// Create a resource to identify this service (like Python example) +// Create a resource to identify this service var resource = ResourceBuilder.CreateDefault() .AddService(ServiceName, serviceVersion: "1.0.0") .AddAttributes(new Dictionary @@ -40,23 +42,17 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(SourceName) // Our custom activity source .AddSource("Microsoft.Extensions.AI.Agents") // Agent Framework telemetry .AddHttpClientInstrumentation() // Capture HTTP calls to OpenAI - .AddOtlpExporter(options => - { - options.Endpoint = new Uri(otlpEndpoint); - }) + .AddOtlpExporter(options => { options.Endpoint = new Uri(otlpEndpoint); }) .Build(); -// Setup metrics with resource and instrument name filtering (like Python example) +// Setup metrics with resource and instrument name filtering using var meterProvider = Sdk.CreateMeterProviderBuilder() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0")) .AddMeter(SourceName) // Our custom meter .AddMeter("Microsoft.Extensions.AI.Agents") // Agent Framework metrics .AddHttpClientInstrumentation() // HTTP client metrics .AddRuntimeInstrumentation() // .NET runtime metrics - .AddOtlpExporter(options => - { - options.Endpoint = new Uri(otlpEndpoint); - }) + .AddOtlpExporter(options => { options.Endpoint = new Uri(otlpEndpoint); }) .Build(); // Setup structured logging with OpenTelemetry @@ -74,16 +70,19 @@ serviceCollection.AddLogging(loggingBuilder => loggingBuilder options.IncludeFormattedMessage = true; })); -var serviceProvider = serviceCollection.BuildServiceProvider(); -var logger = serviceProvider.GetRequiredService>(); - using var activitySource = new ActivitySource(SourceName); using var meter = new Meter(SourceName); -// Create custom metrics (similar to Python example) +// Create custom metrics var interactionCounter = meter.CreateCounter("agent_interactions_total", description: "Total number of agent interactions"); var responseTimeHistogram = meter.CreateHistogram("agent_response_time_seconds", description: "Agent response time in seconds"); +#endregion + +var serviceProvider = serviceCollection.BuildServiceProvider(); +var loggerFactory = serviceProvider.GetRequiredService(); +var appLogger = loggerFactory.CreateLogger(); + Console.WriteLine(""" === OpenTelemetry Aspire Demo === This demo shows OpenTelemetry integration with the Agent Framework. @@ -91,34 +90,39 @@ Console.WriteLine(""" Type your message and press Enter. Type 'exit' or empty message to quit. """); -// Log application startup -logger.LogInformation("OpenTelemetry Aspire Demo application started"); -logger.LogInformation("OTLP endpoint configured: {OtlpEndpoint}", otlpEndpoint); -logger.LogDebug("Service name: {ServiceName}, Source name: {SourceName}", ServiceName, SourceName); - -// Create the chat client var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT environment variable is not set."); var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; -logger.LogInformation("Initializing Azure OpenAI client with endpoint: {Endpoint}", endpoint); -logger.LogDebug("Using deployment: {DeploymentName}", deploymentName); +// Log application startup +appLogger.LogInformation("OpenTelemetry Aspire Demo application started"); -// Create a logger specifically for the agent -var loggerFactory = serviceProvider.GetRequiredService(); +[Description("Get the weather for a given location.")] +static async Task GetWeather([Description("The location to get the weather for.")] string location) +{ + await Task.Delay(2000); + return $"The weather in {location} is cloudy with a high of 15°C."; +} -// Create the agent with OpenTelemetry instrumentation -logger.LogInformation("Creating Agent with OpenTelemetry instrumentation"); +// To ensure chat client's function calling is captured in the open telemetry, the chat client needs to have UseOpenTelemetry after UseFunctionInvocation +using var instrumentedChatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetChatClient(deploymentName) + .AsIChatClient() // Converts a native OpenAI SDK ChatClient into a Microsoft.Extensions.AI.IChatClient + .AsBuilder() + .UseFunctionInvocation() + .UseOpenTelemetry(loggerFactory: loggerFactory, sourceName: SourceName, (cfg) => { cfg.EnableSensitiveData = true; }) + .Build(); -using var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) - .GetChatClient(deploymentName) - .CreateAIAgent( +appLogger.LogInformation("Creating Agent with OpenTelemetry instrumentation"); +// Create the agent with the instrumented chat client +using var agent = new ChatClientAgent(instrumentedChatClient, name: "OpenTelemetryDemoAgent", - instructions: "You are a helpful assistant that provides concise and informative responses.") - .WithOpenTelemetry(loggerFactory, SourceName); + instructions: "You are a helpful assistant that provides concise and informative responses.", + tools: [AIFunctionFactory.Create(GetWeather)]) + .WithOpenTelemetry(loggerFactory, SourceName); // Enable telemetry on the agent var thread = agent.GetNewThread(); -logger.LogInformation("Agent created successfully with ID: {AgentId}", agent.Id); +appLogger.LogInformation("Agent created successfully with ID: {AgentId}", agent.Id); // Create a parent span for the entire agent session using var sessionActivity = activitySource.StartActivity("Agent Session"); @@ -127,8 +131,8 @@ sessionActivity?.SetTag("agent.name", "OpenTelemetryDemoAgent"); sessionActivity?.SetTag("session.id", sessionId); sessionActivity?.SetTag("session.start_time", DateTimeOffset.UtcNow.ToString("O")); -logger.LogInformation("Starting agent session with ID: {SessionId}", sessionId); -using (logger.BeginScope(new Dictionary { ["SessionId"] = sessionId, ["AgentName"] = "OpenTelemetryDemoAgent" })) +appLogger.LogInformation("Starting agent session with ID: {SessionId}", sessionId); +using (appLogger.BeginScope(new Dictionary { ["SessionId"] = sessionId, ["AgentName"] = "OpenTelemetryDemoAgent" })) { var interactionCount = 0; @@ -139,12 +143,12 @@ using (logger.BeginScope(new Dictionary { ["SessionId"] = sessio if (string.IsNullOrWhiteSpace(userInput) || userInput.Equals("exit", StringComparison.OrdinalIgnoreCase)) { - logger.LogInformation("User requested to exit the session"); + appLogger.LogInformation("User requested to exit the session"); break; } interactionCount++; - logger.LogInformation("Processing user interaction #{InteractionNumber}: {UserInput}", interactionCount, userInput); + appLogger.LogInformation("Processing user interaction #{InteractionNumber}: {UserInput}", interactionCount, userInput); // Create a child span for each individual interaction using var activity = activitySource.StartActivity("Agent Interaction"); @@ -156,7 +160,7 @@ using (logger.BeginScope(new Dictionary { ["SessionId"] = sessio try { - logger.LogDebug("Starting agent execution for interaction #{InteractionNumber}", interactionCount); + appLogger.LogDebug("Starting agent execution for interaction #{InteractionNumber}", interactionCount); Console.Write("Agent: "); // Run the agent (this will create its own internal telemetry spans) @@ -177,7 +181,7 @@ using (logger.BeginScope(new Dictionary { ["SessionId"] = sessio activity?.SetTag("response.success", true); - logger.LogInformation("Agent interaction #{InteractionNumber} completed successfully in {ResponseTime:F2} seconds", + appLogger.LogInformation("Agent interaction #{InteractionNumber} completed successfully in {ResponseTime:F2} seconds", interactionCount, responseTime); } catch (Exception ex) @@ -197,7 +201,7 @@ using (logger.BeginScope(new Dictionary { ["SessionId"] = sessio activity?.SetTag("error.message", ex.Message); activity?.SetStatus(ActivityStatusCode.Error, ex.Message); - logger.LogError(ex, "Agent interaction #{InteractionNumber} failed after {ResponseTime:F2} seconds: {ErrorMessage}", + appLogger.LogError(ex, "Agent interaction #{InteractionNumber} failed after {ResponseTime:F2} seconds: {ErrorMessage}", interactionCount, responseTime, ex.Message); } } @@ -206,7 +210,7 @@ using (logger.BeginScope(new Dictionary { ["SessionId"] = sessio sessionActivity?.SetTag("session.total_interactions", interactionCount); sessionActivity?.SetTag("session.end_time", DateTimeOffset.UtcNow.ToString("O")); - logger.LogInformation("Agent session completed. Total interactions: {TotalInteractions}", interactionCount); + appLogger.LogInformation("Agent session completed. Total interactions: {TotalInteractions}", interactionCount); } // End of logging scope -logger.LogInformation("OpenTelemetry Aspire Demo application shutting down"); +appLogger.LogInformation("OpenTelemetry Aspire Demo application shutting down"); diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientExtensions.cs index 8085faf6f4..045dc3ef27 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientExtensions.cs @@ -18,7 +18,7 @@ internal static class ChatClientExtensions chatBuilder.UseAgentInvocation(); } - if (chatClient.GetService() is null) + if (chatClient.GetService() is null && chatClient.GetService() is null) { _ = chatBuilder.Use((IChatClient innerClient, IServiceProvider services) => { @@ -33,7 +33,17 @@ internal static class ChatClientExtensions if (options?.ChatOptions?.Tools is { Count: > 0 }) { // When tools are provided in the constructor, set the tools for the whole lifecycle of the chat client - agentChatClient.GetService()!.AdditionalTools = options.ChatOptions.Tools; + var newFunctionService = agentChatClient.GetService(); + var oldFunctionService = agentChatClient.GetService(); + + if (newFunctionService is not null) + { + newFunctionService.AdditionalTools = options.ChatOptions.Tools; + } + else + { + oldFunctionService!.AdditionalTools = options.ChatOptions.Tools; + } } return agentChatClient;