Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions samples/basic/copilotatudio-agentToagent/dotnet/AddTokenHandler.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <param name="settings">Direct To engine connection settings.</param>
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<AuthenticationResult> 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;
}

/// <summary>
/// Handles sending the request and adding the token to the request.
/// </summary>
/// <param name="request">Request to be sent</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected override async Task<HttpResponseMessage> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// This sample uses an HttpClientHandler to add an authentication token to the request.
/// In this case its using client secret for the request.
/// </summary>
/// <param name="settings">Direct To engine connection settings.</param>
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<AuthenticationResult> 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;
}

/// <summary>
/// Handles sending the request and adding the token to the request.
/// </summary>
/// <param name="request">Request to be sent</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected override async Task<HttpResponseMessage> 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);
}
}
}
101 changes: 101 additions & 0 deletions samples/basic/copilotatudio-agentToagent/dotnet/ChatConsoleService.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// This class is responsible for handling the Chat Console service and managing the conversation between the user and the Copilot Studio hosted Agent.
/// </summary>
/// <param name="copilotClient">Connection Settings for connecting to Copilot Studio</param>
internal class ChatConsoleService(CopilotClient copilotClient) : IHostedService
{
/// <summary>
/// This is the main thread loop that manages the back and forth communication with the Copilot Studio Agent.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
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();
}

/// <summary>
/// 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.
/// </summary>
/// <param name="act"></param>
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.67.2" />
<PackageReference Include="Microsoft.Agents.CopilotStudio.Client" Version="1.2.*-*"/>
</ItemGroup>

</Project>
Loading