-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor of SK Sample to add more OTEL and Refactor for behavior of Teams / Messaging #46
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
Open
MattB-msft
wants to merge
48
commits into
main
Choose a base branch
from
users/mbarbour/refactorSKSample
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 5 commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
dcb96cf
Refactor of SK Sample to add more OTEL and Refactor for behavior of T…
MattB-msft 67f2e9c
Update devin agent sample (#48)
walterluna 10f3bd1
Add streaming support and refactor Agent365 logic
MattB-msft 2916688
Update LocalPackages path in nuget.config
MattB-msft 8b508ae
Comment out `EnabledOtlpExporter` configuration
MattB-msft d8899e0
Add copyright headers and clean up unused directives
MattB-msft e826736
updating samples read me docs (#47)
abdulanu0 9751074
Update Node.js OpenAI sample (#50)
pontemonti c7451e9
Introducing thinking indicator in Perplexity sample agent (#37)
aubreyquinn 38aa59c
updating auth handler (#53)
abdulanu0 caece1a
Update Python Agent Framework sample (#58)
pontemonti 3e650a1
Update Python OpenAI sample agent (#57)
pontemonti c0ba1ea
Introducing observability in Perplexity agent (#44)
aubreyquinn a8cf16a
Add prompt injection defense to agent system prompt (#40)
efpiva a819085
Update Package References to Use npm Registry + Remove local preinsta…
JesuTerraz 41cbe76
Add temporariy thumbnails to the related samples (#63)
Alive-Fish 6c3516d
Update .NET Semantic Kernel Sample Agent (#61)
pontemonti df09bfd
Replace SemanticKernelSampleAgent with WeatherAgent
MattB-msft e9b1283
Update tooling manifest (#60)
pontemonti c1306c8
Introducing Microsoft Teams manifest file for Perplexity (#51)
aubreyquinn dad4562
Refactor auth handlers and improve observability logic
MattB-msft 2f41b20
Add manifest template for n8n Sample (#49)
rbrighenti 1006ab1
Potential fix for code scanning alert no. 1: Workflow does not contai…
pontemonti 6206e14
Add CI workflow for Node.js OpenAI sample agent (#65)
pontemonti a228c22
Remove local reference in python samples (#62)
JesuTerraz 1ecfaab
Google ADK Sample with Tooling (#70)
JesuTerraz a913033
Updated formatting on readme file (#71)
aubreyquinn 21372bc
Perplexity: introducing the published agents-a365 packages (#72)
aubreyquinn 1942933
Add Devin Agent's manifest sample (#52)
walterluna ed5456b
reference public package dependencies (#64)
walterluna 40b5ced
Perplexity: added telemetry markers to all paths in the code (#73)
aubreyquinn e3a96e0
Remove `Assets` project and `nuget.config` from solutions
MattB-msft 552c146
Refactor of SK Sample to add more OTEL and Refactor for behavior of T…
MattB-msft ec574fb
Add streaming support and refactor Agent365 logic
MattB-msft 6f43e2b
Update LocalPackages path in nuget.config
MattB-msft 7a8dbd4
Comment out `EnabledOtlpExporter` configuration
MattB-msft 606af84
Add copyright headers and clean up unused directives
MattB-msft d721e21
Replace SemanticKernelSampleAgent with WeatherAgent
MattB-msft 86c94fc
Refactor auth handlers and improve observability logic
MattB-msft a5b39f8
Remove `Assets` project and `nuget.config` from solutions
MattB-msft 07159dc
Rebase
MattB-msft c328b68
Rebasing fixes.
MattB-msft 824be09
fix path on build.
MattB-msft 79d8fc7
Remove Warnings.
MattB-msft 7afde0e
Renamed AgentFramework Sample. Project names
MattB-msft 2377345
Renamed Weather agent to MyAgent
MattB-msft 581e5c8
Updated Names
MattB-msft 5413fa6
Update CI workflow for .NET Agent Framework solution
MattB-msft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
198 changes: 198 additions & 0 deletions
198
dotnet/AspireOTelServiceDefaults/AgentOTELExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // 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.Logging; | ||
| using Microsoft.Extensions.ServiceDiscovery; | ||
| using OpenTelemetry; | ||
| using OpenTelemetry.Metrics; | ||
| using OpenTelemetry.Resources; | ||
| using OpenTelemetry.Trace; | ||
|
|
||
| namespace Microsoft.Extensions.Hosting | ||
| { | ||
| // 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<TBuilder>(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<ServiceDiscoveryOptions>(options => | ||
| // { | ||
| // options.AllowedSchemes = ["https"]; | ||
| // }); | ||
|
|
||
| return builder; | ||
| } | ||
|
|
||
| public static TBuilder ConfigureOpenTelemetry<TBuilder>(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: "Agent365SemanticKernelSampleAgent", | ||
| serviceVersion: "1.0.0", | ||
| serviceInstanceId: Environment.MachineName) | ||
| .AddAttributes(new Dictionary<string, object> | ||
| { | ||
| ["deployment.environment"] = builder.Environment.EnvironmentName, | ||
| ["service.namespace"] = "Microsoft.Agents" | ||
| })) | ||
| .WithMetrics(metrics => | ||
| { | ||
| metrics.AddAspNetCoreInstrumentation() | ||
| .AddHttpClientInstrumentation() | ||
| .AddRuntimeInstrumentation(); | ||
| }) | ||
| .WithTracing(tracing => | ||
| { | ||
| tracing.AddSource(builder.Environment.ApplicationName) | ||
| .AddSource( | ||
| "Agent365SemanticKernelSampleAgent", | ||
| "Microsoft.Agents.Builder", | ||
| "Microsoft.Agents.Hosting", | ||
| "Agent365SemanticKernelSampleAgent.MyAgent", | ||
| "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<TBuilder>(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<TBuilder>(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; | ||
| } | ||
|
|
||
| } | ||
| } |
22 changes: 22 additions & 0 deletions
22
dotnet/AspireOTelServiceDefaults/AspireOTelServiceDefaults.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net8.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| <IsAspireSharedProject>true</IsAspireSharedProject> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <FrameworkReference Include="Microsoft.AspNetCore.App" /> | ||
|
|
||
| <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.9.0" /> | ||
| <PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.5.0" /> | ||
| <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" /> | ||
| <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" /> | ||
| <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" /> | ||
| <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" /> | ||
| <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| | ||
| Microsoft Visual Studio Solution File, Format Version 12.00 | ||
| # Visual Studio Version 17 | ||
| VisualStudioVersion = 17.14.36414.22 | ||
| MinimumVisualStudioVersion = 10.0.40219.1 | ||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticKernelSampleAgent", "sample-agent\SemanticKernelSampleAgent.csproj", "{8CBB159F-2929-49A8-C300-E6F8194FB636}" | ||
| EndProject | ||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireOTelServiceDefaults", "..\AspireOTelServiceDefaults\AspireOTelServiceDefaults.csproj", "{AD051E44-BC52-C3A1-EDA8-A8CE3731A0CB}" | ||
| EndProject | ||
| Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Assets", "Assets", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" | ||
| ProjectSection(SolutionItems) = preProject | ||
| nuget.config = nuget.config | ||
| EndProjectSection | ||
| EndProject | ||
| Global | ||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
| Debug|Any CPU = Debug|Any CPU | ||
| Release|Any CPU = Release|Any CPU | ||
| EndGlobalSection | ||
| GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
| {8CBB159F-2929-49A8-C300-E6F8194FB636}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {8CBB159F-2929-49A8-C300-E6F8194FB636}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {8CBB159F-2929-49A8-C300-E6F8194FB636}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {8CBB159F-2929-49A8-C300-E6F8194FB636}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| {AD051E44-BC52-C3A1-EDA8-A8CE3731A0CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {AD051E44-BC52-C3A1-EDA8-A8CE3731A0CB}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {AD051E44-BC52-C3A1-EDA8-A8CE3731A0CB}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {AD051E44-BC52-C3A1-EDA8-A8CE3731A0CB}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| EndGlobalSection | ||
| GlobalSection(SolutionProperties) = preSolution | ||
| HideSolutionNode = FALSE | ||
| EndGlobalSection | ||
| GlobalSection(ExtensibilityGlobals) = postSolution | ||
| SolutionGuid = {4BB18351-B926-45B2-918B-D5E337BA126F} | ||
| EndGlobalSection | ||
| EndGlobal |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| <configuration> | ||
| <packageSources> | ||
| <add key="Nuget" value="https://api.nuget.org/v3/index.json" /> | ||
| <add key="LocalPackages" value="REPLACE PATH TO A365SDK PACKAGE DIRECTORY or REMOVE" /> | ||
| </packageSources> | ||
| </configuration> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System; | ||
| using System.Diagnostics; | ||
| using System.Diagnostics.Metrics; | ||
|
|
||
| namespace SemanticKernelSampleAgent | ||
| { | ||
| public static class AgentMetrics | ||
| { | ||
| public static readonly string SourceName = "Agent365SemanticKernelSampleAgent"; | ||
|
|
||
| public static readonly ActivitySource ActivitySource = new(SourceName); | ||
|
|
||
| private static readonly Meter Meter = new(SourceName); | ||
|
|
||
| public static readonly Counter<long> MessageProcessedCounter = Meter.CreateCounter<long>( | ||
| "agent.messages.processed", | ||
| "messages", | ||
| "Number of messages processed by the agent"); | ||
|
|
||
| public static readonly Counter<long> RouteExecutedCounter = Meter.CreateCounter<long>( | ||
| "agent.routes.executed", | ||
| "routes", | ||
| "Number of routes executed by the agent"); | ||
|
|
||
| public static readonly Histogram<double> MessageProcessingDuration = Meter.CreateHistogram<double>( | ||
| "agent.message.processing.duration", | ||
| "ms", | ||
| "Duration of message processing in milliseconds"); | ||
|
|
||
| public static readonly Histogram<double> RouteExecutionDuration = Meter.CreateHistogram<double>( | ||
| "agent.route.execution.duration", | ||
| "ms", | ||
| "Duration of route execution in milliseconds"); | ||
|
|
||
| public static readonly UpDownCounter<long> ActiveConversations = Meter.CreateUpDownCounter<long>( | ||
| "agent.conversations.active", | ||
| "conversations", | ||
| "Number of active conversations"); | ||
|
|
||
|
|
||
| public static Activity InitializeMessageHandlingActivity(string HandlerName, ITurnContext context) | ||
| { | ||
| var activity = ActivitySource.StartActivity("AgentNotificationActivityAsync"); | ||
MattB-msft marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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?.SetTag("agent.isagentic", context.IsAgenticRequest()); | ||
| activity?.SetTag("caller.id", context.Activity.From?.Id); | ||
|
|
||
| activity?.AddEvent(new ActivityEvent("message.received", DateTimeOffset.UtcNow, new() | ||
| { | ||
| ["message.id"] = context.Activity.Id, | ||
| ["message.text"] = context.Activity.Text, | ||
| ["caller.id"] = context.Activity.From?.Id, | ||
| ["agent.isagentic"] = context.IsAgenticRequest(), | ||
| ["channel.id"] = context.Activity.ChannelId?.ToString() | ||
| })); | ||
| return activity!; | ||
| } | ||
|
|
||
| public static void FinalizeMessageHandlingActivity(Activity activity, ITurnContext context, long duration, bool success) | ||
| { | ||
| AssertionHelpers.ThrowIfNull(activity, nameof(activity)); | ||
|
|
||
| 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(); | ||
| } | ||
|
|
||
|
|
||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.