-
Notifications
You must be signed in to change notification settings - Fork 16
feat: OpenTelemetry instrumentation + Agent365 baggage support #475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 26 commits
4ad3ff5
1e8f696
b1612cb
3a483e7
37ab50c
6318b74
f246f1a
ff6c4f5
e603538
4228e91
54f6f90
f46d188
452375b
4f188fd
a0cb054
17646ad
7f9bc09
174130c
7a06dd2
41a5e93
6bc1e37
00062de
47db33a
6d38558
098ab0e
0d8987d
9ed8e8d
0683fa4
12ceab7
8b9a1b5
3b2b090
1a9458c
dc5e5c4
c6ff508
279dac3
2c4af92
2f76767
2935bb5
3382c03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <!--<PackageReference Include="Microsoft.OpenTelemetry" Version="1.0.2" />--> | ||
|
rido-min marked this conversation as resolved.
Outdated
|
||
| <PackageReference Include="Microsoft.OpenTelemetry" Version="1.0.2" /> | ||
| <PackageReference Include="OpenTelemetry.Api" Version="1.15.3" /> | ||
| <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.3" /> | ||
| <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" /> | ||
| <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" /> | ||
| <PackageReference Include="Microsoft.Extensions.AI" Version="10.5.1" /> | ||
| <PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.5.1" /> | ||
| <PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" /> | ||
| <PackageReference Include="ModelContextProtocol" Version="1.2.0" /> | ||
| <PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.5.0" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\src\Microsoft.Teams.Apps\Microsoft.Teams.Apps.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Collections.Concurrent; | ||
| using System.Text.Json; | ||
| using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts; | ||
| using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes; | ||
| using Microsoft.Extensions.AI; | ||
| using Microsoft.Teams.Apps; | ||
| using Microsoft.Teams.Apps.Api.Clients; | ||
| using Microsoft.Teams.Apps.Handlers; | ||
| using Microsoft.Teams.Apps.Schema; | ||
| using Microsoft.Teams.Apps.Schema.Entities; | ||
| using Microsoft.Teams.Core; | ||
| using Microsoft.Teams.Core.Hosting; | ||
|
|
||
|
rido-min marked this conversation as resolved.
|
||
| namespace ObservabilityBot; | ||
|
|
||
| public class ObservabilityBotApp : TeamsBotApplication | ||
| { | ||
| private readonly IChatClient _chatClient; | ||
| private readonly ChatOptions _chatOptions; | ||
| private readonly ConcurrentDictionary<string, List<ChatMessage>> _chatHistories = new(); | ||
| private readonly string _deploymentName; | ||
|
|
||
| public ObservabilityBotApp( | ||
| ConversationClient conversationClient, | ||
| UserTokenClient userTokenClient, | ||
| ApiClient teamsApiClient, | ||
| IHttpContextAccessor httpContextAccessor, | ||
| ILogger<ObservabilityBotApp> logger, | ||
| IChatClient chatClient, | ||
| ChatOptions chatOptions, | ||
| BotApplicationOptions? options = null, | ||
| TeamsBotApplicationOptions? teamsOptions = null) | ||
| : base(conversationClient, userTokenClient, teamsApiClient, httpContextAccessor, logger, options, teamsOptions) | ||
| { | ||
| _chatClient = chatClient; | ||
| _chatOptions = chatOptions; | ||
| _deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "unknown"; | ||
|
|
||
| this.OnMessage(HandleMessageAsync); | ||
| } | ||
|
|
||
| private async Task HandleMessageAsync(Context<MessageActivity> context, CancellationToken ct) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(context.Activity); | ||
| ArgumentNullException.ThrowIfNull(context.Activity.Conversation); | ||
| ArgumentNullException.ThrowIfNull(context.Activity.Conversation.Id); | ||
|
|
||
| await context.Typing(string.Empty, ct); | ||
|
|
||
| var conversationId = context.Activity.Conversation.Id; | ||
| var history = _chatHistories.GetOrAdd(conversationId, _ => []); | ||
|
|
||
| lock (history) | ||
| { | ||
| history.Add(new ChatMessage(ChatRole.User, context.Activity.Text)); | ||
| } | ||
|
|
||
| // Build Agent365 scope contracts from the turn context. | ||
| var recipient = context.Activity.Recipient; | ||
| var agentDetails = new AgentDetails( | ||
| agentId: recipient?.AgenticAppId ?? recipient?.Id, | ||
| agentName: recipient?.Name, | ||
| agenticUserId: recipient?.AgenticUserId, | ||
| agentBlueprintId: recipient?.AgenticAppBlueprintId, | ||
| tenantId: recipient?.TenantId); | ||
|
|
||
| var request = new Request( | ||
| content: context.Activity.Text, | ||
| conversationId: conversationId, | ||
| channel: new Channel(context.Activity.ChannelId)); | ||
|
|
||
| // === InferenceScope: wraps the LLM + tool-call loop === | ||
| var inferenceDetails = new InferenceCallDetails( | ||
| InferenceOperationType.Chat, | ||
| model: _deploymentName, | ||
| providerName: "AzureOpenAI"); | ||
|
|
||
| List<ChatMessage> snapshot; | ||
| lock (history) { snapshot = [.. history]; } | ||
|
|
||
| ChatResponse chatResponse; | ||
| using (var inferenceScope = InferenceScope.Start(request, inferenceDetails, agentDetails)) | ||
| { | ||
| chatResponse = await _chatClient.GetResponseAsync(snapshot, _chatOptions, ct); | ||
|
|
||
| if (chatResponse.Usage is { } usage) | ||
| { | ||
| if (usage.InputTokenCount is { } inputTokens) | ||
| inferenceScope.RecordInputTokens((int)inputTokens); | ||
| if (usage.OutputTokenCount is { } outputTokens) | ||
| inferenceScope.RecordOutputTokens((int)outputTokens); | ||
| } | ||
|
|
||
| var finishReason = chatResponse.FinishReason?.Value ?? "stop"; | ||
| inferenceScope.RecordFinishReasons([finishReason]); | ||
| } | ||
|
|
||
| lock (history) | ||
| { | ||
| history.AddRange(chatResponse.Messages); | ||
| } | ||
|
|
||
| // === ExecuteToolScope: record each tool invocation === | ||
| var toolCalls = chatResponse.Messages | ||
| .SelectMany(m => m.Contents.OfType<FunctionCallContent>()) | ||
| .GroupBy(fc => fc.CallId ?? fc.Name ?? "") | ||
| .ToDictionary(g => g.Key, g => g.First()); | ||
|
|
||
| foreach (var funcResult in chatResponse.Messages | ||
| .SelectMany(m => m.Contents.OfType<FunctionResultContent>())) | ||
| { | ||
| toolCalls.TryGetValue(funcResult.CallId ?? "", out var matchingCall); | ||
|
|
||
| var toolDetails = new ToolCallDetails( | ||
| toolName: matchingCall?.Name ?? "unknown", | ||
| arguments: matchingCall?.Arguments is { } args ? JsonSerializer.Serialize(args) : null, | ||
| toolCallId: funcResult.CallId); | ||
|
|
||
| using var toolScope = ExecuteToolScope.Start(request, toolDetails, agentDetails); | ||
| if (funcResult.Result is not null) | ||
| { | ||
| toolScope.RecordResponse(funcResult.Result.ToString()!); | ||
| } | ||
| } | ||
|
|
||
| // Extract citations from tool results. | ||
| var citations = chatResponse.Messages | ||
| .SelectMany(m => m.Contents.OfType<FunctionResultContent>()) | ||
| .Where(frc => frc.Result is not null) | ||
| .SelectMany(frc => | ||
| { | ||
| try | ||
| { | ||
| var json = JsonSerializer.Deserialize<JsonElement>(frc.Result!.ToString()!); | ||
| if (json.TryGetProperty("structuredContent", out var sc) && | ||
| sc.TryGetProperty("results", out var results)) | ||
| { | ||
| return results.EnumerateArray() | ||
| .Where(r => r.TryGetProperty("contentUrl", out _)) | ||
| .Select(r => ( | ||
| Title: r.GetProperty("title").GetString() ?? "", | ||
| Url: r.GetProperty("contentUrl").GetString() ?? "", | ||
| Content: r.TryGetProperty("content", out var c) ? c.GetString() ?? "" : "" | ||
| )); | ||
| } | ||
| } | ||
| catch { } | ||
| return []; | ||
|
rido-min marked this conversation as resolved.
|
||
| }) | ||
| .DistinctBy(c => c.Url) | ||
| .Take(5).ToList(); | ||
|
|
||
| var responseText = chatResponse.Text; | ||
|
|
||
| for (int i = 0; i < citations.Count; i++) | ||
| { | ||
| responseText += $"[{i + 1}] "; | ||
| } | ||
|
|
||
| // === OutputScope: record the agent's reply === | ||
| using (OutputScope.Start(request, new Response([responseText]), agentDetails)) | ||
| { | ||
| } | ||
|
|
||
| var responseMsg = TeamsActivity.CreateBuilder() | ||
| .WithText(responseText, TextFormats.Markdown) | ||
| .AddMention(context.Activity?.From!) | ||
| .Build(); | ||
|
|
||
| responseMsg.AddAIGenerated(); | ||
|
Check failure on line 173 in core/samples/ObservabilityBot/ObservabilityBotApp.cs
|
||
|
|
||
| for (int i = 0; i < citations.Count; i++) | ||
| { | ||
| var citation = citations[i]; | ||
| var abstract_ = citation.Content.Length > 160 ? citation.Content[..157] + "..." : citation.Content; | ||
| responseMsg.AddCitation(i + 1, new CitationAppearance() { Name = citation.Title, Url = new Uri(citation.Url), Abstract = abstract_, Icon = CitationIcon.Text }); | ||
|
Check failure on line 179 in core/samples/ObservabilityBot/ObservabilityBotApp.cs
|
||
| } | ||
|
|
||
| await context.Send(responseMsg, ct); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Azure.AI.OpenAI; | ||
| using Microsoft.Extensions.AI; | ||
| using Microsoft.Identity.Abstractions; | ||
| using Microsoft.Identity.Web; | ||
| using Microsoft.OpenTelemetry; | ||
| using Microsoft.Teams.Apps; | ||
| using Microsoft.Teams.Apps.Diagnostics; | ||
| using Microsoft.Teams.Core.Diagnostics; | ||
| using ModelContextProtocol.Client; | ||
| using ObservabilityBot; | ||
| using OpenTelemetry; | ||
| using OpenTelemetry.Resources; | ||
|
rido-min marked this conversation as resolved.
|
||
|
|
||
|
|
||
| string[] activitySources = [CoreTelemetryNames.ActivitySourceName, TeamsBotApplicationTelemetry.ActivitySourceName, "Experimental.Microsoft.Extensions.AI", "ModelContextProtocol"]; | ||
| string[] meterNames = [CoreTelemetryNames.MeterName, TeamsBotApplicationTelemetry.MeterName, "Experimental.Microsoft.Agents.AI", "ModelContextProtocol"]; | ||
|
|
||
| WebApplicationBuilder builder = WebApplication.CreateBuilder(args); | ||
| IServiceProvider? rootProvider = null; | ||
| builder.Services.AddTeamsBotApplication<ObservabilityBotApp>(); | ||
|
|
||
| builder.Services.AddOpenTelemetry() | ||
| .ConfigureResource(r => r | ||
| .AddService(serviceName: "ObservabilityBot", serviceVersion: "0.0.1") | ||
| .AddAttributes(new Dictionary<string, object> | ||
| { | ||
| ["deployment.environment"] = builder.Environment.EnvironmentName, | ||
| ["service.namespace"] = "Microsoft.Teams" | ||
| })) | ||
| .UseMicrosoftOpenTelemetry(o => { | ||
| o.Exporters = ExportTarget.Otlp | ExportTarget.Agent365 | ExportTarget.AzureMonitor; | ||
| o.Instrumentation.EnableHttpClientInstrumentation = true; | ||
| o.Instrumentation.EnableAspNetCoreInstrumentation = true; | ||
| o.Agent365.Exporter.UseS2SEndpoint = true; | ||
| o.Agent365.Exporter.TokenResolver = async (agentId, tenantId) => | ||
| { | ||
| var provider = rootProvider!.GetRequiredService<IAuthorizationHeaderProvider>(); | ||
| var options = new AuthorizationHeaderProviderOptions { AcquireTokenOptions = new() { AuthenticationOptionsName = "AzureAd", Tenant = tenantId } }; | ||
| options.WithAgentIdentity(agentId); | ||
| var token = await provider.CreateAuthorizationHeaderForAppAsync( | ||
| "api://9b975845-388f-4429-889e-eab1ef63949c/.default", options); | ||
| return token.Substring("Bearer".Length).Trim(); | ||
| }; | ||
| }) | ||
| .WithTracing(t => t.AddSource(activitySources)) | ||
| .WithMetrics(m => m.AddMeter(meterNames)); | ||
|
|
||
| builder.Logging.AddOpenTelemetry(o => o.IncludeFormattedMessage = true); | ||
|
|
||
| // Register MCP clients | ||
| builder.Services.AddKeyedSingleton("msdocs", (sp, key) => | ||
| McpClient.CreateAsync( | ||
| new HttpClientTransport(new() | ||
| { | ||
| Endpoint = new Uri("https://learn.microsoft.com/api/mcp"), | ||
| TransportMode = HttpTransportMode.AutoDetect, | ||
| Name = "msdocs" | ||
| }))); | ||
|
|
||
| // Register IChatClient | ||
| var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidDataException("AZURE_OPENAI_ENDPOINT not found"); | ||
| var azoai_key = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY") ?? throw new InvalidDataException("AZURE_OPENAI_KEY not found"); | ||
| var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? throw new InvalidDataException("AZURE_OPENAI_DEPLOYMENT not found"); | ||
|
|
||
| builder.Services.AddSingleton<IChatClient>(sp => | ||
| new ChatClientBuilder( | ||
| new AzureOpenAIClient(new Uri(endpoint), new System.ClientModel.ApiKeyCredential(azoai_key)) | ||
| .GetChatClient(deploymentName) | ||
| .AsIChatClient()) | ||
| .UseFunctionInvocation() | ||
| .UseOpenTelemetry(sourceName: "Experimental.Microsoft.Extensions.AI") | ||
| .UseLogging(sp.GetRequiredService<ILoggerFactory>()) | ||
| .Build()); | ||
|
|
||
| builder.Services.AddSingleton<ChatOptions>(sp => | ||
| { | ||
| var msdocsClient = sp.GetRequiredKeyedService<Task<McpClient>>("msdocs").GetAwaiter().GetResult(); | ||
| var msdocsTools = msdocsClient.ListToolsAsync().GetAwaiter().GetResult(); | ||
|
|
||
| return new ChatOptions | ||
| { | ||
| AllowMultipleToolCalls = true, | ||
| Instructions = "Use the following tools to answer the user's question. If you don't know the answer, use the 'Search Microsoft Docs' tool to find relevant information. Use calendar tools for scheduling-related queries.", | ||
| Tools = [..msdocsTools] | ||
| }; | ||
| }); | ||
|
|
||
| WebApplication app = builder.Build(); | ||
| rootProvider = app.Services; | ||
| app.MapGet("/", () => "ObservabilityBot is running. Telemetry source: " + CoreTelemetryNames.ActivitySourceName); | ||
|
|
||
| app.UseTeamsBotApplication<ObservabilityBotApp>(); | ||
|
|
||
| app.Run(); | ||
Uh oh!
There was an error while loading. Please reload this page.