Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Copilot Studio Client API and SPA Sample #59

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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
14 changes: 9 additions & 5 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
<PropertyGroup>
<!-- Not enabled by default for the repo. -->
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<!-- To avoid NU1507 we must disable library packs from getting added by the SDK.
Error:
The project E:\repos\msazure\One\PowerPlatform-ISVEx-ToolsCore\src\cli\Analyzers\bolt.Analyzers\bolt.Analyzers\bolt.Analyzers.csproj is using CentralPackageVersionManagement, a NuGet preview feature.
E:\repos\msazure\One\PowerPlatform-ISVEx-ToolsCore\src\cli\Analyzers\bolt.Analyzers\bolt.Analyzers\bolt.Analyzers.csproj : warning NU1507: There are 2 package sources defined in your configuration. When using central package management, please map your package sources with package source mapping (https://aka.ms/nuget-package-source-mapping) or specify a single package source. The following sources are defined: https://pkgs.dev.azure.com/msazure/One/_packaging/CAP_ISVExp_Tools_Upstream/nuget/v3/index.json, C:\Program Files\dotnet\library-packs
The 'library-packs' source is added by the SDK.
<!-- To avoid NU1507 we must disable library packs from getting added by the SDK.
Error:
The project E:\repos\msazure\One\PowerPlatform-ISVEx-ToolsCore\src\cli\Analyzers\bolt.Analyzers\bolt.Analyzers\bolt.Analyzers.csproj is using CentralPackageVersionManagement, a NuGet preview feature.
E:\repos\msazure\One\PowerPlatform-ISVEx-ToolsCore\src\cli\Analyzers\bolt.Analyzers\bolt.Analyzers\bolt.Analyzers.csproj : warning NU1507: There are 2 package sources defined in your configuration. When using central package management, please map your package sources with package source mapping (https://aka.ms/nuget-package-source-mapping) or specify a single package source. The following sources are defined: https://pkgs.dev.azure.com/msazure/One/_packaging/CAP_ISVExp_Tools_Upstream/nuget/v3/index.json, C:\Program Files\dotnet\library-packs
The 'library-packs' source is added by the SDK.
-->
<DisableImplicitLibraryPacksFolder>true</DisableImplicitLibraryPacksFolder>
</PropertyGroup>
Expand All @@ -16,6 +16,10 @@
<Microsoft_AspNetCore_PkgVer>8.0.11</Microsoft_AspNetCore_PkgVer>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="$(Microsoft_AspNetCore_PkgVer)" />
<PackageVersion Include="Microsoft.Identity.Web" Version="3.2.0" />
<PackageVersion Include="Microsoft.Identity.Web.DownstreamApi" Version="3.2.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.1.0" />
<PackageVersion Include="CsvHelper" Version="33.0.1" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.1.0-preview.1.25064.3" />
Expand Down
7 changes: 7 additions & 0 deletions src/Microsoft.Agents.SDK.sln
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InMeetingNotificationsBot",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagMentionBot", "samples\Teams\bot-tag-mention\TagMentionBot.csproj", "{BC5EFA6C-7EB5-4803-B7C5-093892E9DBB8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CopilotStudioClientSampleAPI", "samples\copilot_studio_client_api_and_spa\CopilotStudioClientSampleAPI\CopilotStudioClientSampleAPI.csproj", "{D54EEE0D-64F7-4F29-B36A-BF0D70801FFB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -318,6 +320,10 @@ Global
{A839D635-0382-4E4C-8052-1F18B71434EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A839D635-0382-4E4C-8052-1F18B71434EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A839D635-0382-4E4C-8052-1F18B71434EE}.Release|Any CPU.Build.0 = Release|Any CPU
{D54EEE0D-64F7-4F29-B36A-BF0D70801FFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D54EEE0D-64F7-4F29-B36A-BF0D70801FFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D54EEE0D-64F7-4F29-B36A-BF0D70801FFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D54EEE0D-64F7-4F29-B36A-BF0D70801FFB}.Release|Any CPU.Build.0 = Release|Any CPU
{7D1A1CE5-6D9B-4D31-AC77-C3B1787F575D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D1A1CE5-6D9B-4D31-AC77-C3B1787F575D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D1A1CE5-6D9B-4D31-AC77-C3B1787F575D}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -390,6 +396,7 @@ Global
{71813B2D-D6A8-4388-9541-9B1287C93F19} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{6A27C156-842F-409C-86B7-D9CC0A38F02F} = {7A18F0C9-F8AF-4168-B954-6563BB2C1A90}
{A839D635-0382-4E4C-8052-1F18B71434EE} = {674A812C-7287-4883-97F9-697D83750648}
{D54EEE0D-64F7-4F29-B36A-BF0D70801FFB} = {674A812C-7287-4883-97F9-697D83750648}
{7D1A1CE5-6D9B-4D31-AC77-C3B1787F575D} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{06E490F7-F0BB-E3C4-54FE-5210627292A1} = {183D0E91-B84E-46D7-B653-6D85B4CCF804}
{BC5EFA6C-7EB5-4803-B7C5-093892E9DBB8} = {183D0E91-B84E-46D7-B653-6D85B4CCF804}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web.Resource;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using Microsoft.Agents.CopilotStudio.Client;
using Microsoft.Extensions.Logging;
using CopilotStudioClientSampleAPI.Services;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication;
using CopilotStudioClientSampleAPI.Models;
using static System.Runtime.InteropServices.JavaScript.JSType;
using System.Text.Json;
using System.Collections.Generic;
using Microsoft.Extensions.Options;

namespace CopilotStudioClientSampleAPI.Controllers
{
[Authorize]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
[Route("api/[controller]")]
[ApiController]
public class Chat : ControllerBase
{
private readonly CopilotConversationCache _copilotConversationCache;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
private readonly ILogger<Chat> _logger;
private readonly ConnectionSettings _directToEngineSettings;
private readonly AzureAdSettings _azureAdSettings;
private readonly IEnumerable<string> _requestedScopes;

public Chat(IHttpClientFactory httpClientFactory,
IConfiguration configuration,
ILogger<Chat> logger,
CopilotConversationCache copilotConversationCache,
IOptions<AzureAdSettings> azureAdSettings
)
{
_copilotConversationCache = copilotConversationCache;
_httpClientFactory = httpClientFactory;
_configuration = configuration;
_logger = logger;
_azureAdSettings = azureAdSettings.Value;

// Bind DirectToEngineSettings
_directToEngineSettings = new ConnectionSettings(_configuration.GetSection("DirectToEngineSettings"));
if (_directToEngineSettings == null)
{
throw new ArgumentException("DirectToEngineSettings not found in config");
}
// Get Configs
_requestedScopes = _configuration.GetSection("API").Get<IEnumerable<string>>() ?? [];
}

[HttpDelete]
public IActionResult Delete(string botIdentifier)
{
try
{
var currentUser = User.Claims.Where(t => t.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier").FirstOrDefault()?.Value;
_copilotConversationCache.RemoveConversation(currentUser!, botIdentifier);
return Ok();
}
catch
{
return BadRequest();
}
}

[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] MessageRequest request)
{
JwtSecurityToken? jwtToken = ExtractJwtToken();
if (jwtToken == null)
{
return Unauthorized();
}

// Define the function to get the access token
Func<string, Task<string>> tokenProviderFunction = async (url) =>
{
// Build Confidential Application
var application = ConfidentialClientApplicationBuilder.Create(_azureAdSettings.ClientId)
.WithClientSecret(_azureAdSettings.ClientSecret)
.WithTenantId(_azureAdSettings.TenantId)
.Build();

// Aquire token on behalf of user
UserAssertion userAssertion = new(jwtToken.RawData, "urn:ietf:params:oauth:grant-type:jwt-bearer");
var result = await application.AcquireTokenOnBehalfOf(_requestedScopes, userAssertion).ExecuteAsync();
return result.AccessToken;
};

// Set the required agent in CopilotStudio
if (!string.IsNullOrEmpty(request.BotIdentifier))
{
_directToEngineSettings.BotIdentifier = request.BotIdentifier;
}

// Create Copilot Client
var copilotClient = new CopilotClient(_directToEngineSettings, _httpClientFactory, tokenProviderFunction, _logger, "mcs");

// Get the current User
var currentUser = User.Claims.Where(t => t.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier").FirstOrDefault()?.Value;

// Get the current conversation, null if not exist
var conversationId = _copilotConversationCache.GetConversation(currentUser!, _directToEngineSettings.BotIdentifier!);

var chatResponses = new List<ChatResponse>();
try
{
var chatClient = new ChatConsoleService(copilotClient);
using var cts = new CancellationTokenSource();
// Get the CancellationToken from the CancellationTokenSource
CancellationToken token = cts.Token;

// If not conversation Start a conversation
if (conversationId == null)
{
var conversation = await chatClient.StartAsync(token);
chatResponses.AddRange(conversation.ChatResponses!);
_copilotConversationCache.AddConversation(currentUser!, _directToEngineSettings.BotIdentifier!, conversation.ConversationId!);
}

// If a message if avaialable sent it to the Copilot Studio Agent
if (request.Message != string.Empty)
{
var existingConversationId = _copilotConversationCache.GetConversation(currentUser!, _directToEngineSettings.BotIdentifier!);
var questionResponse = await chatClient.Ask(request.Message, existingConversationId!, token);
chatResponses.AddRange(questionResponse);
}
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return BadRequest("Error with communicating with Copilot Studio Agent");
}

string prettyJson = JsonSerializer.Serialize(chatResponses, new JsonSerializerOptions
{
WriteIndented = true
});

return Content(prettyJson, "application/json");
}

private JwtSecurityToken? ExtractJwtToken()
{
// Get the raw JWT token from the Authorization header
var authHeader = HttpContext.Request.Headers.Authorization.ToString();
var token = authHeader.StartsWith("Bearer ") ? authHeader["Bearer ".Length..].Trim() : string.Empty;

// Parse the token and cast it to JwtSecurityToken
var handler = new JwtSecurityTokenHandler();
try
{
var jwtToken = handler.ReadToken(token) as JwtSecurityToken;
return jwtToken;
}
catch (Exception)
{
return null;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

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

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
<PackageReference Include="Microsoft.Identity.Web" />
<PackageReference Include="Microsoft.Identity.Web.DownstreamApi" />
<PackageReference Include="Swashbuckle.AspNetCore" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\libraries\Client\Microsoft.Agents.Client\Microsoft.Agents.Client.csproj" />
<ProjectReference Include="..\..\..\libraries\Client\Microsoft.Agents.CopilotStudio.Client\Microsoft.Agents.CopilotStudio.Client.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@CopilotStudioClientSampleAPI_HostAddress = http://localhost:5051

GET {{CopilotStudioClientSampleAPI_HostAddress}}/weatherforecast/
Accept: application/json

###
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace CopilotStudioClientSampleAPI.Models
{
public class AzureAdSettings
{
public string Instance { get; set; }

Check warning on line 5 in src/samples/copilot_studio_client_api_and_spa/CopilotStudioClientSampleAPI/Models/AzureADSettings.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Instance' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 5 in src/samples/copilot_studio_client_api_and_spa/CopilotStudioClientSampleAPI/Models/AzureADSettings.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Instance' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string TenantId { get; set; }

Check warning on line 6 in src/samples/copilot_studio_client_api_and_spa/CopilotStudioClientSampleAPI/Models/AzureADSettings.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'TenantId' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string ClientId { get; set; }

Check warning on line 7 in src/samples/copilot_studio_client_api_and_spa/CopilotStudioClientSampleAPI/Models/AzureADSettings.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'ClientId' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string CallbackPath { get; set; }

Check warning on line 8 in src/samples/copilot_studio_client_api_and_spa/CopilotStudioClientSampleAPI/Models/AzureADSettings.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'CallbackPath' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string Scopes { get; set; }

Check warning on line 9 in src/samples/copilot_studio_client_api_and_spa/CopilotStudioClientSampleAPI/Models/AzureADSettings.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Scopes' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string ClientSecret { get; set; }

Check warning on line 10 in src/samples/copilot_studio_client_api_and_spa/CopilotStudioClientSampleAPI/Models/AzureADSettings.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'ClientSecret' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public List<string> ClientCertificates { get; set; }

Check warning on line 11 in src/samples/copilot_studio_client_api_and_spa/CopilotStudioClientSampleAPI/Models/AzureADSettings.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'ClientCertificates' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace CopilotStudioClientSampleAPI.Models;

public class Conversation
{
public required string ConversationId { get; set; }
public required List<ChatResponse> ChatResponses { get; set; }
}
public class ChatResponse
{
public string? Role { get; set; }
public List<Content>? Content { get; set; }
}
public class MessageRequest
{
public required string Message { get; set; }
public required string BotIdentifier { get; set; }
}

public class Content
{
public required string Type { get; set; }
public required string Text { get; set; }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using CopilotStudioClientSampleAPI.Models;
using Microsoft.Agents.CopilotStudio.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

// Add CORS policy
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowReactApp", policy =>
{
policy.WithOrigins("http://localhost:3000") // React app origin
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // Include if using cookies/authentication
});
});

// Bind AzureAd settings
builder.Services.Configure<AzureAdSettings>(builder.Configuration.GetSection("AzureAd"));

// Register HttpClientFactory
builder.Services.AddHttpClient();

// Register CopilotClientProvider as a singleton
builder.Services.AddSingleton<CopilotStudioClientSampleAPI.Services.CopilotConversationCache>();


// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Use CORS policy
app.UseCors("AllowReactApp");

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthentication();

app.UseAuthorization();

app.MapControllers();

app.Run();
Loading
Loading