-
Notifications
You must be signed in to change notification settings - Fork 34
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
Add EchoBot dotnet Sample #72
Closed
+505
−6
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
d1ea100
ignore launchSettings.json
rido-min f2cd3dd
start echo-bot from net repo
rido-min 3519266
Add CI workflow for building .NET samples
rido-min ef2a04d
add upstream
rido-min f6bb2c0
fix warnings in extensions
rido-min 59398d6
run code clean
rido-min ef91f3a
Update README with debugging instructions and remove obsolete nuget.c…
rido-min 5008264
Update README with instructions to debug with tunnels
rido-min File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
name: ci-dotnet | ||
|
||
on: | ||
workflow_dispatch: | ||
pull_request: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v4 | ||
|
||
- name: Build dotnet samples | ||
run: dotnet build samples/Samples.sln |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -395,3 +395,4 @@ dist/ | |
devTools/ | ||
node_modules/ | ||
|
||
launchSettings.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,34 @@ | ||
# M365 Agents SDK Samples | ||
|
||
To enable debugging, open this folder using Visual Studio Code (VSCode). Follow these steps: | ||
Here you can find samples showing how to use the Agents SDK in different languages. | ||
|
||
1. Launch VSCode. | ||
2. Select `File` > `Open Folder...`. | ||
3. Navigate to the `/workspaces/Agents/samples` directory and open it. | ||
4. Once the folder is open, you can start debugging by setting breakpoints and running the samples. | ||
|
||
## Samples list | ||
|
||
|Category | Name | Description | node | dotnet | python | | ||
|---------|-------------|-------------|--------|--------|--------| | ||
| Basic | Echo Bot | Simplest bot | [basic/echo-bot/nodejs](./basic/echo-bot/nodejs) | TBD | TBD | | ||
| Basic | Echo Bot | Simplest bot | [basic/echo-bot/nodejs](./basic/echo-bot/nodejs) | [basic/echo-bot/dotnet](./basic/echo-bot/dotnet) | TBD | | ||
| Basic | Copilot Studio Client | Consume CopilotStudio Agent | [basic/copilotstudio-client/nodejs](./basic/copilotstudio-client/nodejs) | TBD | TBD | | ||
| Complex | Copilot Studio Skill | Call the echo bot from a Copilot Studio skill | [complex/copilotstudio-skill/nodejs](./complex/copilotstudio-skill/nodejs) | TBD | TBD | | ||
|
||
|
||
## Debugging in localhost | ||
|
||
To debug your Agent in `localhost` you can: | ||
|
||
1. Use a client emulator, such as the [Teams App Test Tool](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/debug-your-teams-app-test-tool), or the [BotFramework Emulator](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-debug-emulator) | ||
1. Use the WebChat UI, by registering one instance of Azure Bot Service, and configure the endpoint to use a tunnel to localhost (eg, by using devtunnels) | ||
|
||
### How to register an ABS instance with DevTunnels | ||
|
||
1. Install devtunnels and the Azure CLI | ||
1. This repo contains a CodeSpaces configuration with all the required tools already installed. | ||
1. Login into devtunnels: `devtunnel user login`. | ||
1. Login into Azure: `az login`. | ||
1. Run the script `_tools/configure-abs-tunnel.sh`. | ||
1. This script configures devtunnels and ABS, using the machine name, and produces a `.env` file. | ||
1. Copy the generated `<your-machine>.env` file to the sample folder with file name `.env`. | ||
1. Start the tunnel before debugging `devtunnel host <tunnel_id>` | ||
|
||
> [!Tip] | ||
> You can use the same ABS instance and tunnel to debug any of these samples, with the only caveat that you can only use one at a time. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
| ||
Microsoft Visual Studio Solution File, Format Version 12.00 | ||
# Visual Studio Version 17 | ||
VisualStudioVersion = 17.13.35818.85 d17.13 | ||
MinimumVisualStudioVersion = 10.0.40219.1 | ||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoBot", "basic\echo-bot\dotnet\EchoBot.csproj", "{B1FC20C1-9900-FAC4-B8E6-223888633020}" | ||
EndProject | ||
Global | ||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
Debug|Any CPU = Debug|Any CPU | ||
Release|Any CPU = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
{B1FC20C1-9900-FAC4-B8E6-223888633020}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{B1FC20C1-9900-FAC4-B8E6-223888633020}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{B1FC20C1-9900-FAC4-B8E6-223888633020}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{B1FC20C1-9900-FAC4-B8E6-223888633020}.Release|Any CPU.Build.0 = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(SolutionProperties) = preSolution | ||
HideSolutionNode = FALSE | ||
EndGlobalSection | ||
GlobalSection(ExtensibilityGlobals) = postSolution | ||
SolutionGuid = {46F8E711-A447-4AB5-8357-03A5B5D21B49} | ||
EndGlobalSection | ||
EndGlobal |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
#!/bin/bash | ||
|
||
resource_group=$HOSTNAME-dev | ||
tunnel_id=$HOSTNAME-tunnel | ||
|
||
if ! az account show &> /dev/null; then | ||
echo "Please login to Azure CLI." | ||
az login | ||
fi | ||
|
||
if ! devtunnel list &> /dev/null; then | ||
echo "Please login to devtunnel." | ||
devtunnel user login | ||
fi | ||
|
||
if ! devtunnel show $tunnel_id &> /dev/null; then | ||
echo "Creating tunnel: $tunnel_id in port 3978" | ||
devtunnel create $tunnel_id -a | ||
devtunnel port create $tunnel_id -p 3978 | ||
fi | ||
|
||
if ! az group show --name $resource_group &> /dev/null; then | ||
echo "Creating resource group: $resource_group" | ||
az group create --name $resource_group --location eastus | ||
fi | ||
|
||
tunnel_details=$(devtunnel show $tunnel_id -j -v) | ||
tunnel_url=$(echo $tunnel_details | grep -oP '(?<="portForwardingUris": \[ ")[^"]+(?=" \])') | ||
echo $tunnel_url | ||
|
||
appId=$(az ad app create --display-name $tunnel_id --sign-in-audience "AzureADMyOrg" --query appId -o tsv) | ||
echo "Created AppId: " $appId | ||
secretJson=$(az ad app credential reset --id $appId | jq .) | ||
|
||
clientId=$(echo $secretJson | jq .appId | tr -d '"') | ||
tenantId=$(echo $secretJson | jq .tenant | tr -d '"') | ||
clientSecret=$(echo $secretJson | jq .password | tr -d '"') | ||
|
||
echo "clientId=$clientId" > "$tunnel_id.env" | ||
echo "tenantId=$tenantId" >> "$tunnel_id.env" | ||
echo "clientSecret=$clientSecret" >> "$tunnel_id.env" | ||
echo "DEBUG=agents:*.info" >> "$tunnel_id.env" | ||
|
||
echo "Env File created: $tunnel_id.env" | ||
|
||
endpoint=$tunnel_url"api/messages" | ||
|
||
botJson=$(az bot create \ | ||
--app-type SingleTenant \ | ||
--appid $appId \ | ||
--tenant-id $tenantId \ | ||
--name $tunnel_id \ | ||
--resource-group $resource_group \ | ||
--endpoint $endpoint) | ||
|
||
echo $botJson | ||
$teamsBotJson=$(az bot msteams create -n $tunnel_id -g $resource_group) | ||
echo $teamsBotJson | ||
|
||
echo "Bot created: $tunnel_id" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using Microsoft.Agents.Authentication; | ||
using Microsoft.AspNetCore.Authentication.JwtBearer; | ||
using Microsoft.IdentityModel.Protocols; | ||
using Microsoft.IdentityModel.Protocols.OpenIdConnect; | ||
using Microsoft.IdentityModel.Tokens; | ||
using Microsoft.IdentityModel.Validators; | ||
using System.Collections.Concurrent; | ||
using System.Globalization; | ||
using System.IdentityModel.Tokens.Jwt; | ||
|
||
namespace Microsoft.Agents.Samples | ||
{ | ||
public static class AspNetExtensions | ||
{ | ||
private static readonly ConcurrentDictionary<string, ConfigurationManager<OpenIdConnectConfiguration>> _openIdMetadataCache = new(); | ||
|
||
/// <summary> | ||
/// Adds token validation typical for ABS/SMBA and Bot-to-bot. | ||
/// default to Azure Public Cloud. | ||
/// </summary> | ||
/// <param name="services"></param> | ||
/// <param name="configuration"></param> | ||
/// <param name="tokenValidationSectionName">Name of the config section to read.</param> | ||
/// <param name="logger">Optional logger to use for authentication event logging.</param> | ||
/// <remarks> | ||
/// Configuration: | ||
/// <code> | ||
/// "TokenValidation": { | ||
/// "Audiences": [ | ||
/// "{required:bot-appid}" | ||
/// ], | ||
/// "TenantId": "{recommended:tenant-id}", | ||
/// "ValidIssuers": [ | ||
/// "{default:Public-AzureBotService}" | ||
/// ], | ||
/// "IsGov": {optional:false}, | ||
/// "AzureBotServiceOpenIdMetadataUrl": optional, | ||
/// "OpenIdMetadataUrl": optional, | ||
/// "AzureBotServiceTokenHandling": "{optional:true}" | ||
/// "OpenIdMetadataRefresh": "optional-12:00:00" | ||
/// } | ||
/// </code> | ||
/// | ||
/// `IsGov` can be omitted, in which case public Azure Bot Service and Azure Cloud metadata urls are used. | ||
/// `ValidIssuers` can be omitted, in which case the Public Azure Bot Service issuers are used. | ||
/// `TenantId` can be omitted if the Agent is not being called by another Agent. Otherwise it is used to add other known issuers. Only when `ValidIssuers` is omitted. | ||
/// `AzureBotServiceOpenIdMetadataUrl` can be omitted. In which case default values in combination with `IsGov` is used. | ||
/// `OpenIdMetadataUrl` can be omitted. In which case default values in combination with `IsGov` is used. | ||
/// `AzureBotServiceTokenHandling` defaults to true and should always be true until Azure Bot Service sends Entra ID token. | ||
/// </remarks> | ||
public static void AddBotAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation", ILogger logger = null!) | ||
{ | ||
IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); | ||
List<string> validTokenIssuers = tokenValidationSection.GetSection("ValidIssuers").Get<List<string>>()!; | ||
List<string> audiences = tokenValidationSection.GetSection("Audiences").Get<List<string>>()!; | ||
|
||
if (!tokenValidationSection.Exists()) | ||
{ | ||
logger?.LogError($"Missing configuration section '{tokenValidationSectionName}'. This section is required to be present in appsettings.json"); | ||
throw new InvalidOperationException($"Missing configuration section '{tokenValidationSectionName}'. This section is required to be present in appsettings.json"); | ||
} | ||
|
||
// 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", | ||
"https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", | ||
"https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", | ||
]; | ||
|
||
string tenantId = tokenValidationSection["TenantId"]!; | ||
if (!string.IsNullOrEmpty(tenantId)) | ||
{ | ||
validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, tenantId)); | ||
validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, tenantId)); | ||
} | ||
} | ||
|
||
if (audiences == null || audiences.Count == 0) | ||
{ | ||
throw new ArgumentException($"{tokenValidationSectionName}:Audiences requires at least one value"); | ||
} | ||
|
||
bool isGov = tokenValidationSection.GetValue<bool>("IsGov", false); | ||
bool azureBotServiceTokenHandling = tokenValidationSection.GetValue<bool>("AzureBotServiceTokenHandling", true); | ||
|
||
// If the `AzureBotServiceOpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate ABS tokens. | ||
string? azureBotServiceOpenIdMetadataUrl = tokenValidationSection["AzureBotServiceOpenIdMetadataUrl"]; | ||
if (string.IsNullOrEmpty(azureBotServiceOpenIdMetadataUrl)) | ||
{ | ||
azureBotServiceOpenIdMetadataUrl = isGov ? AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl : AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl; | ||
} | ||
|
||
// If the `OpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate Entra ID tokens. | ||
string? openIdMetadataUrl = tokenValidationSection["OpenIdMetadataUrl"]; | ||
if (string.IsNullOrEmpty(openIdMetadataUrl)) | ||
{ | ||
openIdMetadataUrl = isGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl; | ||
} | ||
|
||
TimeSpan openIdRefreshInterval = tokenValidationSection.GetValue<TimeSpan>("OpenIdMetadataRefresh", BaseConfigurationManager.DefaultAutomaticRefreshInterval); | ||
|
||
services.AddAuthentication(options => | ||
{ | ||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; | ||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; | ||
}) | ||
.AddJwtBearer(options => | ||
{ | ||
options.SaveToken = true; | ||
options.TokenValidationParameters = new TokenValidationParameters | ||
{ | ||
ValidateIssuer = true, | ||
ValidateAudience = true, | ||
ValidateLifetime = true, | ||
ClockSkew = TimeSpan.FromMinutes(5), | ||
ValidIssuers = validTokenIssuers, | ||
ValidAudiences = audiences, | ||
ValidateIssuerSigningKey = true, | ||
RequireSignedTokens = true, | ||
}; | ||
|
||
// Using Microsoft.IdentityModel.Validators | ||
options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); | ||
|
||
options.Events = new JwtBearerEvents | ||
{ | ||
// Create a ConfigurationManager based on the requestor. This is to handle ABS non-Entra tokens. | ||
OnMessageReceived = async context => | ||
{ | ||
string authorizationHeader = context.Request.Headers.Authorization.ToString(); | ||
|
||
if (string.IsNullOrEmpty(authorizationHeader)) | ||
{ | ||
// Default to AadTokenValidation handling | ||
context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; | ||
await Task.CompletedTask.ConfigureAwait(false); | ||
return; | ||
} | ||
|
||
string[] parts = authorizationHeader?.Split(' ')!; | ||
if (parts.Length != 2 || parts[0] != "Bearer") | ||
{ | ||
// Default to AadTokenValidation handling | ||
context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; | ||
await Task.CompletedTask.ConfigureAwait(false); | ||
return; | ||
} | ||
|
||
JwtSecurityToken token = new(parts[1]); | ||
string? issuer = token.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.IssuerClaim)?.Value; | ||
|
||
if (azureBotServiceTokenHandling && AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer)) | ||
{ | ||
// Use the Bot Framework authority for this configuration manager | ||
context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(azureBotServiceOpenIdMetadataUrl, key => | ||
{ | ||
return new ConfigurationManager<OpenIdConnectConfiguration>(azureBotServiceOpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) | ||
{ | ||
AutomaticRefreshInterval = openIdRefreshInterval | ||
}; | ||
}); | ||
} | ||
else | ||
{ | ||
context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(openIdMetadataUrl, key => | ||
{ | ||
return new ConfigurationManager<OpenIdConnectConfiguration>(openIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) | ||
{ | ||
AutomaticRefreshInterval = openIdRefreshInterval | ||
}; | ||
}); | ||
} | ||
|
||
await Task.CompletedTask.ConfigureAwait(false); | ||
}, | ||
|
||
OnTokenValidated = context => | ||
{ | ||
logger?.LogDebug("TOKEN Validated"); | ||
return Task.CompletedTask; | ||
}, | ||
OnForbidden = context => | ||
{ | ||
logger?.LogWarning(context.Result.ToString()); | ||
return Task.CompletedTask; | ||
}, | ||
OnAuthenticationFailed = context => | ||
{ | ||
logger?.LogWarning(context.Exception.ToString()); | ||
return Task.CompletedTask; | ||
} | ||
}; | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// 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 EchoBot | ||
{ | ||
// 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. | ||
[Authorize] | ||
[ApiController] | ||
[Route("api/messages")] | ||
public class BotController(IBotHttpAdapter adapter, IBot bot) : ControllerBase | ||
{ | ||
[HttpPost] | ||
public Task PostAsync(CancellationToken cancellationToken) | ||
=> adapter.ProcessAsync(Request, Response, bot, cancellationToken); | ||
|
||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure if having 2 CI is the right thing (we could also add some path filters), or just build ALL samples across languages in a single GHAction.