From 3290a8d121ca30cc1baa6d7d78deddbb436caecd Mon Sep 17 00:00:00 2001 From: Johan Broberg Date: Wed, 5 Nov 2025 11:56:27 -0800 Subject: [PATCH] Add .NET Semantic Kernel Sample Agent --- .../sample-agent/Agent-Code-Walkthrough.md | 85 ++++++ .../sample-agent/Agents/Agent365Agent.cs | 117 ++++++++ .../Agents/Agent365AgentResponse.cs | 24 ++ .../sample-agent/AspNetExtensions.cs | 270 ++++++++++++++++++ .../semantic-kernel/sample-agent/MyAgent.cs | 269 +++++++++++++++++ .../TermsAndConditionsAcceptedPlugin.cs | 15 + .../TermsAndConditionsNotAcceptedPlugin.cs | 21 ++ .../semantic-kernel/sample-agent/Program.cs | 110 +++++++ .../Properties/launchSettings.json | 14 + dotnet/semantic-kernel/sample-agent/README.md | 166 +++++++++++ .../SemanticKernelSampleAgent.csproj | 39 +++ .../SemanticKernelSampleAgent.sln | 25 ++ .../sample-agent/ToolingManifest.json | 19 ++ .../sample-agent/appManifest/color.png | Bin 0 -> 3415 bytes .../sample-agent/appManifest/manifest.json | 50 ++++ .../sample-agent/appManifest/outline.png | Bin 0 -> 407 bytes .../sample-agent/appsettings.json | 70 +++++ .../semantic-kernel/sample-agent/nuget.config | 5 + 18 files changed, 1299 insertions(+) create mode 100644 dotnet/semantic-kernel/sample-agent/Agent-Code-Walkthrough.md create mode 100644 dotnet/semantic-kernel/sample-agent/Agents/Agent365Agent.cs create mode 100644 dotnet/semantic-kernel/sample-agent/Agents/Agent365AgentResponse.cs create mode 100644 dotnet/semantic-kernel/sample-agent/AspNetExtensions.cs create mode 100644 dotnet/semantic-kernel/sample-agent/MyAgent.cs create mode 100644 dotnet/semantic-kernel/sample-agent/Plugins/TermsAndConditionsAcceptedPlugin.cs create mode 100644 dotnet/semantic-kernel/sample-agent/Plugins/TermsAndConditionsNotAcceptedPlugin.cs create mode 100644 dotnet/semantic-kernel/sample-agent/Program.cs create mode 100644 dotnet/semantic-kernel/sample-agent/Properties/launchSettings.json create mode 100644 dotnet/semantic-kernel/sample-agent/README.md create mode 100644 dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.csproj create mode 100644 dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.sln create mode 100644 dotnet/semantic-kernel/sample-agent/ToolingManifest.json create mode 100644 dotnet/semantic-kernel/sample-agent/appManifest/color.png create mode 100644 dotnet/semantic-kernel/sample-agent/appManifest/manifest.json create mode 100644 dotnet/semantic-kernel/sample-agent/appManifest/outline.png create mode 100644 dotnet/semantic-kernel/sample-agent/appsettings.json create mode 100644 dotnet/semantic-kernel/sample-agent/nuget.config diff --git a/dotnet/semantic-kernel/sample-agent/Agent-Code-Walkthrough.md b/dotnet/semantic-kernel/sample-agent/Agent-Code-Walkthrough.md new file mode 100644 index 00000000..16aa7415 --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/Agent-Code-Walkthrough.md @@ -0,0 +1,85 @@ +# Agent Code Walkthrough + +This document provides a detailed walkthrough of the code for this agent. The +agent is designed to perform specific tasks autonomously, interacting with the +user as needed. + +## Key Files in this Solution +- `Program.cs`: + - This is the entry point for the application. It sets up the necessary services + and middleware for the agent. + - Tracing is configured here to help with debugging and monitoring the agent's activities. + ```csharp + builder.Services + .AddTracing(config => config + .WithSemanticKernel()); + + builder.Services + .AddOpenTelemetry() + .WithTracing(tracing => tracing + .AddConsoleExporter()); + ``` +- `MyAgent.cs`: + - This file contains the implementation of the agent's core logic, including how + it registers handling of activities. + - The constructor has three lines that register the agent's handling of activities: + - `this.OnAgentNotification("*", AgentNotificationActivityAsync);`: + - This registers a handler for notifications, such as when the agent + receives an email or a user mentions the agent in a comment in a Word document. + - `OnActivity(ActivityTypes.InstallationUpdate, OnHireMessageAsync);`: + - This registers the `InstallationUpdate` activity type, which is triggered + when the agent is installed ("hired") or uninstalled ("offboarded"). + - `OnActivity(ActivityTypes.Message, MessageActivityAsync, rank: RouteRank.Last);`: + - This registers a handler for messages sent to the agent. + - Based on the activity handlers registered above, when the agent receives a message + about an activity, the relevant handler is invoked to process the activity. +- `Agents/Agent365Agent.cs`: + - This file contains the implementation of the Agent 365 specific logic, including how it + integrates with the Agent 365 platform and handles user interactions. + - We call `IMcpToolRegistrationService.AddToolServersToAgent(...)` to register + the Agent 365 tools with the agent. +- `Plugins/TermsAndConditionsAcceptedPlugin.cs`: + - This file contains a Semantic Kernel plugin that handles the scenario where + the user has accepted the terms and conditions. + - This contains a simple tool that allows the user to reject the terms and conditions + if they change their mind. +- `Plugins/TermsAndConditionsNotAcceptedPlugin.cs`: + - This file contains a Semantic Kernel plugin that handles the scenario where + the user has not accepted the terms and conditions. + - This contains a simple tool that allows the user to accept the terms and conditions. + +## Activities Handled by the Agent + +### InstallationUpdate Activity + +- This activity is triggered when the agent is installed or uninstalled. +- The `OnHireMessageAsync` method in `MyAgent.cs` handles this activity: + - If the agent is installed, it sends a welcome message to the user, asking + the user to accept the terms and conditions. + - If the agent is uninstalled, it sends a goodbye message to the user, and it + resets the user's acceptance of the terms and conditions. +- The `TermsAndConditionsAccepted` flag has been implemented as a static property + in the `MyAgent` class for simplicity. In a production scenario, this should be + stored in a persistent storage solution. It is only intended as a simple example + to demonstrate the `InstallationUpdate` activity. + +### Notification Activity + +- This activity is triggered when the agent receives a notification, such as + when the user mentions the agent in a comment in a Word document or when the + agent receives an email. +- The `AgentNotificationActivityAsync` method in `MyAgent.cs` handles this activity: + - It processes the notification and takes appropriate action based on the content + of the notification. + +### Message Activity + +- This activity is triggered when the agent receives a message from the user. +- The `MessageActivityAsync` method in `MyAgent.cs` handles this activity: + - It processes the message and takes appropriate action based on the content + of the message. + +### Activity Protocol Samples + +For more information on the activity protocol and sample payloads, please refer to the +[Activity Protocol Samples](Activity-Protocol-Samples.md). diff --git a/dotnet/semantic-kernel/sample-agent/Agents/Agent365Agent.cs b/dotnet/semantic-kernel/sample-agent/Agents/Agent365Agent.cs new file mode 100644 index 00000000..c96b0e5b --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/Agents/Agent365Agent.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Agent365SemanticKernelSampleAgent.Plugins; +using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services; +using Microsoft.Agents.Builder; +using Microsoft.Agents.Builder.App.UserAuth; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using System; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; + +namespace Agent365SemanticKernelSampleAgent.Agents; + +public class Agent365Agent +{ + private readonly Kernel _kernel; + private readonly ChatCompletionAgent _agent; + + private const string AgentName = "Agent365Agent"; + private const string TermsAndConditionsNotAcceptedInstructions = "The user has not accepted the terms and conditions. You must ask the user to accept the terms and conditions before you can help them with any tasks. You may use the 'accept_terms_and_conditions' function to accept the terms and conditions on behalf of the user. If the user tries to perform any action before accepting the terms and conditions, you must use the 'terms_and_conditions_not_accepted' function to inform them that they must accept the terms and conditions to proceed."; + private const string TermsAndConditionsAcceptedInstructions = "You may ask follow up questions until you have enough information to answer the user's question."; + private string AgentInstructions() => $@" + You are a friendly assistant that helps office workers with their daily tasks. + {(MyAgent.TermsAndConditionsAccepted ? TermsAndConditionsAcceptedInstructions : TermsAndConditionsNotAcceptedInstructions)} + + Respond in JSON format with the following JSON schema: + + {{ + ""contentType"": ""'Text'"", + ""content"": ""{{The content of the responsein plain text}}"" + }} + "; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider to use for dependency injection. + public Agent365Agent(Kernel kernel, IServiceProvider service, IMcpToolRegistrationService toolService, UserAuthorization userAuthorization, ITurnContext turnContext) + { + this._kernel = kernel; + + // Only add the A365 tools if the user has accepted the terms and conditions + if (MyAgent.TermsAndConditionsAccepted) + { + // Provide the tool service with necessary parameters to connect to A365 + // The environmentId will be extracted programmatically + string environmentId = Environment.GetEnvironmentVariable("ENVIRONMENT_ID") ?? string.Empty; + this._kernel.ImportPluginFromType(); + + toolService.AddToolServersToAgent(kernel, environmentId, userAuthorization, turnContext); + } + else + { + // If the user has not accepted the terms and conditions, import the plugin that allows them to accept or reject + this._kernel.ImportPluginFromObject(new TermsAndConditionsNotAcceptedPlugin(), "license"); + } + + // Define the agent + this._agent = + new() + { + Id = turnContext.Activity.Recipient.AgenticAppId ?? Guid.NewGuid().ToString(), + Instructions = AgentInstructions(), + Name = AgentName, + Kernel = this._kernel, + Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() + { +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true }), +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + ResponseFormat = "json_object", + }), + }; + } + + /// + /// Invokes the agent with the given input and returns the response. + /// + /// A message to process. + /// An instance of + public async Task InvokeAgentAsync(string input, ChatHistory chatHistory) + { + ArgumentNullException.ThrowIfNull(chatHistory); + AgentThread thread = new ChatHistoryAgentThread(); + ChatMessageContent message = new(AuthorRole.User, input); + chatHistory.Add(message); + + StringBuilder sb = new(); + await foreach (ChatMessageContent response in this._agent.InvokeAsync(chatHistory, thread: thread)) + { + chatHistory.Add(response); + sb.Append(response.Content); + } + + // Make sure the response is in the correct format and retry if necessary + try + { + string resultContent = sb.ToString(); + var jsonNode = JsonNode.Parse(resultContent); + Agent365AgentResponse result = new() + { + Content = jsonNode!["content"]!.ToString(), + ContentType = Enum.Parse(jsonNode["contentType"]!.ToString(), true) + }; + return result; + } + catch (Exception je) + { + return await InvokeAgentAsync($"That response did not match the expected format. Please try again. Error: {je.Message}", chatHistory); + } + } +} diff --git a/dotnet/semantic-kernel/sample-agent/Agents/Agent365AgentResponse.cs b/dotnet/semantic-kernel/sample-agent/Agents/Agent365AgentResponse.cs new file mode 100644 index 00000000..991b4c42 --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/Agents/Agent365AgentResponse.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ComponentModel; +using System.Text.Json.Serialization; + +namespace Agent365SemanticKernelSampleAgent.Agents; + +public enum Agent365AgentResponseContentType +{ + [JsonPropertyName("text")] + Text +} + +public class Agent365AgentResponse +{ + [JsonPropertyName("contentType")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public Agent365AgentResponseContentType ContentType { get; set; } + + [JsonPropertyName("content")] + [Description("The content of the response. May only be plain text.")] + public string? Content { get; set; } +} diff --git a/dotnet/semantic-kernel/sample-agent/AspNetExtensions.cs b/dotnet/semantic-kernel/sample-agent/AspNetExtensions.cs new file mode 100644 index 00000000..5ffbff7e --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/AspNetExtensions.cs @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Authentication; +using Microsoft.Agents.Core; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Validators; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +public static class AspNetExtensions +{ + private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + + /// + /// Adds AspNet token validation typical for ABS/SMBA and agent-to-agent using settings in configuration. + /// + /// + /// + /// Name of the config section to read. + /// Optional logger to use for authentication event logging. + /// + /// This extension reads settings from configuration. If configuration is missing JWT token + /// is not enabled. + ///

