diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandler.cs b/samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandler.cs new file mode 100644 index 00000000..f5d39f0d --- /dev/null +++ b/samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandler.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Net.Http.Headers; +using System.Runtime.InteropServices; +using Microsoft.Identity.Client.Extensions.Msal; +using Microsoft.Identity.Client; +using Microsoft.Agents.CopilotStudio.Client; + +namespace CopilotStudioClientSample +{ + /// + /// This sample uses an HttpClientHandler to add an authentication token to the request. + /// This is used for the interactive authentication flow. + /// For more information on how to setup various authentication flows, see the Microsoft Identity documentation at https://aka.ms/msal. + /// + /// Direct To engine connection settings. + internal class AddTokenHandler(SampleConnectionSettings settings) : DelegatingHandler(new HttpClientHandler()) + { + private static readonly string _keyChainServiceName = "copilot_studio_client_app"; + private static readonly string _keyChainAccountName = "copilot_studio_client"; + + private async Task AuthenticateAsync(CancellationToken ct = default!) + { + ArgumentNullException.ThrowIfNull(settings); + + // Gets the correct scope for connecting to Copilot Studio based on the settings provided. + string[] scopes = [CopilotClient.ScopeFromSettings(settings)]; + + // Setup a Public Client application for authentication. + IPublicClientApplication app = PublicClientApplicationBuilder.Create(settings.AppClientId) + .WithAuthority(AadAuthorityAudience.AzureAdMyOrg) + .WithTenantId(settings.TenantId) + .WithRedirectUri("http://localhost") + .Build(); + + string currentDir = Path.Combine(AppContext.BaseDirectory, "mcs_client_console"); + + if (!Directory.Exists(currentDir)) + { + Directory.CreateDirectory(currentDir); + } + + StorageCreationPropertiesBuilder storageProperties = new("TokenCache", currentDir); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + storageProperties.WithLinuxUnprotectedFile(); + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + storageProperties.WithMacKeyChain(_keyChainServiceName, _keyChainAccountName); + } + MsalCacheHelper tokenCacheHelper = await MsalCacheHelper.CreateAsync(storageProperties.Build()); + tokenCacheHelper.RegisterCache(app.UserTokenCache); + + IAccount? account = (await app.GetAccountsAsync()).FirstOrDefault(); + + AuthenticationResult authResponse; + try + { + authResponse = await app.AcquireTokenSilent(scopes, account).ExecuteAsync(ct); + //authResponse = await app.AcquireTokenInteractive(scopes).ExecuteAsync(ct); + } + catch (MsalUiRequiredException) + { + authResponse = await app.AcquireTokenInteractive(scopes).ExecuteAsync(ct); + } + return authResponse; + } + + /// + /// Handles sending the request and adding the token to the request. + /// + /// Request to be sent + /// + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.Headers.Authorization is null) + { + AuthenticationResult authResponse = await AuthenticateAsync(cancellationToken); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResponse.AccessToken); + } + return await base.SendAsync(request, cancellationToken); + } + } +} diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandlerS2S.cs b/samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandlerS2S.cs new file mode 100644 index 00000000..652ff085 --- /dev/null +++ b/samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandlerS2S.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.CopilotStudio.Client; +using System.Net.Http.Headers; +using System.Runtime.InteropServices; +using Microsoft.Identity.Client.Extensions.Msal; +using Microsoft.Identity.Client; + +namespace CopilotStudioClientSample +{ + /// + /// This sample uses an HttpClientHandler to add an authentication token to the request. + /// In this case its using client secret for the request. + /// + /// Direct To engine connection settings. + internal class AddTokenHandlerS2S(SampleConnectionSettings settings) : DelegatingHandler(new HttpClientHandler()) + { + private static readonly string _keyChainServiceName = "copilot_studio_client_app"; + private static readonly string _keyChainAccountName = "copilot_studio_client"; + + private IConfidentialClientApplication? _confidentialClientApplication; + private string[]? _scopes; + + private async Task AuthenticateAsync(CancellationToken ct = default!) + { + if (_confidentialClientApplication == null) + { + ArgumentNullException.ThrowIfNull(settings); + _scopes = [CopilotClient.ScopeFromSettings(settings)]; + _confidentialClientApplication = ConfidentialClientApplicationBuilder.Create(settings.AppClientId) + .WithAuthority(AzureCloudInstance.AzurePublic, settings.TenantId) + .WithClientSecret(settings.AppClientSecret) + .Build(); + + string currentDir = Path.Combine(AppContext.BaseDirectory, "mcs_client_console"); + + if (!Directory.Exists(currentDir)) + { + Directory.CreateDirectory(currentDir); + } + + StorageCreationPropertiesBuilder storageProperties = new("AppTokenCache", currentDir); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + storageProperties.WithLinuxUnprotectedFile(); + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + storageProperties.WithMacKeyChain(_keyChainServiceName, _keyChainAccountName); + } + MsalCacheHelper tokenCacheHelper = await MsalCacheHelper.CreateAsync(storageProperties.Build()); + tokenCacheHelper.RegisterCache(_confidentialClientApplication.AppTokenCache); + } + + AuthenticationResult authResponse; + authResponse = await _confidentialClientApplication.AcquireTokenForClient(_scopes).ExecuteAsync(ct); + return authResponse; + } + + /// + /// Handles sending the request and adding the token to the request. + /// + /// Request to be sent + /// + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.Headers.Authorization is null) + { + AuthenticationResult authResponse = await AuthenticateAsync(cancellationToken); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResponse.AccessToken); + } + return await base.SendAsync(request, cancellationToken); + } + } +} diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/ChatConsoleService.cs b/samples/basic/copilotatudio-agentToagent/dotnet/ChatConsoleService.cs new file mode 100644 index 00000000..b07fdeb7 --- /dev/null +++ b/samples/basic/copilotatudio-agentToagent/dotnet/ChatConsoleService.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Core.Models; +using Microsoft.Agents.CopilotStudio.Client; + +namespace CopilotStudioClientSample; + +/// +/// This class is responsible for handling the Chat Console service and managing the conversation between the user and the Copilot Studio hosted Agent. +/// +/// Connection Settings for connecting to Copilot Studio +internal class ChatConsoleService(CopilotClient copilotClient) : IHostedService +{ + /// + /// This is the main thread loop that manages the back and forth communication with the Copilot Studio Agent. + /// + /// + /// + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew(); + Console.Write("\nagent> "); + + // Attempt to connect to the copilot studio hosted agent here + // if successful, this will loop though all events that the Copilot Studio agent sends to the client setup the conversation. + await foreach (Activity act in copilotClient.StartConversationAsync(emitStartConversationEvent:true, cancellationToken:cancellationToken)) + { + System.Diagnostics.Trace.WriteLine($">>>>MessageLoop Duration: {sw.Elapsed.ToDurationString()}"); + sw.Restart(); + if (act is null) + { + throw new InvalidOperationException("Activity is null"); + } + // for each response, report to the UX + PrintActivity(act); + } + + // Once we are connected and have initiated the conversation, begin the message loop with the Console. + while (!cancellationToken.IsCancellationRequested) + { + Console.Write("\nuser> "); + string question = Console.ReadLine()!; // Get user input from the console to send. + Console.Write("\nagent> "); + // Send the user input to the Copilot Studio agent and await the response. + // In this case we are not sending a conversation ID, as the agent is already connected by "StartConversationAsync", a conversation ID is persisted by the underlying client. + sw.Restart(); + await foreach (Activity act in copilotClient.AskQuestionAsync(question, null, cancellationToken)) + { + System.Diagnostics.Trace.WriteLine($">>>>MessageLoop Duration: {sw.Elapsed.ToDurationString()}"); + // for each response, report to the UX + PrintActivity(act); + sw.Restart(); + } + } + sw.Stop(); + } + + /// + /// This method is responsible for writing formatted data to the console. + /// This method does not handle all of the possible activity types and formats, it is focused on just a few common types. + /// + /// + static void PrintActivity(IActivity act) + { + switch (act.Type) + { + case "message": + if (act.TextFormat == "markdown") + { + Console.WriteLine(act.Text); + if (act.SuggestedActions?.Actions.Count > 0) + { + Console.WriteLine("Suggested actions:\n"); + act.SuggestedActions.Actions.ToList().ForEach(action => Console.WriteLine("\t" + action.Text)); + } + } + else + { + Console.Write($"\n{act.Text}\n"); + } + break; + case "typing": + Console.Write("."); + break; + case "event": + Console.Write("+"); + break; + default: + Console.Write($"[{act.Type}]"); + break; + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + System.Diagnostics.Trace.TraceInformation("Stopping"); + return Task.CompletedTask; + } +} diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/CopilotStudioClient.csproj b/samples/basic/copilotatudio-agentToagent/dotnet/CopilotStudioClient.csproj new file mode 100644 index 00000000..3d9354d8 --- /dev/null +++ b/samples/basic/copilotatudio-agentToagent/dotnet/CopilotStudioClient.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/DualCopilotChatService.cs b/samples/basic/copilotatudio-agentToagent/dotnet/DualCopilotChatService.cs new file mode 100644 index 00000000..a882728d --- /dev/null +++ b/samples/basic/copilotatudio-agentToagent/dotnet/DualCopilotChatService.cs @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Core.Models; +using Microsoft.Agents.CopilotStudio.Client; + +namespace CopilotStudioClientSample; + +/// +/// This class manages communication between two Copilot Studio agents. +/// It allows sending messages from Copilot 1 to Copilot 2 and provides an interactive chat interface. +/// +internal class DualCopilotChatService : IHostedService +{ + private readonly IServiceProvider _serviceProvider; + private CopilotClient _copilot1Client; + private CopilotClient _copilot2Client; + + public DualCopilotChatService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _copilot1Client = _serviceProvider.GetRequiredKeyedService("copilot1"); + _copilot2Client = _serviceProvider.GetRequiredKeyedService("copilot2"); + } + + /// + /// Main service loop that provides interactive chat with both copilots + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + Console.WriteLine("=== Dual Copilot Chat Service Started ==="); + Console.WriteLine("Commands:"); + Console.WriteLine(" 1: - Send message to Copilot 1"); + Console.WriteLine(" 2: - Send message to Copilot 2"); + Console.WriteLine(" relay: - Send to Copilot 1, then relay response to Copilot 2"); + Console.WriteLine(" quit - Exit"); + Console.WriteLine("=========================================="); + + // Initialize both copilots + await InitializeCopilot(_copilot1Client, "Copilot 1", cancellationToken); + await InitializeCopilot(_copilot2Client, "Copilot 2", cancellationToken); + + // Start interactive chat loop + await InteractiveChatLoop(cancellationToken); + } + + /// + /// Initialize a copilot client and start its conversation + /// + private async Task InitializeCopilot(CopilotClient copilotClient, string name, CancellationToken cancellationToken) + { + Console.WriteLine($"\n[{name}] Initializing..."); + + await foreach (Activity act in copilotClient.StartConversationAsync(emitStartConversationEvent: true, cancellationToken: cancellationToken)) + { + if (act is null) continue; + Console.WriteLine($"[{name}] {GetActivityText(act)}"); + } + + Console.WriteLine($"[{name}] Ready!"); + } + + /// + /// Interactive chat loop handling user commands + /// + private async Task InteractiveChatLoop(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + Console.Write("\nCommand> "); + string input = Console.ReadLine()!; + + if (string.IsNullOrWhiteSpace(input)) continue; + if (input.ToLower() == "quit") break; + + try + { + await ProcessCommand(input, cancellationToken); + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] {ex.Message}"); + } + } + } + + /// + /// Process user commands for copilot interactions + /// + private async Task ProcessCommand(string input, CancellationToken cancellationToken) + { + if (input.StartsWith("1:")) + { + string message = input.Substring(2).Trim(); + await SendToCopilot(_copilot1Client, "Copilot 1", message, cancellationToken); + } + else if (input.StartsWith("2:")) + { + string message = input.Substring(2).Trim(); + await SendToCopilot(_copilot2Client, "Copilot 2", message, cancellationToken); + } + else if (input.StartsWith("relay:")) + { + string message = input.Substring(6).Trim(); + await RelayMessage(message, cancellationToken); + } + else + { + Console.WriteLine("[ERROR] Invalid command. Use 1:, 2:, relay:, or quit"); + } + } + + /// + /// Send a message to a specific copilot + /// + private async Task SendToCopilot(CopilotClient copilotClient, string name, string message, CancellationToken cancellationToken) + { + Console.WriteLine($"\n[YOU β†’ {name}] {message}"); + Console.Write($"[{name}] "); + + await foreach (Activity act in copilotClient.AskQuestionAsync(message, null, cancellationToken)) + { + if (act is null) continue; + Console.Write(GetActivityText(act)); + } + Console.WriteLine(); // New line after response + } + + /// + /// Relay a message from Copilot 1 to Copilot 2 + /// This demonstrates inter-copilot communication + /// + private async Task RelayMessage(string originalMessage, CancellationToken cancellationToken) + { + Console.WriteLine($"\n[RELAY] Starting relay: {originalMessage}"); + + // Step 1: Send to Copilot 1 + Console.WriteLine($"[YOU β†’ Copilot 1] {originalMessage}"); + Console.Write("[Copilot 1] "); + + string copilot1Response = ""; + await foreach (Activity act in _copilot1Client.AskQuestionAsync(originalMessage, null, cancellationToken)) + { + if (act is null) continue; + string actText = GetActivityText(act); + Console.Write(actText); + + if (act.Type == "message" && !string.IsNullOrEmpty(act.Text)) + { + copilot1Response += act.Text + " "; + } + } + Console.WriteLine(); // New line + + if (string.IsNullOrWhiteSpace(copilot1Response)) + { + Console.WriteLine("[ERROR] No response from Copilot 1 to relay"); + return; + } + + // Step 2: Relay Copilot 1's response to Copilot 2 + string relayMessage = $"Copilot 1 said: {copilot1Response.Trim()}"; + Console.WriteLine($"\n[Copilot 1 β†’ Copilot 2] {relayMessage}"); + Console.Write("[Copilot 2] "); + + await foreach (Activity act in _copilot2Client.AskQuestionAsync(relayMessage, null, cancellationToken)) + { + if (act is null) continue; + Console.Write(GetActivityText(act)); + } + Console.WriteLine(); // New line + + Console.WriteLine("[RELAY] Complete!"); + } + + /// + /// Extract readable text from an activity + /// + private static string GetActivityText(IActivity act) + { + return act.Type switch + { + "message" when act.TextFormat == "markdown" => act.Text ?? "", + "message" => act.Text ?? "", + "typing" => ".", + "event" => "+", + _ => $"[{act.Type}]" + }; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Console.WriteLine("Dual Copilot Chat Service stopped."); + return Task.CompletedTask; + } +} diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/MCSA2AREADME.md b/samples/basic/copilotatudio-agentToagent/dotnet/MCSA2AREADME.md new file mode 100644 index 00000000..bfe5aac6 --- /dev/null +++ b/samples/basic/copilotatudio-agentToagent/dotnet/MCSA2AREADME.md @@ -0,0 +1,290 @@ +# Copilot Studio Client Console Sample + +This sample demonstrates how to use the `Microsoft.Agents.CopilotStudio.Client` package to create a console application that connects to and communicates with agents hosted in Microsoft Copilot Studio in DIFFERENT environments. This sample is for environments in same tenant, but can be extended across. +This builds on original sample of copilotstudio-client, so make sure you have followed that prior to coming to this step. + +## πŸ—οΈ Architecture Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Console App β”‚ β”‚ Azure AD β”‚ β”‚ Copilot Studio β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚AddTokenHdlr │◄┼────┼►│ Auth β”‚ β”‚ β”‚ β”‚ Agent 1 β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ Service β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚CopilotClient│◄┼─────────────────────────────┼►│ Agent 2 β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## πŸ“‹ Prerequisites + +Before running this sample, you'll need: + +1. **Microsoft Copilot Studio access** with permissions to create and publish agents +2. **Azure AD tenant** with permissions to create app registrations +3. **.NET 8.0 SDK** installed on your development machine +4. **Visual Studio Code** or **Visual Studio** for debugging + +## πŸ€– Step 1: Create Agents in Copilot Studio + +### Create Your First Agent +1. Navigate to [Microsoft Copilot Studio](https://copilotstudio.microsoft.com/) +2. Click **"Create a copilot"** or **"New copilot"** +3. Configure your agent: + - **Name**: `TestAgent` (or your preferred name) + - **Description**: Brief description of your agent's purpose + - **Language**: Select your preferred language +4. **Publish your agent**: + - Click **"Publish"** in the top navigation + - Follow the publishing wizard to make your agent available + +### Create Your Second Agent (Optional) +Repeat the above steps to create a second agent for testing multiple agent configurations. + +### Collect Agent Information +For each agent you created: + +1. In Copilot Studio, navigate to **Settings** β†’ **Advanced** β†’ **Metadata** +2. **Copy and save** the following values (you'll need them for configuration): + - **Schema Name**: (e.g., `cr73a_testAgent`) + - **Environment ID**: (e.g., `b4ccc464-f5b7-e266-bada-06122c419e03`) + +## πŸ” Step 2: Create Azure AD App Registration + +### Create the App Registration +1. Open the [Azure Portal](https://portal.azure.com/) +2. Navigate to **Azure Active Directory** (or **Microsoft Entra ID**) +3. Go to **App registrations** β†’ **New registration** +4. Configure the registration: + - **Name**: `Copilot Studio Client Sample` + - **Supported account types**: `Accounts in this organizational directory only` + - **Redirect URI**: + - Platform: `Public client/native (mobile & desktop)` + - URI: `http://localhost` +5. Click **Register** + +### Configure API Permissions +1. In your newly created app registration, go to **API permissions** +2. Click **Add a permission** +3. Select **APIs my organization uses** tab +4. Search for and select **"Power Platform API"** + > ⚠️ **Note**: If you don't see "Power Platform API", follow [these instructions](https://learn.microsoft.com/power-platform/admin/programmability-authentication-v2#step-2-configure-api-permissions) to add it to your tenant first. +5. Select **Delegated permissions** +6. Expand **CopilotStudio** and check **`CopilotStudio.Copilots.Invoke`** +7. Click **Add permissions** +8. **(Recommended)** Click **Grant admin consent** for your organization + +### Collect App Registration Information +From the **Overview** page of your app registration, copy and save: +- **Application (client) ID**: (e.g., `525dc2dd-a78d-4770-9c21-71bea65fab56`) +- **Directory (tenant) ID**: (e.g., `391bca38-63d1-4cbd-ba78-c74369b91888`) + +## βš™οΈ Step 3: Configure the Application Settings + +### Update appsettings.json +1. Open `appsettings.json` in the project root +2. Update the configuration sections with the values you collected: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "CopilotStudioClientSettings1": { + "DirectConnectUrl": "", + "EnvironmentId": "YOUR_ENVIRONMENT_ID_HERE", + "SchemaName": "YOUR_SCHEMA_NAME_HERE", + "TenantId": "YOUR_TENANT_ID_HERE", + "UseS2SConnection": false, + "AppClientId": "YOUR_APP_CLIENT_ID_HERE", + "AppClientSecret": "" + }, + "CopilotStudioClientSettings2": { + "DirectConnectUrl": "", + "EnvironmentId": "YOUR_SECOND_ENVIRONMENT_ID_HERE", + "SchemaName": "YOUR_SECOND_SCHEMA_NAME_HERE", + "TenantId": "YOUR_TENANT_ID_HERE", + "UseS2SConnection": false, + "AppClientId": "YOUR_APP_CLIENT_ID_HERE", + "AppClientSecret": "" + } +} +``` + +### Configuration Reference +| Setting | Description | Example Value | +|---------|-------------|---------------| +| `EnvironmentId` | Environment ID from Copilot Studio | `b4ccc464-f5b7-e266-bada-06122c419e03` | +| `SchemaName` | Schema name from Copilot Studio | `cr73a_testAgent` | +| `TenantId` | Directory (tenant) ID from Azure AD | `391bca38-63d1-4cbd-ba78-c74369b91888` | +| `AppClientId` | Application (client) ID from Azure AD | `525dc2dd-a78d-4770-9c21-71bea65fab56` | +| `UseS2SConnection` | Set to `false` for interactive authentication | `false` | +| `AppClientSecret` | Leave empty for public client apps | `""` | + +## πŸƒβ€β™‚οΈ Step 4: Build and Run the Sample + +### Method 1: Using .NET CLI +```bash +# Navigate to the project directory +cd [your-project-directory] + +# Restore dependencies +dotnet restore + +# Build the project +dotnet build + +# Run the application +dotnet run +``` + +### Method 2: Using Visual Studio Code +1. Open the project folder in VS Code +2. Press `F5` or go to **Run** β†’ **Start Debugging** +3. Select the **"Debug Program.cs"** configuration + +### Method 3: Using Visual Studio +1. Open `CopilotStudioClient.csproj` in Visual Studio +2. Press `F5` or click **Debug** β†’ **Start Debugging** + +## πŸ”„ Authentication Flow + +### Interactive Authentication Process +1. **First Run**: The application will open your default web browser +2. **Sign In**: Enter your Microsoft credentials +3. **Consent**: Grant permissions to access Copilot Studio on your behalf +4. **Token Caching**: Subsequent runs will use cached tokens (stored in `mcs_client_console` folder) + +### Authentication Components +- **`AddTokenHandler.cs`**: Handles interactive user authentication using MSAL +- **`AddTokenHandlerS2S.cs`**: Handles service-to-service authentication (when `UseS2SConnection` is `true`) +- **Token Cache**: Stored locally for improved performance on subsequent runs + +## πŸ’¬ Using the Console Interface + +### Console Commands and Interactions +Once the application starts successfully, you'll see: + +``` +agent> [Initial agent response/greeting] + +user> [Type your message here] +``` + +### Console Interface Guide +| Prompt | Description | +|--------|-------------| +| `user>` | Your input prompt - type your questions or commands here | +| `agent>` | Agent responses from Copilot Studio | +| `.` | Indicates the agent is typing | +| `+` | Indicates system events (connection, processing, etc.) | +| `[message]` | System activity type indicators | + +### Example Interaction +``` +agent> Hello! I'm your Copilot Studio agent. How can I help you today? + +user> What's the weather like? + +agent> I'd be happy to help you with weather information. However, I'll need to know your location first. Could you please tell me which city or area you're interested in? + +user> Seattle, WA + +agent> [Agent provides weather information for Seattle] +``` + +## πŸ”§ Troubleshooting + +### Common Issues and Solutions + +#### 1. Breakpoints Not Being Hit +**Problem**: Debugging breakpoints in `AddTokenHandler.cs` are not being triggered. + +**Solutions**: +- **Check Authentication Cache**: Delete the `mcs_client_console` folder in your application's base directory to force re-authentication +- **Verify Configuration**: Ensure `UseS2SConnection` is set to `false` in `appsettings.json` +- **Check Build Configuration**: Ensure you're running in Debug mode, not Release +- **Verify Token Flow**: The `AddTokenHandler` is only called when HTTP requests are made to Copilot Studio + +#### 2. Authentication Errors +**Problem**: "Failed to authenticate" or login window doesn't appear. + +**Solutions**: +- Verify your `AppClientId` and `TenantId` are correct +- Ensure the redirect URI in Azure AD is exactly `http://localhost` (not https) +- Check that the app registration has the correct API permissions +- Try running the application as Administrator + +#### 3. Agent Connection Issues +**Problem**: Cannot connect to the Copilot Studio agent. + +**Solutions**: +- Verify the `EnvironmentId` and `SchemaName` are correct +- Ensure your agent is published in Copilot Studio +- Check that you have access to the environment where the agent is hosted +- Verify your Azure AD user has permissions to invoke the agent + +#### 4. Token Cache Issues +**Problem**: Authentication fails after it previously worked. + +**Solutions**: +- Delete the token cache directory: `{AppContext.BaseDirectory}/mcs_client_console` +- Clear browser cookies for Microsoft login pages +- Try authenticating in an incognito/private browser window + +### Debug Settings +To enable more detailed logging, update `appsettings.json`: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.Identity.Client": "Debug" + } + } +} +``` + +## πŸ“ Project Structure + +``` +CopilotStudioClient/ +β”œβ”€β”€ Program.cs # Application entry point and DI configuration +β”œβ”€β”€ ChatConsoleService.cs # Main chat loop and console interface +β”œβ”€β”€ AddTokenHandler.cs # Interactive authentication handler +β”œβ”€β”€ AddTokenHandlerS2S.cs # Service-to-service authentication handler +β”œβ”€β”€ SampleConnectionSettings.cs # Configuration model +β”œβ”€β”€ TimeSpanExtensions.cs # Utility extensions +β”œβ”€β”€ appsettings.json # Application configuration +β”œβ”€β”€ CopilotStudioClient.csproj # Project file +└── README.md # Documentation +``` + +## πŸ”— Additional Resources + +- [Microsoft Copilot Studio Documentation](https://docs.microsoft.com/power-virtual-agents/) +- [Power Platform API Authentication](https://learn.microsoft.com/power-platform/admin/programmability-authentication-v2) +- [Microsoft Identity Platform Documentation](https://docs.microsoft.com/azure/active-directory/develop/) +- [MSAL.NET Documentation](https://docs.microsoft.com/azure/active-directory/develop/msal-net-overview) + +## 🀝 Support + +If you encounter issues with this sample: + +1. Check the [Troubleshooting](#-troubleshooting) section above +2. Review the console output for detailed error messages +3. Enable debug logging for more detailed information +4. Ensure all prerequisites and configuration steps have been completed correctly + +--- + +**Happy Chatting with your Copilot Studio Agents! πŸš€** diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/Program.cs b/samples/basic/copilotatudio-agentToagent/dotnet/Program.cs new file mode 100644 index 00000000..d899b120 --- /dev/null +++ b/samples/basic/copilotatudio-agentToagent/dotnet/Program.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CopilotStudioClientSample; +using Microsoft.Agents.CopilotStudio.Client; + +// Setup the Direct To Engine client example. + +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + +// Get the configuration settings for both copilots from the appsettings.json file. +SampleConnectionSettings settings1 = new SampleConnectionSettings(builder.Configuration.GetSection("CopilotStudioClientSettings1")); +SampleConnectionSettings settings2 = new SampleConnectionSettings(builder.Configuration.GetSection("CopilotStudioClientSettings2")); + +// Create http clients for both copilots +builder.Services.AddHttpClient("copilot1").ConfigurePrimaryHttpMessageHandler(() => +{ + if (settings1.UseS2SConnection) + { + return new AddTokenHandlerS2S(settings1); + } + else + { + return new AddTokenHandler(settings1); + } +}); + +builder.Services.AddHttpClient("copilot2").ConfigurePrimaryHttpMessageHandler(() => +{ + if (settings2.UseS2SConnection) + { + return new AddTokenHandlerS2S(settings2); + } + else + { + return new AddTokenHandler(settings2); + } +}); + +// add Settings and instances of both Copilot Clients to the Current services. +builder.Services + .AddSingleton(settings1) + .AddSingleton(settings2) + .AddKeyedTransient("copilot1", (s, key) => + { + var logger = s.GetRequiredService().CreateLogger(); + return new CopilotClient(settings1, s.GetRequiredService(), logger, "copilot1"); + }) + .AddKeyedTransient("copilot2", (s, key) => + { + var logger = s.GetRequiredService().CreateLogger(); + return new CopilotClient(settings2, s.GetRequiredService(), logger, "copilot2"); + }) + .AddHostedService(); +IHost host = builder.Build(); +host.Run(); diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/Properties/launchSettings.TEMPLATE.json b/samples/basic/copilotatudio-agentToagent/dotnet/Properties/launchSettings.TEMPLATE.json new file mode 100644 index 00000000..d1bd1db8 --- /dev/null +++ b/samples/basic/copilotatudio-agentToagent/dotnet/Properties/launchSettings.TEMPLATE.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "mcs.console": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DirectToEngineSettings__TenantId": "", + "DirectToEngineSettings__EnvironmentId": "", + "DirectToEngineSettings__AppClientId": "", + "DirectToEngineSettings__BotIdentifier": "" + } + } + } +} diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/SampleConnectionSettings.cs b/samples/basic/copilotatudio-agentToagent/dotnet/SampleConnectionSettings.cs new file mode 100644 index 00000000..b5f9a8ec --- /dev/null +++ b/samples/basic/copilotatudio-agentToagent/dotnet/SampleConnectionSettings.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.CopilotStudio.Client; + +namespace CopilotStudioClientSample +{ + /// + /// Connection Settings extension for the sample to include appID and TenantId for creating authentication token. + /// + internal class SampleConnectionSettings : ConnectionSettings + { + /// + /// Use S2S connection for authentication. + /// + public bool UseS2SConnection { get; set; } = false; + + /// + /// Tenant ID for creating the authentication for the connection + /// + public string? TenantId { get; set; } + /// + /// Application ID for creating the authentication for the connection + /// + public string? AppClientId { get; set; } + + /// + /// Application secret for creating the authentication for the connection + /// + public string? AppClientSecret { get; set; } + + /// + /// Create ConnectionSettings from a configuration section. + /// + /// + /// + public SampleConnectionSettings(IConfigurationSection config) :base (config) + { + AppClientId = config[nameof(AppClientId)] ?? throw new ArgumentException($"{nameof(AppClientId)} not found in config"); + TenantId = config[nameof(TenantId)] ?? throw new ArgumentException($"{nameof(TenantId)} not found in config"); + UseS2SConnection = config.GetValue(nameof(UseS2SConnection), false); + AppClientSecret = config[nameof(AppClientSecret)]; + } + } +} diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/TimeSpanExtensions.cs b/samples/basic/copilotatudio-agentToagent/dotnet/TimeSpanExtensions.cs new file mode 100644 index 00000000..2e784ac7 --- /dev/null +++ b/samples/basic/copilotatudio-agentToagent/dotnet/TimeSpanExtensions.cs @@ -0,0 +1,15 @@ +namespace CopilotStudioClientSample +{ + internal static class TimeSpanExtensions + { + /// + /// Returns a duration in the format hh:mm:ss:fff + /// + /// + /// + internal static string ToDurationString(this TimeSpan timeSpan) + { + return timeSpan.ToString(@"hh\:mm\:ss\.fff"); + } + } +} diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/appsettings.json b/samples/basic/copilotatudio-agentToagent/dotnet/appsettings.json new file mode 100644 index 00000000..aa2057b1 --- /dev/null +++ b/samples/basic/copilotatudio-agentToagent/dotnet/appsettings.json @@ -0,0 +1,26 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Error", + "Microsoft.Hosting.Lifetime": "Warning" + } + }, + "CopilotStudioClientSettings1": { + "DirectConnectUrl": "", + "EnvironmentId": "b4ccc464-f5b7-e266-bada-06122c419e03", + "SchemaName": "cr73a_testAgent", + "TenantId": "391bca38-63d1-4cbd-ba78-c74369b91888", + "UseS2SConnection": false, + "AppClientId": "525dc2dd-a78d-4770-9c21-71bea65fab56", + "AppClientSecret": "" + }, + "CopilotStudioClientSettings2": { + "DirectConnectUrl": "", + "EnvironmentId": "a865c82b-3806-efa9-a5a3-df5889dfa013", + "SchemaName": "cr082_agent2", + "TenantId": "391bca38-63d1-4cbd-ba78-c74369b91888", + "UseS2SConnection": false, + "AppClientId": "525dc2dd-a78d-4770-9c21-71bea65fab56", + "AppClientSecret": "" + } +} diff --git a/samples/basic/copilotatudio-agentToagent/dotnet/dotnet.sln b/samples/basic/copilotatudio-agentToagent/dotnet/dotnet.sln new file mode 100644 index 00000000..38fdd958 --- /dev/null +++ b/samples/basic/copilotatudio-agentToagent/dotnet/dotnet.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CopilotStudioClient", "CopilotStudioClient.csproj", "{207DA8CF-1247-A6F5-60AA-6C634C5E5BF3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {207DA8CF-1247-A6F5-60AA-6C634C5E5BF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {207DA8CF-1247-A6F5-60AA-6C634C5E5BF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {207DA8CF-1247-A6F5-60AA-6C634C5E5BF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {207DA8CF-1247-A6F5-60AA-6C634C5E5BF3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8713DD82-C2D9-41B9-B2D7-480AB84702E5} + EndGlobalSection +EndGlobal