From f677b8e1e6485dfa84e7ba3b75734542a22faec3 Mon Sep 17 00:00:00 2001 From: Sellakumaran Kanagarathnam Date: Fri, 1 May 2026 16:17:42 -0700 Subject: [PATCH 1/9] Upgrade Agent Framework sample to Agent365 GA (1.0.0) with observability instrumentation - Upgraded Microsoft.Agents.A365.*, Microsoft.OpenTelemetry, Azure.AI.OpenAI, and related packages to GA 1.0.0 releases - Added end-to-end tracing with InvokeAgentScope (per turn) and ExecuteToolScope (per tool call) - Refactored DateTimeFunctionTool to instance class with DI for observability support - Migrated from AgentThread to AgentSession for conversation state management - Expanded appsettings.json with Agent365Observability config section; clarified Blueprint vs Agent Identity placeholders --- .../sample-agent/Agent/MyAgent.cs | 52 +++++++++++++------ .../AgentFrameworkSampleAgent.csproj | 18 +++---- .../agent-framework/sample-agent/Program.cs | 5 ++ .../Tools/DateTimeFunctionTool.cs | 30 +++++++++-- .../sample-agent/Tools/WeatherLookupTool.cs | 40 ++++++++++++++ .../sample-agent/appsettings.json | 30 +++++++---- 6 files changed, 139 insertions(+), 36 deletions(-) diff --git a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs index e189e7b5..5d4669c0 100644 --- a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs +++ b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs @@ -4,6 +4,9 @@ using Agent365AgentFrameworkSampleAgent.telemetry; using Agent365AgentFrameworkSampleAgent.Tools; using Microsoft.Agents.A365.Observability.Hosting.Caching; +using Microsoft.Agents.A365.Observability.Hosting.Extensions; +using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; +using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes; using Microsoft.Agents.A365.Runtime.Utils; using Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Services; using Microsoft.Agents.AI; @@ -254,10 +257,18 @@ await A365OtelWrapper.InvokeObservedAgentOperation( try { var userText = turnContext.Activity.Text?.Trim() ?? string.Empty; + using var invokeScope = InvokeAgentScope.Start( + request: new Request(userText), + scopeDetails: new InvokeAgentScopeDetails(endpoint: new Uri("http://localhost:3978")), + agentDetails: BuildAgentDetails()) + .FromTurnContext(turnContext); + + invokeScope.RecordInputMessages(new[] { userText }); + var _agent = await GetClientAgent(turnContext, turnState, _toolService, ToolAuthHandlerName); - // Read or Create the conversation thread for this conversation. - AgentThread? thread = GetConversationThread(_agent, turnState); + // Read or Create the conversation session for this conversation. + AgentSession? thread = await GetConversationSessionAsync(_agent, turnState, cancellationToken); if (turnContext?.Activity?.Attachments?.Count > 0) { @@ -270,15 +281,19 @@ await A365OtelWrapper.InvokeObservedAgentOperation( } } + var collectedOutput = new System.Text.StringBuilder(); // Stream the response back to the user as we receive it from the agent. await foreach (var response in _agent!.RunStreamingAsync(userText, thread, cancellationToken: cancellationToken)) { if (response.Role == ChatRole.Assistant && !string.IsNullOrEmpty(response.Text)) { turnContext?.StreamingResponse.QueueTextChunk(response.Text); + collectedOutput.Append(response.Text); } } - turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerializer.ToJson(thread.Serialize())); + invokeScope.RecordOutputMessages(new[] { collectedOutput.ToString() }); + var serializedSession = await _agent!.SerializeSessionAsync(thread!); + turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerializer.ToJson(serializedSession)); } finally { @@ -340,7 +355,8 @@ await A365OtelWrapper.InvokeObservedAgentOperation( // Create the local tools: var toolList = new List(); WeatherLookupTool weatherLookupTool = new(context, _configuration!); - toolList.Add(AIFunctionFactory.Create(DateTimeFunctionTool.getDate)); + DateTimeFunctionTool dateTimeTool = new(_configuration!); + toolList.Add(AIFunctionFactory.Create(dateTimeTool.GetCurrentDateTime)); toolList.Add(AIFunctionFactory.Create(weatherLookupTool.GetCurrentWeatherForLocation)); toolList.Add(AIFunctionFactory.Create(weatherLookupTool.GetWeatherForecastForLocation)); @@ -391,25 +407,25 @@ await A365OtelWrapper.InvokeObservedAgentOperation( } } - // Create Chat Options with tools: + // Create Chat Options with tools and instructions: var toolOptions = new ChatOptions { Temperature = (float?)0.2, - Tools = toolList + Tools = toolList, + Instructions = GetAgentInstructions(displayName) }; // Create the chat Client passing in agent instructions and tools: return new ChatClientAgent(_chatClient!, new ChatClientAgentOptions { - Instructions = GetAgentInstructions(displayName), ChatOptions = toolOptions, - ChatMessageStoreFactory = ctx => + ChatHistoryProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions { #pragma warning disable MEAI001 // MessageCountingChatReducer is for evaluation purposes only and is subject to change or removal in future updates - return new InMemoryChatMessageStore(new MessageCountingChatReducer(10), ctx.SerializedState, ctx.JsonSerializerOptions); + ChatReducer = new MessageCountingChatReducer(10) #pragma warning restore MEAI001 // MessageCountingChatReducer is for evaluation purposes only and is subject to change or removal in future updates - } + }) }) .AsBuilder() .UseOpenTelemetry(sourceName: AgentMetrics.SourceName, (cfg) => cfg.EnableSensitiveData = true) @@ -422,21 +438,19 @@ await A365OtelWrapper.InvokeObservedAgentOperation( /// ChatAgent /// State Manager for the Agent. /// - private static AgentThread GetConversationThread(AIAgent? agent, ITurnState turnState) + private static async Task GetConversationSessionAsync(AIAgent? agent, ITurnState turnState, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(agent); - AgentThread thread; string? agentThreadInfo = turnState.Conversation.GetValue("conversation.threadInfo", () => null); if (string.IsNullOrEmpty(agentThreadInfo)) { - thread = agent.GetNewThread(); + return await agent.CreateSessionAsync(cancellationToken); } else { JsonElement ele = ProtocolJsonSerializer.ToObject(agentThreadInfo); - thread = agent.DeserializeThread(ele); + return await agent.DeserializeSessionAsync(ele, cancellationToken: cancellationToken); } - return thread; } private string GetToolCacheKey(ITurnState turnState) @@ -450,5 +464,13 @@ private string GetToolCacheKey(ITurnState turnState) } return userToolCacheKey; } + + private AgentDetails BuildAgentDetails() => + new AgentDetails( + agentId: _configuration?["Agent365Observability:AgentId"] ?? "local-dev", + agentName: _configuration?["Agent365Observability:AgentName"] ?? "my-agent", + agentDescription: _configuration?["Agent365Observability:AgentDescription"] ?? "", + agentBlueprintId: _configuration?["Agent365Observability:AgentBlueprintId"] ?? "", + tenantId: _configuration?["Agent365Observability:TenantId"] ?? "local-dev"); } } diff --git a/dotnet/agent-framework/sample-agent/AgentFrameworkSampleAgent.csproj b/dotnet/agent-framework/sample-agent/AgentFrameworkSampleAgent.csproj index 8b72a305..1445bed6 100644 --- a/dotnet/agent-framework/sample-agent/AgentFrameworkSampleAgent.csproj +++ b/dotnet/agent-framework/sample-agent/AgentFrameworkSampleAgent.csproj @@ -13,20 +13,20 @@ - + - - + + - - - - - - + + + + + + diff --git a/dotnet/agent-framework/sample-agent/Program.cs b/dotnet/agent-framework/sample-agent/Program.cs index ad55e9ce..309c3cc4 100644 --- a/dotnet/agent-framework/sample-agent/Program.cs +++ b/dotnet/agent-framework/sample-agent/Program.cs @@ -15,6 +15,7 @@ using Microsoft.Agents.Storage.Transcript; using Microsoft.Extensions.AI; using Microsoft.OpenTelemetry; +using OpenTelemetry; using System.Reflection; @@ -29,6 +30,10 @@ : ExportTarget.Agent365; }); +// Register custom activity source so spans from AgentMetrics are captured by the TracerProvider +builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddSource(AgentMetrics.SourceName)); + builder.Configuration.AddUserSecrets(Assembly.GetExecutingAssembly()); builder.Services.AddControllers(); builder.Services.AddHttpClient("WebClient", client => client.Timeout = TimeSpan.FromSeconds(600)); diff --git a/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs b/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs index 983cf264..c6d5c607 100644 --- a/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs +++ b/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs @@ -1,17 +1,41 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; +using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes; using System.ComponentModel; namespace Agent365AgentFrameworkSampleAgent.Tools { - public static class DateTimeFunctionTool + public class DateTimeFunctionTool(IConfiguration configuration) { [Description("Use this tool to get the current date and time")] - public static string getDate(string input) + public string GetCurrentDateTime() { + var toolCallDetails = new ToolCallDetails( + toolName: nameof(GetCurrentDateTime), + arguments: "{}", + toolCallId: Guid.NewGuid().ToString(), + description: "Returns the current date and time", + toolType: "function", + endpoint: new Uri("local://datetime") + ); + using var toolScope = ExecuteToolScope.Start( + request: new Request("Get current date and time"), + details: toolCallDetails, + agentDetails: BuildAgentDetails()); + string date = DateTimeOffset.Now.ToString("D", null); + toolScope.RecordResponse(date); return date; } + + private AgentDetails BuildAgentDetails() => + new AgentDetails( + agentId: configuration["Agent365Observability:AgentId"] ?? "local-dev", + agentName: configuration["Agent365Observability:AgentName"] ?? "my-agent", + agentDescription: configuration["Agent365Observability:AgentDescription"] ?? "", + agentBlueprintId: configuration["Agent365Observability:AgentBlueprintId"] ?? "", + tenantId: configuration["Agent365Observability:TenantId"] ?? "local-dev"); } } diff --git a/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs b/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs index a30f8fbc..3de3f4cb 100644 --- a/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs +++ b/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; +using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes; using Microsoft.Agents.Builder; using Microsoft.Agents.Core; using Microsoft.Agents.Core.Models; @@ -39,6 +41,19 @@ public class WeatherLookupTool(ITurnContext turnContext, IConfiguration configur { AssertionHelpers.ThrowIfNull(turnContext, nameof(turnContext)); + var toolCallDetails = new ToolCallDetails( + toolName: nameof(GetCurrentWeatherForLocation), + arguments: $"{{\"location\":\"{location}\",\"state\":\"{state}\"}}", + toolCallId: Guid.NewGuid().ToString(), + description: "Retrieves current weather for a city/state", + toolType: "function", + endpoint: new Uri("https://api.openweathermap.org") + ); + using var toolScope = ExecuteToolScope.Start( + request: new Request($"Get current weather for {location}, {state}"), + details: toolCallDetails, + agentDetails: BuildAgentDetails()); + // Notify the user that we are looking up the weather Console.WriteLine($"Looking up the Current Weather in {location}"); @@ -80,6 +95,7 @@ await turnContext.SendActivityAsync( if (weather.IsSuccess) { WeatherRoot wInfo = weather.Response; + toolScope.RecordResponse(System.Text.Json.JsonSerializer.Serialize(wInfo)); return wInfo; } } @@ -87,9 +103,18 @@ await turnContext.SendActivityAsync( { System.Diagnostics.Trace.WriteLine($"Failed to complete API Call to OpenWeather: {openWeatherLocation!.Error}"); } + toolScope.RecordResponse("null"); return null; } + private AgentDetails BuildAgentDetails() => + new AgentDetails( + agentId: configuration["Agent365Observability:AgentId"] ?? "local-dev", + agentName: configuration["Agent365Observability:AgentName"] ?? "my-agent", + agentDescription: configuration["Agent365Observability:AgentDescription"] ?? "", + agentBlueprintId: configuration["Agent365Observability:AgentBlueprintId"] ?? "", + tenantId: configuration["Agent365Observability:TenantId"] ?? "local-dev"); + /// /// Retrieves the weather forecast for a specified location. /// This method uses the OpenWeatherMap API to fetch the weather forecast data for a given city and state. @@ -115,6 +140,19 @@ await turnContext.SendActivityAsync( [Description("Retrieves the Weather forecast for a location, location is a city name")] public async Task?> GetWeatherForecastForLocation(string location, string state) { + var toolCallDetails = new ToolCallDetails( + toolName: nameof(GetWeatherForecastForLocation), + arguments: $"{{\"location\":\"{location}\",\"state\":\"{state}\"}}", + toolCallId: Guid.NewGuid().ToString(), + description: "Retrieves weather forecast for a city/state", + toolType: "function", + endpoint: new Uri("https://api.openweathermap.org") + ); + using var toolScope = ExecuteToolScope.Start( + request: new Request($"Get weather forecast for {location}, {state}"), + details: toolCallDetails, + agentDetails: BuildAgentDetails()); + // Notify the user that we are looking up the weather Console.WriteLine($"Looking up the Weather Forecast in {location}"); @@ -145,6 +183,7 @@ await turnContext.SendActivityAsync( if (weather.IsSuccess) { var result = weather.Response.Items; + toolScope.RecordResponse(System.Text.Json.JsonSerializer.Serialize(result)); return result; } } @@ -152,6 +191,7 @@ await turnContext.SendActivityAsync( { System.Diagnostics.Trace.WriteLine($"Failed to complete API Call to OpenWeather: {openWeatherLocation!.Error}"); } + toolScope.RecordResponse("null"); return null; } } diff --git a/dotnet/agent-framework/sample-agent/appsettings.json b/dotnet/agent-framework/sample-agent/appsettings.json index ea668f2d..b6723a63 100644 --- a/dotnet/agent-framework/sample-agent/appsettings.json +++ b/dotnet/agent-framework/sample-agent/appsettings.json @@ -14,7 +14,8 @@ "Settings": { "Scopes": [ "https://graph.microsoft.com/.default" - ] + ], + "AlternateBlueprintConnectionName": "ServiceConnection" } } // To use OBO auth instead, uncomment the following lines. @@ -35,7 +36,7 @@ "TokenValidation": { "Audiences": [ - "{{ClientId}}" // this is the Client ID used for the Azure Bot + "{{BOT_ID}}" // Agent Identity App ID (Enterprise App in Entra, NOT the Blueprint) ] }, @@ -54,10 +55,11 @@ "Settings": { "AuthType": "UserManagedIdentity", // this is the AuthType for the connection, valid values can be found in Microsoft.Agents.Authentication.Msal.Model.AuthTypes. "AuthorityEndpoint": "https://login.microsoftonline.com/{{BOT_TENANT_ID}}", - "ClientId": "{{BOT_ID}}", // this is the BluePrint Client ID used for the connection. + "ClientId": "{{BLUEPRINT_ID}}", // Blueprint App ID — from a365.generated.config.json: agentBlueprintId "Scopes": [ "5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default" - ] + ], + "AgentId": "{{BOT_ID}}" // Agent Identity App ID — Enterprise App in Entra (different from Blueprint above) } } }, @@ -69,10 +71,20 @@ ], "AIServices": { "AzureOpenAI": { - "DeploymentName": "----", // This is the Deployment (as opposed to model) Name of the Azure OpenAI model - "Endpoint": "----", // This is the Endpoint of the Azure OpenAI model deployment - "ApiKey": "----" // This is the API Key of the Azure OpenAI model deployment + "DeploymentName": "gpt-4o", // This is the Deployment (as opposed to model) Name of the Azure OpenAI model + "Endpoint": "<>", // This is the Endpoint of the Azure OpenAI model deployment + "ApiKey": "<>" // This is the API Key of the Azure OpenAI model deployment } }, - "OpenWeatherApiKey": "----" //https://openweathermap.org/price - You will need to create a free account to get an API key (its at the bottom of the page). -} + "OpenWeatherApiKey": "----", //https://openweathermap.org/price - You will need to create a free account to get an API key (its at the bottom of the page). + "EnableAgent365Exporter": true, + "Agent365Observability": { + "AgentId": "{{BOT_ID}}", // this is the Agent ID used for observability reporting + "AgentName": "My Agent", + "AgentDescription": "My agent description", + "TenantId": "{{BOT_TENANT_ID}}", + "AgentBlueprintId": "{{BLUEPRINT_ID}}", // this is the Blueprint ID for the agent + "ClientId": "{{BLUEPRINT_ID}}", // Blueprint App ID — used by ObservabilityTokenService to acquire tokens via the FMI chain + "ClientSecret": "<>" + } +} \ No newline at end of file From 9a182f724d8183af73c2e9529af4e88d8fe7259c Mon Sep 17 00:00:00 2001 From: Sellakumaran Kanagarathnam Date: Mon, 4 May 2026 14:55:28 -0700 Subject: [PATCH 2/9] Remove Agent365 observability and improve tool argument handling Removed Agent365 tracing/scoping dependencies and related code from MyAgent.cs, including input/output message recording and AgentDetails usage. Renamed thread to session for clarity. In Program.cs, removed custom OpenTelemetry activity source registration and unused imports. Updated WeatherLookupTool.cs to serialize ToolCallDetails arguments using System.Text.Json. Updated appsettings.json with a clearer OpenWeatherApiKey placeholder and removed EnableAgent365Exporter. --- .../sample-agent/Agent/MyAgent.cs | 27 +++---------------- .../agent-framework/sample-agent/Program.cs | 5 ---- .../sample-agent/Tools/WeatherLookupTool.cs | 4 +-- .../sample-agent/appsettings.json | 3 +-- 4 files changed, 6 insertions(+), 33 deletions(-) diff --git a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs index 5d4669c0..5d850dda 100644 --- a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs +++ b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs @@ -4,9 +4,6 @@ using Agent365AgentFrameworkSampleAgent.telemetry; using Agent365AgentFrameworkSampleAgent.Tools; using Microsoft.Agents.A365.Observability.Hosting.Caching; -using Microsoft.Agents.A365.Observability.Hosting.Extensions; -using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; -using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes; using Microsoft.Agents.A365.Runtime.Utils; using Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Services; using Microsoft.Agents.AI; @@ -257,18 +254,10 @@ await A365OtelWrapper.InvokeObservedAgentOperation( try { var userText = turnContext.Activity.Text?.Trim() ?? string.Empty; - using var invokeScope = InvokeAgentScope.Start( - request: new Request(userText), - scopeDetails: new InvokeAgentScopeDetails(endpoint: new Uri("http://localhost:3978")), - agentDetails: BuildAgentDetails()) - .FromTurnContext(turnContext); - - invokeScope.RecordInputMessages(new[] { userText }); - var _agent = await GetClientAgent(turnContext, turnState, _toolService, ToolAuthHandlerName); // Read or Create the conversation session for this conversation. - AgentSession? thread = await GetConversationSessionAsync(_agent, turnState, cancellationToken); + AgentSession? session = await GetConversationSessionAsync(_agent, turnState, cancellationToken); if (turnContext?.Activity?.Attachments?.Count > 0) { @@ -281,18 +270,15 @@ await A365OtelWrapper.InvokeObservedAgentOperation( } } - var collectedOutput = new System.Text.StringBuilder(); // Stream the response back to the user as we receive it from the agent. - await foreach (var response in _agent!.RunStreamingAsync(userText, thread, cancellationToken: cancellationToken)) + await foreach (var response in _agent!.RunStreamingAsync(userText, session, cancellationToken: cancellationToken)) { if (response.Role == ChatRole.Assistant && !string.IsNullOrEmpty(response.Text)) { turnContext?.StreamingResponse.QueueTextChunk(response.Text); - collectedOutput.Append(response.Text); } } - invokeScope.RecordOutputMessages(new[] { collectedOutput.ToString() }); - var serializedSession = await _agent!.SerializeSessionAsync(thread!); + var serializedSession = await _agent!.SerializeSessionAsync(session!); turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerializer.ToJson(serializedSession)); } finally @@ -465,12 +451,5 @@ private string GetToolCacheKey(ITurnState turnState) return userToolCacheKey; } - private AgentDetails BuildAgentDetails() => - new AgentDetails( - agentId: _configuration?["Agent365Observability:AgentId"] ?? "local-dev", - agentName: _configuration?["Agent365Observability:AgentName"] ?? "my-agent", - agentDescription: _configuration?["Agent365Observability:AgentDescription"] ?? "", - agentBlueprintId: _configuration?["Agent365Observability:AgentBlueprintId"] ?? "", - tenantId: _configuration?["Agent365Observability:TenantId"] ?? "local-dev"); } } diff --git a/dotnet/agent-framework/sample-agent/Program.cs b/dotnet/agent-framework/sample-agent/Program.cs index 309c3cc4..ad55e9ce 100644 --- a/dotnet/agent-framework/sample-agent/Program.cs +++ b/dotnet/agent-framework/sample-agent/Program.cs @@ -15,7 +15,6 @@ using Microsoft.Agents.Storage.Transcript; using Microsoft.Extensions.AI; using Microsoft.OpenTelemetry; -using OpenTelemetry; using System.Reflection; @@ -30,10 +29,6 @@ : ExportTarget.Agent365; }); -// Register custom activity source so spans from AgentMetrics are captured by the TracerProvider -builder.Services.AddOpenTelemetry() - .WithTracing(tracing => tracing.AddSource(AgentMetrics.SourceName)); - builder.Configuration.AddUserSecrets(Assembly.GetExecutingAssembly()); builder.Services.AddControllers(); builder.Services.AddHttpClient("WebClient", client => client.Timeout = TimeSpan.FromSeconds(600)); diff --git a/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs b/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs index 3de3f4cb..960dfb95 100644 --- a/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs +++ b/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs @@ -43,7 +43,7 @@ public class WeatherLookupTool(ITurnContext turnContext, IConfiguration configur var toolCallDetails = new ToolCallDetails( toolName: nameof(GetCurrentWeatherForLocation), - arguments: $"{{\"location\":\"{location}\",\"state\":\"{state}\"}}", + arguments: System.Text.Json.JsonSerializer.Serialize(new { location, state }), toolCallId: Guid.NewGuid().ToString(), description: "Retrieves current weather for a city/state", toolType: "function", @@ -142,7 +142,7 @@ private AgentDetails BuildAgentDetails() => { var toolCallDetails = new ToolCallDetails( toolName: nameof(GetWeatherForecastForLocation), - arguments: $"{{\"location\":\"{location}\",\"state\":\"{state}\"}}", + arguments: System.Text.Json.JsonSerializer.Serialize(new { location, state }), toolCallId: Guid.NewGuid().ToString(), description: "Retrieves weather forecast for a city/state", toolType: "function", diff --git a/dotnet/agent-framework/sample-agent/appsettings.json b/dotnet/agent-framework/sample-agent/appsettings.json index b6723a63..889c45da 100644 --- a/dotnet/agent-framework/sample-agent/appsettings.json +++ b/dotnet/agent-framework/sample-agent/appsettings.json @@ -76,8 +76,7 @@ "ApiKey": "<>" // This is the API Key of the Azure OpenAI model deployment } }, - "OpenWeatherApiKey": "----", //https://openweathermap.org/price - You will need to create a free account to get an API key (its at the bottom of the page). - "EnableAgent365Exporter": true, + "OpenWeatherApiKey": "<>", //https://openweathermap.org/price - You will need to create a free account to get an API key (its at the bottom of the page). "Agent365Observability": { "AgentId": "{{BOT_ID}}", // this is the Agent ID used for observability reporting "AgentName": "My Agent", From 6d2a76ce77443ecaff6f6c7f7cf07544d92c8a2c Mon Sep 17 00:00:00 2001 From: Sellakumaran Kanagarathnam Date: Mon, 4 May 2026 15:10:51 -0700 Subject: [PATCH 3/9] Refactor AgentDetails construction into helper class Extract AgentDetails construction logic into a new AgentDetailsHelper class to eliminate code duplication in DateTimeFunctionTool and WeatherLookupTool. This centralizes configuration-based AgentDetails creation, improving maintainability and code clarity. --- .../sample-agent/Tools/AgentDetailsHelper.cs | 18 ++++++++++++++++++ .../sample-agent/Tools/DateTimeFunctionTool.cs | 9 +-------- .../sample-agent/Tools/WeatherLookupTool.cs | 12 ++---------- 3 files changed, 21 insertions(+), 18 deletions(-) create mode 100644 dotnet/agent-framework/sample-agent/Tools/AgentDetailsHelper.cs diff --git a/dotnet/agent-framework/sample-agent/Tools/AgentDetailsHelper.cs b/dotnet/agent-framework/sample-agent/Tools/AgentDetailsHelper.cs new file mode 100644 index 00000000..f9f0d228 --- /dev/null +++ b/dotnet/agent-framework/sample-agent/Tools/AgentDetailsHelper.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; + +namespace Agent365AgentFrameworkSampleAgent.Tools +{ + internal static class AgentDetailsHelper + { + internal static AgentDetails Build(IConfiguration configuration) => + new AgentDetails( + agentId: configuration["Agent365Observability:AgentId"] ?? "local-dev", + agentName: configuration["Agent365Observability:AgentName"] ?? "my-agent", + agentDescription: configuration["Agent365Observability:AgentDescription"] ?? "", + agentBlueprintId: configuration["Agent365Observability:AgentBlueprintId"] ?? "", + tenantId: configuration["Agent365Observability:TenantId"] ?? "local-dev"); + } +} diff --git a/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs b/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs index c6d5c607..9fbf1174 100644 --- a/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs +++ b/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs @@ -23,19 +23,12 @@ public string GetCurrentDateTime() using var toolScope = ExecuteToolScope.Start( request: new Request("Get current date and time"), details: toolCallDetails, - agentDetails: BuildAgentDetails()); + agentDetails: AgentDetailsHelper.Build(configuration)); string date = DateTimeOffset.Now.ToString("D", null); toolScope.RecordResponse(date); return date; } - private AgentDetails BuildAgentDetails() => - new AgentDetails( - agentId: configuration["Agent365Observability:AgentId"] ?? "local-dev", - agentName: configuration["Agent365Observability:AgentName"] ?? "my-agent", - agentDescription: configuration["Agent365Observability:AgentDescription"] ?? "", - agentBlueprintId: configuration["Agent365Observability:AgentBlueprintId"] ?? "", - tenantId: configuration["Agent365Observability:TenantId"] ?? "local-dev"); } } diff --git a/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs b/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs index 960dfb95..0dfece97 100644 --- a/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs +++ b/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs @@ -52,7 +52,7 @@ public class WeatherLookupTool(ITurnContext turnContext, IConfiguration configur using var toolScope = ExecuteToolScope.Start( request: new Request($"Get current weather for {location}, {state}"), details: toolCallDetails, - agentDetails: BuildAgentDetails()); + agentDetails: AgentDetailsHelper.Build(configuration)); // Notify the user that we are looking up the weather Console.WriteLine($"Looking up the Current Weather in {location}"); @@ -107,14 +107,6 @@ await turnContext.SendActivityAsync( return null; } - private AgentDetails BuildAgentDetails() => - new AgentDetails( - agentId: configuration["Agent365Observability:AgentId"] ?? "local-dev", - agentName: configuration["Agent365Observability:AgentName"] ?? "my-agent", - agentDescription: configuration["Agent365Observability:AgentDescription"] ?? "", - agentBlueprintId: configuration["Agent365Observability:AgentBlueprintId"] ?? "", - tenantId: configuration["Agent365Observability:TenantId"] ?? "local-dev"); - /// /// Retrieves the weather forecast for a specified location. /// This method uses the OpenWeatherMap API to fetch the weather forecast data for a given city and state. @@ -151,7 +143,7 @@ private AgentDetails BuildAgentDetails() => using var toolScope = ExecuteToolScope.Start( request: new Request($"Get weather forecast for {location}, {state}"), details: toolCallDetails, - agentDetails: BuildAgentDetails()); + agentDetails: AgentDetailsHelper.Build(configuration)); // Notify the user that we are looking up the weather Console.WriteLine($"Looking up the Weather Forecast in {location}"); From 5fbbc3701fec07f6eb9908761586adbb1bbcd203 Mon Sep 17 00:00:00 2001 From: Sellakumaran Kanagarathnam Date: Tue, 5 May 2026 13:19:33 -0700 Subject: [PATCH 4/9] Migrate .NET agent-framework sample to Microsoft.OpenTelemetry distro - Replace custom A365 observability wrappers (AgentMetrics, A365OtelWrapper, AgentDetailsHelper) with zero-code auto-instrumentation via UseMicrosoftOpenTelemetry(); explicitly re-enable HTTP, ASP.NET Core, and Azure SDK instrumentation suppressed by Agent365-only export mode - Remove ExecuteToolScope/ToolCallDetails blocks from WeatherLookupTool and DateTimeFunctionTool; remove IExporterTokenCache from MyAgent constructor; remove ObservabilityAuthHandlerName and InvokeObservedAgentOperation wrappers from all three activity handlers - Keep .UseOpenTelemetry(sourceName: null, ...) on ChatClientAgent builder to preserve gen_ai.* semantic attributes (model, tools, messages, token counts) on Azure OpenAI call spans - Add Observability section to README and design.md documenting the spans produced, OTEL_SERVICE_NAME env var, and why no custom tracing code is needed Co-Authored-By: Claude Sonnet 4.6 --- .../sample-agent/Agent/MyAgent.cs | 179 ++++++++---------- .../agent-framework/sample-agent/Program.cs | 14 +- dotnet/agent-framework/sample-agent/README.md | 40 ++++ .../sample-agent/Tools/AgentDetailsHelper.cs | 18 -- .../Tools/DateTimeFunctionTool.cs | 22 +-- .../sample-agent/Tools/WeatherLookupTool.cs | 32 ---- .../sample-agent/docs/design.md | 79 ++++---- .../sample-agent/telemetry/A365OtelWrapper.cs | 83 -------- .../sample-agent/telemetry/AgentMetrics.cs | 142 -------------- 9 files changed, 167 insertions(+), 442 deletions(-) delete mode 100644 dotnet/agent-framework/sample-agent/Tools/AgentDetailsHelper.cs delete mode 100644 dotnet/agent-framework/sample-agent/telemetry/A365OtelWrapper.cs delete mode 100644 dotnet/agent-framework/sample-agent/telemetry/AgentMetrics.cs diff --git a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs index 5d850dda..cbbacb68 100644 --- a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs +++ b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Agent365AgentFrameworkSampleAgent.telemetry; using Agent365AgentFrameworkSampleAgent.Tools; -using Microsoft.Agents.A365.Observability.Hosting.Caching; using Microsoft.Agents.A365.Runtime.Utils; using Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Services; using Microsoft.Agents.AI; @@ -59,7 +57,6 @@ private static string GetAgentInstructions(string? userName) private readonly IChatClient? _chatClient = null; private readonly IConfiguration? _configuration = null; - private readonly IExporterTokenCache? _agentTokenCache = null; private readonly ILogger? _logger = null; private readonly IMcpToolRegistrationService? _toolService = null; // Setup reusable auto sign-in handlers for user authorization (configurable via appsettings.json) @@ -98,13 +95,11 @@ private static bool ShouldSkipToolingOnErrors() public MyAgent(AgentApplicationOptions options, IChatClient chatClient, IConfiguration configuration, - IExporterTokenCache agentTokenCache, IMcpToolRegistrationService toolService, ILogger logger) : base(options) { _chatClient = chatClient; _configuration = configuration; - _agentTokenCache = agentTokenCache; _logger = logger; _toolService = toolService; @@ -133,19 +128,13 @@ public MyAgent(AgentApplicationOptions options, protected async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) { - await AgentMetrics.InvokeObservedAgentOperation( - "WelcomeMessage", - turnContext, - async () => + foreach (ChannelAccount member in turnContext.Activity.MembersAdded) { - foreach (ChannelAccount member in turnContext.Activity.MembersAdded) + if (member.Id != turnContext.Activity.Recipient.Id) { - if (member.Id != turnContext.Activity.Recipient.Id) - { - await turnContext.SendActivityAsync(AgentWelcomeMessage); - } + await turnContext.SendActivityAsync(AgentWelcomeMessage); } - }); + } } /// @@ -154,26 +143,20 @@ await AgentMetrics.InvokeObservedAgentOperation( /// protected async Task OnInstallationUpdateAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) { - await AgentMetrics.InvokeObservedAgentOperation( - "InstallationUpdate", - turnContext, - async () => - { - _logger?.LogInformation( - "InstallationUpdate received — Action: '{Action}', DisplayName: '{Name}', UserId: '{Id}'", - turnContext.Activity.Action ?? "(none)", - turnContext.Activity.From?.Name ?? "(unknown)", - turnContext.Activity.From?.Id ?? "(unknown)"); + _logger?.LogInformation( + "InstallationUpdate received — Action: '{Action}', DisplayName: '{Name}', UserId: '{Id}'", + turnContext.Activity.Action ?? "(none)", + turnContext.Activity.From?.Name ?? "(unknown)", + turnContext.Activity.From?.Id ?? "(unknown)"); - if (turnContext.Activity.Action == InstallationUpdateActionTypes.Add) - { - await turnContext.SendActivityAsync(MessageFactory.Text(AgentHireMessage), cancellationToken); - } - else if (turnContext.Activity.Action == InstallationUpdateActionTypes.Remove) - { - await turnContext.SendActivityAsync(MessageFactory.Text(AgentFarewellMessage), cancellationToken); - } - }); + if (turnContext.Activity.Action == InstallationUpdateActionTypes.Add) + { + await turnContext.SendActivityAsync(MessageFactory.Text(AgentHireMessage), cancellationToken); + } + else if (turnContext.Activity.Action == InstallationUpdateActionTypes.Remove) + { + await turnContext.SendActivityAsync(MessageFactory.Text(AgentFarewellMessage), cancellationToken); + } } /// @@ -201,100 +184,88 @@ protected async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnSta // Select the appropriate auth handler based on request type // For agentic requests, use the agentic auth handler // For non-agentic requests, use OBO auth handler (supports bearer token or configured auth) - string? ObservabilityAuthHandlerName; string? ToolAuthHandlerName; if (turnContext.IsAgenticRequest()) { - ObservabilityAuthHandlerName = ToolAuthHandlerName = AgenticAuthHandlerName; + ToolAuthHandlerName = AgenticAuthHandlerName; } else { // Non-agentic: use OBO auth handler if configured - ObservabilityAuthHandlerName = ToolAuthHandlerName = OboAuthHandlerName; + ToolAuthHandlerName = OboAuthHandlerName; } - await A365OtelWrapper.InvokeObservedAgentOperation( - "MessageProcessor", - turnContext, - turnState, - _agentTokenCache, - UserAuthorization, - ObservabilityAuthHandlerName ?? string.Empty, - _logger, - async () => + // Send an immediate acknowledgment — this arrives as a separate message before the LLM response. + // Each SendActivityAsync call produces a discrete Teams message, enabling the multiple-messages pattern. + // NOTE: For Teams agentic identities, streaming is buffered into a single message by the SDK; + // use SendActivityAsync for any messages that must arrive immediately. + await turnContext.SendActivityAsync(MessageFactory.Text("Got it — working on it…"), cancellationToken).ConfigureAwait(false); + + // Send typing indicator immediately on the main thread (awaited so it arrives before the LLM call starts). + await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), cancellationToken).ConfigureAwait(false); + + // Background loop refreshes the "..." animation every ~4s (it times out after ~5s). + // Only visible in 1:1 and small group chats. + using var typingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var typingTask = Task.Run(async () => { - // Send an immediate acknowledgment — this arrives as a separate message before the LLM response. - // Each SendActivityAsync call produces a discrete Teams message, enabling the multiple-messages pattern. - // NOTE: For Teams agentic identities, streaming is buffered into a single message by the SDK; - // use SendActivityAsync for any messages that must arrive immediately. - await turnContext.SendActivityAsync(MessageFactory.Text("Got it — working on it…"), cancellationToken).ConfigureAwait(false); - - // Send typing indicator immediately on the main thread (awaited so it arrives before the LLM call starts). - await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), cancellationToken).ConfigureAwait(false); - - // Background loop refreshes the "..." animation every ~4s (it times out after ~5s). - // Only visible in 1:1 and small group chats. - using var typingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var typingTask = Task.Run(async () => + try { - try + while (!typingCts.IsCancellationRequested) { - while (!typingCts.IsCancellationRequested) - { - await Task.Delay(TimeSpan.FromSeconds(4), typingCts.Token).ConfigureAwait(false); - await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), typingCts.Token).ConfigureAwait(false); - } + await Task.Delay(TimeSpan.FromSeconds(4), typingCts.Token).ConfigureAwait(false); + await turnContext.SendActivityAsync(Activity.CreateTypingActivity(), typingCts.Token).ConfigureAwait(false); } - catch (OperationCanceledException) { /* expected on cancel */ } - }, typingCts.Token); + } + catch (OperationCanceledException) { /* expected on cancel */ } + }, typingCts.Token); - // StreamingResponse is best-effort: in Teams with agentic identity the SDK may buffer/downscale it. - // The ack + typing loop above handle the immediate UX; streaming remains for non-Teams / WebChat clients. - await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Just a moment please..").ConfigureAwait(false); - try - { - var userText = turnContext.Activity.Text?.Trim() ?? string.Empty; - var _agent = await GetClientAgent(turnContext, turnState, _toolService, ToolAuthHandlerName); + // StreamingResponse is best-effort: in Teams with agentic identity the SDK may buffer/downscale it. + // The ack + typing loop above handle the immediate UX; streaming remains for non-Teams / WebChat clients. + await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Just a moment please..").ConfigureAwait(false); + try + { + var userText = turnContext.Activity.Text?.Trim() ?? string.Empty; + var _agent = await GetClientAgent(turnContext, turnState, _toolService, ToolAuthHandlerName); - // Read or Create the conversation session for this conversation. - AgentSession? session = await GetConversationSessionAsync(_agent, turnState, cancellationToken); + // Read or Create the conversation session for this conversation. + AgentSession? session = await GetConversationSessionAsync(_agent, turnState, cancellationToken); - if (turnContext?.Activity?.Attachments?.Count > 0) + if (turnContext?.Activity?.Attachments?.Count > 0) + { + foreach (var attachment in turnContext.Activity.Attachments) { - foreach (var attachment in turnContext.Activity.Attachments) + if (attachment.ContentType == "application/vnd.microsoft.teams.file.download.info" && !string.IsNullOrEmpty(attachment.ContentUrl)) { - if (attachment.ContentType == "application/vnd.microsoft.teams.file.download.info" && !string.IsNullOrEmpty(attachment.ContentUrl)) - { - userText += $"\n\n[User has attached a file: {attachment.Name}. The file can be downloaded from {attachment.ContentUrl}]"; - } + userText += $"\n\n[User has attached a file: {attachment.Name}. The file can be downloaded from {attachment.ContentUrl}]"; } } + } - // Stream the response back to the user as we receive it from the agent. - await foreach (var response in _agent!.RunStreamingAsync(userText, session, cancellationToken: cancellationToken)) + // Stream the response back to the user as we receive it from the agent. + await foreach (var response in _agent!.RunStreamingAsync(userText, session, cancellationToken: cancellationToken)) + { + if (response.Role == ChatRole.Assistant && !string.IsNullOrEmpty(response.Text)) { - if (response.Role == ChatRole.Assistant && !string.IsNullOrEmpty(response.Text)) - { - turnContext?.StreamingResponse.QueueTextChunk(response.Text); - } + turnContext?.StreamingResponse.QueueTextChunk(response.Text); } - var serializedSession = await _agent!.SerializeSessionAsync(session!); - turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerializer.ToJson(serializedSession)); } - finally + var serializedSession = await _agent!.SerializeSessionAsync(session!); + turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerializer.ToJson(serializedSession)); + } + finally + { + typingCts.Cancel(); + try + { + await typingTask.ConfigureAwait(false); + } + catch (OperationCanceledException) { - typingCts.Cancel(); - try - { - await typingTask.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Expected: typingTask is canceled when typingCts is canceled; no further action required. - } - await turnContext.StreamingResponse.EndStreamAsync(cancellationToken).ConfigureAwait(false); + // Expected: typingTask is canceled when typingCts is canceled; no further action required. } - }); + await turnContext.StreamingResponse.EndStreamAsync(cancellationToken).ConfigureAwait(false); + } } @@ -341,7 +312,7 @@ await A365OtelWrapper.InvokeObservedAgentOperation( // Create the local tools: var toolList = new List(); WeatherLookupTool weatherLookupTool = new(context, _configuration!); - DateTimeFunctionTool dateTimeTool = new(_configuration!); + DateTimeFunctionTool dateTimeTool = new(); toolList.Add(AIFunctionFactory.Create(dateTimeTool.GetCurrentDateTime)); toolList.Add(AIFunctionFactory.Create(weatherLookupTool.GetCurrentWeatherForLocation)); toolList.Add(AIFunctionFactory.Create(weatherLookupTool.GetWeatherForecastForLocation)); @@ -414,7 +385,7 @@ await A365OtelWrapper.InvokeObservedAgentOperation( }) }) .AsBuilder() - .UseOpenTelemetry(sourceName: AgentMetrics.SourceName, (cfg) => cfg.EnableSensitiveData = true) + .UseOpenTelemetry(sourceName: null, (cfg) => cfg.EnableSensitiveData = true) .Build(); } diff --git a/dotnet/agent-framework/sample-agent/Program.cs b/dotnet/agent-framework/sample-agent/Program.cs index ad55e9ce..05a0c940 100644 --- a/dotnet/agent-framework/sample-agent/Program.cs +++ b/dotnet/agent-framework/sample-agent/Program.cs @@ -3,7 +3,6 @@ using Agent365AgentFrameworkSampleAgent; using Agent365AgentFrameworkSampleAgent.Agent; -using Agent365AgentFrameworkSampleAgent.telemetry; using Azure; using Azure.AI.OpenAI; using Microsoft.Agents.A365.Tooling.Extensions.AgentFramework.Services; @@ -27,6 +26,12 @@ o.Exporters = builder.Environment.IsDevelopment() ? ExportTarget.Agent365 | ExportTarget.Console : ExportTarget.Agent365; + + // Agent365-only export suppresses infrastructure instrumentation by default. + // Re-enable explicitly so HTTP calls (Azure OpenAI, auth, Teams) appear in traces. + o.Instrumentation.EnableAspNetCoreInstrumentation = true; + o.Instrumentation.EnableHttpClientInstrumentation = true; + o.Instrumentation.EnableAzureSdkInstrumentation = true; }); builder.Configuration.AddUserSecrets(Assembly.GetExecutingAssembly()); @@ -84,7 +89,7 @@ .AsIChatClient() .AsBuilder() .UseFunctionInvocation() - .UseOpenTelemetry(sourceName: AgentMetrics.SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) + .UseOpenTelemetry(sourceName: null, configure: (cfg) => cfg.EnableSensitiveData = true) .Build(); }); @@ -106,10 +111,7 @@ // Map the /api/messages endpoint to the AgentApplication app.MapPost("/api/messages", async (HttpRequest request, HttpResponse response, IAgentHttpAdapter adapter, IAgent agent, CancellationToken cancellationToken) => { - await AgentMetrics.InvokeObservedHttpOperation("agent.process_message", async () => - { - await adapter.ProcessAsync(request, response, agent, cancellationToken); - }).ConfigureAwait(false); + await adapter.ProcessAsync(request, response, agent, cancellationToken); }); // Health check endpoint for CI/CD pipelines and monitoring diff --git a/dotnet/agent-framework/sample-agent/README.md b/dotnet/agent-framework/sample-agent/README.md index d98b6c7a..726f6629 100644 --- a/dotnet/agent-framework/sample-agent/README.md +++ b/dotnet/agent-framework/sample-agent/README.md @@ -116,6 +116,46 @@ finally > **Note**: Typing indicators are only visible in 1:1 chats and small group chats — not in channels. +## Observability + +This sample uses the [`Microsoft.OpenTelemetry`](https://www.nuget.org/packages/Microsoft.OpenTelemetry) distro, configured in `Program.cs` with a single call: + +```csharp +builder.UseMicrosoftOpenTelemetry(o => +{ + o.Exporters = builder.Environment.IsDevelopment() + ? ExportTarget.Agent365 | ExportTarget.Console + : ExportTarget.Agent365; + + o.Instrumentation.EnableAspNetCoreInstrumentation = true; + o.Instrumentation.EnableHttpClientInstrumentation = true; + o.Instrumentation.EnableAzureSdkInstrumentation = true; +}); +``` + +This produces the following spans automatically — no custom tracing code required: + +| Span | Source | What it captures | +|---|---|---| +| `POST /api/messages` | ASP.NET Core | Inbound request — method, path, status code | +| `POST login.microsoftonline.com` | HttpClient | MSAL token acquisition | +| `POST smba.trafficmanager.net` | HttpClient | Outbound Teams messages | +| `POST …openai.azure.com/…/chat/completions` | HttpClient | Raw Azure OpenAI HTTP call | +| `chat ` | `Microsoft.Extensions.AI` | Full `gen_ai.*` semantics — model, tool definitions, system prompt, input/output messages, token counts, finish reason | +| `invoke_agent ` | `Microsoft.Agents.AI` | Agent-level span — agent ID, input/output, token counts | + +The `gen_ai.*` attributes on the `chat` span come from `.UseOpenTelemetry()` on the `ChatClientAgent` builder in `Program.cs`. This is the only non-distro observability call in the sample and is worth keeping — it enriches every LLM call with structured semantic data at no cost. + +### Service Name + +By default the OTel SDK sets `service.name` to `unknown_service:`. Set the standard `OTEL_SERVICE_NAME` environment variable to give your service a meaningful name in traces: + +```bash +OTEL_SERVICE_NAME="Agent Framework Sample" +``` + +Set this in your local launch profile, deployment environment, or container configuration to match your service catalog. + ## Running the Agent To set up and test this agent, refer to the [Configure Agent Testing](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/testing?tabs=dotnet) guide for complete instructions. diff --git a/dotnet/agent-framework/sample-agent/Tools/AgentDetailsHelper.cs b/dotnet/agent-framework/sample-agent/Tools/AgentDetailsHelper.cs deleted file mode 100644 index f9f0d228..00000000 --- a/dotnet/agent-framework/sample-agent/Tools/AgentDetailsHelper.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; - -namespace Agent365AgentFrameworkSampleAgent.Tools -{ - internal static class AgentDetailsHelper - { - internal static AgentDetails Build(IConfiguration configuration) => - new AgentDetails( - agentId: configuration["Agent365Observability:AgentId"] ?? "local-dev", - agentName: configuration["Agent365Observability:AgentName"] ?? "my-agent", - agentDescription: configuration["Agent365Observability:AgentDescription"] ?? "", - agentBlueprintId: configuration["Agent365Observability:AgentBlueprintId"] ?? "", - tenantId: configuration["Agent365Observability:TenantId"] ?? "local-dev"); - } -} diff --git a/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs b/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs index 9fbf1174..3571bcfe 100644 --- a/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs +++ b/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs @@ -1,34 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; -using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes; using System.ComponentModel; namespace Agent365AgentFrameworkSampleAgent.Tools { - public class DateTimeFunctionTool(IConfiguration configuration) + public class DateTimeFunctionTool { [Description("Use this tool to get the current date and time")] public string GetCurrentDateTime() { - var toolCallDetails = new ToolCallDetails( - toolName: nameof(GetCurrentDateTime), - arguments: "{}", - toolCallId: Guid.NewGuid().ToString(), - description: "Returns the current date and time", - toolType: "function", - endpoint: new Uri("local://datetime") - ); - using var toolScope = ExecuteToolScope.Start( - request: new Request("Get current date and time"), - details: toolCallDetails, - agentDetails: AgentDetailsHelper.Build(configuration)); - - string date = DateTimeOffset.Now.ToString("D", null); - toolScope.RecordResponse(date); - return date; + return DateTimeOffset.Now.ToString("D", null); } - } } diff --git a/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs b/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs index 0dfece97..a30f8fbc 100644 --- a/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs +++ b/dotnet/agent-framework/sample-agent/Tools/WeatherLookupTool.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; -using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes; using Microsoft.Agents.Builder; using Microsoft.Agents.Core; using Microsoft.Agents.Core.Models; @@ -41,19 +39,6 @@ public class WeatherLookupTool(ITurnContext turnContext, IConfiguration configur { AssertionHelpers.ThrowIfNull(turnContext, nameof(turnContext)); - var toolCallDetails = new ToolCallDetails( - toolName: nameof(GetCurrentWeatherForLocation), - arguments: System.Text.Json.JsonSerializer.Serialize(new { location, state }), - toolCallId: Guid.NewGuid().ToString(), - description: "Retrieves current weather for a city/state", - toolType: "function", - endpoint: new Uri("https://api.openweathermap.org") - ); - using var toolScope = ExecuteToolScope.Start( - request: new Request($"Get current weather for {location}, {state}"), - details: toolCallDetails, - agentDetails: AgentDetailsHelper.Build(configuration)); - // Notify the user that we are looking up the weather Console.WriteLine($"Looking up the Current Weather in {location}"); @@ -95,7 +80,6 @@ await turnContext.SendActivityAsync( if (weather.IsSuccess) { WeatherRoot wInfo = weather.Response; - toolScope.RecordResponse(System.Text.Json.JsonSerializer.Serialize(wInfo)); return wInfo; } } @@ -103,7 +87,6 @@ await turnContext.SendActivityAsync( { System.Diagnostics.Trace.WriteLine($"Failed to complete API Call to OpenWeather: {openWeatherLocation!.Error}"); } - toolScope.RecordResponse("null"); return null; } @@ -132,19 +115,6 @@ await turnContext.SendActivityAsync( [Description("Retrieves the Weather forecast for a location, location is a city name")] public async Task?> GetWeatherForecastForLocation(string location, string state) { - var toolCallDetails = new ToolCallDetails( - toolName: nameof(GetWeatherForecastForLocation), - arguments: System.Text.Json.JsonSerializer.Serialize(new { location, state }), - toolCallId: Guid.NewGuid().ToString(), - description: "Retrieves weather forecast for a city/state", - toolType: "function", - endpoint: new Uri("https://api.openweathermap.org") - ); - using var toolScope = ExecuteToolScope.Start( - request: new Request($"Get weather forecast for {location}, {state}"), - details: toolCallDetails, - agentDetails: AgentDetailsHelper.Build(configuration)); - // Notify the user that we are looking up the weather Console.WriteLine($"Looking up the Weather Forecast in {location}"); @@ -175,7 +145,6 @@ await turnContext.SendActivityAsync( if (weather.IsSuccess) { var result = weather.Response.Items; - toolScope.RecordResponse(System.Text.Json.JsonSerializer.Serialize(result)); return result; } } @@ -183,7 +152,6 @@ await turnContext.SendActivityAsync( { System.Diagnostics.Trace.WriteLine($"Failed to complete API Call to OpenWeather: {openWeatherLocation!.Error}"); } - toolScope.RecordResponse("null"); return null; } } diff --git a/dotnet/agent-framework/sample-agent/docs/design.md b/dotnet/agent-framework/sample-agent/docs/design.md index c3e43533..bcdf8a76 100644 --- a/dotnet/agent-framework/sample-agent/docs/design.md +++ b/dotnet/agent-framework/sample-agent/docs/design.md @@ -12,22 +12,22 @@ This sample demonstrates a weather-focused agent built using the Microsoft Agent - Streaming responses to clients - Conversation thread management - Dual authentication (agentic and OBO handlers) -- Microsoft Agent 365 observability integration +- Auto-instrumentation via `Microsoft.OpenTelemetry` distro ## Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ Program.cs │ -│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────────┐ │ -│ │ OpenTelemetry│ │ A365 Tracing│ │ ASP.NET Authentication │ │ -│ └─────────────┘ └─────────────┘ └──────────────────────────┘ │ +│ ┌──────────────────────┐ ┌──────────────────────────────────┐ │ +│ │ Microsoft.OpenTelemetry│ │ ASP.NET Authentication │ │ +│ └──────────────────────┘ └──────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ Dependency Injection Container ││ -│ │ ┌─────────┐ ┌───────────┐ ┌──────────┐ ┌───────────────┐ ││ -│ │ │IChatClient│ │IMcpToolSvc│ │IStorage │ │ITokenCache │ ││ -│ │ └─────────┘ └───────────┘ └──────────┘ └───────────────┘ ││ +│ │ ┌─────────┐ ┌───────────┐ ┌──────────┐ ││ +│ │ │IChatClient│ │IMcpToolSvc│ │IStorage │ ││ +│ │ └─────────┘ └───────────┘ └──────────┘ ││ │ └─────────────────────────────────────────────────────────────┘│ └─────────────────────────────────────────────────────────────────┘ │ @@ -58,7 +58,7 @@ This sample demonstrates a weather-focused agent built using the Microsoft Agent ### Program.cs Entry point that configures: -- OpenTelemetry and Agent 365 tracing +- `Microsoft.OpenTelemetry` distro (`UseMicrosoftOpenTelemetry`) - MCP tool services (`IMcpToolRegistrationService`, `IMcpToolServerConfigurationService`) - Authentication middleware - IChatClient with Azure OpenAI @@ -79,36 +79,28 @@ Local tool implementation for weather queries: ### Tools/DateTimeFunctionTool.cs Utility tool for date/time queries. -### telemetry/AgentMetrics.cs -Custom observability helpers for tracing agent operations. - ## Message Flow ``` -1. HTTP POST /api/messages - │ -2. AgentMetrics.InvokeObservedHttpOperation() +1. HTTP POST /api/messages [auto-instrumented by ASP.NET Core] │ -3. adapter.ProcessAsync() → MyAgent.OnMessageAsync() +2. adapter.ProcessAsync() → MyAgent.OnMessageAsync() │ -4. A365OtelWrapper.InvokeObservedAgentOperation() - │ └── Observability context setup +3. Send ack + typing indicator to Teams │ -5. StreamingResponse.QueueInformativeUpdateAsync("Just a moment...") - │ -6. GetClientAgent() +4. GetClientAgent() │ ├── Create local tools (DateTime, Weather) │ ├── GetMcpToolsAsync() from MCP servers │ └── Build ChatClientAgent with instructions │ -7. GetConversationThread() - Load or create thread +5. GetConversationSessionAsync() - Load or create session │ -8. agent.RunStreamingAsync() +6. agent.RunStreamingAsync() [auto-instrumented: gen_ai.* on chat span] │ └── Stream responses to client │ -9. Save thread state +7. Save session state │ -10. StreamingResponse.EndStreamAsync() +8. StreamingResponse.EndStreamAsync() ``` ## Tool Integration @@ -169,25 +161,38 @@ var a365Tools = await toolService.GetMcpToolsAsync( ### Environment Variables ```bash ASPNETCORE_ENVIRONMENT=Development -BEARER_TOKEN=your-bearer-token # Development only -SKIP_TOOLING_ON_ERRORS=true # Development fallback +OTEL_SERVICE_NAME=Agent Framework Sample # Sets service.name in traces +BEARER_TOKEN=your-bearer-token # Development only +SKIP_TOOLING_ON_ERRORS=true # Development fallback ``` ## Observability -### Tracing Setup +Observability is provided entirely by the `Microsoft.OpenTelemetry` distro — no custom tracing code in this sample. + +### Setup ```csharp -builder.ConfigureOpenTelemetry(); -builder.Services.AddAgenticTracingExporter(clusterCategory: "production"); -builder.AddA365Tracing(config => { - config.WithAgentFramework(); +builder.UseMicrosoftOpenTelemetry(o => +{ + o.Exporters = builder.Environment.IsDevelopment() + ? ExportTarget.Agent365 | ExportTarget.Console + : ExportTarget.Agent365; + + o.Instrumentation.EnableAspNetCoreInstrumentation = true; + o.Instrumentation.EnableHttpClientInstrumentation = true; + o.Instrumentation.EnableAzureSdkInstrumentation = true; }); ``` -### Observed Operations -- `agent.process_message` - HTTP endpoint -- `MessageProcessor` - Message handling -- `WelcomeMessage` - Welcome flow +### Auto-instrumented Spans +- `POST /api/messages` — inbound request (ASP.NET Core) +- `POST login.microsoftonline.com` — MSAL token acquisition (HttpClient) +- `POST smba.trafficmanager.net` — outbound Teams messages (HttpClient) +- `POST …openai.azure.com/…/chat/completions` — Azure OpenAI HTTP call (HttpClient) +- `chat ` — `gen_ai.*` semantic attributes: model, tools, messages, tokens (Microsoft.Extensions.AI) +- `invoke_agent ` — agent-level span with agent ID and token counts (Microsoft.Agents.AI) + +The `gen_ai.*` attributes come from `.UseOpenTelemetry(sourceName: null, ...)` on the `ChatClientAgent` builder — the only explicit instrumentation call in the sample. ## Authentication @@ -238,8 +243,8 @@ turnState.Conversation.SetValue("conversation.threadInfo", ProtocolJsonSerialize ```xml - - + + ``` diff --git a/dotnet/agent-framework/sample-agent/telemetry/A365OtelWrapper.cs b/dotnet/agent-framework/sample-agent/telemetry/A365OtelWrapper.cs deleted file mode 100644 index fd408907..00000000 --- a/dotnet/agent-framework/sample-agent/telemetry/A365OtelWrapper.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Agents.A365.Observability.Hosting.Caching; -using Microsoft.Agents.A365.Observability.Runtime.Common; -using Microsoft.Agents.A365.Runtime.Utils; -using Microsoft.Agents.Builder; -using Microsoft.Agents.Builder.App.UserAuth; -using Microsoft.Agents.Builder.State; - -namespace Agent365AgentFrameworkSampleAgent.telemetry -{ - public static class A365OtelWrapper - { - public static async Task InvokeObservedAgentOperation( - string operationName, - ITurnContext turnContext, - ITurnState turnState, - IExporterTokenCache? agentTokenCache, - UserAuthorization authSystem, - string authHandlerName, - ILogger? logger, - Func func - ) - { - // Wrap the operation with AgentSDK observability. - await AgentMetrics.InvokeObservedAgentOperation( - operationName, - turnContext, - async () => - { - // Resolve the tenant and agent id being used to communicate with A365 services. - (string agentId, string tenantId) = await ResolveTenantAndAgentId(turnContext, authSystem, authHandlerName); - - using var baggageScope = new BaggageBuilder() - .TenantId(tenantId) - .AgentId(agentId) - .Build(); - - try - { - agentTokenCache?.RegisterObservability(agentId, tenantId, new AgenticTokenStruct( - authSystem, - turnContext, - authHandlerName - ), EnvironmentUtils.GetObservabilityAuthenticationScope()); - } - catch (Exception ex) - { - logger?.LogWarning($"There was an error registering for observability: {ex.Message}"); - } - - // Invoke the actual operation. - await func().ConfigureAwait(false); - }).ConfigureAwait(false); - } - - /// - /// Resolve Tenant and Agent Id from the turn context. - /// - /// - /// - private static async Task<(string agentId, string tenantId)> ResolveTenantAndAgentId(ITurnContext turnContext, UserAuthorization authSystem, string authHandlerName) - { - string agentId = ""; - if (turnContext.Activity.IsAgenticRequest()) - { - agentId = turnContext.Activity.GetAgenticInstanceId(); - } - else - { - if (authSystem != null && !string.IsNullOrEmpty(authHandlerName)) - agentId = Utility.ResolveAgentIdentity(turnContext, await authSystem.GetTurnTokenAsync(turnContext, authHandlerName)); - } - agentId = agentId ?? Guid.Empty.ToString(); - string? tempTenantId = turnContext?.Activity?.Conversation?.TenantId ?? turnContext?.Activity?.Recipient?.TenantId; - string tenantId = tempTenantId ?? Guid.Empty.ToString(); - - return (agentId, tenantId); - } - - } -} diff --git a/dotnet/agent-framework/sample-agent/telemetry/AgentMetrics.cs b/dotnet/agent-framework/sample-agent/telemetry/AgentMetrics.cs deleted file mode 100644 index a43af9fb..00000000 --- a/dotnet/agent-framework/sample-agent/telemetry/AgentMetrics.cs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Agents.Builder; -using Microsoft.Agents.Core; -using OpenTelemetry; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using System; -using System.Diagnostics; -using System.Diagnostics.Metrics; - -namespace Agent365AgentFrameworkSampleAgent.telemetry -{ - public static class AgentMetrics - { - public static readonly string SourceName = "A365.AgentFramework"; - - public static readonly ActivitySource ActivitySource = new(SourceName); - - private static readonly Meter Meter = new ("A365.AgentFramework", "1.0.0"); - - public static readonly Counter MessageProcessedCounter = Meter.CreateCounter( - "agent.messages.processed", - "messages", - "Number of messages processed by the agent"); - - public static readonly Counter RouteExecutedCounter = Meter.CreateCounter( - "agent.routes.executed", - "routes", - "Number of routes executed by the agent"); - - public static readonly Histogram MessageProcessingDuration = Meter.CreateHistogram( - "agent.message.processing.duration", - "ms", - "Duration of message processing in milliseconds"); - - public static readonly Histogram RouteExecutionDuration = Meter.CreateHistogram( - "agent.route.execution.duration", - "ms", - "Duration of route execution in milliseconds"); - - public static readonly UpDownCounter ActiveConversations = Meter.CreateUpDownCounter( - "agent.conversations.active", - "conversations", - "Number of active conversations"); - - - public static Activity InitializeMessageHandlingActivity(string handlerName, ITurnContext context) - { - var activity = ActivitySource.StartActivity(handlerName); - activity?.SetTag("Activity.Type", context.Activity.Type.ToString()); - activity?.SetTag("Agent.IsAgentic", context.IsAgenticRequest()); - activity?.SetTag("Caller.Id", context.Activity.From?.Id); - activity?.SetTag("Conversation.Id", context.Activity.Conversation?.Id); - activity?.SetTag("Channel.Id", context.Activity.ChannelId?.ToString()); - activity?.SetTag("Message.Text.Length", context.Activity.Text?.Length ?? 0); - - activity?.AddEvent(new ActivityEvent("Message.Processed", DateTimeOffset.UtcNow, new() - { - ["Agent.IsAgentic"] = context.IsAgenticRequest(), - ["Caller.Id"] = context.Activity.From?.Id, - ["Channel.Id"] = context.Activity.ChannelId?.ToString(), - ["Message.Id"] = context.Activity.Id, - ["Message.Text"] = context.Activity.Text - })); - return activity!; - } - - public static void FinalizeMessageHandlingActivity(Activity activity, ITurnContext context, long duration, bool success) - { - MessageProcessingDuration.Record(duration, - new("Conversation.Id", context.Activity.Conversation?.Id ?? "unknown"), - new("Channel.Id", context.Activity.ChannelId?.ToString() ?? "unknown")); - - RouteExecutedCounter.Add(1, - new("Route.Type", "message_handler"), - new("Conversation.Id", context.Activity.Conversation?.Id ?? "unknown")); - - if (success) - { - activity?.SetStatus(ActivityStatusCode.Ok); - } - else - { - activity?.SetStatus(ActivityStatusCode.Error); - } - activity?.Stop(); - activity?.Dispose(); - } - - public static Task InvokeObservedHttpOperation(string operationName, Action func) - { - using var activity = ActivitySource.StartActivity(operationName); - try - { - func(); - activity?.SetStatus(ActivityStatusCode.Ok); - } - catch (Exception ex) - { - activity?.SetStatus(ActivityStatusCode.Error, ex.Message); - activity?.AddEvent(new ActivityEvent("exception", DateTimeOffset.UtcNow, new() - { - ["exception.type"] = ex.GetType().FullName, - ["exception.message"] = ex.Message, - ["exception.stacktrace"] = ex.StackTrace - })); - throw; - } - return Task.CompletedTask; - } - - public static Task InvokeObservedAgentOperation(string operationName, ITurnContext context, Func func) - { - MessageProcessedCounter.Add(1); - // Init the activity for observability - var activity = InitializeMessageHandlingActivity(operationName, context); - var routeStopwatch = Stopwatch.StartNew(); - try - { - return func(); - } - catch (Exception ex) - { - activity?.SetStatus(ActivityStatusCode.Error, ex.Message); - activity?.AddEvent(new ActivityEvent("exception", DateTimeOffset.UtcNow, new() - { - ["exception.type"] = ex.GetType().FullName, - ["exception.message"] = ex.Message, - ["exception.stacktrace"] = ex.StackTrace - })); - throw; - } - finally - { - routeStopwatch.Stop(); - FinalizeMessageHandlingActivity(activity, context, routeStopwatch.ElapsedMilliseconds, true); - } - } - } -} From 424a39005feeb2217ac69ec79571d12202293846 Mon Sep 17 00:00:00 2001 From: Sellakumaran Kanagarathnam Date: Tue, 5 May 2026 13:55:50 -0700 Subject: [PATCH 5/9] fix(dotnet/agent-framework): address Copilot PR review comments in MyAgent.cs - Remove redundant null-conditional on turnContext (non-null guaranteed by guard at top of OnMessageAsync) at lines 234 and 250 - Use StringBuilder to accumulate userText in the attachment loop instead of repeated string concatenation - Use LINQ Where filter in WelcomeMessageAsync instead of foreach + inner if Co-Authored-By: Claude Sonnet 4.6 --- .../sample-agent/Agent/MyAgent.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs index cbbacb68..c7086d08 100644 --- a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs +++ b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs @@ -13,6 +13,7 @@ using Microsoft.Agents.Core.Serialization; using Microsoft.Extensions.AI; using System.Collections.Concurrent; +using System.Text; using System.Text.Json; namespace Agent365AgentFrameworkSampleAgent.Agent @@ -128,12 +129,10 @@ public MyAgent(AgentApplicationOptions options, protected async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) { - foreach (ChannelAccount member in turnContext.Activity.MembersAdded) + foreach (ChannelAccount member in (turnContext.Activity.MembersAdded ?? []) + .Where(m => m.Id != turnContext.Activity.Recipient.Id)) { - if (member.Id != turnContext.Activity.Recipient.Id) - { - await turnContext.SendActivityAsync(AgentWelcomeMessage); - } + await turnContext.SendActivityAsync(AgentWelcomeMessage); } } @@ -225,29 +224,30 @@ protected async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnSta await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Just a moment please..").ConfigureAwait(false); try { - var userText = turnContext.Activity.Text?.Trim() ?? string.Empty; + var userTextBuilder = new StringBuilder(turnContext.Activity.Text?.Trim() ?? string.Empty); var _agent = await GetClientAgent(turnContext, turnState, _toolService, ToolAuthHandlerName); // Read or Create the conversation session for this conversation. AgentSession? session = await GetConversationSessionAsync(_agent, turnState, cancellationToken); - if (turnContext?.Activity?.Attachments?.Count > 0) + if (turnContext.Activity?.Attachments?.Count > 0) { foreach (var attachment in turnContext.Activity.Attachments) { if (attachment.ContentType == "application/vnd.microsoft.teams.file.download.info" && !string.IsNullOrEmpty(attachment.ContentUrl)) { - userText += $"\n\n[User has attached a file: {attachment.Name}. The file can be downloaded from {attachment.ContentUrl}]"; + userTextBuilder.Append($"\n\n[User has attached a file: {attachment.Name}. The file can be downloaded from {attachment.ContentUrl}]"); } } } + var userText = userTextBuilder.ToString(); // Stream the response back to the user as we receive it from the agent. await foreach (var response in _agent!.RunStreamingAsync(userText, session, cancellationToken: cancellationToken)) { if (response.Role == ChatRole.Assistant && !string.IsNullOrEmpty(response.Text)) { - turnContext?.StreamingResponse.QueueTextChunk(response.Text); + turnContext.StreamingResponse.QueueTextChunk(response.Text); } } var serializedSession = await _agent!.SerializeSessionAsync(session!); From fdc3d34c8c477586cf1fc86253365eda65403e84 Mon Sep 17 00:00:00 2001 From: Sellakumaran Kanagarathnam Date: Tue, 5 May 2026 14:27:42 -0700 Subject: [PATCH 6/9] fix(dotnet/agent-framework): use discard in WelcomeMessageAsync foreach The loop variable was unused after moving the filter into .Where(); replace with var _ to suppress the "useless assignment" code scanning warning. Co-Authored-By: Claude Sonnet 4.6 --- dotnet/agent-framework/sample-agent/Agent/MyAgent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs index c7086d08..a9fb7de1 100644 --- a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs +++ b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs @@ -129,7 +129,7 @@ public MyAgent(AgentApplicationOptions options, protected async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) { - foreach (ChannelAccount member in (turnContext.Activity.MembersAdded ?? []) + foreach (var _ in (turnContext.Activity.MembersAdded ?? []) .Where(m => m.Id != turnContext.Activity.Recipient.Id)) { await turnContext.SendActivityAsync(AgentWelcomeMessage); From 41797e8a7463250e86fd43b0885413f23cbc9c32 Mon Sep 17 00:00:00 2001 From: Grant Harris Date: Tue, 5 May 2026 22:37:58 -0700 Subject: [PATCH 7/9] remove e2e tests temporarily and update package versions --- .../workflows/e2e-dotnet-agent-framework.yml | 110 ---------- .../workflows/e2e-dotnet-semantic-kernel.yml | 110 ---------- .github/workflows/e2e-nodejs-langchain.yml | 117 ---------- .github/workflows/e2e-nodejs-openai.yml | 117 ---------- .../workflows/e2e-python-agent-framework.yml | 129 ----------- .github/workflows/e2e-python-openai.yml | 129 ----------- .../sample-agent/Agents/MyAgent.cs | 8 +- .../semantic-kernel/sample-agent/Program.cs | 35 ++- .../SemanticKernelSampleAgent.csproj | 22 +- .../sample-agent/telemetry/A365OtelWrapper.cs | 16 -- .../telemetry/AgentOTELExtensions.cs | 207 ------------------ 11 files changed, 25 insertions(+), 975 deletions(-) delete mode 100644 dotnet/semantic-kernel/sample-agent/telemetry/AgentOTELExtensions.cs diff --git a/.github/workflows/e2e-dotnet-agent-framework.yml b/.github/workflows/e2e-dotnet-agent-framework.yml index 9a7c3349..e7413971 100644 --- a/.github/workflows/e2e-dotnet-agent-framework.yml +++ b/.github/workflows/e2e-dotnet-agent-framework.yml @@ -56,114 +56,4 @@ jobs: -Runtime "dotnet" ` -WorkingDirectory "${{ env.SAMPLE_PATH }}" - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate appsettings.json - shell: pwsh - run: | - $configMappings = @{ - "AIServices:AzureOpenAI:Endpoint" = "${{ secrets.DOTNET_AF_AZURE_OPENAI_ENDPOINT }}" - "AIServices:AzureOpenAI:ApiKey" = "${{ secrets.DOTNET_AF_AZURE_OPENAI_API_KEY }}" - "AIServices:AzureOpenAI:DeploymentName" = "${{ secrets.DOTNET_AF_AZURE_OPENAI_DEPLOYMENT }}" - "TokenValidation:Enabled" = "false" - "Connections:ServiceConnection:Settings:AuthType" = "ClientSecret" - "Connections:ServiceConnection:Settings:ClientId" = "${{ secrets.DOTNET_AF_CLIENT_ID }}" - "Connections:ServiceConnection:Settings:ClientSecret" = "${{ secrets.DOTNET_AF_CLIENT_SECRET }}" - "Connections:ServiceConnection:Settings:TenantId" = "${{ secrets.TENANT_ID }}" - "Connections:ServiceConnection:Settings:AuthorityEndpoint" = "https://login.microsoftonline.com/${{ secrets.TENANT_ID }}" - "Connections:ServiceConnection:Settings:Scopes:0" = "5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default" - } - & "${{ env.SCRIPTS_PATH }}/Generate-AppSettings.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/appsettings.json" ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "dotnet run --no-build" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "dotnet" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - throw "Agent process has stopped" - } - } - $response = Invoke-WebRequest -Uri "http://localhost:${{ env.AGENT_PORT }}/api/health" -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health: $($response.StatusCode)" -ForegroundColor Green - - name: Restore E2E Test Dependencies - run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --verbosity normal ` - --logger "console;verbosity=detailed" ` - --logger "trx;LogFileName=test-results-dotnet-af.trx" - env: - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Emit Test Conversations - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Emit-TestConversations.ps1" ` - -TestResultsDir "${{ runner.temp }}/TestConversations" - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Stop Agent Process - if: always() - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -AgentPID $agentPid - } - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-dotnet-af - path: | - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*.trx - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*-logs.txt - retention-days: 7 diff --git a/.github/workflows/e2e-dotnet-semantic-kernel.yml b/.github/workflows/e2e-dotnet-semantic-kernel.yml index 1736e848..64f2412a 100644 --- a/.github/workflows/e2e-dotnet-semantic-kernel.yml +++ b/.github/workflows/e2e-dotnet-semantic-kernel.yml @@ -56,114 +56,4 @@ jobs: -Runtime "dotnet" ` -WorkingDirectory "${{ env.SAMPLE_PATH }}" - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate appsettings.json - shell: pwsh - run: | - $configMappings = @{ - "AIServices:UseAzureOpenAI" = "true" - "AIServices:AzureOpenAI:Endpoint" = "${{ secrets.DOTNET_SK_AZURE_OPENAI_ENDPOINT }}" - "AIServices:AzureOpenAI:ApiKey" = "${{ secrets.DOTNET_SK_AZURE_OPENAI_API_KEY }}" - "AIServices:AzureOpenAI:DeploymentName" = "${{ secrets.DOTNET_SK_AZURE_OPENAI_DEPLOYMENT }}" - "TokenValidation:Enabled" = "false" - "TokenValidation:TenantId" = "${{ secrets.TENANT_ID }}" - "Connections:ServiceConnection:Settings:AuthType" = "ClientSecret" - "Connections:ServiceConnection:Settings:ClientId" = "${{ secrets.DOTNET_SK_CLIENT_ID }}" - "Connections:ServiceConnection:Settings:ClientSecret" = "${{ secrets.DOTNET_SK_CLIENT_SECRET }}" - "Connections:ServiceConnection:Settings:TenantId" = "${{ secrets.TENANT_ID }}" - } - & "${{ env.SCRIPTS_PATH }}/Generate-AppSettings.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/appsettings.json" ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "dotnet run --no-build" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "dotnet" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - throw "Agent process has stopped" - } - } - $response = Invoke-WebRequest -Uri "http://localhost:${{ env.AGENT_PORT }}/api/health" -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health: $($response.StatusCode)" -ForegroundColor Green - - name: Restore E2E Test Dependencies - run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --verbosity normal ` - --logger "console;verbosity=detailed" ` - --logger "trx;LogFileName=test-results-dotnet-sk.trx" - env: - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Emit Test Conversations - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Emit-TestConversations.ps1" ` - -TestResultsDir "${{ runner.temp }}/TestConversations" - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Stop Agent Process - if: always() - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -AgentPID $agentPid - } - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-dotnet-sk - path: | - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*.trx - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*-logs.txt - retention-days: 7 diff --git a/.github/workflows/e2e-nodejs-langchain.yml b/.github/workflows/e2e-nodejs-langchain.yml index fd88cccc..5adf0ee1 100644 --- a/.github/workflows/e2e-nodejs-langchain.yml +++ b/.github/workflows/e2e-nodejs-langchain.yml @@ -66,121 +66,4 @@ jobs: -Runtime "nodejs" ` -WorkingDirectory "${{ env.SAMPLE_PATH }}" - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate .env configuration - shell: pwsh - run: | - $configMappings = @{ - "NODE_ENV" = "development" - "AZURE_OPENAI_API_KEY" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_API_KEY }}" - "AZURE_OPENAI_ENDPOINT" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_ENDPOINT }}" - "AZURE_OPENAI_DEPLOYMENT" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_DEPLOYMENT }}" - "AZURE_OPENAI_API_VERSION" = "2024-12-01-preview" - "connections__service_connection__settings__authType" = "ClientSecret" - "connections__service_connection__settings__clientId" = "${{ secrets.NODEJS_OPENAI_AGENT_ID }}" - "connections__service_connection__settings__clientSecret" = "${{ secrets.NODEJS_OPENAI_CLIENT_SECRET }}" - "connections__service_connection__settings__tenantId" = "${{ secrets.TENANT_ID }}" - "connectionsMap__0__serviceUrl" = "*" - "connectionsMap__0__connection" = "service_connection" - "agentic_scopes" = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default" - } - & "${{ env.SCRIPTS_PATH }}/Generate-EnvConfig.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/.env" ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Port ${{ env.AGENT_PORT }} ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "node dist/index.js" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "nodejs" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - throw "Agent process has stopped" - } - } - $agentUrl = "http://localhost:${{ env.AGENT_PORT }}" - $healthResponse = Invoke-WebRequest -Uri "$agentUrl/api/health" -Method GET -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health check: $($healthResponse.StatusCode)" -ForegroundColor Green - - name: Restore E2E Test Dependencies - run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --verbosity normal ` - --logger "console;verbosity=detailed" ` - --logger "trx;LogFileName=test-results-nodejs-langchain.trx" ` - --filter "Category=HTTP" - env: - AGENT_URL: http://localhost:${{ env.AGENT_PORT }} - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Emit Test Conversations - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Emit-TestConversations.ps1" ` - -TestResultsDir "${{ runner.temp }}/TestConversations" - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Stop Agent Process - if: always() - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -AgentPID $agentPid - } - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-nodejs-langchain - path: | - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*.trx - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*-logs.txt - retention-days: 7 diff --git a/.github/workflows/e2e-nodejs-openai.yml b/.github/workflows/e2e-nodejs-openai.yml index 366a0f03..9c8ca775 100644 --- a/.github/workflows/e2e-nodejs-openai.yml +++ b/.github/workflows/e2e-nodejs-openai.yml @@ -66,121 +66,4 @@ jobs: -Runtime "nodejs" ` -WorkingDirectory "${{ env.SAMPLE_PATH }}" - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate .env configuration - shell: pwsh - run: | - $configMappings = @{ - "NODE_ENV" = "development" - "AZURE_OPENAI_API_KEY" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_API_KEY }}" - "AZURE_OPENAI_ENDPOINT" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_ENDPOINT }}" - "AZURE_OPENAI_DEPLOYMENT" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_DEPLOYMENT }}" - "AZURE_OPENAI_API_VERSION" = "2024-12-01-preview" - "connections__service_connection__settings__authType" = "ClientSecret" - "connections__service_connection__settings__clientId" = "${{ secrets.NODEJS_OPENAI_AGENT_ID }}" - "connections__service_connection__settings__clientSecret" = "${{ secrets.NODEJS_OPENAI_CLIENT_SECRET }}" - "connections__service_connection__settings__tenantId" = "${{ secrets.TENANT_ID }}" - "connectionsMap__0__serviceUrl" = "*" - "connectionsMap__0__connection" = "service_connection" - "agentic_scopes" = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default" - } - & "${{ env.SCRIPTS_PATH }}/Generate-EnvConfig.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/.env" ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Port ${{ env.AGENT_PORT }} ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "node dist/index.js" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "nodejs" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - throw "Agent process has stopped" - } - } - $agentUrl = "http://localhost:${{ env.AGENT_PORT }}" - $healthResponse = Invoke-WebRequest -Uri "$agentUrl/api/health" -Method GET -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health check: $($healthResponse.StatusCode)" -ForegroundColor Green - - name: Restore E2E Test Dependencies - run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --verbosity normal ` - --logger "console;verbosity=detailed" ` - --logger "trx;LogFileName=test-results-nodejs-openai.trx" ` - --filter "Category=HTTP" - env: - AGENT_URL: http://localhost:${{ env.AGENT_PORT }} - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Emit Test Conversations - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Emit-TestConversations.ps1" ` - -TestResultsDir "${{ runner.temp }}/TestConversations" - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Stop Agent Process - if: always() - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -AgentPID $agentPid - } - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-nodejs-openai - path: | - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*.trx - ${{ env.E2E_TESTS_PATH }}/TestResults/**/*-logs.txt - retention-days: 7 diff --git a/.github/workflows/e2e-python-agent-framework.yml b/.github/workflows/e2e-python-agent-framework.yml index a4f3c05f..342a550a 100644 --- a/.github/workflows/e2e-python-agent-framework.yml +++ b/.github/workflows/e2e-python-agent-framework.yml @@ -60,133 +60,4 @@ jobs: -Runtime "python" ` -WorkingDirectory "${{ env.SAMPLE_PATH }}" - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate .env configuration - shell: pwsh - run: | - $configMappings = @{ - "AZURE_OPENAI_API_KEY" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_API_KEY }}" - "AZURE_OPENAI_ENDPOINT" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_ENDPOINT }}" - "AZURE_OPENAI_DEPLOYMENT" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_DEPLOYMENT }}" - "AZURE_OPENAI_API_VERSION" = "2024-12-01-preview" - "USE_AGENTIC_AUTH" = "false" - "MCP_PLATFORM_ENDPOINT" = "https://agent365.svc.cloud.microsoft" - "AGENT_ID" = "${{ secrets.PYTHON_OPENAI_AGENT_ID }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__AUTHTYPE" = "ClientSecret" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID" = "${{ secrets.PYTHON_OPENAI_AGENT_ID }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET" = "${{ secrets.PYTHON_OPENAI_CLIENT_SECRET }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID" = "${{ secrets.TENANT_ID }}" - "CONNECTIONSMAP__0__SERVICEURL" = "*" - "CONNECTIONSMAP__0__CONNECTION" = "SERVICE_CONNECTION" - "ENABLE_OBSERVABILITY" = "true" - } - & "${{ env.SCRIPTS_PATH }}/Generate-EnvConfig.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/.env" ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Port ${{ env.AGENT_PORT }} ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "uv run python start_with_generic_host.py" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "python" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - Write-Host "=== Verifying Agent ===" -ForegroundColor Cyan - - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - Write-Host "Checking agent process (PID: $agentPid)..." -ForegroundColor Gray - - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - - $logFile = "${{ env.SAMPLE_PATH }}/agent.log" - if (Test-Path $logFile) { - Write-Host "Agent logs:" -ForegroundColor Yellow - Get-Content $logFile - } - throw "Agent process has stopped" - } - } - - $agentUrl = "http://localhost:${{ env.AGENT_PORT }}" - Write-Host "Verifying agent at $agentUrl..." -ForegroundColor Gray - $healthResponse = Invoke-WebRequest -Uri "$agentUrl/api/health" -Method GET -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health check: $($healthResponse.StatusCode)" -ForegroundColor Green - - - name: Restore E2E Test Dependencies - shell: pwsh - run: | - dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --filter "Category=HTTP" ` - --logger "trx;LogFileName=test-results.trx" ` - --results-directory "${{ runner.temp }}/TestResults" - env: - AGENT_PORT: ${{ env.AGENT_PORT }} - AGENT_URL: http://localhost:${{ env.AGENT_PORT }} - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-python-agent-framework - path: ${{ runner.temp }}/TestResults/ - retention-days: 30 - - name: Emit Test Conversations - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Emit-TestConversations.ps1" ` - -TestResultsDir "${{ runner.temp }}/TestConversations" - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Cleanup Agent Process - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" ` - -AgentPID "${{ steps.start-agent.outputs.AGENT_PID }}" ` - -Port ${{ env.AGENT_PORT }} diff --git a/.github/workflows/e2e-python-openai.yml b/.github/workflows/e2e-python-openai.yml index e666053c..06b449c5 100644 --- a/.github/workflows/e2e-python-openai.yml +++ b/.github/workflows/e2e-python-openai.yml @@ -60,133 +60,4 @@ jobs: -Runtime "python" ` -WorkingDirectory "${{ env.SAMPLE_PATH }}" - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate .env configuration - shell: pwsh - run: | - $configMappings = @{ - "AZURE_OPENAI_API_KEY" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_API_KEY }}" - "AZURE_OPENAI_ENDPOINT" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_ENDPOINT }}" - "AZURE_OPENAI_DEPLOYMENT" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_DEPLOYMENT }}" - "AZURE_OPENAI_API_VERSION" = "2024-12-01-preview" - "USE_AGENTIC_AUTH" = "false" - "MCP_PLATFORM_ENDPOINT" = "https://agent365.svc.cloud.microsoft" - "AGENT_ID" = "${{ secrets.PYTHON_OPENAI_AGENT_ID }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__AUTHTYPE" = "ClientSecret" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID" = "${{ secrets.PYTHON_OPENAI_AGENT_ID }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET" = "${{ secrets.PYTHON_OPENAI_CLIENT_SECRET }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID" = "${{ secrets.TENANT_ID }}" - "CONNECTIONSMAP__0__SERVICEURL" = "*" - "CONNECTIONSMAP__0__CONNECTION" = "SERVICE_CONNECTION" - "ENABLE_OBSERVABILITY" = "true" - } - & "${{ env.SCRIPTS_PATH }}/Generate-EnvConfig.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/.env" ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Port ${{ env.AGENT_PORT }} ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "uv run python start_with_generic_host.py" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "python" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - Write-Host "=== Verifying Agent ===" -ForegroundColor Cyan - - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - Write-Host "Checking agent process (PID: $agentPid)..." -ForegroundColor Gray - - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - - $logFile = "${{ env.SAMPLE_PATH }}/agent.log" - if (Test-Path $logFile) { - Write-Host "Agent logs:" -ForegroundColor Yellow - Get-Content $logFile - } - throw "Agent process has stopped" - } - } - - $agentUrl = "http://localhost:${{ env.AGENT_PORT }}" - Write-Host "Verifying agent at $agentUrl..." -ForegroundColor Gray - $healthResponse = Invoke-WebRequest -Uri "$agentUrl/api/health" -Method GET -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health check: $($healthResponse.StatusCode)" -ForegroundColor Green - - - name: Restore E2E Test Dependencies - shell: pwsh - run: | - dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --filter "Category=HTTP" ` - --logger "trx;LogFileName=test-results.trx" ` - --results-directory "${{ runner.temp }}/TestResults" - env: - AGENT_PORT: ${{ env.AGENT_PORT }} - AGENT_URL: http://localhost:${{ env.AGENT_PORT }} - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-python-openai - path: ${{ runner.temp }}/TestResults/ - retention-days: 30 - - name: Emit Test Conversations - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Emit-TestConversations.ps1" ` - -TestResultsDir "${{ runner.temp }}/TestConversations" - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Cleanup Agent Process - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" ` - -AgentPID "${{ steps.start-agent.outputs.AGENT_PID }}" ` - -Port ${{ env.AGENT_PORT }} diff --git a/dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs b/dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs index 1dbaeab0..5a111a3c 100644 --- a/dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs +++ b/dotnet/semantic-kernel/sample-agent/Agents/MyAgent.cs @@ -5,7 +5,6 @@ using Agent365SemanticKernelSampleAgent.telemetry; using AgentNotification; using Microsoft.Agents.A365.Notifications.Models; -using Microsoft.Agents.A365.Observability.Caching; using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services; using Microsoft.Agents.Builder; using Microsoft.Agents.Builder.App; @@ -27,7 +26,6 @@ public class MyAgent : AgentApplication { private readonly Kernel _kernel; private readonly IMcpToolRegistrationService _toolsService; - private readonly IExporterTokenCache _agentTokenCache; private readonly ILogger _logger; private readonly IConfiguration _configuration; // Setup reusable auto sign-in handlers @@ -38,12 +36,11 @@ public class MyAgent : AgentApplication internal static bool IsApplicationInstalled { get; set; } = false; internal static bool TermsAndConditionsAccepted { get; set; } = false; - public MyAgent(AgentApplicationOptions options, IConfiguration configuration, Kernel kernel, IMcpToolRegistrationService toolService, IExporterTokenCache agentTokenCache, ILogger logger) : base(options) + public MyAgent(AgentApplicationOptions options, IConfiguration configuration, Kernel kernel, IMcpToolRegistrationService toolService, ILogger logger) : base(options) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _kernel = kernel ?? throw new ArgumentNullException(nameof(kernel)); _toolsService = toolService ?? throw new ArgumentNullException(nameof(toolService)); - _agentTokenCache = agentTokenCache ?? throw new ArgumentNullException(nameof(agentTokenCache)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); // Disable for development purpose. In production, you would typically want to have the user accept the terms and conditions on first use and then store that in a retrievable location. @@ -100,7 +97,6 @@ await A365OtelWrapper.InvokeObservedAgentOperation( "MessageProcessor", turnContext, turnState, - _agentTokenCache, UserAuthorization, ObservabilityAuthHandlerName, _logger, @@ -187,7 +183,6 @@ await A365OtelWrapper.InvokeObservedAgentOperation( "AgentNotificationActivityAsync", turnContext, turnState, - _agentTokenCache, UserAuthorization, ObservabilityAuthHandlerName, _logger, @@ -302,7 +297,6 @@ await A365OtelWrapper.InvokeObservedAgentOperation( "OnHireMessageAsync", turnContext, turnState, - _agentTokenCache, UserAuthorization, ObservabilityAuthHandlerName, _logger, diff --git a/dotnet/semantic-kernel/sample-agent/Program.cs b/dotnet/semantic-kernel/sample-agent/Program.cs index 35c0ccb4..c70100ef 100644 --- a/dotnet/semantic-kernel/sample-agent/Program.cs +++ b/dotnet/semantic-kernel/sample-agent/Program.cs @@ -3,9 +3,7 @@ using Agent365SemanticKernelSampleAgent.Agents; using Agent365SemanticKernelSampleAgent.telemetry; -using Microsoft.Agents.A365.Observability; -using Microsoft.Agents.A365.Observability.Extensions.SemanticKernel; -using Microsoft.Agents.A365.Observability.Runtime; +using Microsoft.OpenTelemetry; using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services; using Microsoft.Agents.A365.Tooling.Services; using Microsoft.Agents.Builder; @@ -23,8 +21,19 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -// Setup Aspire service defaults, including OpenTelemetry, Service Discovery, Resilience, and Health Checks - builder.ConfigureOpenTelemetry(); +// Configure OpenTelemetry distro — Console exporter only in Development to avoid PII leaks +builder.UseMicrosoftOpenTelemetry(o => +{ + o.Exporters = builder.Environment.IsDevelopment() + ? ExportTarget.Agent365 | ExportTarget.Console + : ExportTarget.Agent365; + + // Agent365-only export suppresses infrastructure instrumentation by default. + // Re-enable explicitly so HTTP calls (Azure OpenAI, auth, Teams) appear in traces. + o.Instrumentation.EnableAspNetCoreInstrumentation = true; + o.Instrumentation.EnableHttpClientInstrumentation = true; + o.Instrumentation.EnableAzureSdkInstrumentation = true; +}); if (builder.Environment.IsDevelopment()) { @@ -57,15 +66,6 @@ apiKey: builder.Configuration.GetSection("AIServices:OpenAI").GetValue("ApiKey")!); } -// Configure observability. -builder.Services.AddAgenticTracingExporter(); - -// Add A365 tracing with Semantic Kernel integration -builder.AddA365Tracing(config => -{ - config.WithSemanticKernel(); -}); - // Add AgentApplicationOptions from appsettings section "AgentApplication". builder.AddAgentApplicationOptions(); @@ -97,12 +97,9 @@ app.UseAuthorization(); // This receives incoming messages from Azure Bot Service or other SDK Agents -var incomingRoute = app.MapPost("/api/messages", async (HttpRequest request, HttpResponse response, IAgentHttpAdapter adapter, IAgent agent, CancellationToken cancellationToken) => +app.MapPost("/api/messages", async (HttpRequest request, HttpResponse response, IAgentHttpAdapter adapter, IAgent agent, CancellationToken cancellationToken) => { - await AgentMetrics.InvokeObservedHttpOperation("agent.process_message", async () => - { - await adapter.ProcessAsync(request, response, agent, cancellationToken); - }).ConfigureAwait(false); + await adapter.ProcessAsync(request, response, agent, cancellationToken); }); // Health check endpoint for CI/CD pipelines and monitoring diff --git a/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.csproj b/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.csproj index af011466..d4fd4645 100644 --- a/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.csproj +++ b/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.csproj @@ -22,24 +22,18 @@ - + - - - - - - + + + + + + - - - - - - - + diff --git a/dotnet/semantic-kernel/sample-agent/telemetry/A365OtelWrapper.cs b/dotnet/semantic-kernel/sample-agent/telemetry/A365OtelWrapper.cs index e716e942..048e4240 100644 --- a/dotnet/semantic-kernel/sample-agent/telemetry/A365OtelWrapper.cs +++ b/dotnet/semantic-kernel/sample-agent/telemetry/A365OtelWrapper.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Agents.A365.Observability.Caching; using Microsoft.Agents.A365.Observability.Runtime.Common; using Microsoft.Agents.A365.Runtime.Utils; using Microsoft.Agents.Builder; @@ -19,7 +18,6 @@ public static async Task InvokeObservedAgentOperation( string operationName, ITurnContext turnContext, ITurnState turnState, - IExporterTokenCache? agentTokenCache, UserAuthorization authSystem, string authHandlerName, ILogger? logger, @@ -40,20 +38,6 @@ await AgentMetrics.InvokeObservedAgentOperation( .AgentId(agentId) .Build(); - try - { - agentTokenCache?.RegisterObservability(agentId, tenantId, new AgenticTokenStruct - { - UserAuthorization = authSystem, - TurnContext = turnContext, - AuthHandlerName = authHandlerName - }, EnvironmentUtils.GetObservabilityAuthenticationScope()); - } - catch (Exception ex) - { - logger?.LogWarning($"There was an error registering for observability: {ex.Message}"); - } - // Invoke the actual operation. await func().ConfigureAwait(false); }).ConfigureAwait(false); diff --git a/dotnet/semantic-kernel/sample-agent/telemetry/AgentOTELExtensions.cs b/dotnet/semantic-kernel/sample-agent/telemetry/AgentOTELExtensions.cs deleted file mode 100644 index 173067d5..00000000 --- a/dotnet/semantic-kernel/sample-agent/telemetry/AgentOTELExtensions.cs +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery; -using OpenTelemetry; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Agent365SemanticKernelSampleAgent.telemetry -{ - // Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. - // This can be used by ASP.NET Core apps, Azure Functions, and other .NET apps using the Generic Host. - // This allows you to use the local aspire desktop and monitor Agents SDK operations. - // To learn more about using the local aspire desktop, see https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone?tabs=bash - public static class AgentOTELExtensions - { - private const string HealthEndpointPath = "/health"; - private const string AlivenessEndpointPath = "/alive"; - - public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - builder.ConfigureOpenTelemetry(); - - builder.AddDefaultHealthChecks(); - - builder.Services.AddServiceDiscovery(); - - builder.Services.ConfigureHttpClientDefaults(http => - { - // Turn on resilience by default - http.AddStandardResilienceHandler(); - - // Turn on service discovery by default - http.AddServiceDiscovery(); - }); - - // Uncomment the following to restrict the allowed schemes for service discovery. - // builder.Services.Configure(options => - // { - // options.AllowedSchemes = ["https"]; - // }); - - return builder; - } - - public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - builder.Logging.AddOpenTelemetry(logging => - { - logging.IncludeFormattedMessage = true; - logging.IncludeScopes = true; - }); - - builder.Services.AddOpenTelemetry() - .ConfigureResource(r => r - .Clear() - .AddService( - serviceName: "A365.SemanticKernel", - serviceVersion: "1.0.0", - serviceInstanceId: Environment.MachineName) - .AddAttributes(new Dictionary - { - ["deployment.environment"] = builder.Environment.EnvironmentName, - ["service.namespace"] = "Microsoft.Agents" - })) - .WithMetrics(metrics => - { - metrics.AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation() - .AddMeter("agent.messages.processed", - "agent.routes.executed", - "agent.conversations.active", - "agent.route.execution.duration", - "agent.message.processing.duration"); - }) - .WithTracing(tracing => - { - tracing.AddSource(builder.Environment.ApplicationName) - .AddSource( - "A365.SemanticKernel", - "A365.SemanticKernel.MyAgent", - "Microsoft.Agents.Builder", - "Microsoft.Agents.Hosting", - "Microsoft.AspNetCore", - "System.Net.Http" - ) - .AddAspNetCoreInstrumentation(tracing => - { - // Exclude health check requests from tracing - tracing.Filter = context => - !context.Request.Path.StartsWithSegments(HealthEndpointPath) - && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath); - tracing.RecordException = true; - tracing.EnrichWithHttpRequest = (activity, request) => - { - activity.SetTag("http.request.body.size", request.ContentLength); - activity.SetTag("user_agent", request.Headers.UserAgent); - }; - tracing.EnrichWithHttpResponse = (activity, response) => - { - activity.SetTag("http.response.body.size", response.ContentLength); - }; - }) - // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) - //.AddGrpcClientInstrumentation() - .AddHttpClientInstrumentation(o => - { - o.RecordException = true; - // Enrich outgoing request/response with extra tags - o.EnrichWithHttpRequestMessage = (activity, request) => - { - activity.SetTag("http.request.method", request.Method); - activity.SetTag("http.request.host", request.RequestUri?.Host); - activity.SetTag("http.request.useragent", request.Headers?.UserAgent); - }; - o.EnrichWithHttpResponseMessage = (activity, response) => - { - activity.SetTag("http.response.status_code", (int)response.StatusCode); - //activity.SetTag("http.response.headers", response.Content.Headers); - // Convert response.Content.Headers to a string array: "HeaderName=val1,val2" - var headerList = response.Content?.Headers? - .Select(h => $"{h.Key}={string.Join(",", h.Value)}") - .ToArray(); - - if (headerList is { Length: > 0 }) - { - // Set as an array tag (preferred for OTEL exporters supporting array-of-primitive attributes) - activity.SetTag("http.response.headers", headerList); - - // (Optional) Also emit individual header tags (comment out if too high-cardinality) - // foreach (var h in response.Content.Headers) - // { - // activity.SetTag($"http.response.header.{h.Key.ToLowerInvariant()}", string.Join(",", h.Value)); - // } - } - - }; - // Example filter: suppress telemetry for health checks - o.FilterHttpRequestMessage = request => - !request.RequestUri?.AbsolutePath.Contains("health", StringComparison.OrdinalIgnoreCase) ?? true; - }); - }); - - //builder.AddOpenTelemetryExporters(); - return builder; - } - - public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); - - return builder; - } - - public static WebApplication MapDefaultEndpoints(this WebApplication app) - { - // Adding health checks endpoints to applications in non-development environments has security implications. - // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. - if (app.Environment.IsDevelopment()) - { - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks(HealthEndpointPath); - - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions - { - Predicate = r => r.Tags.Contains("live") - }); - } - - return app; - } - - private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder - { - var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); - - if (useOtlpExporter) - { - builder.Services.AddOpenTelemetry().UseOtlpExporter(); - } - - // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) - //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) - //{ - // builder.Services.AddOpenTelemetry() - // .UseAzureMonitor(); - //} - - return builder; - } - - } -} From 20cd5b1a5f19d2ba223a5f9169131cc1a06fbb3c Mon Sep 17 00:00:00 2001 From: Grant Harris <96964444+gwharris7@users.noreply.github.com> Date: Tue, 5 May 2026 22:47:55 -0700 Subject: [PATCH 8/9] fix copyright header Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs b/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs index 3571bcfe..154618f1 100644 --- a/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs +++ b/dotnet/agent-framework/sample-agent/Tools/DateTimeFunctionTool.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.ComponentModel; From 1f356eca71cefc69118cd0c12ecf03334ead8072 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 05:49:27 +0000 Subject: [PATCH 9/9] Fix system prompt to reference correct tool name DateTimeFunctionTool.GetCurrentDateTime Agent-Logs-Url: https://github.com/microsoft/Agent365-Samples/sessions/a2eca8a7-110d-43e3-9b47-787442d25c08 Co-authored-by: gwharris7 <96964444+gwharris7@users.noreply.github.com> --- dotnet/agent-framework/sample-agent/Agent/MyAgent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs index a9fb7de1..05c86fbc 100644 --- a/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs +++ b/dotnet/agent-framework/sample-agent/Agent/MyAgent.cs @@ -38,7 +38,7 @@ You will speak like a friendly and professional virtual assistant. You may ask follow up questions until you have enough information to answer the customers question, but once you have the current weather or a forecast, make sure to format it nicely in text. - For current weather, Use the {{WeatherLookupTool.GetCurrentWeatherForLocation}}, you should include the current temperature, low and high temperatures, wind speed, humidity, and a short description of the weather. - For forecast's, Use the {{WeatherLookupTool.GetWeatherForecastForLocation}}, you should report on the next 5 days, including the current day, and include the date, high and low temperatures, and a short description of the weather. - - You should use the {{DateTimePlugin.GetDateTime}} to get the current date and time. + - You should use the {{DateTimeFunctionTool.GetCurrentDateTime}} to get the current date and time. Otherwise you should use the tools available to you to help answer the user's questions. """;