The minimum, but typical, configuration is:

+ /// + /// "TokenValidation": { + /// "Enabled": boolean, + /// "Audiences": [ + /// "{{ClientId}}" // this is the Client ID used for the Azure Bot + /// ], + /// "TenantId": "{{TenantId}}" + /// } + /// + /// The full options are: + /// + /// "TokenValidation": { + /// "Enabled": boolean, + /// "Audiences": [ + /// "{required:agent-appid}" + /// ], + /// "TenantId": "{recommended:tenant-id}", + /// "ValidIssuers": [ + /// "{default:Public-AzureBotService}" + /// ], + /// "IsGov": {optional:false}, + /// "AzureBotServiceOpenIdMetadataUrl": optional, + /// "OpenIdMetadataUrl": optional, + /// "AzureBotServiceTokenHandling": "{optional:true}" + /// "OpenIdMetadataRefresh": "optional-12:00:00" + /// } + /// + ///
+ public static void AddAgentAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation") + { + IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); + + if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + { + // Noop if TokenValidation section missing or disabled. + System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); + return; + } + + services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); + } + + /// + /// Adds AspNet token validation typical for ABS/SMBA and agent-to-agent. + /// + public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) + { + AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); + + // Must have at least one Audience. + if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) + { + throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences requires at least one ClientId"); + } + + // Audience values must be GUID's + foreach (var audience in validationOptions.Audiences) + { + if (!Guid.TryParse(audience, out _)) + { + throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences values must be a GUID"); + } + } + + // If ValidIssuers is empty, default for ABS Public Cloud + if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) + { + validationOptions.ValidIssuers = + [ + "https://api.botframework.com", + "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", + "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", + "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", + "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", + "https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", + "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", + ]; + + if (!string.IsNullOrEmpty(validationOptions.TenantId) && Guid.TryParse(validationOptions.TenantId, out _)) + { + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, validationOptions.TenantId)); + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, validationOptions.TenantId)); + } + } + + // If the `AzureBotServiceOpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate ABS tokens. + if (string.IsNullOrEmpty(validationOptions.AzureBotServiceOpenIdMetadataUrl)) + { + validationOptions.AzureBotServiceOpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl : AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl; + } + + // If the `OpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate Entra ID tokens. + if (string.IsNullOrEmpty(validationOptions.OpenIdMetadataUrl)) + { + validationOptions.OpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl; + } + + var openIdMetadataRefresh = validationOptions.OpenIdMetadataRefresh ?? BaseConfigurationManager.DefaultAutomaticRefreshInterval; + + _ = services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5), + ValidIssuers = validationOptions.ValidIssuers, + ValidAudiences = validationOptions.Audiences, + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + }; + + // Using Microsoft.IdentityModel.Validators + options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); + + options.Events = new JwtBearerEvents + { + // Create a ConfigurationManager based on the requestor. This is to handle ABS non-Entra tokens. + OnMessageReceived = async context => + { + string authorizationHeader = context.Request.Headers.Authorization.ToString(); + + if (string.IsNullOrEmpty(authorizationHeader)) + { + // Default to AadTokenValidation handling + context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + await Task.CompletedTask.ConfigureAwait(false); + return; + } + + string[] parts = authorizationHeader?.Split(' ')!; + if (parts.Length != 2 || parts[0] != "Bearer") + { + // Default to AadTokenValidation handling + context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + await Task.CompletedTask.ConfigureAwait(false); + return; + } + + JwtSecurityToken token = new(parts[1]); + string issuer = token.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.IssuerClaim)?.Value!; + + if (validationOptions.AzureBotServiceTokenHandling && AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer)) + { + // Use the Azure Bot authority for this configuration manager + context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => + { + return new ConfigurationManager(validationOptions.AzureBotServiceOpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) + { + AutomaticRefreshInterval = openIdMetadataRefresh + }; + }); + } + else + { + context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.OpenIdMetadataUrl, key => + { + return new ConfigurationManager(validationOptions.OpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) + { + AutomaticRefreshInterval = openIdMetadataRefresh + }; + }); + } + + await Task.CompletedTask.ConfigureAwait(false); + }, + + OnTokenValidated = context => + { + return Task.CompletedTask; + }, + OnForbidden = context => + { + return Task.CompletedTask; + }, + OnAuthenticationFailed = context => + { + return Task.CompletedTask; + } + }; + }); + } + + public class TokenValidationOptions + { + public IList? Audiences { get; set; } + + /// + /// TenantId of the Azure Bot. Optional but recommended. + /// + public string? TenantId { get; set; } + + /// + /// Additional valid issuers. Optional, in which case the Public Azure Bot Service issuers are used. + /// + public IList? ValidIssuers { get; set; } + + /// + /// Can be omitted, in which case public Azure Bot Service and Azure Cloud metadata urls are used. + /// + public bool IsGov { get; set; } = false; + + /// + /// Azure Bot Service OpenIdMetadataUrl. Optional, in which case default value depends on IsGov. + /// + /// + /// + public string? AzureBotServiceOpenIdMetadataUrl { get; set; } + + /// + /// Entra OpenIdMetadataUrl. Optional, in which case default value depends on IsGov. + /// + /// + /// + public string? OpenIdMetadataUrl { get; set; } + + /// + /// Determines if Azure Bot Service tokens are handled. Defaults to true and should always be true until Azure Bot Service sends Entra ID token. + /// + public bool AzureBotServiceTokenHandling { get; set; } = true; + + /// + /// OpenIdMetadata refresh interval. Defaults to 12 hours. + /// + public TimeSpan? OpenIdMetadataRefresh { get; set; } + } +} diff --git a/dotnet/semantic-kernel/sample-agent/MyAgent.cs b/dotnet/semantic-kernel/sample-agent/MyAgent.cs new file mode 100644 index 00000000..c3b10097 --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/MyAgent.cs @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Agent365SemanticKernelSampleAgent.Agents; +using AgentNotification; +using Microsoft.Agents.A365.Notifications.Models; +using Microsoft.Agents.A365.Observability.Caching; +using Microsoft.Agents.A365.Observability.Runtime.Common; +using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services; +using Microsoft.Agents.Builder; +using Microsoft.Agents.Builder.App; +using Microsoft.Agents.Builder.State; +using Microsoft.Agents.Core.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Agent365SemanticKernelSampleAgent; + +public class MyAgent : AgentApplication +{ + private readonly Kernel _kernel; + private readonly IMcpToolRegistrationService _toolsService; + private readonly IExporterTokenCache _agentTokenCache; + private readonly ILogger _logger; + + public MyAgent(AgentApplicationOptions options, Kernel kernel, IMcpToolRegistrationService toolService, IExporterTokenCache agentTokenCache, ILogger logger) : base(options) + { + _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)); + + bool useAgenticAuth = Environment.GetEnvironmentVariable("USE_AGENTIC_AUTH") == "true"; + var autoSignInHandlers = useAgenticAuth ? new[] { "agentic" } : null; + + // Register Agentic specific Activity routes. These will only be used if the incoming Activity is Agentic. + this.OnAgentNotification("*", AgentNotificationActivityAsync,RouteRank.Last, autoSignInHandlers: autoSignInHandlers); + + OnActivity(ActivityTypes.InstallationUpdate, OnHireMessageAsync); + OnActivity(ActivityTypes.Message, MessageActivityAsync, rank: RouteRank.Last, autoSignInHandlers: autoSignInHandlers); + } + + internal static bool IsApplicationInstalled { get; set; } = false; + internal static bool TermsAndConditionsAccepted { get; set; } = false; + + protected async Task MessageActivityAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) + { + using var baggageScope = new BaggageBuilder() + .TenantId(turnContext.Activity.Recipient.TenantId) + .AgentId(turnContext.Activity.Recipient.AgenticAppId) + .Build(); + + try + { + _agentTokenCache.RegisterObservability(turnContext.Activity.Recipient.AgenticAppId, turnContext.Activity.Recipient.TenantId, new AgenticTokenStruct + { + UserAuthorization = UserAuthorization, + TurnContext = turnContext + }, EnvironmentUtils.GetObservabilityAuthenticationScope()); + } + catch (Exception ex) + { + _logger.LogWarning($"There was an error registering for observability: {ex.Message}"); + } + + // Setup local service connection + ServiceCollection serviceCollection = [ + new ServiceDescriptor(typeof(ITurnState), turnState), + new ServiceDescriptor(typeof(ITurnContext), turnContext), + new ServiceDescriptor(typeof(Kernel), _kernel), + ]; + + if (!IsApplicationInstalled) + { + await turnContext.SendActivityAsync(MessageFactory.Text("Please install the application before sending messages."), cancellationToken); + return; + } + + var agent365Agent = this.GetAgent365Agent(serviceCollection, turnContext); + if (!TermsAndConditionsAccepted) + { + if (turnContext.Activity.ChannelId.Channel == Channels.Msteams) + { + var response = await agent365Agent.InvokeAgentAsync(turnContext.Activity.Text, new ChatHistory()); + await OutputResponseAsync(turnContext, turnState, response, cancellationToken); + return; + } + } + if (turnContext.Activity.ChannelId.Channel == Channels.Msteams) + { + await TeamsMessageActivityAsync(agent365Agent, turnContext, turnState, cancellationToken); + } + else + { + await turnContext.SendActivityAsync(MessageFactory.Text($"Sorry, I do not know how to respond to messages from channel '{turnContext.Activity.ChannelId}'."), cancellationToken); + } + } + + private async Task AgentNotificationActivityAsync(ITurnContext turnContext, ITurnState turnState, AgentNotificationActivity activity, CancellationToken cancellationToken) + { + using var baggageScope = new BaggageBuilder() + .TenantId(turnContext.Activity.Recipient.TenantId) + .AgentId(turnContext.Activity.Recipient.AgenticAppId) + .Build(); + + try + { + _agentTokenCache.RegisterObservability(turnContext.Activity.Recipient.AgenticAppId, turnContext.Activity.Recipient.TenantId, new AgenticTokenStruct + { + UserAuthorization = UserAuthorization, + TurnContext = turnContext + }, EnvironmentUtils.GetObservabilityAuthenticationScope()); + } + catch (Exception ex) + { + _logger.LogWarning($"There was an error registering for observability: {ex.Message}"); + } + + // Setup local service connection + ServiceCollection serviceCollection = [ + new ServiceDescriptor(typeof(ITurnState), turnState), + new ServiceDescriptor(typeof(ITurnContext), turnContext), + new ServiceDescriptor(typeof(Kernel), _kernel), + ]; + + if (!IsApplicationInstalled) + { + await turnContext.SendActivityAsync(MessageFactory.Text("Please install the application before sending notifications."), cancellationToken); + return; + } + + var agent365Agent = this.GetAgent365Agent(serviceCollection, turnContext); + if (!TermsAndConditionsAccepted) + { + var response = await agent365Agent.InvokeAgentAsync(turnContext.Activity.Text, new ChatHistory()); + await OutputResponseAsync(turnContext, turnState, response, cancellationToken); + return; + } + + switch (activity.NotificationType) + { + case NotificationTypeEnum.EmailNotification: + await turnContext.StreamingResponse.QueueInformativeUpdateAsync($"Thanks for the email notification! Working on a response..."); + if (activity.EmailNotification == null) + { + turnContext.StreamingResponse.QueueTextChunk("I could not find the email notification details."); + await turnContext.StreamingResponse.EndStreamAsync(cancellationToken); + return; + } + + var chatHistory = new ChatHistory(); + var emailContent = await agent365Agent.InvokeAgentAsync($"You have a new email from {activity.From.Name} with id '{activity.EmailNotification.Id}', ConversationId '{activity.EmailNotification.ConversationId}'. Please retrieve this message and return it in text format.", chatHistory); + var response = await agent365Agent.InvokeAgentAsync($"You have received the following email. Please follow any instructions in it. {emailContent.Content}", chatHistory); + var responseEmailActivity = MessageFactory.Text(""); + responseEmailActivity.Entities.Add(new EmailResponse(response.Content)); + await turnContext.SendActivityAsync(responseEmailActivity, cancellationToken); + //await OutputResponseAsync(turnContext, turnState, response, cancellationToken); + return; + case NotificationTypeEnum.WpxComment: + await turnContext.StreamingResponse.QueueInformativeUpdateAsync($"Thanks for the Word notification! Working on a response...", cancellationToken); + if (activity.WpxCommentNotification == null) + { + turnContext.StreamingResponse.QueueTextChunk("I could not find the Word notification details."); + await turnContext.StreamingResponse.EndStreamAsync(cancellationToken); + return; + } + var driveId = "default"; + chatHistory = new ChatHistory(); + var wordContent = await agent365Agent.InvokeAgentAsync($"You have a new comment on the Word document with id '{activity.WpxCommentNotification.DocumentId}', comment id '{activity.WpxCommentNotification.ParentCommentId}', drive id '{driveId}'. Please retrieve the Word document as well as the comments in the Word document and return it in text format.", chatHistory); + + var commentToAgent = activity.Text; + response = await agent365Agent.InvokeAgentAsync($"You have received the following Word document content and comments. Please follow refer to these when responding to comment '{commentToAgent}'. {wordContent.Content}", chatHistory); + var responseWpxActivity = MessageFactory.Text(response.Content!); + await turnContext.SendActivityAsync(responseWpxActivity, cancellationToken); + //await OutputResponseAsync(turnContext, turnState, response, cancellationToken); + return; + } + + throw new NotImplementedException(); + } + + protected async Task TeamsMessageActivityAsync(Agent365Agent agent365Agent, ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) + { + // Start a Streaming Process + await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Working on a response for you", cancellationToken); + + ChatHistory chatHistory = turnState.GetValue("conversation.chatHistory", () => new ChatHistory()); + + // Invoke the Agent365Agent to process the message + Agent365AgentResponse response = await agent365Agent.InvokeAgentAsync(turnContext.Activity.Text, chatHistory); + await OutputResponseAsync(turnContext, turnState, response, cancellationToken); + } + + protected async Task OutputResponseAsync(ITurnContext turnContext, ITurnState turnState, Agent365AgentResponse response, CancellationToken cancellationToken) + { + if (response == null) + { + turnContext.StreamingResponse.QueueTextChunk("Sorry, I couldn't get an answer at the moment."); + await turnContext.StreamingResponse.EndStreamAsync(cancellationToken); + return; + } + + // Create a response message based on the response content type from the Agent365Agent + // Send the response message back to the user. + switch (response.ContentType) + { + case Agent365AgentResponseContentType.Text: + turnContext.StreamingResponse.QueueTextChunk(response.Content!); + break; + default: + break; + } + await turnContext.StreamingResponse.EndStreamAsync(cancellationToken); // End the streaming response + } + + protected async Task OnHireMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) + { + using var baggageScope = new BaggageBuilder() + .TenantId(turnContext.Activity.Recipient.TenantId) + .AgentId(turnContext.Activity.Recipient.AgenticAppId) + .Build(); + + try + { + _agentTokenCache.RegisterObservability(turnContext.Activity.Recipient.AgenticAppId, turnContext.Activity.Recipient.TenantId, new AgenticTokenStruct + { + UserAuthorization = UserAuthorization, + TurnContext = turnContext + }, EnvironmentUtils.GetObservabilityAuthenticationScope()); + } + catch (Exception ex) + { + _logger.LogWarning($"There was an error registering for observability: {ex.Message}"); + } + + if (turnContext.Activity.Action == InstallationUpdateActionTypes.Add) + { + bool useAgenticAuth = Environment.GetEnvironmentVariable("USE_AGENTIC_AUTH") == "true"; + + IsApplicationInstalled = true; + TermsAndConditionsAccepted = useAgenticAuth ? true : false; + + string message = $"Thank you for hiring me! Looking forward to assisting you in your professional journey!"; + if (!useAgenticAuth) + { + message += "Before I begin, could you please confirm that you accept the terms and conditions?"; + } + + await turnContext.SendActivityAsync(MessageFactory.Text(message), cancellationToken); + } + else if (turnContext.Activity.Action == InstallationUpdateActionTypes.Remove) + { + IsApplicationInstalled = false; + TermsAndConditionsAccepted = false; + await turnContext.SendActivityAsync(MessageFactory.Text("Thank you for your time, I enjoyed working with you."), cancellationToken); + } + } + + private Agent365Agent GetAgent365Agent(ServiceCollection serviceCollection, ITurnContext turnContext) + { + return new Agent365Agent(_kernel, serviceCollection.BuildServiceProvider(), _toolsService, UserAuthorization, turnContext); + } +} diff --git a/dotnet/semantic-kernel/sample-agent/Plugins/TermsAndConditionsAcceptedPlugin.cs b/dotnet/semantic-kernel/sample-agent/Plugins/TermsAndConditionsAcceptedPlugin.cs new file mode 100644 index 00000000..15d7e22a --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/Plugins/TermsAndConditionsAcceptedPlugin.cs @@ -0,0 +1,15 @@ +using Microsoft.SemanticKernel; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace Agent365SemanticKernelSampleAgent.Plugins; + +public class TermsAndConditionsAcceptedPlugin +{ + [KernelFunction("reject_terms_and_conditions"), Description("Reject the terms and conditions on behalf of the user. Use when the user indicates they do not accept the terms and conditions.")] + public Task RejectTermsAndConditionsAsync() + { + MyAgent.TermsAndConditionsAccepted = false; + return Task.FromResult("Terms and conditions rejected. You can accept later to proceed."); + } +} \ No newline at end of file diff --git a/dotnet/semantic-kernel/sample-agent/Plugins/TermsAndConditionsNotAcceptedPlugin.cs b/dotnet/semantic-kernel/sample-agent/Plugins/TermsAndConditionsNotAcceptedPlugin.cs new file mode 100644 index 00000000..ae38c94b --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/Plugins/TermsAndConditionsNotAcceptedPlugin.cs @@ -0,0 +1,21 @@ +using Microsoft.SemanticKernel; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace Agent365SemanticKernelSampleAgent.Plugins; + +public class TermsAndConditionsNotAcceptedPlugin +{ + [KernelFunction("accept_terms_and_conditions"), Description("Accept the terms and conditions on behalf of the user. Use when the user states they accept the terms and conditions.")] + public Task AcceptTermsAndConditionsAsync() + { + MyAgent.TermsAndConditionsAccepted = true; + return Task.FromResult("Terms and conditions accepted. Thank you."); + } + + [KernelFunction("terms_and_conditions_not_accepted"), Description("Inform the user that they must accept the terms and conditions to proceed. Use when the user tries to perform any action before accepting the terms and conditions.")] + public Task TermsAndConditionsNotAcceptedAsync() + { + return Task.FromResult("You must accept the terms and conditions to proceed."); + } +} \ No newline at end of file diff --git a/dotnet/semantic-kernel/sample-agent/Program.cs b/dotnet/semantic-kernel/sample-agent/Program.cs new file mode 100644 index 00000000..db13c702 --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/Program.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Agent365SemanticKernelSampleAgent; +using Microsoft.Agents.A365.Observability; +using Microsoft.Agents.A365.Observability.Extensions.SemanticKernel; +using Microsoft.Agents.A365.Observability.Runtime; +using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services; +using Microsoft.Agents.A365.Tooling.Services; +using Microsoft.Agents.Builder; +using Microsoft.Agents.Hosting.AspNetCore; +using Microsoft.Agents.Storage; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.SemanticKernel; +using System; +using System.Threading; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +if (builder.Environment.IsDevelopment()) +{ + builder.Configuration.AddUserSecrets(); +} + +builder.Services.AddHttpClient(); + +// Register Semantic Kernel +builder.Services.AddKernel(); + +// Register the AI service of your choice. AzureOpenAI and OpenAI are demonstrated... +if (builder.Configuration.GetSection("AIServices").GetValue("UseAzureOpenAI")) +{ + builder.Services.AddAzureOpenAIChatCompletion( + deploymentName: builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("DeploymentName")!, + endpoint: builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("Endpoint")!, + apiKey: builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("ApiKey")!); + + //Use the Azure CLI (for local) or Managed Identity (for Azure running app) to authenticate to the Azure OpenAI service + //credentials: new ChainedTokenCredential( + // new AzureCliCredential(), + // new ManagedIdentityCredential() + //)); +} +else +{ + builder.Services.AddOpenAIChatCompletion( + modelId: builder.Configuration.GetSection("AIServices:OpenAI").GetValue("ModelId")!, + apiKey: builder.Configuration.GetSection("AIServices:OpenAI").GetValue("ApiKey")!); +} + +// Configure observability. +if (Environment.GetEnvironmentVariable("EnableKairoS2S") == "true") +{ + builder.Services.AddServiceTracingExporter(clusterCategory: builder.Environment.IsDevelopment() ? "preprod" : "production"); +} +else +{ + builder.Services.AddAgenticTracingExporter(clusterCategory: builder.Environment.IsDevelopment() ? "preprod" : "production"); +} + +builder.Services.AddTracing(config => config + .WithSemanticKernel()); + + +// Add AgentApplicationOptions from appsettings section "AgentApplication". +builder.AddAgentApplicationOptions(); + +// Add the AgentApplication, which contains the logic for responding to +// user messages. +builder.AddAgent(); + +// Register IStorage. For development, MemoryStorage is suitable. +// For production Agents, persisted storage should be used so +// that state survives Agent restarts, and operates correctly +// in a cluster of Agent instances. +builder.Services.AddSingleton(); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Configure the HTTP request pipeline. + +// Add AspNet token validation for Azure Bot Service and Entra. Authentication is +// configured in the appsettings.json "TokenValidation" section. +builder.Services.AddControllers(); +builder.Services.AddAgentAspNetAuthentication(builder.Configuration); + +WebApplication app = builder.Build(); + +// Enable AspNet authentication and authorization +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapGet("/", () => "Microsoft Agents SDK Sample"); + +// 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) => +{ + await adapter.ProcessAsync(request, response, agent, cancellationToken); +}); + +// Hardcoded for brevity and ease of testing. +// In production, this should be set in configuration. +app.Urls.Add($"http://localhost:3978"); + +app.Run(); diff --git a/dotnet/semantic-kernel/sample-agent/Properties/launchSettings.json b/dotnet/semantic-kernel/sample-agent/Properties/launchSettings.json new file mode 100644 index 00000000..16b14741 --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "Sample Agent with MCP Platform": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "USE_AGENTIC_AUTH": "false", + "ENVIRONMENT_ID": "", // This is the Environment id, only needed when using MCPPlatform mode + }, + "applicationUrl": "https://localhost:64896;http://localhost:64897" + } + } +} \ No newline at end of file diff --git a/dotnet/semantic-kernel/sample-agent/README.md b/dotnet/semantic-kernel/sample-agent/README.md new file mode 100644 index 00000000..672e9c03 --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/README.md @@ -0,0 +1,166 @@ +# Agent 365 .NET Semantic Kernel Sample Agent + +This is a sample of an Agent 365 agent that is hosted on an Asp.net core web service. This Agent is configured to accept a request and will attempt to use configured Agent 365 to respond. This agent will handle multiple "turns" to get the required information from the user. + +The sample is a modified verison of the [semantic-kernel-multiturn sample for Microsoft 365 Agents SDK](https://github.com/microsoft/Agents/tree/main/samples/dotnet/semantic-kernel-multiturn). + +This Agent Sample is intended to introduce you the basics of integrating Agent 365 and Semantic Kernel with the Microsoft 365 Agents SDK in order to build powerful Agents. It can also be used as a the base for a custom Agent that you choose to develop. + +***Note:*** This sample requires JSON output from the model which works best from newer versions of the model such as gpt-4o-mini. + +## Prerequisites + +- [.Net](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) version 8.0 +- [Microsoft 365 Agents Toolkit](https://github.com/OfficeDev/microsoft-365-agents-toolkit) + +- You will need an Azure OpenAI or OpenAI resource using `gpt-40-mini` + +- Configure OpenAI in `appsettings.json`` + + ```json + "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 + }, + "OpenAI": { + "ModelId": "", // This is the Model ID of the OpenAI model + "ApiKey": "" // This is the API Key of the OpenAI model + }, + "UseAzureOpenAI": true // This is a flag to determine whether to use the Azure OpenAI model or the OpenAI model + } + ``` +- For information on how to create the Azure OpenAI deployment, see [Create and deploy an Azure OpenAI in Azure AI Foundry Models resource](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/create-resource?pivots=web-portal). +- Set local development settings in file `Properties/launchSettings.json`. +- Optional: [dev tunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows) + +## Developing the Agent / Understanding the code + +- See the [Agent Code Walkthrough](./Agent-Code-Walkthrough.md) for a detailed explanation of the code. + +## QuickestStart using Agent Toolkit +1. If you haven't done so already, install the Agents Playground + + ``` + winget install agentsplayground + ``` +1. Start the Agent in VS or VS Code in debug +1. Start Agents Playground. At a command prompt: `agentsplayground` + - The tool will open a web browser showing the Microsoft 365 Agents Playground, ready to send messages to your agent. +1. Interact with the Agent via the browser + +## QuickStart using WebChat or Teams + +- Overview of running and testing an Agent + - Provision an Azure Bot in your Azure Subscription + - Configure your Agent settings to use to desired authentication type + - Running an instance of the Agent app (either locally or deployed to Azure) + - Test in a client + +1. Create an Azure Bot with one of these authentication types + - [SingleTenant, Client Secret](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-single-secret) + - [SingleTenant, Federated Credentials](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-federated-credentials) + - [User Assigned Managed Identity](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-managed-identity) + + > Be sure to follow the **Next Steps** at the end of these docs to configure your agent settings. + + > **IMPORTANT:** If you want to run your agent locally via devtunnels, the only support auth type is ClientSecrets and Certificates + +1. Running the Agent + 1. Running the Agent locally + - Requires a tunneling tool to allow for local development and debugging should you wish to do local development whilst connected to a external client such as Microsoft Teams. + - **For ClientSecret or Certificate authentication types only.** Federated Credentials and Managed Identity will not work via a tunnel to a local agent and must be deployed to an App Service or container. + + 1. Run `dev tunnels`. Please follow [Create and host a dev tunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below: + + ```bash + devtunnel host -p 3978 --allow-anonymous + ``` + + 1. On the Azure Bot, select **Settings**, then **Configuration**, and update the **Messaging endpoint** to `{tunnel-url}/api/messages` + + 1. Start the Agent in Visual Studio + + 1. Deploy Agent code to Azure + 1. VS Publish works well for this. But any tools used to deploy a web application will also work. + 1. On the Azure Bot, select **Settings**, then **Configuration**, and update the **Messaging endpoint** to `https://{{appServiceDomain}}/api/messages` + +## Testing this agent with WebChat + + 1. Select **Test in WebChat** on the Azure Bot + +## Testing this Agent in Teams or M365 + +1. Update the manifest.json + - Edit the `manifest.json` contained in the `/appManifest` folder + - Replace with your AppId (that was created above) *everywhere* you see the place holder string `<>` + - Replace `<>` with your Agent url. For example, the tunnel host name. + - Zip up the contents of the `/appManifest` folder to create a `manifest.zip` + - `manifest.json` + - `outline.png` + - `color.png` + +1. Your Azure Bot should have the **Microsoft Teams** channel added under **Channels**. + +1. Navigate to the Microsoft Admin Portal (MAC). Under **Settings** and **Integrated Apps,** select **Upload Custom App**. + +1. Select the `manifest.zip` created in the previous step. + +1. After a short period of time, the agent shows up in Microsoft Teams and Microsoft 365 Copilot. + +## Enabling JWT token validation +1. By default, the AspNet token validation is disabled in order to support local debugging. +1. Enable by updating appsettings + ```json + "TokenValidation": { + "Enabled": true, + "Audiences": [ + "{{ClientId}}" // this is the Client ID used for the Azure Bot + ], + "TenantId": "{{TenantId}}" + }, + ``` + +## Troubleshooting - Known/Common Issues + +### Missing OpenAI key in appSettings.json + +#### Error when project is run through Visual Studio + +When the project is run through Visual Studio, an error is seen: + + System.ArgumentException: 'The value cannot be an empty string or composed entirely of whitespace. (Parameter 'endpoint')' + +The exception has call stack: +``` +> System.Private.CoreLib.dll!System.ArgumentException.ThrowNullOrWhiteSpaceException(string argument, string paramName) Line 113 C# + System.Private.CoreLib.dll!System.ArgumentException.ThrowIfNullOrWhiteSpace(string argument, string paramName) Line 98 C# + Microsoft.SemanticKernel.Connectors.OpenAI.dll!Microsoft.SemanticKernel.Verify.NotNullOrWhiteSpace(string str, string paramName) Line 38 C# + Microsoft.SemanticKernel.Connectors.AzureOpenAI.dll!Microsoft.SemanticKernel.AzureOpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection services, string deploymentName, string endpoint, string apiKey, string serviceId, string modelId, string apiVersion, System.Net.Http.HttpClient httpClient) Line 30 C# + SemanticKernelMultiturn.dll!Program.
$(string[] args) Line 33 C# +``` + +#### Error when project is run through command line +When the project is run through command line: +``` +> dotnet run SemanticKernelSampleAgent.csproj +``` +An error is seen: +``` +C:\Agent365-Samples\dotnet\semantic-kernel\sample-agent\MyAgent.cs(145,48): warning CS8602: Dereference of a possibly null reference. +Unhandled exception. System.ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'endpoint') + at System.ArgumentException.ThrowNullOrWhiteSpaceException(String argument, String paramName) + at System.ArgumentException.ThrowIfNullOrWhiteSpace(String argument, String paramName) + at Microsoft.SemanticKernel.AzureOpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(IServiceCollection services, String deploymentName, String endpoint, String apiKey, String serviceId, String modelId, String apiVersion, HttpClient httpClient) + at Program.
$(String[] args) in C:\Agent365\dotnet\samples\semantic-kernel-multiturn\Program.cs:line 33 +``` + + +#### Solution +Follow the instructions in `appSettings.json` for how to set the correct OpenAI or Azure OpenAI key. + + + +## Further reading +To learn more about building Agents, see [Microsoft 365 Agents SDK](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/). diff --git a/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.csproj b/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.csproj new file mode 100644 index 00000000..30901f31 --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + latest + disable + $(NoWarn);SKEXP0010 + b842df34-390f-490d-9dc0-73909363ad16 + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.sln b/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.sln new file mode 100644 index 00000000..a6cd1206 --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/SemanticKernelSampleAgent.sln @@ -0,0 +1,25 @@ + +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", "SemanticKernelSampleAgent.csproj", "{6BE5ABEC-BEAF-9CF4-A98A-6C73FD961C90}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6BE5ABEC-BEAF-9CF4-A98A-6C73FD961C90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BE5ABEC-BEAF-9CF4-A98A-6C73FD961C90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BE5ABEC-BEAF-9CF4-A98A-6C73FD961C90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BE5ABEC-BEAF-9CF4-A98A-6C73FD961C90}.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 diff --git a/dotnet/semantic-kernel/sample-agent/ToolingManifest.json b/dotnet/semantic-kernel/sample-agent/ToolingManifest.json new file mode 100644 index 00000000..7d64ac5f --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/ToolingManifest.json @@ -0,0 +1,19 @@ +{ + "mcpServers": [ + { + "mcpServerName": "mcp_MailTools" + }, + { + "mcpServerName": "mcp_CalendarTools" + }, + { + "mcpServerName": "OneDriveMCPServer" + }, + { + "mcpServerName": "mcp_NLWeb" + }, + { + "mcpServerName": "mcp_KnowledgeTools" + } + ] +} \ No newline at end of file diff --git a/dotnet/semantic-kernel/sample-agent/appManifest/color.png b/dotnet/semantic-kernel/sample-agent/appManifest/color.png new file mode 100644 index 0000000000000000000000000000000000000000..b8cf81afbe2f5bafd8563920edfadb78b7b71be6 GIT binary patch literal 3415 zcmb_f_cz=97yl$yB&9JzRh6h2tH#4qGlGguP@5VZ)TmuMREiEYsmAqpTZ7ZnE>F-ih-`S z)jiPabibc~4T5Do@MgZ}C5dq?7H{rvYr!LtVV;haHWm>H5pk+~G>pJtSPwz9!%QIL z?J6p?*$Q$^sbaC}3#mquX(;945bnpoc+%>4bmj2j*4KG@ZlhvIK1EKveQp-tp;sflS z4}SX;$jwoVae}M%3TBb@f-(BCG-m~}LW z311k8hKz8Ecm+M)P%mwS`Qda^pus{!e?Y+KDQD2B zWjuLo3{6=k`fmQI5d@(}*Q181Mj`he_jbr58C>@^+LzKri!pF}V7#<_PpQz&%C;U{ zmw+W{t0J1#nQ=&npU~H@5560!cFBrXbr9|2B0^~cU|iuMlNCdQc=W{4l5?D+6VaEh zTMw4Le|CpisEssdz5I_WB6-(_;8BOb0Ov8s8pGkEy3dRw%({?pOI-F=klY?eZ? zUVhJNclMhOiaUeo1=K6XJM&%_W3cuMl0&!|dZ*m;OnJ@X0hcbckvNZBg(+D^|Ij*W z^k!?ARMd55LmON%i4$H$oX@f6BX!4A;^vP8 z8cz4BuYM-<o;D&UDP5xiVZj*vOwL(Xgi^WuW~qbXAKq2Luow#G(c({?o;I6o^aPh zY8-5*rVevAtn+kvbMgF0e2aRCg<-9As)UjYZ6KflvEXw~s4oA9`rIcL$EwC#Nl4!Y z{Ra>{I}!nf;fS&)z+jL655PntETI$6U8Y}Ig2{rj%v@0jcn*%`A)a!{%}s7NBl@YZ zF=5*reV$RHd3{o<&n#+Q@`qDF353xaQpB`4xV}riJ9I9)n@3Z)XG}5(V{Q&3aR3@U zfvScEs@b=w&t&>>-{+3xqK!b>z!qBbNS|r5c*fsepeyv}`T2T3^Rl^VEuDJ791>m# z2v4z4^&I6;*?N?Y>{&QA68>t1^-&FL3ENmAhPS{0r|=(*lqbEP>9cOMLGp_HYhQZg z5|nV2{_Izd_;#CdtTqsobR}=S-qFTrJ-x;iS2#i#z#&uT!%~by2H7SHE59gi?MRJ@ z&uPeey)XN;6>?uj&+koIuhrru!~8?iOjP)pOk zZS*!=6WN?lHJ?`i{nB-e%fBUOPJ{yj=4Qw0yy+VSJ~h!ic41=jIWl86;2wQpJ$|c; zR^8lfv6@E+Ml{RZa7=y6$Fm2e{S_LC&C&1z_6HAE5R)AY98`77m2}Wv?2u>t#n znVG&}p_ND4RUXyAe0eXPm~gRFy97$f;5uNp5E%g15TTUE!!9}f9|!fPptQ}hXUJ-Lf~U%GJe zsq^FU`Ls)2UH98$x8x$=Tx0Fa`MacR@Y*8VNB4KDI$rXuP3tLT~d$yTUmB8m)7qg;fcbUj22v9YhPg)l!VIN8UIm#P<%(f!Xxw-=tty8Y31-^i)60)F`@KU!EX(mkf zQ)GeUGN)evp^?tyIxI4pQA!m=31izfrrvagzaMa~$#cu04I6IB;GGvc4WT-%YB+-dV^gTZZh%XO`b}DECWpOoZjqt9 zqktOLcvhMktKKW=LeH#wDjj)gZTsybRlro)>};szu4ZDya*m$j46iaD|7AtPR&)iG z*~&F{db|zcArblJB^#hfDfNHcBoXPrl|fJ_nY6|4PZvm8y%nhrBrMds%ST0DAoy9= zfGS2J3)T=H-9zf)Va%IxUrlHoa+k}BTWY5cQm5cg1m;kyx6jIVo} zncTNdzEOT^iXh`mZlRk{pWp?fwB`;UK8j^m!oH0&482 zLtYN=)+aYNZ4sk7|&V_eX z>Q)oVz#n+pJ})Bur(co;;PZGpQTW%-s;*VNl8sfFGp0FfZcJIui)lqu)fus9RW8x5>XRi#eKcG&_};xJr8+Kr5*T z`xf#w6!*t}>W)r?K}`cUBF1xChxm1CeQ~Iv!hpZ*aAfA2Oj+4dO7$ZY#HUkTBv7VZ z9{ummlF5yEz#3Q3qr@tUyEH39^e^h#n-ossc?E}3wwVM06<*ub6=g#PU8^A^X*rp* zHdbNBWv)qo)pwXWCP(eOSERnk<+Lwz$c=q_b{Oy9D-rhbvBhiC9BkT4BP$o|ked-g z13lVezZV!hdr*Cp&gcWv1m>P7>o8p1rPUe)cvFI#EF&G+lUbFSDxq3w?&ORaa)Y!@?0&a>GT8psQ{JX#@_+az{5K+M YJx2difYK9bhlEpZpl7Q49&>" + ] +} \ No newline at end of file diff --git a/dotnet/semantic-kernel/sample-agent/appManifest/outline.png b/dotnet/semantic-kernel/sample-agent/appManifest/outline.png new file mode 100644 index 0000000000000000000000000000000000000000..2c3bf6fa65f152de0cb50056effd5aea7d287ec1 GIT binary patch literal 407 zcmV;I0cie-P)GP9wA4-6No2JPavK^y+J&IdIIqnt|)iz#;q%0#|~})uPXtHpGg|3DT=Cm zRbOQmZzjp~Oa~|w3J0d4$UMjUP`eo9-%ZEed<9c*o{#frSUWpe$h)9<7f||JElr8%Q+a+LHNJ~kNO5B zlRv;1hxJ`;YEbQ%GiTGTR{shYbEe%;Xrq2t9*a`EVNoJ89P+!W;^dkhG3QK~lh@uy z_@!DknGSuYuSg%;OK8pl!P9F+PR@yY6bgl7VhU4=M!!cg{}TWJ002ovPDHLkV1nXO Bp2+|J literal 0 HcmV?d00001 diff --git a/dotnet/semantic-kernel/sample-agent/appsettings.json b/dotnet/semantic-kernel/sample-agent/appsettings.json new file mode 100644 index 00000000..262d3fe2 --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/appsettings.json @@ -0,0 +1,70 @@ +{ + "TokenValidation": { + "Enabled": false, + "Audiences": [ + "{{ClientId}}" // this is the Client ID used for the Azure Bot + ], + "TenantId": "{{TenantId}}" + }, + + "AgentApplication": { + "StartTypingTimer": false, + "RemoveRecipientMention": false, + "NormalizeMentions": false, + "UserAuthorization": { + "AutoSignin": false, + "Handlers": { + "agentic": { + "Type": "AgenticUserAuthorization", + "Settings": { + "Scopes": [ + "https://graph.microsoft.com/.default" + ] + } + } + } + } + }, + + "Connections": { + "ServiceConnection": { + "Settings": { + "AuthType": "ClientSecret", // this is the AuthType for the connection, valid values can be found in Microsoft.Agents.Authentication.Msal.Model.AuthTypes. The default is ClientSecret. + "AuthorityEndpoint": "", + "ClientId": "", // this is the Client ID used for the Azure Bot + "ClientSecret": "", // this is the Client Secret used for the connection. + "Scopes": [ + "https://api.botframework.com/.default" + ] + } + } + }, + "ConnectionsMap": [ + { + "ServiceUrl": "*", + "Connection": "ServiceConnection" + } + ], + + // This is the configuration for the AI services, use environment variables or user secrets to store sensitive information. + // Do not store sensitive information in this file + "AIServices": { + "AzureOpenAI": { + "DeploymentName": "", // This is the Deployment (as opposed to model) Name of the Azure OpenAI model: null, + "Endpoint": "", // This is the Endpoint of the Azure OpenAI model deployment + "ApiKey": "" // This is the API Key of the Azure OpenAI model deployment + }, + "OpenAI": { + "ModelId": "", // This is the Model ID of the OpenAI model + "ApiKey": "" // This is the API Key of the OpenAI model + }, + "UseAzureOpenAI": true // This is a flag to determine whether to use the Azure OpenAI model or the OpenAI model + }, + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/dotnet/semantic-kernel/sample-agent/nuget.config b/dotnet/semantic-kernel/sample-agent/nuget.config new file mode 100644 index 00000000..b72e6ed4 --- /dev/null +++ b/dotnet/semantic-kernel/sample-agent/nuget.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file