From f677b8e1e6485dfa84e7ba3b75734542a22faec3 Mon Sep 17 00:00:00 2001 From: Sellakumaran Kanagarathnam Date: Fri, 1 May 2026 16:17:42 -0700 Subject: [PATCH] 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