From 10fbe301b5a5d147ecf0629571bf414530fa1892 Mon Sep 17 00:00:00 2001 From: MattB Date: Wed, 29 Jan 2025 23:17:53 -0800 Subject: [PATCH 1/4] Add initial implementation of DispatcherAgent with configuration and telemetry support --- .gitignore | 3 + samples/AssemblyInfo.Shared.props | 11 + samples/Build.Common.core.props | 24 ++ samples/Build.Shared.props | 63 ++++ samples/Directory.Packages.props | 88 +++++ .../Dispatcher/Controllers/BotController.cs | 34 ++ .../dotnet/Dispatcher/DispatcherAgent.csproj | 19 ++ .../dotnet/Dispatcher/DispatcherBot.cs | 183 ++++++++++ .../Dispatcher/Interfaces/IMCSAgents.cs | 51 +++ .../KernelPlugins/CustomerServicePlugin.cs | 54 +++ .../Dispatcher/KernelPlugins/WeatherPlugin.cs | 49 +++ .../KernelSupport/SKFunctionFilter.cs | 76 +++++ .../SKHistoryToConversationStore.cs | 86 +++++ .../dotnet/Dispatcher/Model/MCSAgent.cs | 22 ++ .../dotnet/Dispatcher/Model/MCSAgents.cs | 318 ++++++++++++++++++ .../Model/MultiBotConversationStore.cs | 10 + .../dotnet/Dispatcher/Model/OAuthFlowState.cs | 8 + .../Dispatcher/Model/PerfTelemtryStore.cs | 42 +++ .../dispatcher/dotnet/Dispatcher/Program.cs | 64 ++++ .../SampleServiceCollectionExtensions.cs | 102 ++++++ .../Dispatcher/Utils/TimeSpanExtensions.cs | 15 + .../dotnet/Dispatcher/Utils/Utilities.cs | 182 ++++++++++ .../dotnet/Dispatcher/appsettings.json | 75 +++++ .../dispatcher/dotnet/DispatcherAgent.sln | 22 ++ .../complex/dispatcher/dotnet/version.json | 13 + samples/nuget.config | 8 + 26 files changed, 1622 insertions(+) create mode 100644 samples/AssemblyInfo.Shared.props create mode 100644 samples/Build.Common.core.props create mode 100644 samples/Build.Shared.props create mode 100644 samples/Directory.Packages.props create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/Controllers/BotController.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/DispatcherAgent.csproj create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/DispatcherBot.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/Interfaces/IMCSAgents.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/KernelPlugins/CustomerServicePlugin.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/KernelPlugins/WeatherPlugin.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/KernelSupport/SKFunctionFilter.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/KernelSupport/SKHistoryToConversationStore.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/Model/MCSAgent.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/Model/MCSAgents.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/Model/MultiBotConversationStore.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/Model/OAuthFlowState.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/Model/PerfTelemtryStore.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/Program.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/SampleServiceCollectionExtensions.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/Utils/TimeSpanExtensions.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/Utils/Utilities.cs create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/appsettings.json create mode 100644 samples/complex/dispatcher/dotnet/DispatcherAgent.sln create mode 100644 samples/complex/dispatcher/dotnet/version.json create mode 100644 samples/nuget.config diff --git a/.gitignore b/.gitignore index 8a30d25..4d5108e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs +**/launchSettings.json +**/launchSettings.Template.json +**/appsettings.Development.json # Mono auto generated files mono_crash.* diff --git a/samples/AssemblyInfo.Shared.props b/samples/AssemblyInfo.Shared.props new file mode 100644 index 0000000..f19699a --- /dev/null +++ b/samples/AssemblyInfo.Shared.props @@ -0,0 +1,11 @@ + + + + Microsoft Agents Sdk Samples + © Microsoft Corporation. All rights reserved. + 0.1.0.0 + 1.0.0.0 + $(FileVersion) + $(AssemblyVersion) + + diff --git a/samples/Build.Common.core.props b/samples/Build.Common.core.props new file mode 100644 index 0000000..b340244 --- /dev/null +++ b/samples/Build.Common.core.props @@ -0,0 +1,24 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + net8.0 + true + + + + IOperation + + + + + + + + + + + diff --git a/samples/Build.Shared.props b/samples/Build.Shared.props new file mode 100644 index 0000000..98266b9 --- /dev/null +++ b/samples/Build.Shared.props @@ -0,0 +1,63 @@ + + + + + true + false + true + + + + Debug + + + + AnyCPU + 512 + true + $(NoWarn);CS8032;CS8002;CS1668 + true + prompt + 4 + + + + true + full + DEBUG;TRACE + false + + + + TRACE + pdbonly + true + + + + true + full + DEBUG;TRACE + false + x64 + + + + TRACE + pdbonly + true + x64 + + + + + + diff --git a/samples/Directory.Packages.props b/samples/Directory.Packages.props new file mode 100644 index 0000000..175567c --- /dev/null +++ b/samples/Directory.Packages.props @@ -0,0 +1,88 @@ + + + + true + + true + + $(NoWarn);NU1701;NU1900;NU5125;NU5104 + + + + + 8.0.0 + 8.0.11 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/Controllers/BotController.cs b/samples/complex/dispatcher/dotnet/Dispatcher/Controllers/BotController.cs new file mode 100644 index 0000000..4f1507a --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/Controllers/BotController.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.BotBuilder; +using Microsoft.Agents.Hosting.AspNetCore; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DispatcherAgent.Controllers +{ + /// + /// ASP.Net Controller that receives incoming HTTP requests from the Azure Bot Service or other + /// configured event / activity protocol sources. When called, the request has already been + /// authorized and credentials and tokens validated. + /// + /// This is the entry point for the bot to process incoming activities, such as messages. + /// + /// + /// + /// + [Authorize] + [ApiController] + [Route("api/messages")] + public class BotController(IBotHttpAdapter adapter, IBot bot) : ControllerBase + { + [HttpPost] + public async Task PostAsync(CancellationToken cancellationToken) + { + // Delegate the processing of the HTTP POST to the adapter. + // The adapter will invoke the bot. + await adapter.ProcessAsync(Request, Response, bot, cancellationToken); + } + } +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/DispatcherAgent.csproj b/samples/complex/dispatcher/dotnet/Dispatcher/DispatcherAgent.csproj new file mode 100644 index 0000000..ce1f7f9 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/DispatcherAgent.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + 1.0.0.0 + DispatcherAgent + + + + + + + + + + + diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/DispatcherBot.cs b/samples/complex/dispatcher/dotnet/Dispatcher/DispatcherBot.cs new file mode 100644 index 0000000..dc20231 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/DispatcherBot.cs @@ -0,0 +1,183 @@ +using DispatcherAgent.Interfaces; +using DispatcherAgent.KernelPlugins; +using DispatcherAgent.KernelSupport; +using DispatcherAgent.Model; +using DispatcherAgent.Utils; +using Microsoft.Agents.BotBuilder; +using Microsoft.Agents.BotBuilder.Teams; +using Microsoft.Agents.Core.Interfaces; +using Microsoft.Agents.Core.Models; +using Microsoft.Agents.Storage; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +#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. +namespace DispatcherAgent +{ + public class DispatcherBot( + IServiceProvider services, + IChatCompletionService chat, + IWebHostEnvironment hosting, + IStorage storage, + ILogger logger, + IMCSAgents copilotStudioHandler + ) : TeamsActivityHandler + { + IStorage _storage = storage; + + + protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + Console.ResetColor(); + if (turnContext.Activity == null) + return; // exit.. there is nothing to do + + if (turnContext.Activity?.Conversation?.Id != null) + { + Utilities.WriteConverationLinks($">> RECV CONVO ID: {turnContext.Activity?.Conversation?.Id}"); + } + + #region Obliviate history + if (turnContext.Activity?.Text != null) + { + if (turnContext.Activity.Text.Contains("flush history", StringComparison.OrdinalIgnoreCase) || + turnContext.Activity.Text.Contains("Obliviate", StringComparison.OrdinalIgnoreCase)) + { + var (_history1, _storageItem1) = await SKHistoryToConversationStore.GetOrCreateChatHistoryForConversation(_storage, Utilities.GetConversationLinkStorageKey(turnContext, "SK"), cancellationToken); + if (_history1.Count >= 1) + { + _history1.RemoveRange(1, _history1.Count() - 1); + await _storage.WriteAsync(_storageItem1, cancellationToken); + await turnContext.SendActivityAsync(MessageFactory.Text("..Poof.."), cancellationToken); + } + else + { + await turnContext.SendActivityAsync(MessageFactory.Text("..Blink.."), cancellationToken); + } + Utilities.WriteConverationLinks($"<< RESP TO CONVO ID: {turnContext.Activity?.Conversation?.Id}"); + return; + } + } + #endregion + + // Handle direct Reroute to MCS bots. + var isMcsAgent = copilotStudioHandler.IsMCSAgent(copilotStudioHandler.GetAliasFromText(turnContext.Activity?.Text)); + if (isMcsAgent) + { + var isProcessed = await copilotStudioHandler.DispatchToAgent(turnContext, cancellationToken); + if (isProcessed) + { + Utilities.WriteConverationLinks($"<< RESP TO CONVO ID: {turnContext.Activity?.Conversation?.Id}"); + return; + } + } + + // Talking with Semantic Kernel. + // Setup SK Functions to use this context + var builder = Kernel.CreateBuilder(); + builder.Services.AddSingleton(new KernelSupport.SKFunctionFilter()); + builder.Plugins.AddFromObject(new CustomerServicePlugin(copilotStudioHandler, services, turnContext)); + builder.Plugins.AddFromObject(new WeatherPlugin(copilotStudioHandler, services, turnContext)); + Kernel kern = builder.Build(); + + // Setup Prompt Execution settings. + OpenAIPromptExecutionSettings settings = new OpenAIPromptExecutionSettings(); + settings.FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(); + + // Get the current chat history for this conversation from Storage. + var (_history, _storageItem) = await SKHistoryToConversationStore.GetOrCreateChatHistoryForConversation(_storage, Utilities.GetConversationLinkStorageKey(turnContext, "SK"), cancellationToken); + + // talk to Chat Completion Service. + if (turnContext.Activity?.Text != null) + _history.AddUserMessage(turnContext.Activity.Text); + + ChatMessageContent result = await chat.GetChatMessageContentAsync(_history, + kernel: kern, + executionSettings: settings, + cancellationToken: cancellationToken); + + if (result.Role != AuthorRole.Tool) + { + // Only add chat history if the role is not tool, as we do not want to add MCS data to the history right now. + _history.AddMessage(result.Role, result.Content!); + + // respond to request + await turnContext.SendActivityAsync(MessageFactory.Text(result.Content!), cancellationToken); + } + + // Update Storage with current status. + await _storage.WriteAsync(_storageItem, cancellationToken); + Console.WriteLine($"Current History Depth: {_history.Count()}"); + Utilities.WriteConverationLinks($"<< RESP TO CONVO ID: {turnContext.Activity?.Conversation?.Id}"); + } + + protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) + { + // Provide Agent information when a new Member is added. + string info = $"**Agents SDK Multi-Agent Dispatcher Example.**{Environment.NewLine}" + + $"- HostName={Environment.MachineName}.{Environment.NewLine}" + + $"- Environment={hosting.EnvironmentName}.{Environment.NewLine}"; + + typeof(Activity).Assembly.CustomAttributes.ToList().ForEach((attr) => + { + if (attr.AttributeType.Name == "AssemblyInformationalVersionAttribute") + { + info += $"- SDK Version={attr.ConstructorArguments[0].Value}.{Environment.NewLine}"; + return; + } + }); + IActivity message = MessageFactory.Text(info); + var resp = await turnContext.SendActivityAsync(message, cancellationToken); + logger.LogInformation("OnMemberAdd->resp.Id: " + resp.Id); + } + + /// + /// This handler is called when the user clicks on a Sign In button in the Teams Client, Messaging Extension or Task Module. + /// + /// + /// + /// + protected override async Task OnTeamsSigninVerifyStateAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + if (turnContext.Activity != null && turnContext.Activity.Value != null) + { + OAuthFlow _flow = new OAuthFlow("Sign In", "Custom SignIn Message", "MCS01", 30000, true); + var stateKey = Utilities.GetFlowStateStorageKey(turnContext); + var items = await storage.ReadAsync([stateKey], cancellationToken); + OAuthFlowState state = items.TryGetValue(stateKey, out var value) ? (OAuthFlowState)value : new OAuthFlowState(); + + if (!state.FlowStarted) + { + } + else + { + try + { + TokenResponse? tokenResponse = null; + tokenResponse = await _flow.ContinueFlowAsync(turnContext, state.FlowExpires, cancellationToken); + if (tokenResponse != null) + { + await turnContext.SendActivityAsync(MessageFactory.Text("You are now logged in."), cancellationToken); + } + else + { + await turnContext.SendActivityAsync(MessageFactory.Text("Login was not successful please try again."), cancellationToken); + } + } + catch (TimeoutException) + { + await turnContext.SendActivityAsync(MessageFactory.Text("You did not respond in time. Please try again."), cancellationToken); + } + state.FlowStarted = false; + // Store flow state + items[stateKey] = state; + await storage.WriteAsync(items, cancellationToken); + } + } + bool _ = await Task.FromResult(true); + return; + } + } +#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. +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/Interfaces/IMCSAgents.cs b/samples/complex/dispatcher/dotnet/Dispatcher/Interfaces/IMCSAgents.cs new file mode 100644 index 0000000..03055ae --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/Interfaces/IMCSAgents.cs @@ -0,0 +1,51 @@ +using Microsoft.Agents.CopilotStudio.Client; +using Microsoft.Agents.Core.Interfaces; +using Microsoft.Agents.Core.Models; +using DispatcherAgent.Model; + +namespace DispatcherAgent.Interfaces +{ + public interface IMCSAgents + { + /// + /// List of MCS Agents are found. + /// + Dictionary Agents { get; set; } + + /// + /// Get Connection Settings by Alias. + /// + /// + /// + public ConnectionSettings? GetSettingsByAlias(string alias); + + /// + /// Get Display Name by Alias. + /// + /// + /// + public string? GetDisplayNameByAlias(string? alias); + + /// + /// Determines Alias from text + /// + /// + /// + public string? GetAliasFromText(string? text); + + /// + /// Determines if the alias is an MCS Agent + /// + /// + /// + public bool IsMCSAgent(string? alias); + + /// + /// Dispatches request to Agent. + /// + /// + /// + /// + public Task DispatchToAgent(ITurnContext turnContext, CancellationToken cancellationToken); + } +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/KernelPlugins/CustomerServicePlugin.cs b/samples/complex/dispatcher/dotnet/Dispatcher/KernelPlugins/CustomerServicePlugin.cs new file mode 100644 index 0000000..5a26c8d --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/KernelPlugins/CustomerServicePlugin.cs @@ -0,0 +1,54 @@ +using Microsoft.Agents.Core.Interfaces; +using Microsoft.Agents.Core.Models; +using Microsoft.SemanticKernel; +using DispatcherAgent.Interfaces; +using System.ComponentModel; + +namespace DispatcherAgent +{ + public class CustomerServicePlugin + { + IMCSAgents _copilotAgents; + IServiceProvider _serviceProvider; + ITurnContext _turnContext; + + public CustomerServicePlugin(IMCSAgents copilotAgent, IServiceProvider serviceProvider, ITurnContext turnContext) + { + _copilotAgents = copilotAgent; + _serviceProvider = serviceProvider; + _turnContext = turnContext; + } + + [KernelFunction("handle_cas")] + [Description(@" + I would like to return an Order + Return an order + Return order + Its Order Number ORD-12345. + Its Order Number ORD-98765. + I want to return a product I ordered. + I would like to return the product I ordered + Can handle phrases like 'I would like to return my', 'I want to return a product', 'I want to return a product', 'I have a problem with my product', 'My product is broken and I want to return it', 'Order Numbers'. + Can returns of product orders. + Can handle issues with product orders. + Can handle phrases like 'I have a problem with my product', 'My product is broken and I want to return it', 'Order Numbers'.")] + [return: Description("Was successfully processed by Copilot Studio")] + public async Task ProcessWeatherCopilotRequest() + { + CancellationToken cancellationToken = default; + // Process the request + if (!string.IsNullOrEmpty(_turnContext.Activity.Text)) + _turnContext.Activity.Text = $"@cas {_turnContext.Activity.Text}"; + + var isMcsAgent = _copilotAgents.IsMCSAgent(_copilotAgents.GetAliasFromText(_turnContext.Activity.Text)); + if (isMcsAgent) + { + var isProcessed = await _copilotAgents.DispatchToAgent(_turnContext, cancellationToken); + if (isProcessed) + return true; + } + + return false; + } + } +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/KernelPlugins/WeatherPlugin.cs b/samples/complex/dispatcher/dotnet/Dispatcher/KernelPlugins/WeatherPlugin.cs new file mode 100644 index 0000000..057627d --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/KernelPlugins/WeatherPlugin.cs @@ -0,0 +1,49 @@ +using Microsoft.Agents.Core.Interfaces; +using Microsoft.Agents.Core.Models; +using Microsoft.SemanticKernel; +using DispatcherAgent.Interfaces; +using System.ComponentModel; + +namespace DispatcherAgent.KernelPlugins +{ + public class WeatherPlugin + { + IMCSAgents _copilotAgents; + IServiceProvider _serviceProvider; + ITurnContext _turnContext; + + public WeatherPlugin(IMCSAgents copilotAgent, IServiceProvider serviceProvider, ITurnContext turnContext) + { + _copilotAgents = copilotAgent; + _serviceProvider = serviceProvider; + _turnContext = turnContext; + } + + [KernelFunction("handle_weatherrequest")] + [Description(@$"Handles weather requests. + Get Current Weather + Get Forecast for Tomorrow + Get Forecast for a City + Can return return Current weather for a city, current forecast, or future forecasts for a city. + It can process specific commands like 'Get Current Weather', 'Get Forecast for Tomorrow" + )] + [return: Description("Was successfully processed by Copilot Studio")] + public async Task ProcessWeatherCopilotRequest() + { + CancellationToken cancellationToken = default; + // Process the request + if (!string.IsNullOrEmpty(_turnContext.Activity.Text)) + _turnContext.Activity.Text = $"@wb {_turnContext.Activity.Text}"; + + var isMcsAgent = _copilotAgents.IsMCSAgent(_copilotAgents.GetAliasFromText(_turnContext.Activity.Text)); + if (isMcsAgent) + { + var isProcessed = await _copilotAgents.DispatchToAgent(_turnContext, cancellationToken); + if (isProcessed) + return true; + } + + return false; + } + } +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/KernelSupport/SKFunctionFilter.cs b/samples/complex/dispatcher/dotnet/Dispatcher/KernelSupport/SKFunctionFilter.cs new file mode 100644 index 0000000..9f059c8 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/KernelSupport/SKFunctionFilter.cs @@ -0,0 +1,76 @@ +using Microsoft.SemanticKernel; + +#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. +namespace DispatcherAgent.KernelSupport +{ + public class SKFunctionFilter : IAutoFunctionInvocationFilter + { + public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + // Example: get function information + var functionName = context.Function.Name; + + // Example: get chat history + var chatHistory = context.ChatHistory; + + // Example: get information about all functions which will be invoked + var functionCalls = FunctionCallContent.GetFunctionCalls(context.ChatHistory.Last()); + + // In function calling functionality there are two loops. + // Outer loop is "request" loop - it performs multiple requests to LLM until user ask will be satisfied. + // Inner loop is "function" loop - it handles LLM response with multiple function calls. + + // Workflow example: + // 1. Request to LLM #1 -> Response with 3 functions to call. + // 1.1. Function #1 called. + // 1.2. Function #2 called. + // 1.3. Function #3 called. + // 2. Request to LLM #2 -> Response with 2 functions to call. + // 2.1. Function #1 called. + // 2.2. Function #2 called. + + // context.RequestSequenceIndex - it's a sequence number of outer/request loop operation. + // context.FunctionSequenceIndex - it's a sequence number of inner/function loop operation. + // context.FunctionCount - number of functions which will be called per request (based on example above: 3 for first request, 2 for second request). + + // Example: get request sequence index + System.Diagnostics.Trace.WriteLine($"Request sequence index: {context.RequestSequenceIndex}"); + + // Example: get function sequence index + System.Diagnostics.Trace.WriteLine($"Function sequence index: {context.FunctionSequenceIndex}"); + + // Example: get total number of functions which will be called + System.Diagnostics.Trace.WriteLine($"Total number of functions: {context.FunctionCount}"); + + // Calling next filter in pipeline or function itself. + // By skipping this call, next filters and function won't be invoked, and function call loop will proceed to the next function. + await next(context); + + // Example: get function result + var result = context.Result; + + if (!string.IsNullOrEmpty(context.Function.Name) && + (context.Function.Name.Equals("handle_weatherrequest", StringComparison.OrdinalIgnoreCase) || + context.Function.Name.Equals("handle_cas", StringComparison.OrdinalIgnoreCase) + )) + { + if (context.Result.ValueType == typeof(bool)) + { + var resultValue = context.Result.GetValue(); + if (resultValue) + { + //context.Result = null; + context.Terminate = true; + } + } + } + + // Example: override function result value + // context.Result = new FunctionResult(context.Result, "Result from auto function invocation filter"); + + // Example: Terminate function invocation + //context.Terminate = 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. diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/KernelSupport/SKHistoryToConversationStore.cs b/samples/complex/dispatcher/dotnet/Dispatcher/KernelSupport/SKHistoryToConversationStore.cs new file mode 100644 index 0000000..d6ab646 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/KernelSupport/SKHistoryToConversationStore.cs @@ -0,0 +1,86 @@ +using Microsoft.Agents.Storage; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace DispatcherAgent.KernelSupport +{ + public class SKHistoryToConversationStore + { + public required ChatHistory ChatHistory { get; set; } + + public static async Task<(ChatHistory chatHistory, IDictionary? _storageItem)> GetOrCreateChatHistoryForConversation(IStorage storage, string StorageKey, CancellationToken cancellationToken) + { + ChatHistory _history; + var skStorageItem = await storage.ReadAsync([StorageKey], cancellationToken); + if (skStorageItem == null) + { + // reinitialize ChatHistory object. + _history = new(); + } + else + { + if (skStorageItem.Values != null && skStorageItem.Values.Count > 0) + { + SKHistoryToConversationStore sKHistoryToConversationStore = skStorageItem.Values.First(); + _history = sKHistoryToConversationStore.ChatHistory; + } + else + { + _history = new ChatHistory(); + // Initialize ChatHistory object + /* +ChatBot can have a conversation with you about Microsoft, Weather and Product ordering and returns related topics only. +It should not expand it any other topics aside from Microsoft, Weather and Product ordering and returns. + + */ + _history.AddSystemMessage(@" +ChatBot can have a conversation with you about Microsoft and Weather and Product ordering and returns related topics only. +It should not expand it any other topics aside from Microsoft and Weather and and Product ordering and returns. +It should not disclose that it can handle Weather and and Product ordering and returns. + +What is the current weather in Seattle {{WeatherPlugin.handle_weatherrequest}} +What is the current weather in Dallas {{WeatherPlugin.handle_weatherrequest}} +What is the Forecast for tomorrow in Seattle {{WeatherPlugin.handle_weatherrequest}} +What is the Forecast for tomorrow in Dallas {{WeatherPlugin.handle_weatherrequest}} +What is the Forecast for a city {{WeatherPlugin.handle_weatherrequest}} +Get Forecast For Today {{WeatherPlugin.handle_weatherrequest}} +Get Forecast For Tomorrow {{WeatherPlugin.handle_weatherrequest}} +Get Current Weather {{WeatherPlugin.handle_weatherrequest}} +Current weather for a city {{WeatherPlugin.handle_weatherrequest}}. +Forecast weather for a city {{WeatherPlugin.handle_weatherrequest}}. +Forecast for tomorrow {{WeatherPlugin.handle_weatherrequest}}. + +Can you give me product Information on {{CustomerServicePlugin.handle_cas}} +I would like to return the product I ordered {{CustomerServicePlugin.handle_cas}} +I would like to return an Order {{CustomerServicePlugin.handle_cas}} +Return an order {{CustomerServicePlugin.handle_cas}} +Return order {{CustomerServicePlugin.handle_cas}} +I want to return a product I ordered {{CustomerServicePlugin.handle_cas}} +I want to return a order for a Controller {{CustomerServicePlugin.handle_cas}} +I want to return a order for a Xbox {{CustomerServicePlugin.handle_cas}} +I want to return a order for a Bike {{CustomerServicePlugin.handle_cas}} +Order Number {{CustomerServicePlugin.handle_cas}} +Order Numbers {{CustomerServicePlugin.handle_cas}} +ORD-12345 {{CustomerServicePlugin.handle_cas}} +ORD-12346 {{CustomerServicePlugin.handle_cas}} +ORD-98231 {{CustomerServicePlugin.handle_cas}} +Can you help me with my product {{CustomerServicePlugin.handle_cas}} +return a product {{CustomerServicePlugin.handle_cas}} +I have a problem with my product {{CustomerServicePlugin.handle_cas}} +delivery timeframes {{CustomerServicePlugin.handle_cas}} +What is the shipping policy {{CustomerServicePlugin.handle_cas}} +What is the your policy on shipping {{CustomerServicePlugin.handle_cas}} +shipping information {{CustomerServicePlugin.handle_cas}} +can you give me shipping information {{CustomerServicePlugin.handle_cas}} +shipping information {{CustomerServicePlugin.handle_cas}} + +It can give explicit instructions or say 'I don't know' if it does not have an answer."); + skStorageItem.Add(StorageKey, new SKHistoryToConversationStore + { + ChatHistory = _history + }); + } + } + return (_history, skStorageItem); + } + } +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/Model/MCSAgent.cs b/samples/complex/dispatcher/dotnet/Dispatcher/Model/MCSAgent.cs new file mode 100644 index 0000000..20135bf --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/Model/MCSAgent.cs @@ -0,0 +1,22 @@ +using Microsoft.Agents.CopilotStudio.Client; +using System.Text.Json.Serialization; + +namespace DispatcherAgent.Model +{ + public class MCSAgent + { + public required string Alias { get; set; } + + public required string DisplayName { get; set; } + + [JsonPropertyName("ConnectionSettings")] + public required IConfigurationSection ConnectionSettings { get; set; } + + public ConnectionSettings Settings { get; set; } + + public MCSAgent(IConfigurationSection connectionSettings) + { + Settings = new ConnectionSettings(connectionSettings); + } + } +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/Model/MCSAgents.cs b/samples/complex/dispatcher/dotnet/Dispatcher/Model/MCSAgents.cs new file mode 100644 index 0000000..66f3e9d --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/Model/MCSAgents.cs @@ -0,0 +1,318 @@ +using Microsoft.Agents.Authentication; +using Microsoft.Agents.Connector.Types; +using Microsoft.Agents.CopilotStudio.Client; +using Microsoft.Agents.Core.Interfaces; +using Microsoft.Agents.Core.Models; +using Microsoft.Agents.Storage; +using DispatcherAgent.Interfaces; +using SysDiag = System.Diagnostics; +using DispatcherAgent.Utils; + +namespace DispatcherAgent.Model +{ + public class MCSAgents : IMCSAgents + { + public Dictionary Agents { get; set; } + private readonly string _oboExchangeConnectionName; + private readonly IServiceProvider _serviceProvider; + private readonly string _absOboExchangeConnectionName; + + public MCSAgents(IServiceProvider services, IConfiguration configuration, string oboExchangeConnectionName, string absOboExchangeConnectionName, string mcsConnectionsKey = "MCSAgents") + { + ArgumentNullException.ThrowIfNullOrEmpty(mcsConnectionsKey); + Agents = configuration.GetSection(mcsConnectionsKey).Get>() ?? []; + _serviceProvider = services; + _oboExchangeConnectionName = oboExchangeConnectionName; + _absOboExchangeConnectionName = absOboExchangeConnectionName; + } + + // + public ConnectionSettings? GetSettingsByAlias(string? alias) + { + if (string.IsNullOrEmpty(alias)) + return null; + + var found = Agents.Values.Where(agent => agent.Alias.Equals(alias, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); + if (found != null) + { + return found.Settings; + } + else + { + return null; + } + } + + // + public string? GetDisplayNameByAlias(string? alias) + { + if (string.IsNullOrEmpty(alias)) + return null; + + var found = Agents.Values.Where(agent => agent.Alias.Equals(alias, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); + if (found != null) + { + return found.DisplayName; + } + else + { + return alias; + } + } + + // + public string? GetAliasFromText(string? text) + { + if (string.IsNullOrEmpty(text)) + return null; + + if (text.StartsWith('@')) + { + int spaceIndex = text.IndexOf(' ', 1); + if (spaceIndex > 1) + { + return text.Substring(1, spaceIndex - 1); + } + else + { + return text.Substring(1); + } + } + return null; + } + + public bool IsMCSAgent(string? alias) + { + if (string.IsNullOrEmpty(alias)) + return false; + if (GetSettingsByAlias(alias) != null) + return true; + else + return false; + } + + + /// + /// Dispatches request to Copilot Studio Hosted Agent and Manages the Conversation cycle. + /// + /// + /// + /// + /// + public async Task DispatchToAgent(ITurnContext turnContext, CancellationToken cancellationToken) + { + PerfTelemetryStore.AddTelemetry("DispatchToAgent", new PerfTelemetry { ScenarioName = "Start", Duration = TimeSpan.Zero }); + SysDiag.Stopwatch sw = new SysDiag.Stopwatch(); + SysDiag.Stopwatch daRun = new SysDiag.Stopwatch(); + SysDiag.Stopwatch lgnsw = new SysDiag.Stopwatch(); + try + { + if (turnContext == null) + throw new ArgumentNullException(nameof(turnContext)); + + if (_serviceProvider == null) + throw new ArgumentNullException(nameof(ServiceProvider)); + + var logger = _serviceProvider.GetService>(); + var storage = _serviceProvider.GetService(); + var connections = _serviceProvider.GetService(); + var httpClientFactory = _serviceProvider.GetService(); + + if (storage == null) + { + throw new ArgumentNullException(nameof(storage)); + } + if (connections == null) + { + throw new ArgumentNullException(nameof(connections)); + } + if (httpClientFactory == null) + { + throw new ArgumentNullException(nameof(httpClientFactory)); + } + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + var alias = GetAliasFromText(turnContext.Activity.Text); + var mcsConnSettings = GetSettingsByAlias(alias); + if (mcsConnSettings == null) + { + await turnContext.SendActivityAsync(MessageFactory.Text("No Copilot Studio Agent found for this alias.")); + return false; + } + + string conversationStorageLinkKey = Utilities.GetConversationLinkStorageKey(turnContext, alias); + + //System.Diagnostics.Trace.WriteLine($"Created Conversation Link Key: {conversationStorageLinkKey}"); + + var storageItem = await storage.ReadAsync([conversationStorageLinkKey], cancellationToken); + + turnContext.Activity.Text = turnContext.Activity.Text.Replace($"@{alias}", "").Trim(); + await turnContext.SendActivityAsync(new Activity { Type = ActivityTypes.Typing }, cancellationToken); + + var ScopeForAuth = CopilotClient.ScopeFromSettings(mcsConnSettings); + if (ScopeForAuth == null) + { + await turnContext.SendActivityAsync(MessageFactory.Text("No Scope found for this Copilot Studio Agent.")); + return false; + } + lgnsw.Restart(); + PerfTelemetryStore.AddTelemetry("DispatchToAgent-OBO", new PerfTelemetry { ScenarioName = "Before Get Token", Duration = TimeSpan.Zero }); + var auth = await Utilities.LoginFlowHandler( + turnContext, + storage, + connections.GetConnection(_oboExchangeConnectionName), + _absOboExchangeConnectionName, + ScopeForAuth.ToString(), + $"{_oboExchangeConnectionName}-{_absOboExchangeConnectionName}", + cancellationToken); + PerfTelemetryStore.AddTelemetry("DispatchToAgent-OBO", new PerfTelemetry { ScenarioName = "After Get Token", Duration = lgnsw.Elapsed }); + lgnsw.Stop(); + + if (auth != null) + { + lgnsw.Restart(); + CopilotClient cpsClient = new CopilotClient( + mcsConnSettings, + httpClientFactory: httpClientFactory, + tokenProviderFunction: async (s) => + { + return auth.AccessToken; + }, + httpClientName: string.Empty, + logger: logger); + + PerfTelemetryStore.AddTelemetry("DispatchToAgent-CreateClient", new PerfTelemetry { ScenarioName = "After Create CPS Client", Duration = lgnsw.Elapsed }); + bool IsCompleted = false; + await turnContext.SendActivityAsync(new Activity { Type = ActivityTypes.Typing }, cancellationToken); + PerfTelemetryStore.AddTelemetry("DispatchToAgent-CreateClient", new PerfTelemetry { ScenarioName = "Issue Typing", Duration = lgnsw.Elapsed }); + lgnsw.Restart(); + string mcsLocalConversationId = await GetOrCreateLinkedConversationId(cpsClient, storage, storageItem, conversationStorageLinkKey, turnContext, cancellationToken); + System.Diagnostics.Trace.WriteLine($"Got Copilot Studio Conversation ID: {mcsLocalConversationId}"); + PerfTelemetryStore.AddTelemetry("DispatchToAgent", new PerfTelemetry { ScenarioName = "GetOrCreateLinkedConversationId", Duration = lgnsw.Elapsed }); + lgnsw.Restart(); + + int iLoopCount = 0; + while (!IsCompleted) + { + try + { + sw.Restart(); + await foreach (Activity act in cpsClient.AskQuestionAsync(turnContext.Activity, cancellationToken)) + { + iLoopCount++; + + if (act.Type == ActivityTypes.Message && !string.IsNullOrEmpty(act.Text)) + { + // Alias to name + var displayName = string.IsNullOrEmpty(GetDisplayNameByAlias(alias)) ? alias : GetDisplayNameByAlias(alias); + act.Text = $"**<\\\\\\\\> {displayName} >>**{Environment.NewLine}{act.Text}"; + } + //if (act.Type == ActivityTypes.Event ) + //{ + // // await turnContext.SendActivityAsync(new Activity { Type = ActivityTypes.Typing }, cancellationToken); + //} + //act.ReplyToId = null; + if (act.Type == ActivityTypes.Message) + { + act.ChannelData = null; + await turnContext.SendActivityAsync(act, cancellationToken); + } + else if (act.Type != ActivityTypes.Event) + await turnContext.SendActivityAsync(act, cancellationToken); + + PerfTelemetryStore.AddTelemetry("DispatchToAgent", new PerfTelemetry { ScenarioName = $"CallLoop Turn {iLoopCount} - {act.Type}", Duration = sw.Elapsed }); + sw.Restart(); + } + IsCompleted = true; + } + catch (ErrorResponseException error) + { + await turnContext.SendActivityAsync(MessageFactory.Text($"Error: {error.Message}{Environment.NewLine}{error.Body}")); + PerfTelemetryStore.AddTelemetry("DispatchToAgent", new PerfTelemetry { ScenarioName = $"Fault In {iLoopCount}", Duration = sw.Elapsed }); + } + catch (Exception ex) + { + await turnContext.SendActivityAsync(MessageFactory.Text($"Error: {ex.Message}")); + PerfTelemetryStore.AddTelemetry("DispatchToAgent", new PerfTelemetry { ScenarioName = $"Fault In {iLoopCount}", Duration = sw.Elapsed }); + } + finally + { + sw.Stop(); + IsCompleted = true; + } + } + PerfTelemetryStore.AddTelemetry("DispatchToAgent", new PerfTelemetry { ScenarioName = $"Completed Message Loop of CPS Client {iLoopCount}", Duration = lgnsw.Elapsed }); + lgnsw.Stop(); + return true; + } + return true; + } + finally + { + lgnsw.Stop(); + sw.Stop(); + PerfTelemetryStore.AddTelemetry("DispatchToAgent", new PerfTelemetry { ScenarioName = $"Completed of Function run", Duration = daRun.Elapsed }); + PerfTelemetryStore.WriteTelemetry(); + } + } + + private static async Task GetOrCreateLinkedConversationId(CopilotClient cpsClient, IStorage storage, IDictionary? storageItem, string conversationStorageLinkKey, ITurnContext turnContext, CancellationToken cancellationToken) + { + string mcsLocalConversationId = string.Empty; + if (storageItem != null) + { + foreach (var item in storageItem) + { + if (item.Value.SourceConversationId.Equals(turnContext.Activity.Conversation.Id)) + { + mcsLocalConversationId = item.Value.DestinationConversationId; + } + } + } + + if (string.IsNullOrEmpty(mcsLocalConversationId)) + { + await foreach (Activity act in cpsClient.StartConversationAsync(emitStartConversationEvent: false, cancellationToken)) + { + // throw away initial. + if (act.Conversation != null) + { + mcsLocalConversationId = act.Conversation.Id; + if (storageItem == null) + { + Dictionary newStore = new() + { + { + conversationStorageLinkKey, + new MultiBotConversationStore() + { + SourceConversationId = turnContext.Activity.Conversation.Id, + DestinationConversationId = mcsLocalConversationId + } + } + }; + } + else + { + storageItem[conversationStorageLinkKey] = + new MultiBotConversationStore() + { + SourceConversationId = turnContext.Activity.Conversation.Id, + DestinationConversationId = mcsLocalConversationId + }; + } + await storage.WriteAsync(storageItem, cancellationToken); + } + } + } + Utilities.WriteCPSLinks($"CONVO ID: {mcsLocalConversationId}"); + return mcsLocalConversationId; + + } + + + } +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/Model/MultiBotConversationStore.cs b/samples/complex/dispatcher/dotnet/Dispatcher/Model/MultiBotConversationStore.cs new file mode 100644 index 0000000..02dc7f2 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/Model/MultiBotConversationStore.cs @@ -0,0 +1,10 @@ +namespace DispatcherAgent.Model +{ + public class MultiBotConversationStore + { + public required string SourceConversationId { get; set; } + public required string DestinationConversationId { get; set; } + } + + +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/Model/OAuthFlowState.cs b/samples/complex/dispatcher/dotnet/Dispatcher/Model/OAuthFlowState.cs new file mode 100644 index 0000000..ae4deee --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/Model/OAuthFlowState.cs @@ -0,0 +1,8 @@ +namespace DispatcherAgent.Model +{ + public class OAuthFlowState + { + public bool FlowStarted = false; + public DateTime FlowExpires = DateTime.MinValue; + } +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/Model/PerfTelemtryStore.cs b/samples/complex/dispatcher/dotnet/Dispatcher/Model/PerfTelemtryStore.cs new file mode 100644 index 0000000..f91e7d7 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/Model/PerfTelemtryStore.cs @@ -0,0 +1,42 @@ +using DispatcherAgent.Utils; +using System.Diagnostics; + +namespace DispatcherAgent.Model +{ + public class PerfTelemetryStore + { + private static Dictionary> _telemetry = new(); + public static void AddTelemetry(string areaName, PerfTelemetry telemetry) + { + if (!_telemetry.ContainsKey(areaName)) + _telemetry.Add(areaName, new List()); + + _telemetry[areaName].Add(telemetry); + } + + public static void WriteTelemetry() + { + foreach (var item in _telemetry) + { + Console.WriteLine($"Area: {item.Key}"); + Trace.WriteLine($"Area: {item.Key}"); + foreach (var telemetry in item.Value) + { + Console.WriteLine($"\t{telemetry.ScenarioName} Duration: {telemetry.Duration.ToDurationString()}"); + Trace.WriteLine($"\t{telemetry.ScenarioName} Duration: {telemetry.Duration.ToDurationString()}"); + } + } + CleanUp(); + } + public static void CleanUp() + { + _telemetry.Clear(); + } + } + + public class PerfTelemetry + { + public required string ScenarioName { get; set; } + public TimeSpan Duration { get; set; } + } +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/Program.cs b/samples/complex/dispatcher/dotnet/Dispatcher/Program.cs new file mode 100644 index 0000000..bd9cab7 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/Program.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Authentication; +using Microsoft.Agents.Authentication.Msal; +using Microsoft.Agents.BotBuilder; +using Microsoft.Agents.Hosting.AspNetCore; +using Microsoft.Agents.Storage; +using Microsoft.SemanticKernel; +using DispatcherAgent; +using DispatcherAgent.Interfaces; +using DispatcherAgent.Model; + + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddHttpClient(); + +// Add AspNet Authentication suitable for token validation for a Bot Service bot (ABS- or SMBA) +builder.Services.AddBotAspNetAuthentication(builder.Configuration); +builder.Services.AddAzureOpenAIChatCompletion("gpt-4o", builder.Configuration["AOAI_ENDPOINT"]!.ToString(), builder.Configuration["AOAI_APIKEY"]!.ToString()); + +BotSetup(builder); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapGet("/", () => "Microsoft Copilot SDK Sample"); + app.UseDeveloperExceptionPage(); + app.MapControllers().AllowAnonymous(); +} +else +{ + app.MapControllers(); +} + +app.Run(); + +static void BotSetup(IHostApplicationBuilder builder) +{ + ArgumentNullException.ThrowIfNull(builder); + + builder.Services.AddSingleton(o => new MCSAgents(o, builder.Configuration, "McsOBOConnection", "MCS01")); + + // Add default bot MsalAuth support + builder.Services.AddDefaultMsalAuth(builder.Configuration); + + // Add Connections object to access configured token connections. + builder.Services.AddSingleton(); + + // Add factory for ConnectorClient and UserTokenClient creation + builder.Services.AddSingleton(); + + // Add IStorage for turn state persistence + builder.Services.AddSingleton(); + + // Add the BotAdapter + builder.Services.AddCloudAdapter(); + + // Add the Bot + builder.Services.AddTransient(); +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/SampleServiceCollectionExtensions.cs b/samples/complex/dispatcher/dotnet/Dispatcher/SampleServiceCollectionExtensions.cs new file mode 100644 index 0000000..beefd51 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/SampleServiceCollectionExtensions.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Agents.Authentication; +using Microsoft.IdentityModel.Tokens; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; + + +namespace DispatcherAgent +{ + public static class SampleServiceCollectionExtensions + { + /// + /// Adds default token validation typical for ABS/SMBA. If config settings are not supplied, this will + /// default to Azure Public Cloud. + /// + /// + /// + /// + /// + /// Example config: + /// { + /// "TokenValidation": { + /// "ValidIssuers": [ + /// "{default:Public-Azure}" + /// ], + /// "AllowedCallers": [ + /// "{default:*}" + /// ] + /// } + /// } + /// + public static void AddBotAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string botConnectionConfig = "BotServiceConnection") + { + var tokenValidationSection = configuration.GetSection("TokenValidation"); + + var validTokenIssuers = tokenValidationSection.GetSection("ValidIssuers").Get>(); + + // If ValidIssuers is empty, default for ABS Public Cloud + if (validTokenIssuers == null || validTokenIssuers.Count == 0) + { + validTokenIssuers = + [ + "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", + ]; + + var tenantId = configuration[$"{botConnectionConfig}:TenantId"]; + if (!string.IsNullOrEmpty(tenantId)) + { + validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, tenantId)); + validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, tenantId)); + } + } + + //services.AddAuthentication().AddMicrosoftIdentityWebApi(configuration, "SiteIdentity") + 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 = validTokenIssuers, + ValidAudience = configuration[$"Connections:{botConnectionConfig}:Settings:ClientId"], + RequireSignedTokens = true, + SignatureValidator = (token, parameters) => new JwtSecurityToken(token), + }; + + // The following lines Azure AD signing key issuer validation. + //options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); + //options.Events = new JwtBearerEvents + //{ + // OnMessageReceived = async context => + // { + // context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + // await Task.CompletedTask.ConfigureAwait(false); + // } + //}; + }) + //.AddMicrosoftIdentityWebApi(configuration, "SiteIdentity", "IgnoreMe") + //.EnableTokenAcquisitionToCallDownstreamApi() + ////.AddDownstreamApi("graph", configuration.GetSection("graphDownStream")) + //.AddInMemoryTokenCaches() + ; + + ; + } + } +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/Utils/TimeSpanExtensions.cs b/samples/complex/dispatcher/dotnet/Dispatcher/Utils/TimeSpanExtensions.cs new file mode 100644 index 0000000..8bb27c5 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/Utils/TimeSpanExtensions.cs @@ -0,0 +1,15 @@ +namespace DispatcherAgent.Utils +{ + internal static class TimeSpanExtensions + { + /// + /// Returns a duration in the format hh:mm:ss:fff + /// + /// + /// + internal static string ToDurationString(this TimeSpan timspan) + { + return timspan.ToString(@"hh\:mm\:ss\.fff"); + } + } +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/Utils/Utilities.cs b/samples/complex/dispatcher/dotnet/Dispatcher/Utils/Utilities.cs new file mode 100644 index 0000000..0d3e8c9 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/Utils/Utilities.cs @@ -0,0 +1,182 @@ +using Microsoft.Agents.Authentication.Msal; +using Microsoft.Agents.Authentication; +using Microsoft.Identity.Client; +using System.Reflection; +using DispatcherAgent.Model; +using System.Collections.Concurrent; +using System.Diagnostics; +using Microsoft.Agents.BotBuilder; +using Microsoft.Agents.Core.Interfaces; +using Microsoft.Agents.Core.Models; +using Microsoft.Agents.Storage; + +namespace DispatcherAgent.Utils +{ + public static class Utilities + { + private static ConcurrentDictionary _clientApps = new ConcurrentDictionary(); + + public static string GetFlowStateStorageKey(ITurnContext turnContext) + { + var channelId = turnContext.Activity.ChannelId ?? throw new InvalidOperationException("invalid activity-missing channelId"); + var conversationId = turnContext.Activity.Conversation?.Id ?? throw new InvalidOperationException("invalid activity-missing Conversation.Id"); + return $"{channelId}/conversations/{conversationId}/flowState".ToLower(); + } + + public static string GetConversationLinkStorageKey(ITurnContext turnContext, string? remoteName) + { + if (string.IsNullOrEmpty(remoteName)) throw new ArgumentNullException(nameof(remoteName)); + var channelId = turnContext.Activity.ChannelId ?? throw new InvalidOperationException("invalid activity-missing channelId"); + var conversationId = turnContext.Activity.Conversation?.Id ?? throw new InvalidOperationException("invalid activity-missing Conversation.Id"); + var conversationLink = $"{channelId}/conversations/{conversationId}/{remoteName}/conversationLink".ToLower(); + WriteMemoryLinks(conversationLink); + return conversationLink; + } + + public static async Task LoginFlowHandler(ITurnContext turnContext, IStorage storage, IAccessTokenProvider? connection, string absOAuthConnectionName, string Url, string connectionKey, CancellationToken cancellationToken) + { + PerfTelemetryStore.AddTelemetry("LoginFlowHandler", new PerfTelemetry { ScenarioName = "Start", Duration = TimeSpan.Zero }); + Stopwatch loginFlowSW = Stopwatch.StartNew(); + try + { + OAuthFlow _flow = new OAuthFlow("Sign In", "Custom SignIn Message", absOAuthConnectionName, 30000, true); + + if (string.Equals("logout", turnContext.Activity.Text, StringComparison.OrdinalIgnoreCase)) + { + await _flow.SignOutUserAsync(turnContext, cancellationToken); + await turnContext.SendActivityAsync(MessageFactory.Text("You have been signed out."), cancellationToken); + return null; + } + + + TokenResponse? tokenResponse = null; + + // Read flow state for this conversation + var stateKey = GetFlowStateStorageKey(turnContext); + var items = await storage.ReadAsync([stateKey], cancellationToken); + OAuthFlowState state = items.TryGetValue(stateKey, out var value) ? (OAuthFlowState)value : new OAuthFlowState(); + + if (!state.FlowStarted) + { + tokenResponse = await _flow.BeginFlowAsync(turnContext, Microsoft.Agents.Core.Models.Activity.CreateMessageActivity(), cancellationToken); + PerfTelemetryStore.AddTelemetry("LoginFlowHandler", new PerfTelemetry { ScenarioName = "GetUserAssertionToken From ABS", Duration = loginFlowSW.Elapsed }); + // If a TokenResponse is returned, there was a cached token already. Otherwise, start the process of getting a new token. + if (tokenResponse == null) + { + var expires = DateTime.UtcNow.AddMilliseconds(_flow.Timeout ?? TimeSpan.FromMinutes(15).TotalMilliseconds); + + state.FlowStarted = true; + state.FlowExpires = expires; + } + + } + else + { + try + { + tokenResponse = await _flow.ContinueFlowAsync(turnContext, state.FlowExpires, cancellationToken); + PerfTelemetryStore.AddTelemetry("LoginFlowHandler", new PerfTelemetry { ScenarioName = "ContunueUserAssertionToken From ABS", Duration = loginFlowSW.Elapsed }); + if (tokenResponse != null) + { + await turnContext.SendActivityAsync(MessageFactory.Text("You are now logged in."), cancellationToken); + } + else + { + await turnContext.SendActivityAsync(MessageFactory.Text("Login was not successful please try again."), cancellationToken); + } + } + catch (TimeoutException) + { + await turnContext.SendActivityAsync(MessageFactory.Text("You did not respond in time. Please try again."), cancellationToken); + } + + state.FlowStarted = false; + } + + // Store flow state + items[stateKey] = state; + await storage.WriteAsync(items, cancellationToken); + + + if (tokenResponse != null) + { + if (connection != null && connection is MsalAuth authLib) + { + IConfidentialClientApplication? clientApp = null; + if (_clientApps.ContainsKey(connectionKey)) + { + _clientApps.TryRemove(connectionKey, out clientApp); + } + + if (clientApp == null) + { + var method = authLib.GetType().GetMethod("CreateClientApplication", BindingFlags.NonPublic | BindingFlags.Instance); + var holderApp = method?.Invoke(authLib, null); + if (holderApp != null && holderApp is IConfidentialClientApplication confAppNew) + { + clientApp = confAppNew; + _clientApps.TryAdd(connectionKey, confAppNew); + } + } + // invoke the private CreateClientApplication method on authLib + if (clientApp != null && clientApp is IConfidentialClientApplication confApp) + { + var authenticationResult = await confApp.AcquireTokenOnBehalfOf(new string[] { Url }, new UserAssertion(tokenResponse.Token)).ExecuteAsync(); + PerfTelemetryStore.AddTelemetry("LoginFlowHandler", new PerfTelemetry { ScenarioName = "AcquireUserOBOToken", Duration = loginFlowSW.Elapsed }); + return authenticationResult; + } + } + + /* + + var userTokenClient = turnContext.TurnState.Get(); + var token = await userTokenClient.ExchangeTokenAsync( + turnContext.Activity.From.Id, + turnContext.Activity.ChannelId, + config["ConnectionName"], + new TokenExchangeRequest(token:tokenResponse.Token), cancellationToken); + + await turnContext.SendActivityAsync(MessageFactory.Text($"Here is your token {token.Token}"), cancellationToken); + + */ + } + return null; + } + finally + { + loginFlowSW.Stop(); + PerfTelemetryStore.AddTelemetry("LoginFlowHandler", new PerfTelemetry { ScenarioName = "Complete", Duration = loginFlowSW.Elapsed }); + PerfTelemetryStore.WriteTelemetry(); + } + } + + public static void WriteConverationLinks(string text) + { + Console.BackgroundColor = ConsoleColor.Gray; + Console.ForegroundColor = ConsoleColor.Black; + Console.WriteLine(text); + Console.ResetColor(); + } + + public static void WriteMemoryLinks(string text) + { + Console.BackgroundColor = ConsoleColor.Black; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("(STOREACC)"); + Console.ResetColor(); + Console.Write($"{text}\n"); + Console.ResetColor(); + } + + public static void WriteCPSLinks(string text) + { + Console.BackgroundColor = ConsoleColor.Black; + Console.ForegroundColor = ConsoleColor.Green; + Console.Write("(MCS)"); + Console.ResetColor(); + Console.Write($"{text}\n"); + Console.ResetColor(); + } + + } +} diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/appsettings.json b/samples/complex/dispatcher/dotnet/Dispatcher/appsettings.json new file mode 100644 index 0000000..ac50d55 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/appsettings.json @@ -0,0 +1,75 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Error", + "Microsoft.AspNetCore": "Warning" + } + } + /* + "ConnectionName": "MCS01", + + "Connections": { + "BotServiceConnection": { + "Assembly": "Microsoft.Agents.Authentication.Msal", + "Type": "MsalAuth", + "Settings": { + "AuthType": "ClientSecret", + "ClientSecret": "", + "AuthorityEndpoint": "https://login.microsoftonline.com/", + "ClientId": "", + "Scopes": [ + "https://api.botframework.com/.default" + ], + "TenantId": "" + } + }, + + "McsOBOConnection": { + "Assembly": "Microsoft.Agents.Authentication.Msal", + "Type": "MsalAuth", + "Settings": { + "AuthType": "ClientSecret", + "ClientSecret": "", + "AuthorityEndpoint": "https://login.microsoftonline.com/", + "ClientId": "", + "Scopes": [ + "https://api.botframework.com/.default" + ], + "TenantId": "" + } + } + }, + + + "MCSAgents": { + "WeatherBot": { + "Alias": "WB", + "DisplayName": "CS.Weather Agent", + "ConnectionSettings": { + "EnvironmentId": "", + "BotIdentifier": "", + "Cloud": "Prod" + } + }, + "ContosoAssistant": { + "Alias": "CAS", + "DisplayName": "CS.Contoso Assistant", + "ConnectionSettings": { + "EnvironmentId": "", + "BotIdentifier": "", + "Cloud": "Prod" + } + }, + }, + "ConnectionsMap": [ + { + "ServiceUrl": "*", + "Connection": "BotServiceConnection" + } + ], + "AllowedHosts": "*", + "AOAI_ENDPOINT": "", + "AOAI_APIKEY": "" +*/ +} + diff --git a/samples/complex/dispatcher/dotnet/DispatcherAgent.sln b/samples/complex/dispatcher/dotnet/DispatcherAgent.sln new file mode 100644 index 0000000..d0075b6 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/DispatcherAgent.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35527.113 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DispatcherAgent", "Dispatcher\DispatcherAgent.csproj", "{5F2005BE-C29F-4B97-99CF-1F50B674B9ED}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5F2005BE-C29F-4B97-99CF-1F50B674B9ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F2005BE-C29F-4B97-99CF-1F50B674B9ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F2005BE-C29F-4B97-99CF-1F50B674B9ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F2005BE-C29F-4B97-99CF-1F50B674B9ED}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/samples/complex/dispatcher/dotnet/version.json b/samples/complex/dispatcher/dotnet/version.json new file mode 100644 index 0000000..a911b00 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/version.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", + "version": "1.0-beta", + "publicReleaseRefSpec": [ + "^refs/heads/master$", + "^refs/heads/v\\d+(?:\\.\\d+)?$" + ], + "cloudBuild": { + "buildNumber": { + "enabled": true + } + } +} \ No newline at end of file diff --git a/samples/nuget.config b/samples/nuget.config new file mode 100644 index 0000000..3aa5b1e --- /dev/null +++ b/samples/nuget.config @@ -0,0 +1,8 @@ + + + + + + + + From 6d3fae2fc9c18559b36216527e68b4edff3a90ab Mon Sep 17 00:00:00 2001 From: MattB Date: Sat, 15 Feb 2025 16:37:09 -0800 Subject: [PATCH 2/4] Updating Readme for initial configuration. --- .../dispatcher/dotnet/Dispatcher/README.md | 57 +++++++++++++++++++ .../{Model => Utils}/PerfTelemtryStore.cs | 7 +-- 2 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 samples/complex/dispatcher/dotnet/Dispatcher/README.md rename samples/complex/dispatcher/dotnet/Dispatcher/{Model => Utils}/PerfTelemtryStore.cs (87%) diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/README.md b/samples/complex/dispatcher/dotnet/Dispatcher/README.md new file mode 100644 index 0000000..a34c8c7 --- /dev/null +++ b/samples/complex/dispatcher/dotnet/Dispatcher/README.md @@ -0,0 +1,57 @@ +# Dispatcher Demo Agent Setup and configuration +This is a demonstration of a dispatcher or Reasoning agent utilizing the Microsoft 365 Agent SDK,Azure Bot Service, Azure Semantic Kernel, Azure OpenAI and Microsoft Copilot Studio. + +> [!IMPORTANT] +> **This demonstrator is not a "simple sample"**, it is a working bot that is setup to support a semi complex configuration. While this readme is longer then most, **it is VERY IMPORTANT to fully read it and understand the steps to setup the supporting components before attempting to use this demonstration code**. Failure to do so will result in your code not working as expected. + + +This agent is intended as a technical demonstration of an how to create an agent that utilizes Semantic Kernel backed by Azure OpenAI to act as both an information source and a sub agent orchestrator, orchestrating Azure OpenAI and several Microsoft Copilot Studio Agents. + +This agent is known to work with the following clients at the time of this writing: + +- WebChat control hosted in a custom website. +- WebChat Test control hosted in Azure Bot Services +- Microsoft Teams client + +It is likely other clients will work properly with it, as long as OAuth is setup and configured for that client. + +## Prerequisite + +**To run the demonstration on a development workstation (local development), the following tools and SDK's are required:** + +- [.NET SDK](https://dotnet.microsoft.com/download) version 8.0 +- Visual Studio 2022+ with the .net workload installed. +- Access to an Azure Subscription with access to preform the following tasks: + - Create and configure Entra ID Application Identities for use with both user access tokens and application identities. + - Create and configure an [Azure Bot Service](https://aka.ms/AgentsSDK-CreateBot) for your bot + - Create and configure an [Azure App Service](https://learn.microsoft.com/azure/app-service/) to deploy your bot on to. + - 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. + +**Deploying this as a container or app service on Azure is not covered in this document, however it is identical to deploying any other Agent SDK Agent.** + +## Instructions - Required Setup to use this library + +### Clone Sample and confirm builds locally + +### Identities + +- Azure Bot Service Bot identity +- OnBehalf of Identity for communicating to Microsoft Copilot Studio + +### Azure OpenAI + +- Setup a model and create a genreal use AI agent + +### Microsoft Copilot Studio (MCS) + +- Create MCS Agents and collect relevent informaiton. + +### Azure Bot Service + +- Create an Azure bot Service and configure it + +### Configure Dispatcher Agent settings + +### Setup Tunneling + +### Testing default behavior. diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/Model/PerfTelemtryStore.cs b/samples/complex/dispatcher/dotnet/Dispatcher/Utils/PerfTelemtryStore.cs similarity index 87% rename from samples/complex/dispatcher/dotnet/Dispatcher/Model/PerfTelemtryStore.cs rename to samples/complex/dispatcher/dotnet/Dispatcher/Utils/PerfTelemtryStore.cs index f91e7d7..dd21f66 100644 --- a/samples/complex/dispatcher/dotnet/Dispatcher/Model/PerfTelemtryStore.cs +++ b/samples/complex/dispatcher/dotnet/Dispatcher/Utils/PerfTelemtryStore.cs @@ -1,11 +1,10 @@ -using DispatcherAgent.Utils; -using System.Diagnostics; +using System.Diagnostics; -namespace DispatcherAgent.Model +namespace DispatcherAgent.Utils { public class PerfTelemetryStore { - private static Dictionary> _telemetry = new(); + private static Dictionary> _telemetry = new(); public static void AddTelemetry(string areaName, PerfTelemetry telemetry) { if (!_telemetry.ContainsKey(areaName)) From 6ac65d1db5d05b40d613684a49e131311aba8273 Mon Sep 17 00:00:00 2001 From: MattB Date: Tue, 18 Feb 2025 10:50:55 -0800 Subject: [PATCH 3/4] Enhance README with setup instructions and clarify Azure Bot Service identities --- .../dispatcher/dotnet/Dispatcher/Program.cs | 1 + .../dispatcher/dotnet/Dispatcher/README.md | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/Program.cs b/samples/complex/dispatcher/dotnet/Dispatcher/Program.cs index bd9cab7..be42d17 100644 --- a/samples/complex/dispatcher/dotnet/Dispatcher/Program.cs +++ b/samples/complex/dispatcher/dotnet/Dispatcher/Program.cs @@ -21,6 +21,7 @@ builder.Services.AddBotAspNetAuthentication(builder.Configuration); builder.Services.AddAzureOpenAIChatCompletion("gpt-4o", builder.Configuration["AOAI_ENDPOINT"]!.ToString(), builder.Configuration["AOAI_APIKEY"]!.ToString()); + BotSetup(builder); var app = builder.Build(); diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/README.md b/samples/complex/dispatcher/dotnet/Dispatcher/README.md index a34c8c7..eb5673d 100644 --- a/samples/complex/dispatcher/dotnet/Dispatcher/README.md +++ b/samples/complex/dispatcher/dotnet/Dispatcher/README.md @@ -33,9 +33,26 @@ It is likely other clients will work properly with it, as long as OAuth is setup ### Clone Sample and confirm builds locally +To begin, clone this repository. all necessary assets are found in the /Samples folder. This project is setup to use paths and packages referenced in the various prop files in the root of the samples directory. + +- Once you have cloned the project open the visual studio solution using Visual Studio 2022+ in the directory samples/complex/dispatcher/dotnet. +- Build the project in Visual Studio. this will restore all missing dependencies and verify that the project is ready to be used. +- - if needed, resolve any missing dependencies or build complaints before proceeding. + ### Identities -- Azure Bot Service Bot identity +#### Azure Bot Service Bot identity + +This demonstrator is setup to use two identities for the Azure Bot Service(ABS) registration. + +- ABS to Agent identity. This identity is used to connect between ABS and your Agent. You have a few options here, however we will cover only 2 for the purposes of this demonstrator. +- - Local workstation runtime. + +To support running the Agent on your desktop, you will need to create a Application Identity in Azure with a Client secret or Client certificate. This identity should be in the same tenant that your going to configure the ABS Service in. + +- ABS OAuth Identity. This identity is used to create a token for a user. This user access token can then be accessed by the Agent SDK for use to exchange for a downstream service. + +- - OnBehalf of Identity for communicating to Microsoft Copilot Studio ### Azure OpenAI From fc6f859a28981bad551ee170c6a94155cb7bd021 Mon Sep 17 00:00:00 2001 From: MattB Date: Tue, 18 Feb 2025 19:56:30 -0800 Subject: [PATCH 4/4] Update README.md --- .../dispatcher/dotnet/Dispatcher/README.md | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/samples/complex/dispatcher/dotnet/Dispatcher/README.md b/samples/complex/dispatcher/dotnet/Dispatcher/README.md index eb5673d..efd56c8 100644 --- a/samples/complex/dispatcher/dotnet/Dispatcher/README.md +++ b/samples/complex/dispatcher/dotnet/Dispatcher/README.md @@ -41,16 +41,28 @@ To begin, clone this repository. all necessary assets are found in the /Samples ### Identities +This demonstrator is setup to use two identities Entra ID based identites and one API Key identity for Azure OpenAI. Thease instructions will guide you though the required configuration for the identities you need to setup. + #### Azure Bot Service Bot identity -This demonstrator is setup to use two identities for the Azure Bot Service(ABS) registration. +> [!Note] +> **To Run on your Local workstation**, To support running the Agent on your desktop, you will need to create a Application Identity in Azure with a Client secret or Client certificate. This identity should be in the same tenant that will configure the ABS Service in. +> -- ABS to Agent identity. This identity is used to connect between ABS and your Agent. You have a few options here, however we will cover only 2 for the purposes of this demonstrator. -- - Local workstation runtime. +> [!Note] +> **To Run on in Azure**, it is recommend that you use a managed identity and FIC to configure ABS related connections, however it is not required, For the purposes of this set of instructions we will not walk though this configuraiton +> -To support running the Agent on your desktop, you will need to create a Application Identity in Azure with a Client secret or Client certificate. This identity should be in the same tenant that your going to configure the ABS Service in. +- **ABS to Agent identity.** This identity is used to connect between ABS and your Agent. You have a few options here, however we will cover only 2 for the purposes of this demonstrator. + - Create an Azure App identity within the same tenant you will setup your ABS server in. + - This identity should be configured with Client Secret or Client Certificate identity. if you use Client Certificate, the Certiifcate must be registred on your local workstation. + - This app does not need a redirect URI + - Once created, Capture the following information: + - Client\ApplicationID + - TenantID + - Client Secret (if so configured) -- ABS OAuth Identity. This identity is used to create a token for a user. This user access token can then be accessed by the Agent SDK for use to exchange for a downstream service. +- ABS OAuth Identity. This identity is used as the broker identity to exchange a user token for a downstream service. This user access token can then be accessed by the Agent SDK for use to exchange for a downstream service. This identity will have the API scopes setup on it for accessing Copilot Studio - - OnBehalf of Identity for communicating to Microsoft Copilot Studio