diff --git a/.github/workflows/bot-daily-task-reminder-csharp.yml b/.github/workflows/bot-daily-task-reminder-csharp.yml index 9eaea9ce7b..757ea3452c 100644 --- a/.github/workflows/bot-daily-task-reminder-csharp.yml +++ b/.github/workflows/bot-daily-task-reminder-csharp.yml @@ -27,7 +27,7 @@ jobs: - name: Set up .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: '6.0.x' + dotnet-version: '10.0.x' include-prerelease: true - name: Build app diff --git a/.github/workflows/build-complete-samples.yml b/.github/workflows/build-complete-samples.yml index e7129e8df9..290ad89949 100644 --- a/.github/workflows/build-complete-samples.yml +++ b/.github/workflows/build-complete-samples.yml @@ -458,7 +458,7 @@ jobs: - project_path: 'samples/bot-configuration-app/csharp/Bot configuration/Bot Configuration.csproj' name: 'bot-configuration-app' - version: '6.0.x' + version: '10.0.x' - project_path: 'samples/bot-configuration-app-auth/csharp/Bot configuration/Bot Configuration.csproj' name: 'bot-configuration-app-auth' @@ -512,6 +512,10 @@ jobs: name: 'graph-chat-migration' version: '8.0.x' + - project_path: 'samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/MEAISentimentAnalysis.csproj' + name: 'msgext-ai-sentiment-analysis' + version: '10.0.x' + fail-fast: false name: Build All "${{ matrix.name }}" csharp steps: diff --git a/samples/bot-configuration-app/csharp/.gitignore b/samples/bot-configuration-app/csharp/.gitignore deleted file mode 100644 index 763e8697f9..0000000000 --- a/samples/bot-configuration-app/csharp/.gitignore +++ /dev/null @@ -1,288 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -**/Properties/launchSettings.json - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppManifest/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs diff --git a/samples/bot-configuration-app/csharp/Bot Configuration.sln b/samples/bot-configuration-app/csharp/Bot Configuration.sln deleted file mode 100644 index eca9ec84ff..0000000000 --- a/samples/bot-configuration-app/csharp/Bot Configuration.sln +++ /dev/null @@ -1,37 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.7.34302.85 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot Configuration", "Bot configuration\Bot Configuration.csproj", "{783D6E6F-7339-4D74-89D4-B240496CC7A3}" -EndProject -Project("{A9E3F50B-275E-4AF7-ADCE-8BE12D41E305}") = "M365Agent", "M365Agent\M365Agent.ttkproj", "{7D531FB4-DCCB-4DB0-9DE7-B5236577981E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1988FCDA-75C6-4AD1-8B0C-70FEC875A006}" - ProjectSection(SolutionItems) = preProject - Bot Configuration.slnLaunch.user = Bot Configuration.slnLaunch.user - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {783D6E6F-7339-4D74-89D4-B240496CC7A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {783D6E6F-7339-4D74-89D4-B240496CC7A3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {783D6E6F-7339-4D74-89D4-B240496CC7A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {783D6E6F-7339-4D74-89D4-B240496CC7A3}.Release|Any CPU.Build.0 = Release|Any CPU - {7D531FB4-DCCB-4DB0-9DE7-B5236577981E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7D531FB4-DCCB-4DB0-9DE7-B5236577981E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7D531FB4-DCCB-4DB0-9DE7-B5236577981E}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {7D531FB4-DCCB-4DB0-9DE7-B5236577981E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7D531FB4-DCCB-4DB0-9DE7-B5236577981E}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C0264D40-8A26-4CAF-9DE9-0CFF497234D8} - EndGlobalSection -EndGlobal diff --git a/samples/bot-configuration-app/csharp/Bot Configuration.slnLaunch.user b/samples/bot-configuration-app/csharp/Bot Configuration.slnLaunch.user index 541f4d3174..0efc2f8f1c 100644 --- a/samples/bot-configuration-app/csharp/Bot Configuration.slnLaunch.user +++ b/samples/bot-configuration-app/csharp/Bot Configuration.slnLaunch.user @@ -1,31 +1,52 @@ [ { - "Name": "Microsoft Teams (browser)", + "Name": "Microsoft 365 Agents Playground (browser)", "Projects": [ { - "Path": "Bot configuration\\Bot Configuration.csproj", - "Action": "Start", - "DebugTarget": "Start Project" + "Path": "M365Agent\\M365Agent.atkproj", + "Name": "M365Agent\\M365Agent.atkproj", + "Action": "StartWithoutDebugging", + "DebugTarget": "Microsoft 365 Agents Playground (browser)" }, { - "Path": "M365Agent\\M365Agent.ttkproj", - "Action": "StartWithoutDebugging", - "DebugTarget": "Microsoft Teams (browser)" + "Path": "Bot Configuration\\Bot Configuration.csproj", + "Name": "Bot Configuration\\Bot Configuration.csproj", + "Action": "Start", + "DebugTarget": "Microsoft 365 Agents Playground" } ] }, { - "Name": "Microsoft Teams (browser) (skip update app)", + "Name": "Microsoft Teams (browser)", "Projects": [ { - "Path": "Bot configuration\\Bot Configuration.csproj", + "Path": "M365Agent\\M365Agent.atkproj", + "Name": "M365Agent\\M365Agent.atkproj", + "Action": "StartWithoutDebugging", + "DebugTarget": "Microsoft Teams (browser)" + }, + { + "Path": "Bot Configuration\\Bot Configuration.csproj", + "Name": "Bot Configuration\\Bot Configuration.csproj", "Action": "Start", "DebugTarget": "Start Project" - }, + } + ] + }, + { + "Name": "Microsoft Teams (browser) (skip update app)", + "Projects": [ { - "Path": "M365Agent\\M365Agent.ttkproj", + "Path": "M365Agent\\M365Agent.atkproj", + "Name": "M365Agent\\M365Agent.atkproj", "Action": "StartWithoutDebugging", "DebugTarget": "Microsoft Teams (browser) (skip update app)" + }, + { + "Path": "Bot Configuration\\Bot Configuration.csproj", + "Name": "Bot Configuration\\Bot Configuration.csproj", + "Action": "Start", + "DebugTarget": "Start Project" } ] } diff --git a/samples/bot-configuration-app/csharp/Bot Configuration.slnx b/samples/bot-configuration-app/csharp/Bot Configuration.slnx new file mode 100644 index 0000000000..b1b1d1f3f2 --- /dev/null +++ b/samples/bot-configuration-app/csharp/Bot Configuration.slnx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/samples/bot-configuration-app/csharp/Bot Configuration/Config.cs b/samples/bot-configuration-app/csharp/Bot Configuration/Config.cs new file mode 100644 index 0000000000..ef071453ef --- /dev/null +++ b/samples/bot-configuration-app/csharp/Bot Configuration/Config.cs @@ -0,0 +1,15 @@ +namespace Bot_Configuration +{ + public class ConfigOptions + { + public TeamsConfigOptions Teams { get; set; } + } + + public class TeamsConfigOptions + { + public string BotType { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string TenantId { get; set; } + } +} \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot Configuration/Controllers/Controller.cs b/samples/bot-configuration-app/csharp/Bot Configuration/Controllers/Controller.cs new file mode 100644 index 0000000000..c9079c44a2 --- /dev/null +++ b/samples/bot-configuration-app/csharp/Bot Configuration/Controllers/Controller.cs @@ -0,0 +1,383 @@ +using Microsoft.Teams.Api.Activities; +using Microsoft.Teams.Api.TaskModules; +using Microsoft.Teams.Apps; +using Microsoft.Teams.Apps.Activities; +using Microsoft.Teams.Apps.Activities.Invokes; +using Microsoft.Teams.Apps.Annotations; +using Microsoft.Teams.Cards; +using System.Text.Json; +using AdaptiveCard = Microsoft.Teams.Cards.AdaptiveCard; + +namespace Bot_Configuration.Controllers +{ + [TeamsController] + public class Controller() + { + private string chosenFlow = ""; + + [Conversation.MembersAdded] + public async System.Threading.Tasks.Task OnMembersAdded([Context] ConversationUpdateActivity activity, [Context] IContext.Client client, [Context] Microsoft.Teams.Common.Logging.ILogger log) + { + log.Info("[MEMBERS_ADDED] Members added to conversation"); + + var imagePath = "Images/configbutton.png"; + string imageData = ""; + + if (File.Exists(imagePath)) + { + imageData = Convert.ToBase64String(File.ReadAllBytes(imagePath)); + } + + var card = new AdaptiveCard + { + Body = new List + { + new TextBlock("Hello and welcome! With this sample, you can experience the functionality of bot configuration.") + { + Wrap = true, + Size = TextSize.Large, + Weight = TextWeight.Bolder + }, + new TextBlock("Click the button below to open the bot configuration dialog.") + { + Wrap = true, + Size = TextSize.Medium + } + } + }; + + + if (!string.IsNullOrEmpty(imageData)) + { + card.Body.Add(new Image($"data:image/png;base64,{imageData}")); + } + + + card.Actions = new List + { + new TaskFetchAction(new Dictionary { { "opendialogtype", "bot_configuration" } }) + { + Title = "Open Bot Configuration" + } + }; + + await client.Send(card); + } + + [Message] + public async System.Threading.Tasks.Task OnMessage([Context] Microsoft.Teams.Api.Activities.MessageActivity activity, [Context] IContext.Client client, [Context] Microsoft.Teams.Common.Logging.ILogger log) + { + log.Info("[MESSAGE] Message received"); + + if (activity.Text != null) + { + var text = activity.Text.ToLower().Trim(); + if (text == "chosen flow" || text == "bot configuration sample app chosen flow") + { + await client.Send($"Bot configured for {chosenFlow} flow"); + } + } + else if (activity.Value != null) + { + var choiceSelect = activity.Value; + await client.Send($"Selected option is: {choiceSelect}"); + } + } + + /// + /// Handles the config/fetch invoke when bot is configured in a team or group chat + /// + [Invoke("config/fetch")] + public object OnConfigFetch([Context] InvokeActivity activity, [Context] Microsoft.Teams.Common.Logging.ILogger log) + { + log.Info("[CONFIG_FETCH] Bot configuration fetch received"); + + + var configCard = CreateConfigurationCard(); + + return new + { + config = new + { + type = "continue", + value = new + { + title = "Configure Bot", + width = 600, + height = 600, + card = new + { + contentType = "application/vnd.microsoft.card.adaptive", + content = configCard + } + } + } + }; + } + + /// + /// Handles the config/submit invoke when bot configuration is submitted + /// + [Invoke("config/submit")] + public async System.Threading.Tasks.Task OnConfigSubmit([Context] InvokeActivity activity, [Context] IContext.Client client, [Context] Microsoft.Teams.Common.Logging.ILogger log) + { + log.Info("[CONFIG_SUBMIT] Bot configuration submit received"); + + try + { + if (activity.Value == null) + { + log.Info("[CONFIG_SUBMIT] activity.Value is null"); + return new + { + config = new + { + type = "message", + value = "No configuration data was provided." + } + }; + } + + var valueJson = JsonSerializer.Serialize(activity.Value); + log.Info($"[CONFIG_SUBMIT] Full activity.Value: {valueJson}"); + + var valueElement = JsonSerializer.Deserialize(valueJson); + + + string? GetFormValue(string key) + { + + if (valueElement.TryGetProperty(key, out var val)) + { + if (val.ValueKind == JsonValueKind.String) + { + var strValue = val.GetString(); + log.Info($"[CONFIG_SUBMIT] {key} (root) = {strValue}"); + return strValue; + } + if (val.ValueKind == JsonValueKind.True || val.ValueKind == JsonValueKind.False) + { + var boolValue = val.GetBoolean().ToString(); + log.Info($"[CONFIG_SUBMIT] {key} (root) = {boolValue}"); + return boolValue; + } + } + + + if (valueElement.TryGetProperty("data", out var dataElement)) + { + if (dataElement.TryGetProperty(key, out var dataVal)) + { + if (dataVal.ValueKind == JsonValueKind.String) + { + var strValue = dataVal.GetString(); + log.Info($"[CONFIG_SUBMIT] {key} (data) = {strValue}"); + return strValue; + } + if (dataVal.ValueKind == JsonValueKind.True || dataVal.ValueKind == JsonValueKind.False) + { + var boolValue = dataVal.GetBoolean().ToString(); + log.Info($"[CONFIG_SUBMIT] {key} (data) = {boolValue}"); + return boolValue; + } + } + } + + log.Info($"[CONFIG_SUBMIT] {key} not found in root or data"); + return null; + } + + + var type = GetFormValue("dropdown01"); + var priority = GetFormValue("dropdown02"); + var issue = GetFormValue("dropdown1"); + var comment = GetFormValue("dropdown2"); + var assignee = GetFormValue("dropdown3"); + var status = GetFormValue("dropdown4"); + var toggleComment = GetFormValue("toggleComment"); + var toggleAssign = GetFormValue("toggleAssign"); + var toggleTransition = GetFormValue("toggleTransition"); + var toggleStatus = GetFormValue("toggleStatus"); + + + chosenFlow = type ?? "Bug"; + + log.Info($"[CONFIG_SUBMIT] Bot configured with flow: {chosenFlow}"); + + + var summaryCard = new AdaptiveCard + { + Body = new List + { + new TextBlock("The selection you requested is as follows:") + { + Wrap = true, + Size = TextSize.Medium + } + } + }; + + + AddTextBlockIfNotEmpty(summaryCard, "Type", type); + AddTextBlockIfNotEmpty(summaryCard, "Priority", priority); + AddTextBlockIfNotEmpty(summaryCard, "Issue", issue); + AddTextBlockIfNotEmpty(summaryCard, "Comment", comment); + AddTextBlockIfNotEmpty(summaryCard, "Assignee", assignee); + AddTextBlockIfNotEmpty(summaryCard, "Status", status); + + summaryCard.Body.Add(new TextBlock("Actions to be displayed:") + { + Wrap = true + }); + + AddTextBlockIfTrue(summaryCard, "Comment", toggleComment); + AddTextBlockIfTrue(summaryCard, "Assign", toggleAssign); + AddTextBlockIfTrue(summaryCard, "Transition", toggleTransition); + AddTextBlockIfTrue(summaryCard, "Update status", toggleStatus); + + + await client.Send(summaryCard); + + + return new + { + config = new + { + type = "message", + value = "Your request has been submitted successfully!" + } + }; + } + catch (Exception ex) + { + log.Error($"[CONFIG_SUBMIT] Error processing configuration: {ex.Message}"); + log.Error($"[CONFIG_SUBMIT] Stack trace: {ex.StackTrace}"); + return new + { + config = new + { + type = "message", + value = "An error occurred while saving the configuration." + } + }; + } + } + + private AdaptiveCard CreateConfigurationCard() + { + var card = new AdaptiveCard + { + Body = new List + { + new TextBlock("Bot Configuration") + { + Size = TextSize.ExtraLarge, + Weight = TextWeight.Bolder + }, + new TextBlock("For issues that match these criteria:") + { + Weight = TextWeight.Bolder + }, + CreateDropdownSection("Type", "dropdown01", new List + { + new Choice { Title = "Bug", Value = "Bug" }, + new Choice { Title = "Feature Request", Value = "Feature Request" }, + new Choice { Title = "Task", Value = "Task" } + }), + CreateDropdownSection("Priority", "dropdown02", new List + { + new Choice { Title = "Low", Value = "Low" }, + new Choice { Title = "Medium", Value = "Medium" }, + new Choice { Title = "High", Value = "High" } + }), + new TextBlock("Post to channel when:") + { + Weight = TextWeight.Bolder + }, + CreateDropdownSection("Issue", "dropdown1", new List + { + new Choice { Title = "Software Issue", Value = "Software Issue" }, + new Choice { Title = "Server Issue", Value = "Server Issue" }, + new Choice { Title = "Network Issue", Value = "Network Issue" } + }, true), + CreateDropdownSection("Comment", "dropdown2", new List + { + new Choice { Title = "Network problem in server", Value = "Network problem in server" }, + new Choice { Title = "Loadbalancer issue", Value = "Loadbalancer issue" }, + new Choice { Title = "Software needs to be updated", Value = "Software needs to be updated" } + }), + CreateDropdownSection("Assignee", "dropdown3", new List + { + new Choice { Title = "Jasmine Smith", Value = "Jasmine Smith" }, + new Choice { Title = "Ethan Johnson", Value = "Ethan Johnson" }, + new Choice { Title = "Maya Rodriguez", Value = "Maya Rodriguez" } + }), + CreateDropdownSection("Status changed", "dropdown4", new List + { + new Choice { Title = "Open", Value = "Open" }, + new Choice { Title = "Inprogress", Value = "Inprogress" }, + new Choice { Title = "Completed", Value = "Completed" } + }), + new TextBlock("Actions to display") + { + Weight = TextWeight.Bolder + }, + new ToggleInput("Assign") { Id = "toggleAssign", Value = "false" }, + new ToggleInput("Comment") { Id = "toggleComment", Value = "false" }, + new ToggleInput("Transition") { Id = "toggleTransition", Value = "false" }, + new ToggleInput("Update status") { Id = "toggleStatus", Value = "false" } + }, + Actions = new List + { + new SubmitAction + { + Title = "Submit" + } + } + }; + + return card; + } + + private CardElement CreateDropdownSection(string label, string id, List choices, bool isMultiSelect = false) + { + return new Container + { + Items = new List + { + new TextBlock(label) + { + Weight = TextWeight.Bolder + }, + new ChoiceSetInput(choices.ToArray()) + { + Id = id, + IsMultiSelect = isMultiSelect + } + } + }; + } + + private void AddTextBlockIfNotEmpty(AdaptiveCard card, string label, string? value) + { + if (!string.IsNullOrEmpty(value) && value != "null") + { + card.Body.Add(new TextBlock($"{label} : {value}") + { + Wrap = true + }); + } + } + + private void AddTextBlockIfTrue(AdaptiveCard card, string label, string? value) + { + if (value?.ToLower() == "true") + { + card.Body.Add(new TextBlock($"{label} : True") + { + Wrap = true + }); + } + } + } +} \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot Configuration/appsettings.Playground.json b/samples/bot-configuration-app/csharp/Bot Configuration/appsettings.Playground.json new file mode 100644 index 0000000000..0497106b3c --- /dev/null +++ b/samples/bot-configuration-app/csharp/Bot Configuration/appsettings.Playground.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "TenantId": "", + "BotType": "" + } +} \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot configuration/.gitignore b/samples/bot-configuration-app/csharp/Bot configuration/.gitignore index 7466c01f9c..77c7154916 100644 --- a/samples/bot-configuration-app/csharp/Bot configuration/.gitignore +++ b/samples/bot-configuration-app/csharp/Bot configuration/.gitignore @@ -5,6 +5,7 @@ env/.env.*.user env/.env.local appsettings.Development.json .deployment +appsettings.Playground.json # User-specific files *.user @@ -22,4 +23,8 @@ bld/ [Ll]og/ # Notification local store -.notification.localstore.json \ No newline at end of file +.notification.localstore.json +.notification.playgroundstore.json + +# devTools +devTools/ \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot configuration/AdapterWithErrorHandler.cs b/samples/bot-configuration-app/csharp/Bot configuration/AdapterWithErrorHandler.cs deleted file mode 100644 index aa24b0cf07..0000000000 --- a/samples/bot-configuration-app/csharp/Bot configuration/AdapterWithErrorHandler.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// -// Generated with Bot Builder V4 SDK Template for Visual Studio CoreBot v4.6.2 - -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Bot.Builder.TraceExtensions; -using Microsoft.Extensions.Logging; -using Microsoft.Bot.Connector.Authentication; - -namespace Microsoft.Teams.Samples.HelloWorld.Web -{ - public class AdapterWithErrorHandler : CloudAdapter - { - public AdapterWithErrorHandler(BotFrameworkAuthentication botFrameworkAuthentication, ILogger logger) - : base(botFrameworkAuthentication, logger) - { - OnTurnError = async (turnContext, exception) => - { - // Log any leaked exception from the application. - logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); - - // Uncomment below commented line for local debugging. - // await turnContext.SendActivityAsync($"Sorry, it looks like something went wrong. Exception Caught: {exception.Message}"); - - // Send a trace activity, which will be displayed in the Bot Framework Emulator - await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError"); - }; - } - } -} diff --git a/samples/bot-configuration-app/csharp/Bot configuration/Bot Configuration.csproj b/samples/bot-configuration-app/csharp/Bot configuration/Bot Configuration.csproj index e65308dbe5..ca85721905 100644 --- a/samples/bot-configuration-app/csharp/Bot configuration/Bot Configuration.csproj +++ b/samples/bot-configuration-app/csharp/Bot configuration/Bot Configuration.csproj @@ -1,26 +1,36 @@ - + - net6.0 - latest + net10.0 + enable - - + + + + + + - - - - + + PreserveNewest + + - - Always + + + PreserveNewest + None + + + + PreserveNewest + None - \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot configuration/Bots/TeamsBot.cs b/samples/bot-configuration-app/csharp/Bot configuration/Bots/TeamsBot.cs deleted file mode 100644 index 2d9eb13ef7..0000000000 --- a/samples/bot-configuration-app/csharp/Bot configuration/Bots/TeamsBot.cs +++ /dev/null @@ -1,402 +0,0 @@ -using AdaptiveCards; -using Bogus.DataSets; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Teams; -using Microsoft.Bot.Schema; -using Microsoft.Bot.Schema.Teams; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Net.Http; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using System.IO; - -namespace Botconfiguration.Bots -{ - /// - /// TeamsBot handles Teams-specific activities. - /// - public class TeamsBot : TeamsActivityHandler - { - private string chosenFlow = ""; - - /// - /// Handles members added to the conversation. - /// - protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) - { - var imagePath = "Images/configbutton.png"; - var imageData = Convert.ToBase64String(File.ReadAllBytes(imagePath)); - - var adaptiveCardJson = $@" - {{ - ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", - ""type"": ""AdaptiveCard"", - ""version"": ""1.0"", - ""body"": [ - {{ - ""type"": ""TextBlock"", - ""text"": ""Hello and welcome! With this sample, you can experience the functionality of bot configuration. To access Bot configuration, click on the settings button in the bot description card."", - ""wrap"": true, - ""size"": ""large"", - ""weight"": ""bolder"" - }}, - {{ - ""type"": ""Image"", - ""url"": ""data:image/png;base64,{imageData}"", - ""size"": ""auto"" - }} - ], - ""fallbackText"": ""This card requires Adaptive Card support."" - }}"; - - var attachment = new Attachment - { - ContentType = AdaptiveCard.ContentType, - Content = JsonConvert.DeserializeObject(adaptiveCardJson) - }; - - var reply = MessageFactory.Attachment(attachment); - await turnContext.SendActivityAsync(reply, cancellationToken); - } - - /// - /// Handles message activities. - /// - protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) - { - if (turnContext.Activity.Text != null) - { - var text = turnContext.Activity.Text.ToLower().Trim(); - if (text == "chosen flow" || text == "typeahead search adaptive card chosen flow") - { - await turnContext.SendActivityAsync($"Bot configured for {chosenFlow} flow", cancellationToken: cancellationToken); - } - } - else if (turnContext.Activity.Value != null) - { - var choiceSelect = turnContext.Activity.Value; - await turnContext.SendActivityAsync($"Selected option is: {choiceSelect}", cancellationToken: cancellationToken); - } - } - - /// - /// Handles configuration fetch requests. - /// - protected override Task OnTeamsConfigFetchAsync(ITurnContext turnContext, JObject configData, CancellationToken cancellationToken) - { - var response = CreateAdaptiveCardForContinue(); - return Task.FromResult(response); - } - - /// - /// Handles configuration submit requests. - /// - protected override async Task OnTeamsConfigSubmitAsync(ITurnContext turnContext, JObject configData, CancellationToken cancellationToken) - { - try - { - var data = JsonConvert.DeserializeObject(turnContext.Activity.Value.ToString()); - var card = CreateAdaptiveCardFromData(data); - - var attachment = new Attachment - { - ContentType = "application/vnd.microsoft.card.adaptive", - Content = card - }; - - var reply = MessageFactory.Attachment(attachment); - await turnContext.SendActivityAsync(reply, cancellationToken); - - var response = new ConfigResponse - { - Config = new TaskModuleMessageResponse - { - Type = "message", - Value = "Your request has been submitted successfully!" - } - }; - - return response; - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - return null; - } - } - - /// - /// Creates an adaptive card for the continue response. - /// - private ConfigResponseBase CreateAdaptiveCardForContinue() - { - var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 4)) - { - Body = new List - { - new AdaptiveColumnSet - { - Columns = new List - { - new AdaptiveColumn - { - Width = "stretch", - Items = new List - { - new AdaptiveTextBlock - { - Text = "For issues that match these criteria:", - Weight = AdaptiveTextWeight.Bolder, - Type = "TextBlock" - } - } - } - } - }, - CreateDropdownColumnSet("Type", "dropdown01", new List - { - new AdaptiveChoice { Title = "Bug", Value = "Bug" }, - new AdaptiveChoice { Title = "Feature Request", Value = "Feature Request" }, - new AdaptiveChoice { Title = "Task", Value = "Task" } - }), - CreateDropdownColumnSet("Priority", "dropdown02", new List - { - new AdaptiveChoice { Title = "Low", Value = "Low" }, - new AdaptiveChoice { Title = "Medium", Value = "Medium" }, - new AdaptiveChoice { Title = "High", Value = "High" } - }), - new AdaptiveColumnSet - { - Columns = new List - { - new AdaptiveColumn - { - Width = "stretch", - Items = new List - { - new AdaptiveTextBlock - { - Text = "Post to channel when :", - Weight = AdaptiveTextWeight.Bolder, - Type = "TextBlock" - } - } - } - } - }, - CreateDropdownColumnSet("Issue", "dropdown1", new List - { - new AdaptiveChoice { Title = "Software Issue", Value = "Software Issue" }, - new AdaptiveChoice { Title = "Server Issue", Value = "Server Issue" }, - new AdaptiveChoice { Title = "Network Issue", Value = "Network Issue" } - }, true), - CreateDropdownColumnSet("Comment", "dropdown2", new List - { - new AdaptiveChoice { Title = "Network problem in server", Value = "Network problem in server" }, - new AdaptiveChoice { Title = "Loadbalancer issue", Value = "Loadbalancer issue" }, - new AdaptiveChoice { Title = "Software needs to be updated", Value = "Software needs to be updated" } - }), - CreateDropdownColumnSet("Assignee", "dropdown3", new List - { - new AdaptiveChoice { Title = "Jasmine Smith", Value = "Jasmine Smith" }, - new AdaptiveChoice { Title = "Ethan Johnson", Value = "Ethan Johnson" }, - new AdaptiveChoice { Title = "Maya Rodriguez", Value = "Maya Rodriguez" } - }), - CreateDropdownColumnSet("Status changed", "dropdown4", new List - { - new AdaptiveChoice { Title = "Open", Value = "Open" }, - new AdaptiveChoice { Title = "Inprogress", Value = "Inprogress" }, - new AdaptiveChoice { Title = "Completed", Value = "Completed" } - }), - new AdaptiveColumnSet - { - Columns = new List - { - new AdaptiveColumn - { - Width = "stretch", - Items = new List - { - new AdaptiveTextBlock - { - Text = "Actions to display", - Weight = AdaptiveTextWeight.Bolder, - Type = "TextBlock" - } - } - } - } - }, - CreateToggleColumnSet("Assign", "toggleAssign"), - CreateToggleColumnSet("Comment", "toggleComment"), - CreateToggleColumnSet("Transition", "toggleTransition"), - CreateToggleColumnSet("Update status", "toggleStatus") - }, - Actions = new List - { - new AdaptiveSubmitAction - { - Type = AdaptiveSubmitAction.TypeName, - Id = "submit", - Title = "Submit" - } - } - }; - - var response = new ConfigResponse - { - Config = new TaskModuleContinueResponse - { - Value = new TaskModuleTaskInfo - { - Height = 500, - Width = 600, - Title = "Task module fetch response", - Card = new Attachment - { - ContentType = AdaptiveCard.ContentType, - Content = card - } - }, - Type = "continue" - } - }; - - return response; - } - - /// - /// Creates an adaptive card from the provided data. - /// - private AdaptiveCard CreateAdaptiveCardFromData(JObject data) - { - var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0)) - { - Body = new List - { - new AdaptiveTextBlock - { - Text = "The selection you requested is as follows:", - Weight = AdaptiveTextWeight.Bolder, - Type = "TextBlock", - Wrap = true - } - } - }; - - AddTextBlockIfNotEmpty(card, "Type", data["data"]?["dropdown01"]?.ToString()); - AddTextBlockIfNotEmpty(card, "Priority", data["data"]?["dropdown02"]?.ToString()); - AddTextBlockIfNotEmpty(card, "Issue", data["data"]?["dropdown1"]?.ToString()); - AddTextBlockIfNotEmpty(card, "Comment", data["data"]?["dropdown2"]?.ToString()); - AddTextBlockIfNotEmpty(card, "Assignee", data["data"]?["dropdown3"]?.ToString()); - AddTextBlockIfNotEmpty(card, "Status", data["data"]?["dropdown4"]?.ToString()); - - card.Body.Add(new AdaptiveTextBlock - { - Text = "Actions to be displayed:", - Weight = AdaptiveTextWeight.Bolder, - Type = "TextBlock" - }); - - AddTextBlockIfTrue(card, "Status", data["data"]?["toggleStatus"]?.ToString()); - AddTextBlockIfTrue(card, "Assign", data["data"]?["toggleAssign"]?.ToString()); - AddTextBlockIfTrue(card, "Comment", data["data"]?["toggleComment"]?.ToString()); - AddTextBlockIfTrue(card, "Transition", data["data"]?["toggleTransition"]?.ToString()); - - return card; - } - - /// - /// Adds a text block to the card if the value is not empty. - /// - private void AddTextBlockIfNotEmpty(AdaptiveCard card, string label, string value) - { - if (!string.IsNullOrEmpty(value)) - { - card.Body.Add(new AdaptiveTextBlock - { - Text = $"{label} : {value}", - Wrap = true - }); - } - } - - /// - /// Adds a text block to the card if the value is "True". - /// - private void AddTextBlockIfTrue(AdaptiveCard card, string label, string value) - { - if (value == "True") - { - card.Body.Add(new AdaptiveTextBlock - { - Text = $"{label} : {value}", - Wrap = true - }); - } - } - - /// - /// Creates a dropdown column set. - /// - private AdaptiveColumnSet CreateDropdownColumnSet(string label, string id, List choices, bool isMultiSelect = false) - { - return new AdaptiveColumnSet - { - Columns = new List - { - new AdaptiveColumn - { - Width = "stretch", - Items = new List - { - new AdaptiveTextBlock - { - Text = label, - Weight = AdaptiveTextWeight.Bolder, - Type = "TextBlock" - }, - new AdaptiveChoiceSetInput - { - Id = id, - Choices = choices, - IsMultiSelect = isMultiSelect - } - } - } - } - }; - } - - /// - /// Creates a toggle column set. - /// - private AdaptiveColumnSet CreateToggleColumnSet(string title, string id) - { - return new AdaptiveColumnSet - { - Columns = new List - { - new AdaptiveColumn - { - Width = "stretch", - Items = new List - { - new AdaptiveToggleInput - { - Title = title, - Id = id, - Value = "false" - } - } - } - } - }; - } - } -} \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot configuration/Controllers/BotController.cs b/samples/bot-configuration-app/csharp/Bot configuration/Controllers/BotController.cs deleted file mode 100644 index d483885451..0000000000 --- a/samples/bot-configuration-app/csharp/Bot configuration/Controllers/BotController.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// -// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.6.2 - -using Microsoft.AspNetCore.Mvc; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using System.Threading.Tasks; - -namespace Botconfiguration.Controllers -{ - // This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot - // implementation at runtime. Multiple different IBot implementations running at different endpoints can be - // achieved by specifying a more specific type for the bot constructor argument. - [Route("api/messages")] - [ApiController] - public class BotController : ControllerBase - { - private readonly CloudAdapter _adapter; - private readonly IBot _bot; - - public BotController(CloudAdapter adapter, IBot bot) - { - _adapter = adapter; - _bot = bot; - } - - [HttpPost] - public async Task PostAsync() - { - // Delegate the processing of the HTTP POST to the adapter. - // The adapter will invoke the bot. - await _adapter.ProcessAsync(Request, Response, _bot); - } - } -} \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot configuration/Controllers/HomeController.cs b/samples/bot-configuration-app/csharp/Bot configuration/Controllers/HomeController.cs deleted file mode 100644 index 3281617350..0000000000 --- a/samples/bot-configuration-app/csharp/Bot configuration/Controllers/HomeController.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Microsoft.Teams.Samples.HelloWorld.Web.Controllers -{ - public class HomeController : Controller - { - [Route("")] - public ActionResult Index() - { - return View(); - } - } -} \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot configuration/Program.cs b/samples/bot-configuration-app/csharp/Bot configuration/Program.cs index 4527cb7bcc..8c436823a2 100644 --- a/samples/bot-configuration-app/csharp/Bot configuration/Program.cs +++ b/samples/bot-configuration-app/csharp/Bot configuration/Program.cs @@ -1,35 +1,46 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// -// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.6.2 +using Bot_Configuration; +using Bot_Configuration.Controllers; +using Azure.Core; +using Azure.Identity; +using Microsoft.Teams.Api.Auth; +using Microsoft.Teams.Apps; +using Microsoft.Teams.Apps.Extensions; +using Microsoft.Teams.Common.Http; +using Microsoft.Teams.Plugins.AspNetCore.Extensions; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; +var builder = WebApplication.CreateBuilder(args); +var config = builder.Configuration.Get(); -namespace Microsoft.Teams.Samples.HelloWorld.Web +Func> createTokenFactory = async (string[] scopes, string? tenantId) => { - /// - /// The main entry point for the application. - /// - public class Program + var clientId = config.Teams.ClientId; + + var managedIdentityCredential = new ManagedIdentityCredential(clientId); + var tokenRequestContext = new TokenRequestContext(scopes, tenantId: tenantId); + var accessToken = await managedIdentityCredential.GetTokenAsync(tokenRequestContext); + + return new TokenResponse { - /// - /// The main method which is the entry point of the application. - /// - public static void Main(string[] args) + TokenType = "Bearer", + AccessToken = accessToken.Token, + }; +}; +var appBuilder = App.Builder(); + +if (config.Teams.BotType == "UserAssignedMsi") +{ + appBuilder.AddCredentials(new TokenCredentials( + config.Teams.ClientId ?? string.Empty, + async (tenantId, scopes) => { - CreateHostBuilder(args).Build().Run(); + return await createTokenFactory(scopes, tenantId); } - - /// - /// Creates the host builder. - /// - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } + )); } + +builder.Services.AddSingleton(); +builder.AddTeams(appBuilder); + +var app = builder.Build(); +app.UseTeams(); +app.Run(); \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot configuration/Properties/launchSettings.json b/samples/bot-configuration-app/csharp/Bot configuration/Properties/launchSettings.json index ff9d8fe153..3572a7a03f 100644 --- a/samples/bot-configuration-app/csharp/Bot configuration/Properties/launchSettings.json +++ b/samples/bot-configuration-app/csharp/Bot configuration/Properties/launchSettings.json @@ -1,13 +1,26 @@ -{ +{ "profiles": { + // Debug project within Microsoft 365 Agents Playground + "Microsoft 365 Agents Playground": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Playground", + "TEAMSFX_NOTIFICATION_STORE_FILENAME": ".notification.playgroundstore.json", + "UPDATE_TEAMS_APP": "false" + }, + "hotReloadProfile": "aspnetcore" + }, + // Debug project within Teams "Start Project": { "commandName": "Project", "dotnetRunMessages": true, - "applicationUrl": "https://localhost:7130;http://localhost:5130", + "applicationUrl": "http://localhost:5130", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "hotReloadProfile": "aspnetcore" - } + }, } } \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot configuration/Startup.cs b/samples/bot-configuration-app/csharp/Bot configuration/Startup.cs deleted file mode 100644 index dc62b107a5..0000000000 --- a/samples/bot-configuration-app/csharp/Bot configuration/Startup.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// -// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.6.2 - -using Botconfiguration.Bots; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Localization; -using Microsoft.AspNetCore.Mvc.Razor; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Bot.Connector.Authentication; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using System.Globalization; - -namespace Microsoft.Teams.Samples.HelloWorld.Web -{ - /// - /// Configures services and the app's request pipeline. - /// - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - /// - /// Adds services to the container. - /// - public void ConfigureServices(IServiceCollection services) - { - services.AddLocalization(options => options.ResourcesPath = "Resources"); - - services.Configure(options => - { - var supportedCultures = new[] - { - new CultureInfo("en-US"), - new CultureInfo("fr-CA"), - new CultureInfo("hi-IN"), - new CultureInfo("es-MX") - }; - options.DefaultRequestCulture = new RequestCulture("en-US"); - options.SupportedCultures = supportedCultures; - options.SupportedUICultures = supportedCultures; - options.FallBackToParentCultures = false; - }); - - services.AddControllers(); - services.AddMvc() - .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, options => options.ResourcesPath = "Resources") - .AddDataAnnotationsLocalization(); - - // Add configuration singleton - services.AddSingleton(Configuration); - - // Create the Bot Framework Authentication to be used with the Bot Adapter. - services.AddSingleton(); - - // Create the Cloud Adapter with error handling enabled. - services.AddSingleton(); - services.AddSingleton(sp => sp.GetService()); - - // Create the bot as a transient. In this case the ASP Controller is expecting an IBot. - services.AddTransient(); - } - - /// - /// Configures the HTTP request pipeline. - /// - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseHsts(); - } - - app.UseDefaultFiles(); - app.UseStaticFiles(); - app.UseWebSockets(); - - var locOptions = app.ApplicationServices.GetService>(); - app.UseRequestLocalization(locOptions.Value); - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); - }); - - // app.UseHttpsRedirection(); - } - } -} \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot configuration/Views/Home/Index.cshtml b/samples/bot-configuration-app/csharp/Bot configuration/Views/Home/Index.cshtml deleted file mode 100644 index 5b1f59429b..0000000000 --- a/samples/bot-configuration-app/csharp/Bot configuration/Views/Home/Index.cshtml +++ /dev/null @@ -1,5 +0,0 @@ -@using Microsoft.AspNetCore.Mvc.Localization - -@inject IViewLocalizer Localizer - -

@Localizer["Bot configuraton app"]

diff --git a/samples/bot-configuration-app/csharp/Bot configuration/Views/Shared/_Layout.cshtml b/samples/bot-configuration-app/csharp/Bot configuration/Views/Shared/_Layout.cshtml deleted file mode 100644 index 611b1048ae..0000000000 --- a/samples/bot-configuration-app/csharp/Bot configuration/Views/Shared/_Layout.cshtml +++ /dev/null @@ -1,19 +0,0 @@ - - - - Microsoft Teams Hello World Sample App - - - - - @RenderSection("scripts", required: false) - - - -
-
- @RenderBody() -
-
- - diff --git a/samples/bot-configuration-app/csharp/Bot configuration/Views/_ViewStart.cshtml b/samples/bot-configuration-app/csharp/Bot configuration/Views/_ViewStart.cshtml deleted file mode 100644 index 2de62418c0..0000000000 --- a/samples/bot-configuration-app/csharp/Bot configuration/Views/_ViewStart.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@{ - Layout = "~/Views/Shared/_Layout.cshtml"; -} diff --git a/samples/bot-configuration-app/csharp/Bot configuration/appsettings.Development.json b/samples/bot-configuration-app/csharp/Bot configuration/appsettings.Development.json index b49abfc201..3e2c06b635 100644 --- a/samples/bot-configuration-app/csharp/Bot configuration/appsettings.Development.json +++ b/samples/bot-configuration-app/csharp/Bot configuration/appsettings.Development.json @@ -1,9 +1,19 @@ { - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } - } + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "BotType": "SingleTenant", + "TenantId": "" + } +} \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot configuration/appsettings.json b/samples/bot-configuration-app/csharp/Bot configuration/appsettings.json index 900773a488..12a4a8c643 100644 --- a/samples/bot-configuration-app/csharp/Bot configuration/appsettings.json +++ b/samples/bot-configuration-app/csharp/Bot configuration/appsettings.json @@ -1,4 +1,19 @@ { - "MicrosoftAppId": "", - "MicrosoftAppPassword": "" + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "BotType": "", + "TenantId": "" + } } \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot configuration/favicon.ico b/samples/bot-configuration-app/csharp/Bot configuration/favicon.ico deleted file mode 100644 index a3a799985c..0000000000 Binary files a/samples/bot-configuration-app/csharp/Bot configuration/favicon.ico and /dev/null differ diff --git a/samples/bot-configuration-app/csharp/Bot configuration/packages.config b/samples/bot-configuration-app/csharp/Bot configuration/packages.config deleted file mode 100644 index 9343961634..0000000000 --- a/samples/bot-configuration-app/csharp/Bot configuration/packages.config +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/Bot configuration/wwwroot/Content/Site.css b/samples/bot-configuration-app/csharp/Bot configuration/wwwroot/Content/Site.css deleted file mode 100644 index 6c4c11c6b4..0000000000 --- a/samples/bot-configuration-app/csharp/Bot configuration/wwwroot/Content/Site.css +++ /dev/null @@ -1,8 +0,0 @@ -html, body, div.surface, div.panel { - height: 100%; - margin: 0; -} - -div.panel { - padding: 15px; -} diff --git a/samples/bot-configuration-app/csharp/Bot configuration/wwwroot/Content/msteams-16.css b/samples/bot-configuration-app/csharp/Bot configuration/wwwroot/Content/msteams-16.css deleted file mode 100644 index 54f033cae0..0000000000 --- a/samples/bot-configuration-app/csharp/Bot configuration/wwwroot/Content/msteams-16.css +++ /dev/null @@ -1,1272 +0,0 @@ -.theme-light .surface { - background-color: #F0F2F4; - color: #16233A; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.875rem; - font-weight: 400; - line-height: 1.25rem -} - -.theme-light .panel { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background-color: #FFFFFF; - border-color: transparent; - border-radius: 0.1875rem; - border-style: solid; - border-width: 0.125rem; - box-sizing: border-box; - display: flex; - flex-direction: column; - overflow: hidden -} - -.theme-light .panel-header { - flex: 0 0 auto; - margin-left: 2rem; - margin-right: 2rem; - margin-top: 2rem -} - -.theme-light .panel-body { - flex: 1 1 auto; - margin-left: 2rem; - margin-right: 2rem; - overflow: auto -} - -.theme-light .panel-footer { - flex: 0 0 auto; - margin-bottom: 2rem; - margin-left: 2rem; - margin-right: 2rem -} - -.theme-light .button-primary { - background: #5558AF; - border: 0.125rem solid; - border-color: transparent; - border-radius: 0.1875rem; - color: #FFFFFF; - cursor: pointer; - font: inherit; - height: 2rem; - min-width: 6rem; - padding: 0.25rem; - white-space: nowrap -} - - .theme-light .button-primary:hover:enabled { - background: #4C509D; - border-color: transparent; - color: #FFFFFF - } - - .theme-light .button-primary:active { - background: #454A92; - border-color: transparent; - color: #FFFFFF - } - - .theme-light .button-primary:disabled { - background: #F3F4F5; - border-color: transparent; - color: #ABB0B8 - } - - .theme-light .button-primary:focus { - background: #4C509D; - border-color: transparent; - color: #FFFFFF; - outline: 0.125rem solid #FFFFFF; - outline-offset: -0.25rem - } - -.theme-light .button-secondary { - background: #FFFFFF; - border: 0.125rem solid; - border-color: #ABB0B8; - border-radius: 0.1875rem; - color: #525C6D; - cursor: pointer; - font: inherit; - height: 2rem; - min-width: 6rem; - padding: 0.25rem; - white-space: nowrap -} - - .theme-light .button-secondary:hover:enabled { - background: #ABB0B8; - border-color: transparent; - color: #16233A - } - - .theme-light .button-secondary:active { - background: #858C98; - border-color: transparent; - color: #16233A - } - - .theme-light .button-secondary:disabled { - background: #FFFFFF; - border-color: #F3F4F5; - color: #ABB0B8 - } - - .theme-light .button-secondary:focus { - background: #ABB0B8; - border-color: transparent; - color: #16233A; - outline: 0.125rem solid #16233A; - outline-offset: -0.25rem - } - -.theme-light .radio-container { - align-items: center; - background: transparent; - border: none; - display: flex; - outline: none -} - - .theme-light .radio-container + .radio-container { - margin-top: 0.5rem - } - -.theme-light .radio-button { - -moz-user-select: none; - -ms-user-select: none; - -webkit-user-select: none; - background: transparent; - border: 0.0625rem solid; - border-color: #525C6D; - border-radius: 100%; - cursor: pointer; - display: inline-block; - font: inherit; - height: 0.75rem; - margin: 0.125rem; - margin-left: 0.375rem; - padding: 0; - position: relative; - width: 0.75rem -} - - .theme-light .radio-button:hover { - background: transparent; - border-color: #525C6D - } - - .theme-light .radio-button:disabled { - background: #F0F2F4; - border-color: #ABB0B8 - } - - .theme-light .radio-button:disabled + label { - color: #ABB0B8; - cursor: default - } - - .theme-light .radio-button:focus { - box-shadow: 0 0 0 0.125rem #9FA4FE; - outline: none - } - -.theme-light .hidden-input:checked + .radio-button { - background: #5558AF; - border-color: #5558AF -} - - .theme-light .hidden-input:checked + .radio-button + label { - color: #16233A - } - -.theme-light .radio-label { - color: #525C6D; - cursor: pointer; - font-size: 0.75rem; - line-height: 1rem; - margin-left: 0.625rem -} - -.theme-light .radio-group { - display: inline-block -} - -.theme-light .tab-group { - border-bottom: 0.0625rem solid #F3F4F5; - margin: 0; - padding: 0; - width: 100% -} - - .theme-light .tab-group .tab { - background: 0; - border: 0; - border-bottom: transparent 0.25rem solid; - color: #525C6D; - cursor: pointer; - display: inline-block; - font: inherit; - margin: 0; - margin-right: 1.25rem; - outline: none; - padding: 0.25rem - } - - .theme-light .tab-group .tab:hover { - border-bottom-color: #9496CA - } - - .theme-light .tab-group .tab:focus { - background-color: #9FA4FE; - color: #FFFFFF - } - - .theme-light .tab-group .tab-active { - border-bottom-color: #5558AF; - color: #5558AF - } - -.theme-light .tab-active:focus { - border-bottom-color: #FFFFFF -} - -.theme-light .hidden-input { - display: none -} - -.theme-light .toggle { - display: inline-block; - line-height: 1 -} - -.theme-light .toggle-ball { - background-color: #F0F2F4; - border: 0; - border-radius: 1.25rem; - cursor: pointer; - height: 1.25rem; - margin: 0.125rem; - outline: none; - padding: 0; - position: relative; - width: 3.75rem -} - - .theme-light .toggle-ball:before { - background-color: #454A92; - border-radius: 50%; - content: ""; - height: 0.875rem; - left: 0.1875rem; - position: absolute; - top: 0.18750000000000003rem; - transition: 0.2s; - width: 0.875rem - } - -.theme-light .hidden-input:checked + .toggle-ball:before { - background-color: #4C509D; - transform: translateX(2.5rem) -} - -.theme-light .toggle-ball:focus { - box-shadow: 0 0 0 0.125rem #5558AF; - outline: none -} - -.theme-light .hidden-input:checked + .toggle-ball { - background-color: #7FBA00 -} - -.theme-light .font-title { - font-size: 1.5rem; - line-height: 2rem -} - -.theme-light .font-title2 { - font-size: 1.125rem; - line-height: 1.5rem -} - -.theme-light .font-base { - font-size: 0.875rem; - line-height: 1.25rem -} - -.theme-light .font-caption { - font-size: 0.75rem; - line-height: 1rem -} - -.theme-light .font-xsmall { - font-size: 0.625rem; - line-height: 0.6875rem -} - -.theme-light .font-semilight { - font-family: 'Segoe UI Light', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 300 -} - -.theme-light .font-regular { - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 400 -} - -.theme-light .font-semibold { - font-family: 'Segoe UI Semibold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 600 -} - -.theme-light .font-bold { - font-family: 'Segoe UI Bold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 700 -} - -.theme-light .input-container { - overflow: hidden; - position: relative -} - -.theme-light .input-field { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background: #F0F2F4; - border: 0.125rem solid transparent; - border-radius: 0.1875rem; - box-sizing: border-box; - color: #525C6D; - font: inherit; - height: 2rem; - margin: 0; - outline: none; - padding: 0.5rem 0.75rem; - width: 100% -} - -.theme-light .input-error-icon { - bottom: 0.5625rem; - color: #C50E2E; - position: absolute; - right: 0.75rem -} - -.theme-light .label { - border: 0; - color: #4E586A; - display: inline-block; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.75rem; - font-weight: 400; - line-height: 1rem; - margin-bottom: 0.5rem; - margin-left: 0; - margin-right: 0; - margin-top: 0; - padding: 0 -} - -.theme-light .error-label { - border: 0; - color: #C50E2E; - float: right; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.75rem; - font-weight: 400; - line-height: 1rem; - margin-bottom: 0.5rem; - margin-left: 0; - margin-right: 0; - margin-top: 0; - padding: 0 -} - -.theme-light .textarea-container { - display: flex; - flex-direction: column; - overflow: hidden; - position: relative -} - -.theme-light .textarea-field { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background: #F0F2F4; - border: 0.125rem solid transparent; - border-radius: 0.1875rem; - box-sizing: border-box; - color: #525C6D; - flex: 1; - font: inherit; - margin: 0; - min-height: 3.75rem; - outline: none; - padding: 0.5rem 0.75rem; - resize: none -} - - .theme-light .input-field:hover:inactive:enabled, .theme-light .textarea-field:hover:inactive:enabled { - background: #F0F2F4; - border-bottom-color: transparent - } - - .theme-light .input-field:disabled, .theme-light .textarea-field:disabled { - background: #F3F4F5; - border-bottom-color: transparent; - color: #DEE0E3 - } - - .theme-light .input-field:active:enabled, .theme-light .input-field:focus, .theme-light .textarea-field:active:enabled, .theme-light .textarea-field:focus { - background: #F0F2F4; - border-bottom-color: #5558AF - } - -.theme-light .textarea-error-icon { - color: #C50E2E; - position: absolute; - right: 0.75rem; - top: 50% -} - -.theme-dark .surface { - background-color: #2B2B30; - color: #FFFFFF; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.875rem; - font-weight: 400; - line-height: 1.25rem -} - -.theme-dark .panel { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background-color: #404045; - border-color: transparent; - border-radius: 0.1875rem; - border-style: solid; - border-width: 0.125rem; - box-sizing: border-box; - display: flex; - flex-direction: column; - overflow: hidden -} - -.theme-dark .panel-header { - flex: 0 0 auto; - margin-left: 2rem; - margin-right: 2rem; - margin-top: 2rem -} - -.theme-dark .panel-body { - flex: 1 1 auto; - margin-left: 2rem; - margin-right: 2rem; - overflow: auto -} - -.theme-dark .panel-footer { - flex: 0 0 auto; - margin-bottom: 2rem; - margin-left: 2rem; - margin-right: 2rem -} - -.theme-dark .button-primary { - background: #9FA4FE; - border: 0.125rem solid; - border-color: transparent; - border-radius: 0.1875rem; - color: #2B2B30; - cursor: pointer; - font: inherit; - height: 2rem; - min-width: 6rem; - padding: 0.25rem; - white-space: nowrap -} - - .theme-dark .button-primary:hover:enabled { - background: #AEB2FF; - border-color: transparent; - color: #2B2B30 - } - - .theme-dark .button-primary:active { - background: #B8BBFF; - border-color: transparent; - color: #2B2B30 - } - - .theme-dark .button-primary:disabled { - background: #35353A; - border-color: transparent; - color: #77777A - } - - .theme-dark .button-primary:focus { - background: #9FA4FE; - border-color: transparent; - color: #2B2B30; - outline: 0.125rem solid #2B2B30; - outline-offset: -0.25rem - } - -.theme-dark .button-secondary { - background: #404045; - border: 0.125rem solid; - border-color: #77777A; - border-radius: 0.1875rem; - color: #C8C8C9; - cursor: pointer; - font: inherit; - height: 2rem; - min-width: 6rem; - padding: 0.25rem; - white-space: nowrap -} - - .theme-dark .button-secondary:hover:enabled { - background: #77777A; - border-color: transparent; - color: #FFFFFF - } - - .theme-dark .button-secondary:active { - background: #48484D; - border-color: transparent; - color: #FFFFFF - } - - .theme-dark .button-secondary:disabled { - background: #404045; - border-color: #35353A; - color: #77777A - } - - .theme-dark .button-secondary:focus { - background: #77777A; - border-color: transparent; - color: #FFFFFF; - outline: 0.125rem solid #FFFFFF; - outline-offset: -0.25rem - } - -.theme-dark .radio-container { - align-items: center; - background: transparent; - border: none; - display: flex; - outline: none -} - - .theme-dark .radio-container + .radio-container { - margin-top: 0.5rem - } - -.theme-dark .radio-button { - -moz-user-select: none; - -ms-user-select: none; - -webkit-user-select: none; - background: transparent; - border: 0.0625rem solid; - border-color: #C8C8C9; - border-radius: 100%; - cursor: pointer; - display: inline-block; - font: inherit; - height: 0.75rem; - margin: 0.125rem; - margin-left: 0.375rem; - padding: 0; - position: relative; - width: 0.75rem -} - - .theme-dark .radio-button:hover { - background: transparent; - border-color: #C8C8C9 - } - - .theme-dark .radio-button:disabled { - background: #404045; - border-color: #77777A - } - - .theme-dark .radio-button:disabled + label { - color: #77777A; - cursor: default - } - - .theme-dark .radio-button:focus { - box-shadow: 0 0 0 0.125rem #5558AF; - outline: none - } - -.theme-dark .hidden-input:checked + .radio-button { - background: #9FA4FE; - border-color: #9FA4FE -} - - .theme-dark .hidden-input:checked + .radio-button + label { - color: #FFFFFF - } - -.theme-dark .radio-label { - color: #C8C8C9; - cursor: pointer; - font-size: 0.75rem; - line-height: 1rem; - margin-left: 0.625rem -} - -.theme-dark .radio-group { - display: inline-block -} - -.theme-dark .tab-group { - border-bottom: 0.0625rem solid #000000; - margin: 0; - padding: 0; - width: 100% -} - - .theme-dark .tab-group .tab { - background: 0; - border: 0; - border-bottom: transparent 0.25rem solid; - color: #C8C8C9; - cursor: pointer; - display: inline-block; - font: inherit; - margin: 0; - margin-right: 1.25rem; - outline: none; - padding: 0.25rem - } - - .theme-dark .tab-group .tab:hover { - border-bottom-color: #7174AA - } - - .theme-dark .tab-group .tab:focus { - background-color: #5558AF; - color: #FFFFFF - } - - .theme-dark .tab-group .tab-active { - border-bottom-color: #9FA4FE; - color: #9FA4FE - } - -.theme-dark .tab-active:focus { - border-bottom-color: #FFFFFF -} - -.theme-dark .hidden-input { - display: none -} - -.theme-dark .toggle { - display: inline-block; - line-height: 1 -} - -.theme-dark .toggle-ball { - background-color: #2B2B30; - border: 0; - border-radius: 1.25rem; - cursor: pointer; - height: 1.25rem; - margin: 0.125rem; - outline: none; - padding: 0; - position: relative; - width: 3.75rem -} - - .theme-dark .toggle-ball:before { - background-color: #C8C8C9; - border-radius: 50%; - content: ""; - height: 0.875rem; - left: 0.1875rem; - position: absolute; - top: 0.18750000000000003rem; - transition: 0.2s; - width: 0.875rem - } - -.theme-dark .hidden-input:checked + .toggle-ball:before { - background-color: #FFFFFF; - transform: translateX(2.5rem) -} - -.theme-dark .toggle-ball:focus { - box-shadow: 0 0 0 0.125rem #9FA4FE; - outline: none -} - -.theme-dark .hidden-input:checked + .toggle-ball { - background-color: #88BC2B -} - -.theme-dark .font-title { - font-size: 1.5rem; - line-height: 2rem -} - -.theme-dark .font-title2 { - font-size: 1.125rem; - line-height: 1.5rem -} - -.theme-dark .font-base { - font-size: 0.875rem; - line-height: 1.25rem -} - -.theme-dark .font-caption { - font-size: 0.75rem; - line-height: 1rem -} - -.theme-dark .font-xsmall { - font-size: 0.625rem; - line-height: 0.6875rem -} - -.theme-dark .font-semilight { - font-family: 'Segoe UI Light', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 300 -} - -.theme-dark .font-regular { - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 400 -} - -.theme-dark .font-semibold { - font-family: 'Segoe UI Semibold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 600 -} - -.theme-dark .font-bold { - font-family: 'Segoe UI Bold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 700 -} - -.theme-dark .input-container { - overflow: hidden; - position: relative -} - -.theme-dark .input-field { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background: #2B2B30; - border: 0.125rem solid transparent; - border-radius: 0.1875rem; - box-sizing: border-box; - color: #C8C8C9; - font: inherit; - height: 2rem; - margin: 0; - outline: none; - padding: 0.5rem 0.75rem; - width: 100% -} - -.theme-dark .input-error-icon { - bottom: 0.5625rem; - color: #ED1B3E; - position: absolute; - right: 0.75rem -} - -.theme-dark .label { - border: 0; - color: #FFFFFF; - display: inline-block; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.75rem; - font-weight: 400; - line-height: 1rem; - margin-bottom: 0.5rem; - margin-left: 0; - margin-right: 0; - margin-top: 0; - padding: 0 -} - -.theme-dark .error-label { - border: 0; - color: #ED1B3E; - float: right; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.75rem; - font-weight: 400; - line-height: 1rem; - margin-bottom: 0.5rem; - margin-left: 0; - margin-right: 0; - margin-top: 0; - padding: 0 -} - -.theme-dark .textarea-container { - display: flex; - flex-direction: column; - overflow: hidden; - position: relative -} - -.theme-dark .textarea-field { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background: #2B2B30; - border: 0.125rem solid transparent; - border-radius: 0.1875rem; - box-sizing: border-box; - color: #C8C8C9; - flex: 1; - font: inherit; - margin: 0; - min-height: 3.75rem; - outline: none; - padding: 0.5rem 0.75rem; - resize: none -} - - .theme-dark .input-field:hover:inactive:enabled, .theme-dark .textarea-field:hover:inactive:enabled { - background: #2B2B30; - border-bottom-color: transparent - } - - .theme-dark .input-field:disabled, .theme-dark .textarea-field:disabled { - background: #35353A; - border-bottom-color: transparent; - color: #48484D - } - - .theme-dark .input-field:active:enabled, .theme-dark .input-field:focus, .theme-dark .textarea-field:active:enabled, .theme-dark .textarea-field:focus { - background: #2B2B30; - border-bottom-color: #9FA4FE - } - -.theme-dark .textarea-error-icon { - color: #ED1B3E; - position: absolute; - right: 0.75rem; - top: 50% -} - -.theme-contrast .surface { - background-color: #000000; - color: #FFFFFF; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.875rem; - font-weight: 400; - line-height: 1.25rem -} - -.theme-contrast .panel { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background-color: #000000; - border-color: #FFFFFF; - border-radius: 0.1875rem; - border-style: solid; - border-width: 0.125rem; - box-sizing: border-box; - display: flex; - flex-direction: column; - overflow: hidden -} - -.theme-contrast .panel-header { - flex: 0 0 auto; - margin-left: 2rem; - margin-right: 2rem; - margin-top: 2rem -} - -.theme-contrast .panel-body { - flex: 1 1 auto; - margin-left: 2rem; - margin-right: 2rem; - overflow: auto -} - -.theme-contrast .panel-footer { - flex: 0 0 auto; - margin-bottom: 2rem; - margin-left: 2rem; - margin-right: 2rem -} - -.theme-contrast .button-primary { - background: #FFFFFF; - border: 0.125rem solid; - border-color: transparent; - border-radius: 0.1875rem; - color: #000000; - cursor: pointer; - font: inherit; - height: 2rem; - min-width: 6rem; - padding: 0.25rem; - white-space: nowrap -} - - .theme-contrast .button-primary:disabled { - background: #30F42C; - border-color: transparent; - color: #000000 - } - -.theme-contrast .button-secondary { - background: #000000; - border: 0.125rem solid; - border-color: #FFFFFF; - border-radius: 0.1875rem; - color: #FFFFFF; - cursor: pointer; - font: inherit; - height: 2rem; - min-width: 6rem; - padding: 0.25rem; - white-space: nowrap -} - - .theme-contrast .button-primary:hover:enabled, .theme-contrast .button-primary:active, .theme-contrast .button-secondary:hover:enabled, .theme-contrast .button-secondary:active { - background: #FFFF00; - border-color: transparent; - color: #000000 - } - - .theme-contrast .button-secondary:disabled { - background: #000000; - border-color: #30F42C; - color: #30F42C - } - - .theme-contrast .button-primary:focus, .theme-contrast .button-secondary:focus { - background: #FFFF00; - border-color: transparent; - color: #000000; - outline: 0.125rem solid transparent; - outline-offset: -0.25rem - } - -.theme-contrast .radio-container { - align-items: center; - background: transparent; - border: none; - display: flex; - outline: none -} - - .theme-contrast .radio-container + .radio-container { - margin-top: 0.5rem - } - -.theme-contrast .radio-button { - -moz-user-select: none; - -ms-user-select: none; - -webkit-user-select: none; - background: transparent; - border: 0.0625rem solid; - border-color: #FFFFFF; - border-radius: 100%; - cursor: pointer; - display: inline-block; - font: inherit; - height: 0.75rem; - margin: 0.125rem; - margin-left: 0.375rem; - padding: 0; - position: relative; - width: 0.75rem -} - - .theme-contrast .radio-button:hover { - background: transparent; - border-color: #FFFFFF - } - - .theme-contrast .radio-button:disabled { - background: transparent; - border-color: #30F42C - } - - .theme-contrast .radio-button:disabled + label { - color: #30F42C; - cursor: default - } - - .theme-contrast .radio-button:focus { - box-shadow: 0 0 0 0.125rem #FFFF00; - outline: none - } - -.theme-contrast .hidden-input:checked + .radio-button { - background: #00EBFF; - border-color: #00EBFF -} - - .theme-contrast .hidden-input:checked + .radio-button + label { - color: #FFFFFF - } - -.theme-contrast .radio-label { - color: #FFFFFF; - cursor: pointer; - font-size: 0.75rem; - line-height: 1rem; - margin-left: 0.625rem -} - -.theme-contrast .radio-group { - display: inline-block -} - -.theme-contrast .tab-group { - border-bottom: 0.0625rem solid #30F42C; - margin: 0; - padding: 0; - width: 100% -} - - .theme-contrast .tab-group .tab { - background: 0; - border: 0; - border-bottom: transparent 0.25rem solid; - color: #FFFFFF; - cursor: pointer; - display: inline-block; - font: inherit; - margin: 0; - margin-right: 1.25rem; - outline: none; - padding: 0.25rem - } - - .theme-contrast .tab-group .tab:hover { - border-bottom-color: #FFFF00 - } - - .theme-contrast .tab-group .tab:focus { - background-color: #FFFF00; - color: #000000 - } - - .theme-contrast .tab-group .tab-active { - border-bottom-color: #00EBFF; - color: #FFFFFF - } - -.theme-contrast .tab-active:focus { - border-bottom-color: #000000 -} - -.theme-contrast .hidden-input { - display: none -} - -.theme-contrast .toggle { - display: inline-block; - line-height: 1 -} - -.theme-contrast .toggle-ball { - background-color: #FFFFFF; - border: 0; - border-radius: 1.25rem; - cursor: pointer; - height: 1.25rem; - margin: 0.125rem; - outline: none; - padding: 0; - position: relative; - width: 3.75rem -} - - .theme-contrast .toggle-ball:before { - background-color: #FFFF00; - border-radius: 50%; - content: ""; - height: 0.875rem; - left: 0.1875rem; - position: absolute; - top: 0.18750000000000003rem; - transition: 0.2s; - width: 0.875rem - } - -.theme-contrast .hidden-input:checked + .toggle-ball:before { - background-color: #4C509D; - transform: translateX(2.5rem) -} - -.theme-contrast .toggle-ball:focus { - box-shadow: 0 0 0 0.125rem #30F42C; - outline: none -} - -.theme-contrast .hidden-input:checked + .toggle-ball { - background-color: #7FBA00 -} - -.theme-contrast .font-title { - font-size: 1.5rem; - line-height: 2rem -} - -.theme-contrast .font-title2 { - font-size: 1.125rem; - line-height: 1.5rem -} - -.theme-contrast .font-base { - font-size: 0.875rem; - line-height: 1.25rem -} - -.theme-contrast .font-caption { - font-size: 0.75rem; - line-height: 1rem -} - -.theme-contrast .font-xsmall { - font-size: 0.625rem; - line-height: 0.6875rem -} - -.theme-contrast .font-semilight { - font-family: 'Segoe UI Light', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 300 -} - -.theme-contrast .font-regular { - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 400 -} - -.theme-contrast .font-semibold { - font-family: 'Segoe UI Semibold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 600 -} - -.theme-contrast .font-bold { - font-family: 'Segoe UI Bold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-weight: 700 -} - -.theme-contrast .input-container { - overflow: hidden; - position: relative -} - -.theme-contrast .input-field { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background: #000000; - border: 0.125rem solid #FFFFFF; - border-radius: 0.1875rem; - box-sizing: border-box; - color: #FFFFFF; - font: inherit; - height: 2rem; - margin: 0; - outline: none; - padding: 0.5rem 0.75rem; - width: 100% -} - -.theme-contrast .input-error-icon { - bottom: 0.5625rem; - color: #FFFF00; - position: absolute; - right: 0.75rem -} - -.theme-contrast .label { - border: 0; - color: #FFFFFF; - display: inline-block; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.75rem; - font-weight: 400; - line-height: 1rem; - margin-bottom: 0.5rem; - margin-left: 0; - margin-right: 0; - margin-top: 0; - padding: 0 -} - -.theme-contrast .error-label { - border: 0; - color: #FFFF00; - float: right; - font-family: 'Segoe UI', Tahoma, Helvetica, Sans-Serif; - font-size: 0.75rem; - font-weight: 400; - line-height: 1rem; - margin-bottom: 0.5rem; - margin-left: 0; - margin-right: 0; - margin-top: 0; - padding: 0 -} - -.theme-contrast .textarea-container { - display: flex; - flex-direction: column; - overflow: hidden; - position: relative -} - -.theme-contrast .textarea-field { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - background: #000000; - border: 0.125rem solid #FFFFFF; - border-radius: 0.1875rem; - box-sizing: border-box; - color: #FFFFFF; - flex: 1; - font: inherit; - margin: 0; - min-height: 3.75rem; - outline: none; - padding: 0.5rem 0.75rem; - resize: none -} - - .theme-contrast .input-field:hover:inactive:enabled, .theme-contrast .textarea-field:hover:inactive:enabled { - background: #000000; - border-bottom-color: transparent - } - - .theme-contrast .input-field:disabled, .theme-contrast .textarea-field:disabled { - background: #30F42C; - border-bottom-color: #FFFFFF; - color: #FFFFFF - } - - .theme-contrast .input-field:active:enabled, .theme-contrast .input-field:focus, .theme-contrast .textarea-field:active:enabled, .theme-contrast .textarea-field:focus { - background: #000000; - border-bottom-color: #FFFF00 - } - -.theme-contrast .textarea-error-icon { - color: #FFFF00; - position: absolute; - right: 0.75rem; - top: 50% -} diff --git a/samples/bot-configuration-app/csharp/M365Agent/.gitignore b/samples/bot-configuration-app/csharp/M365Agent/.gitignore new file mode 100644 index 0000000000..c5cae9258c --- /dev/null +++ b/samples/bot-configuration-app/csharp/M365Agent/.gitignore @@ -0,0 +1,10 @@ +# TeamsFx files +build +appPackage/build +env/.env.*.user +env/.env.local +appsettings.Development.json +.deployment + +# User-specific files +*.user diff --git a/samples/bot-configuration-app/csharp/M365Agent/M365Agent.atkproj b/samples/bot-configuration-app/csharp/M365Agent/M365Agent.atkproj new file mode 100644 index 0000000000..124eb75046 --- /dev/null +++ b/samples/bot-configuration-app/csharp/M365Agent/M365Agent.atkproj @@ -0,0 +1,9 @@ + + + + b069b3bd-f6bc-cc40-82ab-3fcc2ea50fdf + + + + + \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/M365Agent/M365Agent.ttkproj b/samples/bot-configuration-app/csharp/M365Agent/M365Agent.ttkproj deleted file mode 100644 index 4eda102d12..0000000000 --- a/samples/bot-configuration-app/csharp/M365Agent/M365Agent.ttkproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - 7d531fb4-dccb-4db0-9de7-b5236577981e - - - - - - - - - - \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/M365Agent/appPackage/color.png b/samples/bot-configuration-app/csharp/M365Agent/appPackage/color.png index b8cf81afbe..01aa37e347 100644 Binary files a/samples/bot-configuration-app/csharp/M365Agent/appPackage/color.png and b/samples/bot-configuration-app/csharp/M365Agent/appPackage/color.png differ diff --git a/samples/bot-configuration-app/csharp/M365Agent/appPackage/manifest.json b/samples/bot-configuration-app/csharp/M365Agent/appPackage/manifest.json index a9412eebad..1b44307e3f 100644 --- a/samples/bot-configuration-app/csharp/M365Agent/appPackage/manifest.json +++ b/samples/bot-configuration-app/csharp/M365Agent/appPackage/manifest.json @@ -24,7 +24,7 @@ "accentColor": "#60A18E", "bots": [ { - "botId": "${{AAD_APP_CLIENT_ID}}", + "botId": "${{BOT_ID}}", "needsChannelSelector": false, "scopes": [ "personal", @@ -47,6 +47,5 @@ "messageTeamMembers" ], "validDomains": [ - "${{BOT_DOMAIN}}" ] } \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/M365Agent/appPackage/outline.png b/samples/bot-configuration-app/csharp/M365Agent/appPackage/outline.png index 2c3bf6fa65..f7a4c86447 100644 Binary files a/samples/bot-configuration-app/csharp/M365Agent/appPackage/outline.png and b/samples/bot-configuration-app/csharp/M365Agent/appPackage/outline.png differ diff --git a/samples/bot-configuration-app/csharp/M365Agent/env/.env.dev b/samples/bot-configuration-app/csharp/M365Agent/env/.env.dev new file mode 100644 index 0000000000..df4f9da508 --- /dev/null +++ b/samples/bot-configuration-app/csharp/M365Agent/env/.env.dev @@ -0,0 +1,15 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/M365Agent/env/.env.local b/samples/bot-configuration-app/csharp/M365Agent/env/.env.local index 76ee8a0ba6..86dfacc35a 100644 --- a/samples/bot-configuration-app/csharp/M365Agent/env/.env.local +++ b/samples/bot-configuration-app/csharp/M365Agent/env/.env.local @@ -7,17 +7,8 @@ APP_NAME_SUFFIX=local # Generated during provision, you can also add your own variables. BOT_ID= TEAMS_APP_ID= -RESOURCE_SUFFIX= -AZURE_SUBSCRIPTION_ID= -AZURE_RESOURCE_GROUP_NAME= -AAD_APP_CLIENT_ID= -AAD_APP_OBJECT_ID= -AAD_APP_TENANT_ID= -AAD_APP_OAUTH_AUTHORITY= -AAD_APP_OAUTH_AUTHORITY_HOST= TEAMS_APP_TENANT_ID= -MICROSOFT_APP_TYPE= -MICROSOFT_APP_TENANT_ID= +BOT_OBJECT_ID= TEAMSFX_M365_USER_NAME= BOT_ENDPOINT= diff --git a/samples/bot-configuration-app/csharp/M365Agent/infra/azure.bicep b/samples/bot-configuration-app/csharp/M365Agent/infra/azure.bicep index c3ce051b3d..658e412a21 100644 --- a/samples/bot-configuration-app/csharp/M365Agent/infra/azure.bicep +++ b/samples/bot-configuration-app/csharp/M365Agent/infra/azure.bicep @@ -3,42 +3,84 @@ @description('Used to generate names for all resources in this file') param resourceBaseName string -@description('Required when create Azure Bot service') -param botAadAppClientId string - -param botAppDomain string +param webAppSKU string @maxLength(42) param botDisplayName string -param botServiceName string = resourceBaseName -param botServiceSku string = 'F0' -param microsoftAppType string -param microsoftAppTenantId string +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param identityName string = resourceBaseName +param location string = resourceGroup().location -// Register your web service as a bot with the Bot Framework -resource botService 'Microsoft.BotService/botServices@2021-03-01' = { - kind: 'azurebot' - location: 'global' - name: botServiceName - properties: { - displayName: botDisplayName - endpoint: 'https://${botAppDomain}/api/messages' - msaAppId: botAadAppClientId - msaAppType: microsoftAppType - msaAppTenantId: microsoftAppType == 'SingleTenant' ? microsoftAppTenantId : '' - } +resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + location: location + name: identityName +} + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app' + location: location + name: serverfarmsName sku: { - name: botServiceSku + name: webAppSKU } } -// Connect the bot service to Microsoft Teams -resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { - parent: botService - location: 'global' - name: 'MsTeamsChannel' +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName properties: { - channelName: 'MsTeamsChannel' + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' + } + { + name: 'Teams__ClientId' + value: identity.properties.clientId + } + { + name: 'Teams__TenantId' + value: identity.properties.tenantId + } + { + name: 'Teams__BotType' + value: 'UserAssignedMsi' + } + ] + ftpsState: 'FtpsOnly' + } + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${identity.id}': {} + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + identityClientId: identity.properties.clientId + identityResourceId: identity.id + identityTenantId: identity.properties.tenantId + botAppDomain: webApp.properties.defaultHostName + botDisplayName: botDisplayName } } + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output BOT_AZURE_APP_SERVICE_RESOURCE_ID string = webApp.id +output BOT_DOMAIN string = webApp.properties.defaultHostName +output BOT_ID string = identity.properties.clientId +output BOT_TENANT_ID string = identity.properties.tenantId diff --git a/samples/bot-configuration-app/csharp/M365Agent/infra/azure.parameters.json b/samples/bot-configuration-app/csharp/M365Agent/infra/azure.parameters.json index e65112d07c..129463c7e1 100644 --- a/samples/bot-configuration-app/csharp/M365Agent/infra/azure.parameters.json +++ b/samples/bot-configuration-app/csharp/M365Agent/infra/azure.parameters.json @@ -1,24 +1,15 @@ { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "resourceBaseName": { - "value": "bot${{RESOURCE_SUFFIX}}" - }, - "botAadAppClientId": { - "value": "${{AAD_APP_CLIENT_ID}}" - }, - "botAppDomain": { - "value": "${{BOT_DOMAIN}}" - }, - "botDisplayName": { - "value": "bot-configuration-app" - }, - "microsoftAppType": { - "value": "${{MICROSOFT_APP_TYPE}}" - }, - "microsoftAppTenantId": { - "value": "${{MICROSOFT_APP_TENANT_ID}}" + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "bot${{RESOURCE_SUFFIX}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "Bot Configuration" + } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/M365Agent/infra/botRegistration/azurebot.bicep b/samples/bot-configuration-app/csharp/M365Agent/infra/botRegistration/azurebot.bicep new file mode 100644 index 0000000000..a5a27b8fe4 --- /dev/null +++ b/samples/bot-configuration-app/csharp/M365Agent/infra/botRegistration/azurebot.bicep @@ -0,0 +1,42 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@maxLength(42) +param botDisplayName string + +param botServiceName string = resourceBaseName +param botServiceSku string = 'F0' +param identityResourceId string +param identityClientId string +param identityTenantId string +param botAppDomain string + +// Register your web service as a bot with the Bot Framework +resource botService 'Microsoft.BotService/botServices@2021-03-01' = { + kind: 'azurebot' + location: 'global' + name: botServiceName + properties: { + displayName: botDisplayName + endpoint: 'https://${botAppDomain}/api/messages' + msaAppId: identityClientId + msaAppMSIResourceId: identityResourceId + msaAppTenantId:identityTenantId + msaAppType:'UserAssignedMSI' + } + sku: { + name: botServiceSku + } +} + +// Connect the bot service to Microsoft Teams +resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { + parent: botService + location: 'global' + name: 'MsTeamsChannel' + properties: { + channelName: 'MsTeamsChannel' + } +} diff --git a/samples/bot-configuration-app/csharp/M365Agent/infra/botRegistration/readme.md b/samples/bot-configuration-app/csharp/M365Agent/infra/botRegistration/readme.md new file mode 100644 index 0000000000..d5416243cd --- /dev/null +++ b/samples/bot-configuration-app/csharp/M365Agent/infra/botRegistration/readme.md @@ -0,0 +1 @@ +The `azurebot.bicep` module is provided to help you create Azure Bot service when you don't use Azure to host your app. If you use Azure as infrastrcture for your app, `azure.bicep` under infra folder already leverages this module to create Azure Bot service for you. You don't need to deploy `azurebot.bicep` again. \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/M365Agent/launchSettings.json b/samples/bot-configuration-app/csharp/M365Agent/launchSettings.json index d6491ef52c..2af8ce7a8a 100644 --- a/samples/bot-configuration-app/csharp/M365Agent/launchSettings.json +++ b/samples/bot-configuration-app/csharp/M365Agent/launchSettings.json @@ -1,15 +1,25 @@ { - "profiles": { - // Debug project within Teams - "Microsoft Teams (browser)": { - "commandName": "Project", - "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" - }, - // Launch project within Teams without prepare app dependencies - "Microsoft Teams (browser) (skip update app)": { - "commandName": "Project", - "environmentVariables": { "UPDATE_TEAMS_APP": "false" }, - "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" - } - } + "profiles": { + // Launch project within Microsoft 365 Agents Playground + "Microsoft 365 Agents Playground (browser)": { + "commandName": "Project", + "environmentVariables": { + "UPDATE_TEAMS_APP": "false", + "M365_AGENTS_PLAYGROUND_TARGET_SDK": "teams-ai-v2-dotnet" + }, + "launchTestTool": true, + "launchUrl": "http://localhost:56150", + }, + // Launch project within Teams + "Microsoft Teams (browser)": { + "commandName": "Project", + "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}", + }, + // Launch project within Teams without prepare app dependencies + "Microsoft Teams (browser) (skip update app)": { + "commandName": "Project", + "environmentVariables": { "UPDATE_TEAMS_APP": "false" }, + "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" + }, + } } \ No newline at end of file diff --git a/samples/bot-configuration-app/csharp/M365Agent/m365agents.local.yml b/samples/bot-configuration-app/csharp/M365Agent/m365agents.local.yml index 107ada0774..fc9488b118 100644 --- a/samples/bot-configuration-app/csharp/M365Agent/m365agents.local.yml +++ b/samples/bot-configuration-app/csharp/M365Agent/m365agents.local.yml @@ -1,79 +1,67 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.2/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/m365-agents-toolkits/v1.11/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.2 - -additionalMetadata: - sampleTag: Microsoft-Teams-Samples:bot-configuration-app-csharp +version: v1.11 provision: - - uses: aadApp/create # Creates a new Azure Active Directory (AAD) app to authenticate users if the environment variable that stores clientId is empty - with: - name: bot-configuration-app-aad # Note: when you run aadApp/update, the AAD app name will be updated based on the definition in manifest. If you don't want to change the name, make sure the name in AAD manifest is the same with the name defined here. - generateClientSecret: true # If the value is false, the action will not generate client secret for you - signInAudience: "AzureADMyOrg" # Multitenant - writeToEnvironmentFile: # Write the information of created resources into environment file for the specified environment variable(s). - clientId: AAD_APP_CLIENT_ID - clientSecret: SECRET_AAD_APP_CLIENT_SECRET # Environment variable that starts with `SECRET_` will be stored to the .env.{envName}.user environment file - objectId: AAD_APP_OBJECT_ID - tenantId: AAD_APP_TENANT_ID - authority: AAD_APP_OAUTH_AUTHORITY - authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST - # Creates a Teams app - uses: teamsApp/create with: # Teams app name - name: bot-configuration-app${{APP_NAME_SUFFIX}} + name: Bot Configuration${{APP_NAME_SUFFIX}} # Write the information of created resources into environment file for # the specified environment variable(s). - writeToEnvironmentFile: + writeToEnvironmentFile: teamsAppId: TEAMS_APP_ID - - uses: script + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create with: - run: - echo "::set-teamsfx-env MICROSOFT_APP_TYPE=SingleTenant"; - echo "::set-teamsfx-env MICROSOFT_APP_TENANT_ID=${{AAD_APP_TENANT_ID}}"; + # The Microsoft Entra application's display name + name: Bot Configuration${{APP_NAME_SUFFIX}} + generateClientSecret: true + generateServicePrincipal: true + signInAudience: AzureADMultipleOrgs + writeToEnvironmentFile: + # The Microsoft Entra application's client id created for bot. + clientId: BOT_ID + # The Microsoft Entra application's client secret created for bot. + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile with: - target: ../Bot configuration/appsettings.json + target: ../Bot Configuration/appsettings.Development.json content: - MicrosoftAppId: ${{AAD_APP_CLIENT_ID}} - MicrosoftAppPassword: ${{SECRET_AAD_APP_CLIENT_SECRET}} - MicrosoftAppType: ${{MICROSOFT_APP_TYPE}} - MicrosoftAppTenantId: ${{MICROSOFT_APP_TENANT_ID}} + Teams: + ClientId: ${{BOT_ID}} + ClientSecret: ${{SECRET_BOT_PASSWORD}} + TenantId: ${{TEAMS_APP_TENANT_ID}} - - uses: arm/deploy # Deploy given ARM templates parallelly. + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create with: - subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} # The AZURE_SUBSCRIPTION_ID is a built-in environment variable. TeamsFx will ask you select one subscription if its value is empty. You're free to reference other environment varialbe here, but TeamsFx will not ask you to select subscription if it's empty in this case. - resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} # The AZURE_RESOURCE_GROUP_NAME is a built-in environment variable. TeamsFx will ask you to select or create one resource group if its value is empty. You're free to reference other environment varialbe here, but TeamsFx will not ask you to select or create resource grouop if it's empty in this case. - templates: - - path: ./infra/azure.bicep - parameters: ./infra/azure.parameters.json - deploymentName: Create-resources-for-bot - bicepCliVersion: v0.9.1 # Teams Toolkit will download this bicep CLI version from github for you, will use bicep CLI in PATH if you remove this config. - - - uses: aadApp/update # Apply the AAD manifest to an existing AAD app. Will use the object id in manifest file to determine which AAD app to update. - with: - manifestPath: ./aad.manifest.json # Relative path to teamsfx folder. Environment variables in manifest will be replaced before apply to AAD app - outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json + botId: ${{BOT_ID}} + name: Bot Configuration + messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages + description: "" + channels: + - name: msteams # Validate using manifest schema - uses: teamsApp/validateManifest with: # Path to manifest template manifestPath: ./appPackage/manifest.json - # Build Teams app package with latest env value - uses: teamsApp/zipAppPackage with: # Path to manifest template manifestPath: ./appPackage/manifest.json outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip - outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + outputFolder: ./appPackage/build # Validate app package using validation rules - uses: teamsApp/validateAppPackage with: @@ -81,9 +69,9 @@ provision: appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip # Apply the Teams app manifest to an existing Teams app in - # Teams Developer Portal. + # Developer Portal. # Will use the app id in manifest file to determine which Teams app to update. - uses: teamsApp/update with: # Relative path to this file. This is the path for built zip file. - appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip \ No newline at end of file + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip diff --git a/samples/bot-configuration-app/csharp/M365Agent/m365agents.yml b/samples/bot-configuration-app/csharp/M365Agent/m365agents.yml index 42b41b2d1f..802603baf3 100644 --- a/samples/bot-configuration-app/csharp/M365Agent/m365agents.yml +++ b/samples/bot-configuration-app/csharp/M365Agent/m365agents.yml @@ -1,9 +1,89 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.2/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/m365-agents-toolkits/v1.9/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.2 +version: v1.9 -additionalMetadata: - sampleTag: Microsoft-Teams-Samples:bot-configuration-app-csharp +environmentFolderPath: ./env -environmentFolderPath: ./env \ No newline at end of file +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: Bot Configuration${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-bot + # Microsoft 365 Agents Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputFolder: ./appPackage/build + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + +# Triggered when 'teamsapp deploy' is executed +deploy: + - uses: cli/runDotnetCommand + with: + args: publish --configuration Release --runtime win-x86 --self-contained Bot + Configuration.csproj + workingDirectory: ../Bot Configuration + # Deploy your application to Azure App Service using the zip deploy feature. + # For additional details, refer to https://aka.ms/zip-deploy-to-app-services. + - uses: azureAppService/zipDeploy + with: + # Deploy base folder + artifactFolder: bin/Release/net10.0/win-x86/publish + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}} + workingDirectory: ../Bot Configuration +projectId: cd30fdea-a885-4f8a-bcb8-9204c084148d diff --git a/samples/bot-configuration-app/csharp/README.md b/samples/bot-configuration-app/csharp/README.md index 1a9da634b7..e2445d6364 100644 --- a/samples/bot-configuration-app/csharp/README.md +++ b/samples/bot-configuration-app/csharp/README.md @@ -78,16 +78,16 @@ The simplest way to run this sample in Teams is to use Microsoft 365 Agents Tool - While registering the bot, use `https:///api/messages` as the messaging endpoint. 3. Setup NGROK - - Run ngrok - point to port 3978 + - Run ngrok - point to port 5130 ```bash - ngrok http 3978 --host-header="localhost:3978" + ngrok http 5130 --host-header="localhost:5130" ``` Alternatively, you can also use the `dev tunnels`. Please follow [Create and host a dev tunnel](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below: ```bash - devtunnel host -p 3978 --allow-anonymous + devtunnel host -p 5130 --allow-anonymous ``` 4. Setup for code @@ -97,9 +97,11 @@ The simplest way to run this sample in Teams is to use Microsoft 365 Agents Tool ```bash git clone https://github.com/OfficeDev/Microsoft-Teams-Samples.git ``` - - Modify the `/appsettings.json` and fill in the following details: - - `{{MicrosoftAppId}}` - Generated from Step 1 while doing AAd app registration in Azure portal. + - Modify the `/appsettings.development.json` and fill in the following details: + - `{{ClientId}}` - Generated from Step 1 while doing AAd app registration in Azure portal. - `{{ClientSecret}}` - Generated from Step 1, also referred to as Client secret + - `{{BotType}}` - Default set is SingleTenant + - `{{TenantId}}` - Tenant Id where your app is registered. - Run the bot from a terminal or from Visual Studio: @@ -110,13 +112,13 @@ The simplest way to run this sample in Teams is to use Microsoft 365 Agents Tool - Select `Bot Configuration.csproj` file 6. This step is related to Microsoft Teams app manifest - - **Edit** the `manifest.json` contained in the `appPackage` or `AppManifest_Hub` folder to replace your Microsoft App Id (that was created when you registered your bot earlier) *everywhere* you see the place holder string `{{Microsoft-App-Id}}` + - **Edit** the `manifest.json` contained in the `appPackage` folder to replace your Microsoft App Id (that was created when you registered your bot earlier) *everywhere* you see the place holder string `{{TEAMS_APP_ID}}` and `{{BOT_ID}}` - replace `{{domain-name}}` with base Url of your domain. E.g. if you are using ngrok it would be `https://1234.ngrok-free.app` then your domain-name will be `1234.ngrok-free.app` and if you are using dev tunnels then your domain will be like: `12345.devtunnels.ms`. - **Zip** up the contents of the `Manifest` or `Manifest_hub` folder to create a `manifest.zip` - **Upload** the `manifest.zip` to Teams (in the Apps view click "Upload a custom app") - Add the app to team/groupChat scope (Supported scopes) -**Note:** If you want to test your app across multi hub like: Outlook/Office.com, please update the `manifest.json` in the `/AppManifest_Hub` folder with the required values. +**Note:** If you want to test your app across multi hub like: Outlook/Office.com, please update the `manifest.json` in the `/appPackage` folder with the required values. ## Running the sample @@ -162,7 +164,7 @@ Deploy your project to Azure by following these steps: ## Further reading - [Bot configuration](https://learn.microsoft.com/microsoftteams/platform/bots/how-to/bot-configuration-experience) -- [Bot Framework Documentation](https://docs.botframework.com) +- [Azure AI Bot Service Documentation](https://docs.botframework.com) - [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) - [Send Notification to User in Chat](https://docs.microsoft.com/graph/api/chat-sendactivitynotification?view=graph-rest-beta) - [Send Notification to User in Team](https://docs.microsoft.com/graph/api/team-sendactivitynotification?view=graph-rest-beta&tabs=http) diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder.sln b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder.sln deleted file mode 100644 index b957708c5c..0000000000 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder.sln +++ /dev/null @@ -1,37 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.10.34814.14 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotDailyTaskReminder", "BotDailyTaskReminder\BotDailyTaskReminder.csproj", "{BFDF6B3D-BB62-42DD-88EF-528FFE9FCBAD}" -EndProject -Project("{A9E3F50B-275E-4AF7-ADCE-8BE12D41E305}") = "M365Agent", "M365Agent\M365Agent.ttkproj", "{01E9AC3F-43C7-46DA-89BB-7E70F4B08B5C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9268A822-4DDA-4A0B-A2EB-E3551E6CE680}" - ProjectSection(SolutionItems) = preProject - BotDailyTaskReminder.slnLaunch.user = BotDailyTaskReminder.slnLaunch.user - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {BFDF6B3D-BB62-42DD-88EF-528FFE9FCBAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BFDF6B3D-BB62-42DD-88EF-528FFE9FCBAD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BFDF6B3D-BB62-42DD-88EF-528FFE9FCBAD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BFDF6B3D-BB62-42DD-88EF-528FFE9FCBAD}.Release|Any CPU.Build.0 = Release|Any CPU - {01E9AC3F-43C7-46DA-89BB-7E70F4B08B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {01E9AC3F-43C7-46DA-89BB-7E70F4B08B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {01E9AC3F-43C7-46DA-89BB-7E70F4B08B5C}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {01E9AC3F-43C7-46DA-89BB-7E70F4B08B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {01E9AC3F-43C7-46DA-89BB-7E70F4B08B5C}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {8F7F570D-74C3-4CF3-AACD-361A3D3F8E37} - EndGlobalSection -EndGlobal diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder.slnLaunch.user b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder.slnLaunch.user index de6458746c..75b37d5d87 100644 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder.slnLaunch.user +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder.slnLaunch.user @@ -1,31 +1,52 @@ [ { - "Name": "Microsoft Teams (browser)", + "Name": "Microsoft 365 Agents Playground (browser)", "Projects": [ + { + "Path": "M365Agent\\M365Agent.atkproj", + "Name": "M365Agent\\M365Agent.atkproj", + "Action": "StartWithoutDebugging", + "DebugTarget": "Microsoft 365 Agents Playground (browser)" + }, { "Path": "BotDailyTaskReminder\\BotDailyTaskReminder.csproj", + "Name": "BotDailyTaskReminder\\BotDailyTaskReminder.csproj", "Action": "Start", - "DebugTarget": "Start Project" - }, + "DebugTarget": "Microsoft 365 Agents Playground" + } + ] + }, + { + "Name": "Microsoft Teams (browser)", + "Projects": [ { - "Path": "M365Agent\\M365Agent.ttkproj", + "Path": "M365Agent\\M365Agent.atkproj", + "Name": "M365Agent\\M365Agent.atkproj", "Action": "StartWithoutDebugging", "DebugTarget": "Microsoft Teams (browser)" + }, + { + "Path": "BotDailyTaskReminder\\BotDailyTaskReminder.csproj", + "Name": "BotDailyTaskReminder\\BotDailyTaskReminder.csproj", + "Action": "Start", + "DebugTarget": "Start Project" } ] }, { "Name": "Microsoft Teams (browser) (skip update app)", "Projects": [ + { + "Path": "M365Agent\\M365Agent.atkproj", + "Name": "M365Agent\\M365Agent.atkproj", + "Action": "StartWithoutDebugging", + "DebugTarget": "Microsoft Teams (browser) (skip update app)" + }, { "Path": "BotDailyTaskReminder\\BotDailyTaskReminder.csproj", + "Name": "BotDailyTaskReminder\\BotDailyTaskReminder.csproj", "Action": "Start", "DebugTarget": "Start Project" - }, - { - "Path": "M365Agent\\M365Agent.ttkproj", - "Action": "StartWithoutDebugging", - "DebugTarget": "Microsoft Teams (browser) (skip update app)" } ] } diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder.slnx b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder.slnx new file mode 100644 index 0000000000..4a5917d899 --- /dev/null +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder.slnx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/.gitignore b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/.gitignore index 7466c01f9c..77c7154916 100644 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/.gitignore +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/.gitignore @@ -5,6 +5,7 @@ env/.env.*.user env/.env.local appsettings.Development.json .deployment +appsettings.Playground.json # User-specific files *.user @@ -22,4 +23,8 @@ bld/ [Ll]og/ # Notification local store -.notification.localstore.json \ No newline at end of file +.notification.localstore.json +.notification.playgroundstore.json + +# devTools +devTools/ \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/AdapterWithErrorHandler.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/AdapterWithErrorHandler.cs deleted file mode 100644 index 1402062430..0000000000 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/AdapterWithErrorHandler.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Threading.Tasks; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Connector; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Bot.Schema; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using System.Net.Http; - -namespace BotDailyTaskReminder -{ - /// - /// Custom adapter that adds error handling to the bot's turn logic. - /// - public class AdapterWithErrorHandler : CloudAdapter - { - public AdapterWithErrorHandler(IConfiguration configuration, IHttpClientFactory httpClientFactory, ILogger logger, ConversationState conversationState = default) - : base(configuration, httpClientFactory, logger) - { - OnTurnError = async (turnContext, exception) => - { - // Log any leaked exception from the application. - // NOTE: In production environment, consider logging this to - // Azure Application Insights. - logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); - - // Uncomment below commented line for local debugging. - // await turnContext.SendActivityAsync($"Sorry, it looks like something went wrong. Exception Caught: {exception.Message}"); - - // Send a trace activity, which will be displayed in the Bot Framework Emulator - await SendTraceActivityAsync(turnContext, exception); - }; - } - - /// - /// Sends a trace activity to the Bot Framework Emulator in case of an error. - /// Only sends if the bot is running in the Emulator. - /// - private static async Task SendTraceActivityAsync(ITurnContext turnContext, Exception exception) - { - // Only send a trace activity if we're talking to the Bot Framework Emulator - if (turnContext.Activity.ChannelId == Channels.Emulator) - { - Activity traceActivity = new Activity(ActivityTypes.Trace) - { - Label = "TurnError", - Name = "OnTurnError Trace", - Value = exception.Message, - ValueType = "https://www.botframework.com/schemas/error", - }; - - // Send a trace activity - await turnContext.SendActivityAsync(traceActivity); - } - } - } -} diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/BotDailyTaskReminder.csproj b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/BotDailyTaskReminder.csproj index 194be96d1b..b5536edfc2 100644 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/BotDailyTaskReminder.csproj +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/BotDailyTaskReminder.csproj @@ -1,28 +1,37 @@  - - net6.0 - latest - + + net10.0 + enable + true + - - - - - - - - + + + + + + + + + + - - - Always + + + + + PreserveNewest + None + + + + PreserveNewest + None - - - - - - - + + + + +
\ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Bots/ActivityBot.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Bots/ActivityBot.cs deleted file mode 100644 index 7572ea2522..0000000000 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Bots/ActivityBot.cs +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using AdaptiveCards; -using BotDailyTaskReminder.Models; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Teams; -using Microsoft.Bot.Schema; -using Microsoft.Bot.Schema.Teams; -using Microsoft.Extensions.Configuration; -using Newtonsoft.Json.Linq; - -namespace BotDailyTaskReminder.Bots -{ - /// - /// Handles incoming bot activities such as messages, task module fetch, and task module submission. - /// - public class ActivityBot : TeamsActivityHandler - { - private readonly string _applicationBaseUrl; - protected readonly BotState _conversationState; - private readonly ConcurrentDictionary _conversationReferences; - private readonly ConcurrentDictionary> _taskDetails; - - /// - /// Initializes a new instance of the class. - /// - public ActivityBot(IConfiguration configuration, - ConversationState conversationState, - ConcurrentDictionary conversationReferences, - ConcurrentDictionary> taskDetails) - { - _conversationReferences = conversationReferences; - _conversationState = conversationState; - _taskDetails = taskDetails; - _applicationBaseUrl = configuration["ApplicationBaseUrl"] ?? throw new NullReferenceException("ApplicationBaseUrl"); - } - - /// - /// Handles when a message is addressed to the bot. - /// - /// The turn context of the message activity. - /// Cancellation token for the asynchronous task. - /// A task that represents the work queued to execute. - protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) - { - if (turnContext.Activity.Text.ToLower().Trim() == "create-reminder") - { - // Adds the current conversation reference and sends the task scheduling adaptive card - AddConversationReference(turnContext.Activity as Activity); - await turnContext.SendActivityAsync(MessageFactory.Attachment(GetAdaptiveCardForTaskModule()), cancellationToken); - } - } - - /// - /// Handles the completion of a turn, saving any state changes. - /// - /// The context of the current turn. - /// Cancellation token for the asynchronous task. - /// A task that represents the work queued to execute. - public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) - { - await base.OnTurnAsync(turnContext, cancellationToken); - - // Save any changes made to conversation state during this turn. - await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken); - } - - /// - /// Invoked when the bot is added to a conversation. - /// Sends a welcome message to new members. - /// - /// The members added to the conversation. - /// The context of the current turn. - /// Cancellation token for the asynchronous task. - /// A task that represents the work queued to execute. - protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) - { - foreach (var member in turnContext.Activity.MembersAdded) - { - if (member.Id != turnContext.Activity.Recipient.Id) - { - // Sends a greeting message when a new user is added - await turnContext.SendActivityAsync(MessageFactory.Text("Hello and welcome! Use the command 'create-reminder' to schedule a recurring task and receive reminders."), cancellationToken); - } - } - } - - /// - /// Handles task module fetch requests. - /// - /// The context of the turn. - /// The request payload for the task module. - /// Cancellation token for the asynchronous task. - /// The task module response to send back. - protected override Task OnTeamsTaskModuleFetchAsync(ITurnContext turnContext, TaskModuleRequest taskModuleRequest, CancellationToken cancellationToken) - { - var asJobject = JObject.FromObject(taskModuleRequest.Data); - var buttonType = (string)asJobject.ToObject>()?.Id; - var taskModuleResponse = new TaskModuleResponse(); - - if (buttonType == "schedule") - { - taskModuleResponse.Task = new TaskModuleContinueResponse - { - Type = "continue", - Value = new TaskModuleTaskInfo - { - Url = _applicationBaseUrl + "/ScheduleTask", - Height = 450, - Width = 450, - Title = "Schedule a task", - }, - }; - } - - return Task.FromResult(taskModuleResponse); - } - - /// - /// Handles task module submission requests. - /// - /// The context of the turn. - /// The request payload for the task module. - /// Cancellation token for the asynchronous task. - /// The task module response to send back. - protected override async Task OnTeamsTaskModuleSubmitAsync(ITurnContext turnContext, TaskModuleRequest taskModuleRequest, CancellationToken cancellationToken) - { - var asJobject = JObject.FromObject(taskModuleRequest.Data); - var title = (string)asJobject.ToObject>()?.Title; - var description = (string)asJobject.ToObject>()?.Description; - var dateTime = (DateTime)asJobject.ToObject>()?.DateTime; - var selectedDaysObject = (JArray)asJobject.ToObject>()?.SelectedDays; - var selectedDays = selectedDaysObject.ToObject(); - var date = dateTime.ToLocalTime(); - - // Prepare task details - var taskDetails = new SaveTaskDetail - { - Description = description, - Title = title, - DateTime = new DateTimeOffset(date.Year, date.Month, date.Day, date.Hour, date.Minute, 0, TimeSpan.Zero), - SelectedDays = selectedDays - }; - - // Add the task to the task list - _taskDetails.AddOrUpdate("taskDetails", new List { taskDetails }, (key, currentTaskList) => - { - currentTaskList.Add(taskDetails); - return currentTaskList; - }); - - // Schedule the task - var taskScheduler = new TaskScheduler(); - taskScheduler.Start(date.Hour, date.Minute, _applicationBaseUrl, selectedDays); - - // Send a success message to the user - await turnContext.SendActivityAsync("Task submitted successfully, you will get a recurring reminder for the task at a scheduled time"); - - return null; - } - - /// - /// Creates and returns the adaptive card for scheduling a task. - /// - /// The adaptive card as an attachment. - private Attachment GetAdaptiveCardForTaskModule() - { - var card = new AdaptiveCard(new AdaptiveSchemaVersion("1.2")) - { - Body = new List - { - new AdaptiveTextBlock - { - Text = "Please click here to schedule a recurring task reminder", - Weight = AdaptiveTextWeight.Bolder, - Spacing = AdaptiveSpacing.Medium, - } - }, - Actions = new List - { - new AdaptiveSubmitAction - { - Title = "Schedule task", - Data = new AdaptiveCardAction - { - MsteamsCardAction = new CardAction - { - Type = "task/fetch", - }, - Id = "schedule" - }, - } - }, - }; - - return new Attachment - { - ContentType = AdaptiveCard.ContentType, - Content = card, - }; - } - - /// - /// Adds a conversation reference for the current activity. - /// - /// The bot activity. - private void AddConversationReference(Activity activity) - { - var conversationReference = activity.GetConversationReference(); - _conversationReferences.AddOrUpdate(conversationReference.User.Id, conversationReference, (key, newValue) => conversationReference); - } - } -} diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Config.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Config.cs new file mode 100644 index 0000000000..b131400c57 --- /dev/null +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Config.cs @@ -0,0 +1,16 @@ +namespace BotDailyTaskReminder +{ + public class ConfigOptions + { + public TeamsConfigOptions Teams { get; set; } + public string ApplicationBaseUrl { get; set; } + } + + public class TeamsConfigOptions + { + public string BotType { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string TenantId { get; set; } + } +} \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Controllers/BotController.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Controllers/BotController.cs deleted file mode 100644 index 9cfab11cfd..0000000000 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Controllers/BotController.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// -// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.14.0 - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using System; -using System.Threading.Tasks; - -namespace BotDailyTaskReminder.Controllers -{ - /// - /// This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot - /// implementation at runtime. Multiple different IBot implementations running at different endpoints can be - /// achieved by specifying a more specific type for the bot constructor argument. - /// - [Route("api/messages")] - [ApiController] - public class BotController : ControllerBase - { - private readonly IBotFrameworkHttpAdapter _adapter; - private readonly IBot _bot; - - /// - /// Initializes a new instance of the class. - /// - /// The bot framework HTTP adapter. - /// The bot instance. - public BotController(IBotFrameworkHttpAdapter adapter, IBot bot) - { - _adapter = adapter ?? throw new ArgumentNullException(nameof(adapter)); - _bot = bot ?? throw new ArgumentNullException(nameof(bot)); - } - - /// - /// Handles both POST and GET requests to process messages for the bot. - /// - /// A task that represents the work queued to execute. - [HttpPost] - public async Task PostAsync() - { - // Delegate the processing of the HTTP POST to the adapter. - // The adapter will invoke the bot. - try - { - await _adapter.ProcessAsync(Request, Response, _bot); - } - catch (Exception ex) - { - // Log the exception here if necessary - // For example, use a logger to record the error - await Response.WriteAsync($"Error: {ex.Message}"); - } - } - } -} diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Controllers/Controller.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Controllers/Controller.cs new file mode 100644 index 0000000000..5d1934cc48 --- /dev/null +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Controllers/Controller.cs @@ -0,0 +1,259 @@ +using BotDailyTaskReminder.Models; +using Microsoft.Teams.Api.Activities; +using Microsoft.Teams.Api.Activities.Invokes; +using Microsoft.Teams.Api.TaskModules; +using Microsoft.Teams.Apps; +using Microsoft.Teams.Apps.Activities; +using Microsoft.Teams.Apps.Activities.Invokes; +using Microsoft.Teams.Apps.Annotations; +using Microsoft.Teams.Cards; +using Microsoft.Teams.Common; +using System.Collections.Concurrent; +using System.Text.Json; + + +namespace BotDailyTaskReminder.Controllers +{ + /// + [TeamsController] + public class Controller + { + private readonly ConcurrentDictionary _conversationReferences; + private readonly ConcurrentDictionary> _taskDetails; + private readonly string _applicationBaseUrl; + private IConfiguration _configuration; + + public Controller( + ConcurrentDictionary conversationReferences, + ConcurrentDictionary> taskDetails, + IConfiguration configuration) + { + _conversationReferences = conversationReferences; + _taskDetails = taskDetails; + _configuration = configuration; + _applicationBaseUrl = configuration["ApplicationBaseUrl"] ?? throw new NullReferenceException("ApplicationBaseUrl"); + } + + [Message] + public async System.Threading.Tasks.Task OnMessage( + [Context] Microsoft.Teams.Api.Activities.MessageActivity activity, + [Context] IContext.Client client, + [Context] Microsoft.Teams.Common.Logging.ILogger log) + { + log.Info("Message received"); + + // Store conversation reference for proactive messaging + StoreConversationReference(activity); + + var text = activity.Text?.ToLower().Trim() ?? string.Empty; + + if (text == "create-reminder") + { + // Send adaptive card with button to schedule task + var card = CreateTaskSchedulerCard(); + await client.Send(card); + } + else + { + await client.Typing(); + await client.Send($"You said '{activity.Text}'. Use command 'create-reminder' to schedule a recurring task."); + } + } + + [Conversation.MembersAdded] + public async System.Threading.Tasks.Task OnMembersAdded(IContext context) + { + var welcomeText = "Hello and welcome! Use the command 'create-reminder' to schedule a recurring task and receive reminders."; + foreach (var member in context.Activity.MembersAdded) + { + if (member.Id != context.Activity.Recipient.Id) + { + await context.Send(welcomeText); + } + } + } + + [TaskFetch] + public Microsoft.Teams.Api.TaskModules.Response OnTaskFetch([Context] Tasks.FetchActivity activity, [Context] IContext.Client client, [Context] Microsoft.Teams.Common.Logging.ILogger log) + { + var data = activity.Value?.Data as JsonElement?; + if (data == null) + { + log.Info("[TASK_FETCH] No data found in the activity value"); + return new Microsoft.Teams.Api.TaskModules.Response( + new Microsoft.Teams.Api.TaskModules.MessageTask("No data found in the activity value")); + } + + var dialogType = data.Value.TryGetProperty("opendialogtype", out var dialogTypeElement) && dialogTypeElement.ValueKind == JsonValueKind.String + ? dialogTypeElement.GetString() + : null; + + log.Info($"[TASK_FETCH] Dialog type: {dialogType}"); + + return dialogType switch + { + "webpage_dialog" => CreateWebpageDialog(_configuration, log), + _ => new Microsoft.Teams.Api.TaskModules.Response( + new Microsoft.Teams.Api.TaskModules.MessageTask("Unknown dialog type")) + }; + } + + [TaskSubmit] + public async System.Threading.Tasks.Task OnTaskSubmit( + [Context] Tasks.SubmitActivity activity, + [Context] IContext.Client client, + [Context] Microsoft.Teams.Common.Logging.ILogger log) + { + log.Info($"[TASK_SUBMIT] Activity.Value: {System.Text.Json.JsonSerializer.Serialize(activity.Value)}"); + + var data = activity.Value?.Data as JsonElement?; + + if (data == null) + { + log.Info("[TASK_SUBMIT] No data received - activity.Value.Data is null"); + return new Microsoft.Teams.Api.TaskModules.Response( + new Microsoft.Teams.Api.TaskModules.MessageTask("No data received")); + } + + log.Info($"[TASK_SUBMIT] Data: {data.Value.GetRawText()}"); + + var title = data.Value.TryGetProperty("taskName", out var titleElement) ? titleElement.GetString() : null; + var description = data.Value.TryGetProperty("taskDescription", out var descElement) ? descElement.GetString() : null; + var reminderTimeStr = data.Value.TryGetProperty("reminderTime", out var timeElement) ? timeElement.GetString() : null; + + log.Info($"[TASK_SUBMIT] Parsed values - Title: {title}, Description: {description}, Time: {reminderTimeStr}"); + + if (string.IsNullOrEmpty(title) || string.IsNullOrEmpty(reminderTimeStr)) + { + return new Microsoft.Teams.Api.TaskModules.Response( + new Microsoft.Teams.Api.TaskModules.MessageTask("Please fill in all required fields")); + } + + log.Info($"[TASK_SUBMIT] Task created: {title}, Time: {reminderTimeStr}"); + + // Parse the reminder datetime (comes as ISO string from HTML) + if (!DateTimeOffset.TryParse(reminderTimeStr, out var nextReminder)) + { + return new Microsoft.Teams.Api.TaskModules.Response( + new Microsoft.Teams.Api.TaskModules.MessageTask("Invalid time format")); + } + + // Get selected days from the form + DayOfWeek[] selectedDays; + if (data.Value.TryGetProperty("selectedDays", out var daysElement) && daysElement.ValueKind == JsonValueKind.Array) + { + var daysList = new List(); + foreach (var dayValue in daysElement.EnumerateArray()) + { + if (int.TryParse(dayValue.GetString(), out var dayNum)) + { + daysList.Add((DayOfWeek)dayNum); + } + } + selectedDays = daysList.Count > 0 ? daysList.ToArray() : new[] { DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday }; + } + else + { + // Default to all days if none selected + selectedDays = new[] { DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday }; + } + + // Store the task details + var userId = activity.From?.Id ?? "default"; + var taskDetail = new SaveTaskDetail + { + Title = title, + Description = description ?? string.Empty, + DateTime = nextReminder, + SelectedDays = selectedDays + }; + + _taskDetails.AddOrUpdate(userId, + new List { taskDetail }, + (key, existingList) => + { + existingList.Add(taskDetail); + return existingList; + }); + + // Build frequency description for confirmation message + string frequencyDesc = selectedDays.Length == 7 ? "daily" : + selectedDays.Length == 1 ? $"every {selectedDays[0]}" : + $"on {string.Join(", ", selectedDays)}"; + + // Send confirmation message to chat + var confirmationMessage = $"Task submitted successfully. You will get a recurring reminder for the task at a scheduled time."; + + await client.Send(confirmationMessage); + + return new Microsoft.Teams.Api.TaskModules.Response( + new Microsoft.Teams.Api.TaskModules.MessageTask("Task scheduled successfully!")); + } + + private static Microsoft.Teams.Api.TaskModules.Response CreateWebpageDialog(IConfiguration configuration, [Context] Microsoft.Teams.Common.Logging.ILogger log) + { + var botEndpoint = configuration["ApplicationBaseUrl"]; + if (string.IsNullOrEmpty(botEndpoint)) + { + botEndpoint = "http://localhost:3978"; // Fallback for local development + } + else + { + log.Info($"Using BotEndpoint: {botEndpoint}/tabs/ScheduleTask"); + } + + var taskInfo = new TaskInfo + { + Title = "Webpage Dialog", + Width = new Union(1000), + Height = new Union(800), + Url = $"{botEndpoint}/tabs/ScheduleTask" + }; + + return new Microsoft.Teams.Api.TaskModules.Response( + new Microsoft.Teams.Api.TaskModules.ContinueTask(taskInfo)); + } + + + private void StoreConversationReference(Microsoft.Teams.Api.Activities.MessageActivity activity) + { + var userId = activity.From?.Id; + if (!string.IsNullOrEmpty(userId) && activity.Conversation != null) + { + var reference = new Microsoft.Teams.Api.ConversationReference + { + Bot = activity.Recipient, + Conversation = activity.Conversation, + ChannelId = activity.ChannelId, + ServiceUrl = activity.ServiceUrl + }; + + _conversationReferences.AddOrUpdate(userId, reference, (key, oldValue) => reference); + } + } + + private static Microsoft.Teams.Cards.AdaptiveCard CreateTaskSchedulerCard() + { + var card = new Microsoft.Teams.Cards.AdaptiveCard + { + Body = new List + { + new TextBlock("Please click here to schedule a recurring task reminde") + { + Size = TextSize.Large, + Weight = TextWeight.Bolder + } + }, + Actions = new List + { + new TaskFetchAction(new Dictionary { { "opendialogtype", "webpage_dialog" } }) + { + Title = "Schedule task" + } + } + }; + + return card; + } + } +} \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Controllers/TaskController.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Controllers/TaskController.cs new file mode 100644 index 0000000000..ed0b85fec7 --- /dev/null +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Controllers/TaskController.cs @@ -0,0 +1,90 @@ +using System.Collections.Concurrent; +using BotDailyTaskReminder.Models; +using Microsoft.AspNetCore.Mvc; + +namespace BotDailyTaskReminder.Controllers +{ + /// + /// Controller to handle task reminder API calls. + /// This endpoint is called by the Quartz scheduler. + /// The actual reminder sending is handled through event handlers in the main bot controller. + /// + [Route("api/[controller]")] + [ApiController] + public class TaskController : ControllerBase + { + private readonly ConcurrentDictionary> _taskDetails; + private readonly ConcurrentDictionary _conversationReferences; + + /// + /// Initializes a new instance of the class. + /// + /// The task details storage. + /// The conversation references storage. + public TaskController( + ConcurrentDictionary> taskDetails, + ConcurrentDictionary conversationReferences) + { + _taskDetails = taskDetails; + _conversationReferences = conversationReferences; + } + + /// + /// This endpoint is called by the Quartz scheduler to trigger task reminder checks. + /// The actual proactive messaging needs to be implemented using the Teams SDK's proactive messaging capabilities. + /// + [HttpGet] + public System.Threading.Tasks.Task GetTaskReminder() + { + try + { + // Log the trigger + Console.WriteLine($"[TASK_REMINDER] Reminder check triggered at {DateTime.UtcNow}"); + + // Check if there are tasks to process + var hasTasks = _taskDetails.TryGetValue("taskDetails", out var tasks); + var conversationCount = _conversationReferences.Count; + + if (hasTasks && tasks != null) + { + var currentDateTime = DateTime.Now; + Console.WriteLine($"[TASK_REMINDER] Checking {tasks.Count} tasks against current time: {currentDateTime}"); + + // Check which tasks should trigger + foreach (var task in tasks) + { + if (task.DateTime.Hour == currentDateTime.Hour && + task.DateTime.Minute == currentDateTime.Minute) + { + foreach (var day in task.SelectedDays) + { + if ((int)day == (int)currentDateTime.DayOfWeek || + ((int)day == 7 && (int)currentDateTime.DayOfWeek == 0)) + { + Console.WriteLine($"[TASK_REMINDER] Task '{task.Title}' should be sent now!"); + } + } + } + } + } + + return System.Threading.Tasks.Task.FromResult(Ok(new { + status = "success", + message = "Task reminder check completed", + timestamp = DateTime.UtcNow, + taskCount = hasTasks && tasks != null ? tasks.Count : 0, + conversationCount = conversationCount + })); + } + catch (Exception ex) + { + Console.WriteLine($"[TASK_REMINDER] Error: {ex.Message}"); + return System.Threading.Tasks.Task.FromResult(StatusCode(500, new { + error = "Failed to process task reminders", + details = ex.Message + })); + } + } + } +} + diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Controllers/TaskReminderController.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Controllers/TaskReminderController.cs deleted file mode 100644 index dfd35afd5f..0000000000 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Controllers/TaskReminderController.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using AdaptiveCards; -using BotDailyTaskReminder.Models; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Bot.Schema; -using Microsoft.Extensions.Configuration; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace BotDailyTaskReminder.Controllers -{ - /// - /// Controller to handle task reminders. - /// - [Route("api/task")] - [ApiController] - public class TaskReminderController : ControllerBase - { - private readonly IBotFrameworkHttpAdapter _adapter; - private readonly string _appId; - private readonly ConcurrentDictionary> _taskDetails; - private readonly ConcurrentDictionary _conversationReferences; - - /// - /// Initializes a new instance of the class. - /// - /// The bot framework HTTP adapter. - /// The configuration instance. - /// The conversation references. - /// The task details. - public TaskReminderController(IBotFrameworkHttpAdapter adapter, - IConfiguration configuration, - ConcurrentDictionary conversationReferences, - ConcurrentDictionary> taskDetails) - { - _adapter = adapter; - _conversationReferences = conversationReferences; - _taskDetails = taskDetails; - _appId = configuration["MicrosoftAppId"] ?? string.Empty; - } - - /// - /// This endpoint is called to send task reminder cards. - /// - [HttpGet] - public async Task GetTaskReminder() - { - foreach (var conversationReference in _conversationReferences.Values) - { - await ((BotAdapter)_adapter).ContinueConversationAsync(_appId, conversationReference, BotCallback, default); - } - } - - /// - /// Callback method to send activity. - /// - /// The turn context. - /// The cancellation token. - /// A task that represents the work queued to execute. - private async Task BotCallback(ITurnContext turnContext, CancellationToken cancellationToken) - { - if (_taskDetails.TryGetValue("taskDetails", out var taskList)) - { - var currentDateTime = DateTime.Now; - - foreach (var task in taskList) - { - if (task.DateTime.Hour == currentDateTime.Hour && - task.DateTime.Minute == currentDateTime.Minute && - task.DateTime.Date == currentDateTime.Date) - { - foreach (var day in task.SelectedDays) - { - if ((int)day == (int)currentDateTime.DayOfWeek || - ((int)day == 7 && (int)currentDateTime.DayOfWeek == 0)) - { - await turnContext.SendActivityAsync(MessageFactory.Attachment(GetAdaptiveCardForTaskReminder(task.Title, task.Description)), cancellationToken); - } - } - } - } - } - } - - /// - /// Creates and returns an adaptive card for task reminder. - /// - /// The task title. - /// The task description. - /// The adaptive card as an attachment. - private Attachment GetAdaptiveCardForTaskReminder(string title, string description) - { - var card = new AdaptiveCard(new AdaptiveSchemaVersion("1.2")) - { - Body = new List - { - new AdaptiveTextBlock - { - Text = "Reminder for a scheduled task!", - Weight = AdaptiveTextWeight.Bolder, - Spacing = AdaptiveSpacing.Medium, - }, - new AdaptiveTextBlock - { - Text = "Task title: " + title, - Weight = AdaptiveTextWeight.Default, - Spacing = AdaptiveSpacing.Medium, - Wrap = true, - }, - new AdaptiveTextBlock - { - Text = "Task description: " + description, - Weight = AdaptiveTextWeight.Default, - Spacing = AdaptiveSpacing.Medium, - Wrap = true, - } - }, - }; - - return new Attachment - { - ContentType = AdaptiveCard.ContentType, - Content = card, - }; - } - } -} \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Helper/ScheduleTaskReminder.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Helper/ScheduleTaskReminder.cs index e4aaaf6f4f..e7a7019777 100644 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Helper/ScheduleTaskReminder.cs +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Helper/ScheduleTaskReminder.cs @@ -1,13 +1,6 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - using Quartz; -using System; -using System.Net.Http; -using System.Threading.Tasks; -namespace BotDailyTaskReminder +namespace BotDailyTaskReminder.Helper { /// /// Class to handle scheduled task reminders. diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Helper/TaskScheduler.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Helper/TaskScheduler.cs index 0197e0086e..2659e1a9b6 100644 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Helper/TaskScheduler.cs +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Helper/TaskScheduler.cs @@ -1,12 +1,6 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - using Quartz; -using Quartz.Impl; -using System; -namespace BotDailyTaskReminder +namespace BotDailyTaskReminder.Helper { /// /// Class to handle task scheduling. @@ -25,7 +19,7 @@ public void Start(int hour, int min, string baseUrl, DayOfWeek[] selectedDays) try { // Synchronously get the scheduler and start it - var scheduler = StdSchedulerFactory.GetDefaultScheduler().GetAwaiter().GetResult(); + var scheduler = Quartz.Impl.StdSchedulerFactory.GetDefaultScheduler().GetAwaiter().GetResult(); scheduler.Start(); var job = JobBuilder.Create() diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/AdaptiveCardAction.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/AdaptiveCardAction.cs deleted file mode 100644 index 3bd32046ae..0000000000 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/AdaptiveCardAction.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - -namespace BotDailyTaskReminder.Models -{ - using Microsoft.Bot.Schema; - using Newtonsoft.Json; - - /// - /// Adaptive card action model class. - /// - public class AdaptiveCardAction - { - /// - /// Gets or sets Ms Teams card action type. - /// - [JsonProperty("msteams")] - public CardAction MsteamsCardAction { get; set; } - - /// - /// Gets or sets id value of turncontext activity. - /// - [JsonProperty("id")] - public string Id { get; set; } - } -} \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/CardTaskFetchValue.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/CardTaskFetchValue.cs deleted file mode 100644 index a79fc6cc5c..0000000000 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/CardTaskFetchValue.cs +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - -using Newtonsoft.Json; - -namespace BotDailyTaskReminder.Models -{ - /// - /// Card task fetch value model class. - /// - public class CardTaskFetchValue - { - /// - /// Gets or sets Ms Teams card action type. - /// - [JsonProperty("type")] - public object Type { get; set; } = "task/fetch"; - - /// - /// Gets or sets id value of turncontext activity. - /// - [JsonProperty("id")] - public object Id { get; set; } - - /// - /// Gets or sets data value. - /// - [JsonProperty("data")] - public T Data { get; set; } - } -} \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/SaveTaskDetail.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/SaveTaskDetail.cs index b1a705828a..daf726aa70 100644 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/SaveTaskDetail.cs +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/SaveTaskDetail.cs @@ -1,10 +1,3 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - -using System; -using System.Collections.Generic; - namespace BotDailyTaskReminder.Models { /// @@ -32,4 +25,4 @@ public class SaveTaskDetail /// public DayOfWeek[] SelectedDays { get; set; } } -} \ No newline at end of file +} diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/TaskDetails.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/TaskDetails.cs index cb8270842b..a1985648f2 100644 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/TaskDetails.cs +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Models/TaskDetails.cs @@ -1,38 +1,34 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BotDailyTaskReminder.Models { /// - /// Task details model class. + /// Task details model class for form submissions. /// - public class TaskDetails + public class TaskDetails { /// /// Gets or sets title value of task. /// - [JsonProperty("title")] - public object Title { get; set; } + [JsonPropertyName("title")] + public string Title { get; set; } /// /// Gets or sets description value of task. /// - [JsonProperty("description")] - public object Description { get; set; } + [JsonPropertyName("description")] + public string Description { get; set; } /// /// Gets or sets date-time value of task. /// - [JsonProperty("dateTime")] - public object DateTime { get; set; } + [JsonPropertyName("dateTime")] + public DateTime DateTime { get; set; } /// - /// Gets or sets slected days value of task. + /// Gets or sets selected days value of task. /// - [JsonProperty("selectedDays")] - public object SelectedDays { get; set; } + [JsonPropertyName("selectedDays")] + public string[] SelectedDays { get; set; } } -} \ No newline at end of file +} diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Pages/ScheduleTask.cshtml b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Pages/ScheduleTask.cshtml deleted file mode 100644 index f299c83eca..0000000000 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Pages/ScheduleTask.cshtml +++ /dev/null @@ -1,134 +0,0 @@ -@page -@model BotDailyTaskReminder.Pages.ScheduleTaskModel -@{ -} - - - - - - - - - - - - - - - -
-
-
- - -
- -
-
- - - -
- -
-
- - - -
- -
-
- - - Please select days to be repeated -
-
- -
- -
- -
- -
- -
- -
- - -
-
- - - -
-
-
- diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Pages/ScheduleTask.cshtml.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Pages/ScheduleTask.cshtml.cs deleted file mode 100644 index 573a0ca509..0000000000 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Pages/ScheduleTask.cshtml.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace BotDailyTaskReminder.Pages -{ - public class ScheduleTaskModel - : PageModel - { - public void OnGet() - { - } - } -} diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Program.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Program.cs index e9de43831f..b988a48c6f 100644 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Program.cs +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Program.cs @@ -1,36 +1,76 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// -// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.14.0 +using System.Collections.Concurrent; +using BotDailyTaskReminder; +using BotDailyTaskReminder.Controllers; +using BotDailyTaskReminder.Models; +using BotDailyTaskReminder.Services; +using Azure.Core; +using Azure.Identity; +using Microsoft.Teams.Api.Auth; +using Microsoft.Teams.Apps; +using Microsoft.Teams.Apps.Extensions; +using Microsoft.Teams.Common.Http; +using Microsoft.Teams.Plugins.AspNetCore.Extensions; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; -using System; +var builder = WebApplication.CreateBuilder(args); +var config = builder.Configuration.Get(); -namespace BotDailyTaskReminder +Func> createTokenFactory = async (string[] scopes, string? tenantId) => { - public class Program + var clientId = config.Teams.ClientId; + + var managedIdentityCredential = new ManagedIdentityCredential(clientId); + var tokenRequestContext = new TokenRequestContext(scopes, tenantId: tenantId); + var accessToken = await managedIdentityCredential.GetTokenAsync(tokenRequestContext); + + return new TokenResponse { - public static void Main(string[] args) + TokenType = "Bearer", + AccessToken = accessToken.Token, + }; +}; + +var appBuilder = App.Builder(); + +if (config.Teams.BotType == "UserAssignedMsi") +{ + appBuilder.AddCredentials(new TokenCredentials( + config.Teams.ClientId ?? string.Empty, + async (tenantId, scopes) => { - try - { - // Create and run the host - CreateHostBuilder(args).Build().Run(); - } - catch (Exception ex) - { - // Log the error if the application fails to start - Console.WriteLine($"Application failed to start: {ex.Message}"); - } + return await createTokenFactory(scopes, tenantId); } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - // Specify the Startup class to configure the application - webBuilder.UseStartup(); - }); - } + )); } + +// Register singleton services for state management +builder.Services.AddSingleton(new ConcurrentDictionary()); +builder.Services.AddSingleton(new ConcurrentDictionary>()); + +// Add controllers for API endpoints +builder.Services.AddControllers(); + +// Register the Teams controller with explicit dependencies +builder.Services.AddSingleton(sp => new Controller( + sp.GetRequiredService>(), + sp.GetRequiredService>>(), + builder.Configuration +)); + +// Register the background service for sending reminders +builder.Services.AddHostedService(); + +builder.AddTeams(appBuilder); + +var app = builder.Build(); + +// Enable static files from wwwroot +app.UseStaticFiles(); + +// Map controllers for API endpoints +app.MapControllers(); + +app.UseStaticFiles(); + +app.UseTeams(); +app.AddTab("ScheduleTask", "wwwroot"); +app.Run(); diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Properties/launchSettings.json b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Properties/launchSettings.json index d985e43c6f..3572a7a03f 100644 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Properties/launchSettings.json +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Properties/launchSettings.json @@ -1,13 +1,26 @@ { - "profiles": { - "Start Project": { - "commandName": "Project", - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:7130;http://localhost:5130", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "hotReloadProfile": "aspnetcore" - } - } + "profiles": { + // Debug project within Microsoft 365 Agents Playground + "Microsoft 365 Agents Playground": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Playground", + "TEAMSFX_NOTIFICATION_STORE_FILENAME": ".notification.playgroundstore.json", + "UPDATE_TEAMS_APP": "false" + }, + "hotReloadProfile": "aspnetcore" + }, + // Debug project within Teams + "Start Project": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "hotReloadProfile": "aspnetcore" + }, + } } \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Services/ReminderBackgroundService.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Services/ReminderBackgroundService.cs new file mode 100644 index 0000000000..1ee5d87deb --- /dev/null +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Services/ReminderBackgroundService.cs @@ -0,0 +1,130 @@ +using BotDailyTaskReminder.Models; +using Microsoft.Teams.Api; +using System.Collections.Concurrent; + +namespace BotDailyTaskReminder.Services +{ + public class ReminderBackgroundService : BackgroundService + { + private readonly ConcurrentDictionary _conversationReferences; + private readonly ConcurrentDictionary> _taskDetails; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public ReminderBackgroundService( + ConcurrentDictionary conversationReferences, + ConcurrentDictionary> taskDetails, + IServiceProvider serviceProvider, + ILogger logger) + { + _conversationReferences = conversationReferences; + _taskDetails = taskDetails; + _serviceProvider = serviceProvider; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Reminder Background Service is starting."); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await CheckAndSendReminders(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred while checking reminders"); + } + + // Check every minute + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + } + + _logger.LogInformation("Reminder Background Service is stopping."); + } + + private async Task CheckAndSendReminders(CancellationToken stoppingToken) + { + var now = DateTimeOffset.Now; + _logger.LogInformation($"Checking reminders at {now:g}"); + + foreach (var userTasks in _taskDetails) + { + var userId = userTasks.Key; + var tasks = userTasks.Value; + + if (!_conversationReferences.TryGetValue(userId, out var conversationReference)) + { + _logger.LogWarning($"No conversation reference found for user {userId}"); + continue; + } + + for (int i = 0; i < tasks.Count; i++) + { + var task = tasks[i]; + + // Check if it's time to send the reminder + if (ShouldSendReminder(task, now)) + { + _logger.LogInformation($"Sending reminder for task '{task.Title}' to user {userId}"); + + try + { + await SendProactiveReminder(conversationReference, task, stoppingToken); + + // Update the next reminder time + task.DateTime = CalculateNextReminderTime(task, now); + _logger.LogInformation($"Next reminder for task '{task.Title}' scheduled at {task.DateTime:g}"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to send reminder for task '{task.Title}'"); + } + } + } + } + } + + private bool ShouldSendReminder(SaveTaskDetail task, DateTimeOffset now) + { + // Check if the current day is in the selected days + if (!task.SelectedDays.Contains(now.DayOfWeek)) + { + return false; + } + + // Check if the time has passed (within a 1-minute window) + var timeDifference = Math.Abs((task.DateTime - now).TotalMinutes); + return timeDifference < 1; + } + + private DateTimeOffset CalculateNextReminderTime(SaveTaskDetail task, DateTimeOffset currentTime) + { + var nextTime = task.DateTime.AddDays(1); + + // Find the next day that matches the selected days + while (!task.SelectedDays.Contains(nextTime.DayOfWeek)) + { + nextTime = nextTime.AddDays(1); + } + + return nextTime; + } + + private async Task SendProactiveReminder(ConversationReference conversationReference, SaveTaskDetail task, CancellationToken stoppingToken) + { + using var scope = _serviceProvider.CreateScope(); + var app = scope.ServiceProvider.GetRequiredService(); + + var reminderMessage = $"**Reminder for a scheduled task!**\n\n" + + $"Task title: {task.Title}\n\n" + + $"Task description: {task.Description}"; + + await app.Send(conversationReference.Conversation.Id, reminderMessage); + } + } +} + + diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Startup.cs b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Startup.cs deleted file mode 100644 index 7f94fc9eac..0000000000 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/Startup.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using BotDailyTaskReminder.Bots; -using BotDailyTaskReminder.Models; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Bot.Schema; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System.Collections.Concurrent; -using System.Collections.Generic; - -namespace BotDailyTaskReminder -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers().AddNewtonsoftJson(); - - services.AddHttpClient().AddControllers().AddNewtonsoftJson(); - services.AddRazorPages(); - - // Add logging services for better error tracing in production - services.AddLogging(); - - // Create the Bot Framework Adapter with error handling enabled. - services.AddSingleton(); - - // Create a global hashset for our ConversationReferences - services.AddSingleton>(); - - // Create a global hashset for our save task details - services.AddSingleton>>(); - - // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.) - services.AddSingleton(); - - // Create the Conversation state. (Used by the Dialog system itself.) - services.AddSingleton(); - - // Create the bot as a transient. In this case, the ASP Controller is expecting an IBot. - services.AddTransient(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseDefaultFiles(); - app.UseStaticFiles(); - app.UseWebSockets().UseRouting().UseAuthorization().UseEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapRazorPages(); - }); - } - } -} diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/appsettings.Development.json b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/appsettings.Development.json index b49abfc201..4c75ed3400 100644 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/appsettings.Development.json +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/appsettings.Development.json @@ -1,9 +1,20 @@ { - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } - } + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "ApplicationBaseUrl": "", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "BotType": "SingleTenant", + "TenantId": "" + } +} \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/appsettings.Playground.json b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/appsettings.Playground.json new file mode 100644 index 0000000000..0497106b3c --- /dev/null +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/appsettings.Playground.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "TenantId": "", + "BotType": "" + } +} \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/appsettings.json b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/appsettings.json index f491de3503..c77c204268 100644 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/appsettings.json +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/appsettings.json @@ -1,5 +1,19 @@ { - "MicrosoftAppId": "{{Microsoft-App-Id}}", - "MicrosoftAppPassword": "{{ Microsoft-App-Password}}", - "ApplicationBaseUrl": "{{ Application Base Url }}" -} \ No newline at end of file + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "BotType": "" + }, + "ApplicationBaseUrl": "" +} diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/wwwroot/default.htm b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/wwwroot/default.htm deleted file mode 100644 index 3cd4df82e4..0000000000 --- a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/wwwroot/default.htm +++ /dev/null @@ -1,420 +0,0 @@ - - - - - - - BotTaskReminder - - - - - -
-
-
-
BotTaskReminder
-
-
-
-
-
Your bot is ready!
-
You can test your bot in the Bot Framework Emulator
- by connecting to http://localhost:3978/api/messages.
- -
Visit Azure - Bot Service to register your bot and add it to
- various channels. The bot's endpoint URL typically looks - like this:
-
https://your_bots_hostname/api/messages
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/wwwroot/index.html b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/wwwroot/index.html new file mode 100644 index 0000000000..a6b6759c77 --- /dev/null +++ b/samples/bot-daily-task-reminder/csharp/BotDailyTaskReminder/wwwroot/index.html @@ -0,0 +1,176 @@ + + + + + + Schedule Task + + + + + + + + +
+

Schedule Task

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/.gitignore b/samples/bot-daily-task-reminder/csharp/M365Agent/.gitignore new file mode 100644 index 0000000000..c5cae9258c --- /dev/null +++ b/samples/bot-daily-task-reminder/csharp/M365Agent/.gitignore @@ -0,0 +1,10 @@ +# TeamsFx files +build +appPackage/build +env/.env.*.user +env/.env.local +appsettings.Development.json +.deployment + +# User-specific files +*.user diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/M365Agent.atkproj b/samples/bot-daily-task-reminder/csharp/M365Agent/M365Agent.atkproj new file mode 100644 index 0000000000..124eb75046 --- /dev/null +++ b/samples/bot-daily-task-reminder/csharp/M365Agent/M365Agent.atkproj @@ -0,0 +1,9 @@ + + + + b069b3bd-f6bc-cc40-82ab-3fcc2ea50fdf + + + + + \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/M365Agent.ttkproj b/samples/bot-daily-task-reminder/csharp/M365Agent/M365Agent.ttkproj deleted file mode 100644 index e3279f8797..0000000000 --- a/samples/bot-daily-task-reminder/csharp/M365Agent/M365Agent.ttkproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - 01e9ac3f-43c7-46da-89bb-7e70f4b08b5c - - - - - - - - - - \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/appPackage/color.png b/samples/bot-daily-task-reminder/csharp/M365Agent/appPackage/color.png index b8cf81afbe..01aa37e347 100644 Binary files a/samples/bot-daily-task-reminder/csharp/M365Agent/appPackage/color.png and b/samples/bot-daily-task-reminder/csharp/M365Agent/appPackage/color.png differ diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/appPackage/manifest.json b/samples/bot-daily-task-reminder/csharp/M365Agent/appPackage/manifest.json index efa725e113..adb8448708 100644 --- a/samples/bot-daily-task-reminder/csharp/M365Agent/appPackage/manifest.json +++ b/samples/bot-daily-task-reminder/csharp/M365Agent/appPackage/manifest.json @@ -1,4 +1,4 @@ -{ +{ "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.21/MicrosoftTeams.schema.json", "manifestVersion": "1.21", "version": "1.0.0", @@ -26,13 +26,13 @@ "customEngineAgents": [ { "type": "bot", - "id": "${{AAD_APP_CLIENT_ID}}" + "id": "${{BOT_ID}}" } ] }, "bots": [ { - "botId": "${{AAD_APP_CLIENT_ID}}", + "botId": "${{BOT_ID}}", "scopes": [ "copilot", "personal" diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/appPackage/outline.png b/samples/bot-daily-task-reminder/csharp/M365Agent/appPackage/outline.png index 2c3bf6fa65..f7a4c86447 100644 Binary files a/samples/bot-daily-task-reminder/csharp/M365Agent/appPackage/outline.png and b/samples/bot-daily-task-reminder/csharp/M365Agent/appPackage/outline.png differ diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/env/.env.dev b/samples/bot-daily-task-reminder/csharp/M365Agent/env/.env.dev new file mode 100644 index 0000000000..df4f9da508 --- /dev/null +++ b/samples/bot-daily-task-reminder/csharp/M365Agent/env/.env.dev @@ -0,0 +1,15 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/env/.env.local b/samples/bot-daily-task-reminder/csharp/M365Agent/env/.env.local index f6296d6224..86dfacc35a 100644 --- a/samples/bot-daily-task-reminder/csharp/M365Agent/env/.env.local +++ b/samples/bot-daily-task-reminder/csharp/M365Agent/env/.env.local @@ -7,19 +7,9 @@ APP_NAME_SUFFIX=local # Generated during provision, you can also add your own variables. BOT_ID= TEAMS_APP_ID= -RESOURCE_SUFFIX= -AAD_APP_CLIENT_ID= -AAD_APP_OBJECT_ID= -AAD_APP_TENANT_ID= -AAD_APP_OAUTH_AUTHORITY= -AAD_APP_OAUTH_AUTHORITY_HOST= TEAMS_APP_TENANT_ID= -MICROSOFT_APP_TYPE= -MICROSOFT_APP_TENANT_ID= +BOT_OBJECT_ID= TEAMSFX_M365_USER_NAME= BOT_ENDPOINT= -BOT_DOMAIN= - -AZURE_SUBSCRIPTION_ID= -AZURE_RESOURCE_GROUP_NAME= \ No newline at end of file +BOT_DOMAIN= \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/infra/azure.bicep b/samples/bot-daily-task-reminder/csharp/M365Agent/infra/azure.bicep index c3ce051b3d..658e412a21 100644 --- a/samples/bot-daily-task-reminder/csharp/M365Agent/infra/azure.bicep +++ b/samples/bot-daily-task-reminder/csharp/M365Agent/infra/azure.bicep @@ -3,42 +3,84 @@ @description('Used to generate names for all resources in this file') param resourceBaseName string -@description('Required when create Azure Bot service') -param botAadAppClientId string - -param botAppDomain string +param webAppSKU string @maxLength(42) param botDisplayName string -param botServiceName string = resourceBaseName -param botServiceSku string = 'F0' -param microsoftAppType string -param microsoftAppTenantId string +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param identityName string = resourceBaseName +param location string = resourceGroup().location -// Register your web service as a bot with the Bot Framework -resource botService 'Microsoft.BotService/botServices@2021-03-01' = { - kind: 'azurebot' - location: 'global' - name: botServiceName - properties: { - displayName: botDisplayName - endpoint: 'https://${botAppDomain}/api/messages' - msaAppId: botAadAppClientId - msaAppType: microsoftAppType - msaAppTenantId: microsoftAppType == 'SingleTenant' ? microsoftAppTenantId : '' - } +resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + location: location + name: identityName +} + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app' + location: location + name: serverfarmsName sku: { - name: botServiceSku + name: webAppSKU } } -// Connect the bot service to Microsoft Teams -resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { - parent: botService - location: 'global' - name: 'MsTeamsChannel' +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName properties: { - channelName: 'MsTeamsChannel' + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' + } + { + name: 'Teams__ClientId' + value: identity.properties.clientId + } + { + name: 'Teams__TenantId' + value: identity.properties.tenantId + } + { + name: 'Teams__BotType' + value: 'UserAssignedMsi' + } + ] + ftpsState: 'FtpsOnly' + } + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${identity.id}': {} + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + identityClientId: identity.properties.clientId + identityResourceId: identity.id + identityTenantId: identity.properties.tenantId + botAppDomain: webApp.properties.defaultHostName + botDisplayName: botDisplayName } } + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output BOT_AZURE_APP_SERVICE_RESOURCE_ID string = webApp.id +output BOT_DOMAIN string = webApp.properties.defaultHostName +output BOT_ID string = identity.properties.clientId +output BOT_TENANT_ID string = identity.properties.tenantId diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/infra/azure.parameters.json b/samples/bot-daily-task-reminder/csharp/M365Agent/infra/azure.parameters.json index d8454ee752..2d6b68c2bd 100644 --- a/samples/bot-daily-task-reminder/csharp/M365Agent/infra/azure.parameters.json +++ b/samples/bot-daily-task-reminder/csharp/M365Agent/infra/azure.parameters.json @@ -1,24 +1,15 @@ { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "resourceBaseName": { - "value": "bot${{RESOURCE_SUFFIX}}" - }, - "botAadAppClientId": { - "value": "${{AAD_APP_CLIENT_ID}}" - }, - "botAppDomain": { - "value": "${{BOT_DOMAIN}}" - }, - "botDisplayName": { - "value": "TestBot" - }, - "microsoftAppType": { - "value": "${{MICROSOFT_APP_TYPE}}" - }, - "microsoftAppTenantId": { - "value": "${{MICROSOFT_APP_TENANT_ID}}" + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "bot${{RESOURCE_SUFFIX}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "BotDailyTaskReminder" + } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/infra/botRegistration/azurebot.bicep b/samples/bot-daily-task-reminder/csharp/M365Agent/infra/botRegistration/azurebot.bicep new file mode 100644 index 0000000000..a5a27b8fe4 --- /dev/null +++ b/samples/bot-daily-task-reminder/csharp/M365Agent/infra/botRegistration/azurebot.bicep @@ -0,0 +1,42 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@maxLength(42) +param botDisplayName string + +param botServiceName string = resourceBaseName +param botServiceSku string = 'F0' +param identityResourceId string +param identityClientId string +param identityTenantId string +param botAppDomain string + +// Register your web service as a bot with the Bot Framework +resource botService 'Microsoft.BotService/botServices@2021-03-01' = { + kind: 'azurebot' + location: 'global' + name: botServiceName + properties: { + displayName: botDisplayName + endpoint: 'https://${botAppDomain}/api/messages' + msaAppId: identityClientId + msaAppMSIResourceId: identityResourceId + msaAppTenantId:identityTenantId + msaAppType:'UserAssignedMSI' + } + sku: { + name: botServiceSku + } +} + +// Connect the bot service to Microsoft Teams +resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { + parent: botService + location: 'global' + name: 'MsTeamsChannel' + properties: { + channelName: 'MsTeamsChannel' + } +} diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/infra/botRegistration/readme.md b/samples/bot-daily-task-reminder/csharp/M365Agent/infra/botRegistration/readme.md new file mode 100644 index 0000000000..d5416243cd --- /dev/null +++ b/samples/bot-daily-task-reminder/csharp/M365Agent/infra/botRegistration/readme.md @@ -0,0 +1 @@ +The `azurebot.bicep` module is provided to help you create Azure Bot service when you don't use Azure to host your app. If you use Azure as infrastrcture for your app, `azure.bicep` under infra folder already leverages this module to create Azure Bot service for you. You don't need to deploy `azurebot.bicep` again. \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/launchSettings.json b/samples/bot-daily-task-reminder/csharp/M365Agent/launchSettings.json index d6491ef52c..2af8ce7a8a 100644 --- a/samples/bot-daily-task-reminder/csharp/M365Agent/launchSettings.json +++ b/samples/bot-daily-task-reminder/csharp/M365Agent/launchSettings.json @@ -1,15 +1,25 @@ { - "profiles": { - // Debug project within Teams - "Microsoft Teams (browser)": { - "commandName": "Project", - "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" - }, - // Launch project within Teams without prepare app dependencies - "Microsoft Teams (browser) (skip update app)": { - "commandName": "Project", - "environmentVariables": { "UPDATE_TEAMS_APP": "false" }, - "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" - } - } + "profiles": { + // Launch project within Microsoft 365 Agents Playground + "Microsoft 365 Agents Playground (browser)": { + "commandName": "Project", + "environmentVariables": { + "UPDATE_TEAMS_APP": "false", + "M365_AGENTS_PLAYGROUND_TARGET_SDK": "teams-ai-v2-dotnet" + }, + "launchTestTool": true, + "launchUrl": "http://localhost:56150", + }, + // Launch project within Teams + "Microsoft Teams (browser)": { + "commandName": "Project", + "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}", + }, + // Launch project within Teams without prepare app dependencies + "Microsoft Teams (browser) (skip update app)": { + "commandName": "Project", + "environmentVariables": { "UPDATE_TEAMS_APP": "false" }, + "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" + }, + } } \ No newline at end of file diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/m365agents.local.yml b/samples/bot-daily-task-reminder/csharp/M365Agent/m365agents.local.yml index 908afd190e..771be4c1ef 100644 --- a/samples/bot-daily-task-reminder/csharp/M365Agent/m365agents.local.yml +++ b/samples/bot-daily-task-reminder/csharp/M365Agent/m365agents.local.yml @@ -1,61 +1,54 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.2/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/m365-agents-toolkits/v1.11/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.2 - -additionalMetadata: - sampleTag: Microsoft-Teams-Samples:bot-daily-task-reminder-csharp +version: v1.11 provision: - - uses: aadApp/create # Creates a new Azure Active Directory (AAD) app to authenticate users if the environment variable that stores clientId is empty - with: - name: bot-daily-task-reminder-aad # Note: when you run aadApp/update, the AAD app name will be updated based on the definition in manifest. If you don't want to change the name, make sure the name in AAD manifest is the same with the name defined here. - generateClientSecret: true # If the value is false, the action will not generate client secret for you - signInAudience: "AzureADMultipleOrgs" # Multitenant - writeToEnvironmentFile: # Write the information of created resources into environment file for the specified environment variable(s). - clientId: AAD_APP_CLIENT_ID - clientSecret: SECRET_AAD_APP_CLIENT_SECRET # Environment variable that starts with `SECRET_` will be stored to the .env.{envName}.user environment file - objectId: AAD_APP_OBJECT_ID - tenantId: AAD_APP_TENANT_ID - authority: AAD_APP_OAUTH_AUTHORITY - authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST - # Creates a Teams app - uses: teamsApp/create with: # Teams app name - name: bot-daily-task-reminder${{APP_NAME_SUFFIX}} + name: BotDailyTaskReminder${{APP_NAME_SUFFIX}} # Write the information of created resources into environment file for # the specified environment variable(s). - writeToEnvironmentFile: + writeToEnvironmentFile: teamsAppId: TEAMS_APP_ID - - uses: script + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create with: - run: - echo "::set-teamsfx-env MICROSOFT_APP_TYPE=SingleTenant"; - echo "::set-teamsfx-env MICROSOFT_APP_TENANT_ID=${{AAD_APP_TENANT_ID}}"; + # The Microsoft Entra application's display name + name: BotDailyTaskReminder${{APP_NAME_SUFFIX}} + generateClientSecret: true + generateServicePrincipal: true + signInAudience: AzureADMultipleOrgs + writeToEnvironmentFile: + # The Microsoft Entra application's client id created for bot. + clientId: BOT_ID + # The Microsoft Entra application's client secret created for bot. + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile with: - target: ../BotDailyTaskReminder/appsettings.json + target: ../BotDailyTaskReminder/appsettings.Development.json content: - MicrosoftAppId: ${{AAD_APP_CLIENT_ID}} - MicrosoftAppPassword: ${{SECRET_AAD_APP_CLIENT_SECRET}} - ApplicationBaseUrl: ${{BOT_ENDPOINT}} - MicrosoftAppType: ${{MICROSOFT_APP_TYPE}} - MicrosoftAppTenantId: ${{MICROSOFT_APP_TENANT_ID}} + Teams: + ClientId: ${{BOT_ID}} + ClientSecret: ${{SECRET_BOT_PASSWORD}} + TenantId: ${{TEAMS_APP_TENANT_ID}} - - uses: arm/deploy # Deploy given ARM templates parallelly. + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create with: - subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} # The AZURE_SUBSCRIPTION_ID is a built-in environment variable. TeamsFx will ask you select one subscription if its value is empty. You're free to reference other environment varialbe here, but TeamsFx will not ask you to select subscription if it's empty in this case. - resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} # The AZURE_RESOURCE_GROUP_NAME is a built-in environment variable. TeamsFx will ask you to select or create one resource group if its value is empty. You're free to reference other environment varialbe here, but TeamsFx will not ask you to select or create resource grouop if it's empty in this case. - templates: - - path: ./infra/azure.bicep - parameters: ./infra/azure.parameters.json - deploymentName: Create-resources-for-bot - bicepCliVersion: v0.9.1 # Microsoft 365 Agents Toolkit will download this bicep CLI version from github for you, will use bicep CLI in PATH if you remove this config. + botId: ${{BOT_ID}} + name: BotDailyTaskReminder + messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages + description: "" + channels: + - name: msteams - uses: aadApp/update # Apply the AAD manifest to an existing AAD app. Will use the object id in manifest file to determine which AAD app to update. with: @@ -67,14 +60,13 @@ provision: with: # Path to manifest template manifestPath: ./appPackage/manifest.json - # Build Teams app package with latest env value - uses: teamsApp/zipAppPackage with: # Path to manifest template manifestPath: ./appPackage/manifest.json outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip - outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + outputFolder: ./appPackage/build # Validate app package using validation rules - uses: teamsApp/validateAppPackage with: @@ -87,4 +79,4 @@ provision: - uses: teamsApp/update with: # Relative path to this file. This is the path for built zip file. - appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip \ No newline at end of file + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip diff --git a/samples/bot-daily-task-reminder/csharp/M365Agent/m365agents.yml b/samples/bot-daily-task-reminder/csharp/M365Agent/m365agents.yml index 30cd2bb4bc..e3d5c27e5c 100644 --- a/samples/bot-daily-task-reminder/csharp/M365Agent/m365agents.yml +++ b/samples/bot-daily-task-reminder/csharp/M365Agent/m365agents.yml @@ -1,9 +1,89 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.2/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/m365-agents-toolkits/v1.9/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.2 +version: v1.9 -additionalMetadata: - sampleTag: Microsoft-Teams-Samples:bot-daily-task-reminder-csharp +environmentFolderPath: ./env -environmentFolderPath: ./env \ No newline at end of file +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: BotDailyTaskReminder${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-bot + # Microsoft 365 Agents Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputFolder: ./appPackage/build + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + +# Triggered when 'teamsapp deploy' is executed +deploy: + - uses: cli/runDotnetCommand + with: + args: publish --configuration Release --runtime win-x86 --self-contained + BotDailyTaskReminder.csproj + workingDirectory: ../BotDailyTaskReminder + # Deploy your application to Azure App Service using the zip deploy feature. + # For additional details, refer to https://aka.ms/zip-deploy-to-app-services. + - uses: azureAppService/zipDeploy + with: + # Deploy base folder + artifactFolder: bin/Release/net10.0/win-x86/publish + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}} + workingDirectory: ../BotDailyTaskReminder +projectId: 19adb3ff-67dd-409b-a230-ceddc1e713ff diff --git a/samples/bot-daily-task-reminder/csharp/README.md b/samples/bot-daily-task-reminder/csharp/README.md index 38e70bc7d0..ba3f4bae4a 100644 --- a/samples/bot-daily-task-reminder/csharp/README.md +++ b/samples/bot-daily-task-reminder/csharp/README.md @@ -167,7 +167,7 @@ To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](htt ## Further reading -- [Bot Framework Documentation](https://docs.botframework.com) +- [Azure AI Bot Service Documentation](https://docs.botframework.com) - [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) - [Activity processing](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) - [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/.gitignore b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/.gitignore new file mode 100644 index 0000000000..c5cae9258c --- /dev/null +++ b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/.gitignore @@ -0,0 +1,10 @@ +# TeamsFx files +build +appPackage/build +env/.env.*.user +env/.env.local +appsettings.Development.json +.deployment + +# User-specific files +*.user diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/M365Agent.atkproj b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/M365Agent.atkproj new file mode 100644 index 0000000000..124eb75046 --- /dev/null +++ b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/M365Agent.atkproj @@ -0,0 +1,9 @@ + + + + b069b3bd-f6bc-cc40-82ab-3fcc2ea50fdf + + + + + \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/M365Agent.ttkproj b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/M365Agent.ttkproj deleted file mode 100644 index eebc718798..0000000000 --- a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/M365Agent.ttkproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - 99dc514b-7a11-4e70-9e13-917993c69e17 - - - - - - - - - - \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/appPackage/color.png b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/appPackage/color.png index b8cf81afbe..01aa37e347 100644 Binary files a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/appPackage/color.png and b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/appPackage/color.png differ diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/appPackage/manifest.json b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/appPackage/manifest.json index eb2b628997..a92702f2cf 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/appPackage/manifest.json +++ b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/appPackage/manifest.json @@ -1,4 +1,4 @@ -{ +{ "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.19/MicrosoftTeams.schema.json", "manifestVersion": "1.19", "version": "1.0.0", @@ -24,7 +24,7 @@ "accentColor": "#60A18E", "bots": [ { - "botId": "${{AAD_APP_CLIENT_ID}}", + "botId": "${{BOT_ID}}", "scopes": [ "personal", "team" @@ -34,7 +34,7 @@ ], "composeExtensions": [ { - "botId": "${{AAD_APP_CLIENT_ID}}", + "botId": "${{BOT_ID}}", "canUpdateConfiguration": true, "commands": [ { diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/appPackage/outline.png b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/appPackage/outline.png index 2c3bf6fa65..f7a4c86447 100644 Binary files a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/appPackage/outline.png and b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/appPackage/outline.png differ diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/env/.env.dev b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/env/.env.dev new file mode 100644 index 0000000000..df4f9da508 --- /dev/null +++ b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/env/.env.dev @@ -0,0 +1,15 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/env/.env.local b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/env/.env.local index 76ee8a0ba6..86dfacc35a 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/env/.env.local +++ b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/env/.env.local @@ -7,17 +7,8 @@ APP_NAME_SUFFIX=local # Generated during provision, you can also add your own variables. BOT_ID= TEAMS_APP_ID= -RESOURCE_SUFFIX= -AZURE_SUBSCRIPTION_ID= -AZURE_RESOURCE_GROUP_NAME= -AAD_APP_CLIENT_ID= -AAD_APP_OBJECT_ID= -AAD_APP_TENANT_ID= -AAD_APP_OAUTH_AUTHORITY= -AAD_APP_OAUTH_AUTHORITY_HOST= TEAMS_APP_TENANT_ID= -MICROSOFT_APP_TYPE= -MICROSOFT_APP_TENANT_ID= +BOT_OBJECT_ID= TEAMSFX_M365_USER_NAME= BOT_ENDPOINT= diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/azure.bicep b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/azure.bicep index c3ce051b3d..658e412a21 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/azure.bicep +++ b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/azure.bicep @@ -3,42 +3,84 @@ @description('Used to generate names for all resources in this file') param resourceBaseName string -@description('Required when create Azure Bot service') -param botAadAppClientId string - -param botAppDomain string +param webAppSKU string @maxLength(42) param botDisplayName string -param botServiceName string = resourceBaseName -param botServiceSku string = 'F0' -param microsoftAppType string -param microsoftAppTenantId string +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param identityName string = resourceBaseName +param location string = resourceGroup().location -// Register your web service as a bot with the Bot Framework -resource botService 'Microsoft.BotService/botServices@2021-03-01' = { - kind: 'azurebot' - location: 'global' - name: botServiceName - properties: { - displayName: botDisplayName - endpoint: 'https://${botAppDomain}/api/messages' - msaAppId: botAadAppClientId - msaAppType: microsoftAppType - msaAppTenantId: microsoftAppType == 'SingleTenant' ? microsoftAppTenantId : '' - } +resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + location: location + name: identityName +} + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app' + location: location + name: serverfarmsName sku: { - name: botServiceSku + name: webAppSKU } } -// Connect the bot service to Microsoft Teams -resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { - parent: botService - location: 'global' - name: 'MsTeamsChannel' +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName properties: { - channelName: 'MsTeamsChannel' + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' + } + { + name: 'Teams__ClientId' + value: identity.properties.clientId + } + { + name: 'Teams__TenantId' + value: identity.properties.tenantId + } + { + name: 'Teams__BotType' + value: 'UserAssignedMsi' + } + ] + ftpsState: 'FtpsOnly' + } + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${identity.id}': {} + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + identityClientId: identity.properties.clientId + identityResourceId: identity.id + identityTenantId: identity.properties.tenantId + botAppDomain: webApp.properties.defaultHostName + botDisplayName: botDisplayName } } + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output BOT_AZURE_APP_SERVICE_RESOURCE_ID string = webApp.id +output BOT_DOMAIN string = webApp.properties.defaultHostName +output BOT_ID string = identity.properties.clientId +output BOT_TENANT_ID string = identity.properties.tenantId diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/azure.parameters.json b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/azure.parameters.json index c0f20249e8..5cc400876b 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/azure.parameters.json +++ b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/azure.parameters.json @@ -1,24 +1,15 @@ { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "resourceBaseName": { - "value": "bot${{RESOURCE_SUFFIX}}" - }, - "botAadAppClientId": { - "value": "${{AAD_APP_CLIENT_ID}}" - }, - "botAppDomain": { - "value": "${{BOT_DOMAIN}}" - }, - "botDisplayName": { - "value": "msgext-ai-sentiment-analysis" - }, - "microsoftAppType": { - "value": "${{MICROSOFT_APP_TYPE}}" - }, - "microsoftAppTenantId": { - "value": "${{MICROSOFT_APP_TENANT_ID}}" + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "bot${{RESOURCE_SUFFIX}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "MEAISentimentAnalysis" + } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/botRegistration/azurebot.bicep b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/botRegistration/azurebot.bicep new file mode 100644 index 0000000000..a5a27b8fe4 --- /dev/null +++ b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/botRegistration/azurebot.bicep @@ -0,0 +1,42 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@maxLength(42) +param botDisplayName string + +param botServiceName string = resourceBaseName +param botServiceSku string = 'F0' +param identityResourceId string +param identityClientId string +param identityTenantId string +param botAppDomain string + +// Register your web service as a bot with the Bot Framework +resource botService 'Microsoft.BotService/botServices@2021-03-01' = { + kind: 'azurebot' + location: 'global' + name: botServiceName + properties: { + displayName: botDisplayName + endpoint: 'https://${botAppDomain}/api/messages' + msaAppId: identityClientId + msaAppMSIResourceId: identityResourceId + msaAppTenantId:identityTenantId + msaAppType:'UserAssignedMSI' + } + sku: { + name: botServiceSku + } +} + +// Connect the bot service to Microsoft Teams +resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { + parent: botService + location: 'global' + name: 'MsTeamsChannel' + properties: { + channelName: 'MsTeamsChannel' + } +} diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/botRegistration/readme.md b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/botRegistration/readme.md new file mode 100644 index 0000000000..d5416243cd --- /dev/null +++ b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/infra/botRegistration/readme.md @@ -0,0 +1 @@ +The `azurebot.bicep` module is provided to help you create Azure Bot service when you don't use Azure to host your app. If you use Azure as infrastrcture for your app, `azure.bicep` under infra folder already leverages this module to create Azure Bot service for you. You don't need to deploy `azurebot.bicep` again. \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/launchSettings.json b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/launchSettings.json index 864a121f4f..2af8ce7a8a 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/launchSettings.json +++ b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/launchSettings.json @@ -1,17 +1,25 @@ -{ - "profiles": { - // Debug project within Teams - "Microsoft Teams (browser)": { - "commandName": "Project", - "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" - } - }, - // Launch project within Teams without prepare app dependencies - "Microsoft Teams (browser) (skip update app)": { - "commandName": "Project", - "environmentVariables": { - "UPDATE_TEAMS_APP": "false" - }, - "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" - } +{ + "profiles": { + // Launch project within Microsoft 365 Agents Playground + "Microsoft 365 Agents Playground (browser)": { + "commandName": "Project", + "environmentVariables": { + "UPDATE_TEAMS_APP": "false", + "M365_AGENTS_PLAYGROUND_TARGET_SDK": "teams-ai-v2-dotnet" + }, + "launchTestTool": true, + "launchUrl": "http://localhost:56150", + }, + // Launch project within Teams + "Microsoft Teams (browser)": { + "commandName": "Project", + "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}", + }, + // Launch project within Teams without prepare app dependencies + "Microsoft Teams (browser) (skip update app)": { + "commandName": "Project", + "environmentVariables": { "UPDATE_TEAMS_APP": "false" }, + "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" + }, + } } \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/m365agents.local.yml b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/m365agents.local.yml index d9af757590..639db0eb7e 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/m365agents.local.yml +++ b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/m365agents.local.yml @@ -1,82 +1,73 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.2/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/m365-agents-toolkits/v1.11/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.2 - -additionalMetadata: - sampleTag: Microsoft-Teams-Samples:msgext-ai-sentiment-analysis-csharp +version: v1.11 provision: - - uses: aadApp/create # Creates a new Azure Active Directory (AAD) app to authenticate users if the environment variable that stores clientId is empty - with: - name: msgext-ai-sentiment-analysis-aad # Note: when you run aadApp/update, the AAD app name will be updated based on the definition in manifest. If you don't want to change the name, make sure the name in AAD manifest is the same with the name defined here. - generateClientSecret: true # If the value is false, the action will not generate client secret for you - signInAudience: "AzureADMultipleOrgs" # Multitenant - writeToEnvironmentFile: # Write the information of created resources into environment file for the specified environment variable(s). - clientId: AAD_APP_CLIENT_ID - clientSecret: SECRET_AAD_APP_CLIENT_SECRET # Environment variable that starts with `SECRET_` will be stored to the .env.{envName}.user environment file - objectId: AAD_APP_OBJECT_ID - tenantId: AAD_APP_TENANT_ID - authority: AAD_APP_OAUTH_AUTHORITY - authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST - # Creates a Teams app - uses: teamsApp/create with: # Teams app name - name: msgext-ai-sentiment-analysis-${{TEAMSFX_ENV}} + name: MEAISentimentAnalysis${{APP_NAME_SUFFIX}} # Write the information of created resources into environment file for # the specified environment variable(s). - writeToEnvironmentFile: + writeToEnvironmentFile: teamsAppId: TEAMS_APP_ID - - uses: script + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create with: - run: - echo "::set-teamsfx-env MICROSOFT_APP_TYPE=SingleTenant"; - echo "::set-teamsfx-env MICROSOFT_APP_TENANT_ID=${{AAD_APP_TENANT_ID}}"; + # The Microsoft Entra application's display name + name: MEAISentimentAnalysis${{APP_NAME_SUFFIX}} + generateClientSecret: true + generateServicePrincipal: true + signInAudience: AzureADMultipleOrgs + writeToEnvironmentFile: + # The Microsoft Entra application's client id created for bot. + clientId: BOT_ID + # The Microsoft Entra application's client secret created for bot. + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID # Generate runtime appsettings to JSON file - uses: file/createOrUpdateJsonFile with: - target: ../MEAISentimentAnalysis/appsettings.json + target: ../MEAISentimentAnalysis/appsettings.Development.json content: - MicrosoftAppType: ${{MICROSOFT_APP_TYPE}} - MicrosoftAppId: ${{AAD_APP_CLIENT_ID}} - MicrosoftAppPassword: ${{SECRET_AAD_APP_CLIENT_SECRET}} - ApplicationBaseUrl: ${{BOT_ENDPOINT}} - MicrosoftAppTenantId: ${{MICROSOFT_APP_TENANT_ID}} + Teams: + ClientId: ${{BOT_ID}} + ClientSecret: ${{SECRET_BOT_PASSWORD}} + TenantId: ${{TEAMS_APP_TENANT_ID}} + ApplicationBaseUrl: ${{BOT_ENDPOINT}} - - uses: arm/deploy # Deploy given ARM templates parallelly. + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create with: - subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} # The AZURE_SUBSCRIPTION_ID is a built-in environment variable. TeamsFx will ask you select one subscription if its value is empty. You're free to reference other environment varialbe here, but TeamsFx will not ask you to select subscription if it's empty in this case. - resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} # The AZURE_RESOURCE_GROUP_NAME is a built-in environment variable. TeamsFx will ask you to select or create one resource group if its value is empty. You're free to reference other environment varialbe here, but TeamsFx will not ask you to select or create resource grouop if it's empty in this case. - templates: - - path: ./infra/azure.bicep - parameters: ./infra/azure.parameters.json - deploymentName: Create-resources-for-bot - bicepCliVersion: v0.9.1 # Microsoft 365 Agents Toolkit will download this bicep CLI version from github for you, will use bicep CLI in PATH if you remove this config. - - -# Validate using manifest schema - - uses: teamsApp/validateManifest + botId: ${{BOT_ID}} + name: MEAISentimentAnalysis + messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages + description: "" + channels: + - name: msteams + + - uses: aadApp/update # Apply the AAD manifest to an existing AAD app. Will use the object id in manifest file to determine which AAD app to update. with: - # Path to manifest template - manifestPath: ./appManifest/manifest.json + manifestPath: ./aad.manifest.json # Relative path to teamsfx folder. Environment variables in manifest will be replaced before apply to AAD app + outputFilePath: ./build/aad.manifest.${{TEAMSFX_ENV}}.json # Validate using manifest schema - uses: teamsApp/validateManifest with: # Path to manifest template manifestPath: ./appPackage/manifest.json - # Build Teams app package with latest env value - uses: teamsApp/zipAppPackage with: # Path to manifest template manifestPath: ./appPackage/manifest.json outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip - outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + outputFolder: ./appPackage/build # Validate app package using validation rules - uses: teamsApp/validateAppPackage with: diff --git a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/m365agents.yml b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/m365agents.yml index c97c759584..2e2a721ae2 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/m365agents.yml +++ b/samples/msgext-ai-sentiment-analysis/csharp/M365Agent/m365agents.yml @@ -1,9 +1,89 @@ -# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.2/yaml.schema.json +# yaml-language-server: $schema=https://aka.ms/m365-agents-toolkits/v1.9/yaml.schema.json # Visit https://aka.ms/teamsfx-v5.0-guide for details on this file # Visit https://aka.ms/teamsfx-actions for details on actions -version: v1.2 - -additionalMetadata: - sampleTag: Microsoft-Teams-Samples:msgext-ai-sentiment-analysis-csharp +version: v1.9 environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: MEAISentimentAnalysis${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-bot + # Microsoft 365 Agents Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputFolder: ./appPackage/build + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + +# Triggered when 'teamsapp deploy' is executed +deploy: + - uses: cli/runDotnetCommand + with: + args: publish --configuration Release --runtime win-x86 --self-contained + MEAISentimentAnalysis.csproj + workingDirectory: ../MEAISentimentAnalysis + # Deploy your application to Azure App Service using the zip deploy feature. + # For additional details, refer to https://aka.ms/zip-deploy-to-app-services. + - uses: azureAppService/zipDeploy + with: + # Deploy base folder + artifactFolder: bin/Release/net10.0/win-x86/publish + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}} + workingDirectory: ../MEAISentimentAnalysis +projectId: 277a15fa-b11b-4533-88d8-394f105b635d diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis.slnLaunch.user b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis.slnLaunch.user new file mode 100644 index 0000000000..afe1c4117a --- /dev/null +++ b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis.slnLaunch.user @@ -0,0 +1,53 @@ +[ + { + "Name": "Microsoft 365 Agents Playground (browser)", + "Projects": [ + { + "Path": "M365Agent\\M365Agent.atkproj", + "Name": "M365Agent\\M365Agent.atkproj", + "Action": "StartWithoutDebugging", + "DebugTarget": "Microsoft 365 Agents Playground (browser)" + }, + { + "Path": "MEAISentimentAnalysis\\MEAISentimentAnalysis.csproj", + "Name": "MEAISentimentAnalysis\\MEAISentimentAnalysis.csproj", + "Action": "Start", + "DebugTarget": "Microsoft 365 Agents Playground" + } + ] + }, + { + "Name": "Microsoft Teams (browser)", + "Projects": [ + { + "Path": "M365Agent\\M365Agent.atkproj", + "Name": "M365Agent\\M365Agent.atkproj", + "Action": "StartWithoutDebugging", + "DebugTarget": "Microsoft Teams (browser)" + }, + { + "Path": "MEAISentimentAnalysis\\MEAISentimentAnalysis.csproj", + "Name": "MEAISentimentAnalysis\\MEAISentimentAnalysis.csproj", + "Action": "Start", + "DebugTarget": "Start Project" + } + ] + }, + { + "Name": "Microsoft Teams (browser) (skip update app)", + "Projects": [ + { + "Path": "M365Agent\\M365Agent.atkproj", + "Name": "M365Agent\\M365Agent.atkproj", + "Action": "StartWithoutDebugging", + "DebugTarget": "Microsoft Teams (browser) (skip update app)" + }, + { + "Path": "MEAISentimentAnalysis\\MEAISentimentAnalysis.csproj", + "Name": "MEAISentimentAnalysis\\MEAISentimentAnalysis.csproj", + "Action": "Start", + "DebugTarget": "Start Project" + } + ] + } +] \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis.slnx b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis.slnx new file mode 100644 index 0000000000..3eb17282cc --- /dev/null +++ b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis.slnx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/.gitignore b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/.gitignore index 7466c01f9c..77c7154916 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/.gitignore +++ b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/.gitignore @@ -5,6 +5,7 @@ env/.env.*.user env/.env.local appsettings.Development.json .deployment +appsettings.Playground.json # User-specific files *.user @@ -22,4 +23,8 @@ bld/ [Ll]og/ # Notification local store -.notification.localstore.json \ No newline at end of file +.notification.localstore.json +.notification.playgroundstore.json + +# devTools +devTools/ \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/AdapterWithErrorHandler.cs b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/AdapterWithErrorHandler.cs deleted file mode 100644 index cc7e1fb3a4..0000000000 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/AdapterWithErrorHandler.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Threading.Tasks; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Connector; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Bot.Schema; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using System.Net.Http; - -namespace MEAISentimentAnalysis -{ - public class AdapterWithErrorHandler : CloudAdapter - { - public AdapterWithErrorHandler(IConfiguration configuration, IHttpClientFactory httpClientFactory, ILogger logger, ConversationState conversationState = default) - : base(configuration, httpClientFactory, logger) - { - OnTurnError = async (turnContext, exception) => - { - // Log any leaked exception from the application. - // NOTE: In production environment, you should consider logging this to - // Azure Application Insights. Visit https://aka.ms/bottelemetry to see how - // to add telemetry capture to your bot. - logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); - - // Uncomment below commented line for local debugging.. - // await turnContext.SendActivityAsync($"Sorry, it looks like something went wrong. Exception Caught: {exception.Message}"); - - // Send a trace activity, which will be displayed in the Bot Framework Emulator - await SendTraceActivityAsync(turnContext, exception); - }; - } - - private static async Task SendTraceActivityAsync(ITurnContext turnContext, Exception exception) - { - // Only send a trace activity if we're talking to the Bot Framework Emulator - if (turnContext.Activity.ChannelId == Channels.Emulator) - { - Activity traceActivity = new Activity(ActivityTypes.Trace) - { - Label = "TurnError", - Name = "OnTurnError Trace", - Value = exception.Message, - ValueType = "https://www.botframework.com/schemas/error", - }; - - // Send a trace activity - await turnContext.SendActivityAsync(traceActivity); - } - } - } -} diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Bots/ActivityBot.cs b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Bots/ActivityBot.cs deleted file mode 100644 index 7fbfab5768..0000000000 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Bots/ActivityBot.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// Generated with Bot Builder V4 SDK Template for Visual Studio v4.14.0 - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Teams; -using Microsoft.Bot.Schema; -using Microsoft.Bot.Schema.Teams; -using Microsoft.Extensions.Configuration; -using OpenAI_API; - -namespace MessagingExtensionReminder.Bots -{ - /// - /// Bot Activity handler class. - /// - public class ActivityBot : TeamsActivityHandler - { - private readonly string _applicationBaseUrl; - protected readonly BotState _conversationState; - private readonly string _openAPIKey; - private readonly string _chatCompletionModelName; - private readonly ConcurrentDictionary _conversationReferences; - - public ActivityBot(IConfiguration configuration, ConversationState conversationState, ConcurrentDictionary conversationReferences) - { - _conversationReferences = conversationReferences; - _conversationState = conversationState; - _applicationBaseUrl = configuration["ApplicationBaseUrl"] ?? throw new NullReferenceException("ApplicationBaseUrl"); - _openAPIKey = configuration["SECRET_OPENAI_API_KEY"] ?? throw new NullReferenceException("SECRET_OPENAI_API_KEY"); - _chatCompletionModelName = configuration["CHAT_COMPLETION_MODEL_NAME"] ?? throw new NullReferenceException("CHAT_COMPLETION_MODEL_NAME"); - } - - /// - /// When OnTurn method receives a submit invoke activity on bot turn, it calls this method. - /// - /// Context object containing information cached for a single turn of conversation with a user. - /// Provides context for a turn of a bot and. - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. - /// A task that represents a task module response. - protected override async Task OnTeamsMessagingExtensionFetchTaskAsync( - ITurnContext turnContext, - MessagingExtensionAction action, - CancellationToken cancellationToken) - { - var textToAnalyze = string.Empty; - var sentimentResult = string.Empty; - - textToAnalyze = action.MessagePayload.Body.Content; - textToAnalyze = Regex.Replace(textToAnalyze, @"<[^>]+>|","").Trim(); - - string apiKey = _openAPIKey; - var openAI = new OpenAIAPI(apiKey); - - string systemMessage = "You will be provided with a tweet, and your task is to classify its sentiment as positive, neutral, or negative"; - - // Chat message sent or received from the API inlcudes 'Content'. - var custom_Message = new List - { - new OpenAI_API.Chat.ChatMessage{Content=systemMessage}, - new OpenAI_API.Chat.ChatMessage{Content=textToAnalyze} - }; - - try - { - var response = await openAI.Chat.CreateChatCompletionAsync( - model: _chatCompletionModelName, - messages: custom_Message, - max_tokens: 256 // Adjust as needed - ); - - string assistantReply = response.Choices[0].Message.Content.ToLower(); - - // Extract the sentiment analysis result from the assistant's reply - if (assistantReply.Contains("positive")) - { - sentimentResult = "Positive"; - } - else if (assistantReply.Contains("negative")) - { - sentimentResult = "Negative"; - } - else - { - sentimentResult = "Neutral"; - } - } - catch (Exception ex) - { - Console.WriteLine("Error: " + ex.Message); - } - - // return actual message and sentiment result - return this.GetTaskModuleResponse(textToAnalyze, sentimentResult); - } - - /// - /// Get messaging extension action response for sentiment analysis. - /// - /// text to analyze sentiment. - /// sentiment result for a message. - /// MessagingExtensionActionResponse object. - private MessagingExtensionActionResponse GetTaskModuleResponse(string textToAnalyze, string result) - { - return new MessagingExtensionActionResponse - { - Task = new TaskModuleContinueResponse() - { - Value = new TaskModuleTaskInfo - { - Url = _applicationBaseUrl + "/" + "SentimentModel?title=" + textToAnalyze + "&result=" + result, - Height = 400, - Width = 600, - Title = "Schedule-task", - }, - }, - }; - } - } -} \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Config.cs b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Config.cs new file mode 100644 index 0000000000..9fe507b32a --- /dev/null +++ b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Config.cs @@ -0,0 +1,18 @@ +namespace MEAISentimentAnalysis +{ + public class ConfigOptions + { + public TeamsConfigOptions Teams { get; set; } + } + + public class TeamsConfigOptions + { + public string BotType { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string TenantId { get; set; } + public string ApiKey { get; set; } + public string ModelName { get; set; } + public string ApplicationBaseUrl { get; set; } + } +} \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Controllers/BotController.cs b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Controllers/BotController.cs deleted file mode 100644 index 86bbf47fab..0000000000 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Controllers/BotController.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.14.0 - -using Microsoft.AspNetCore.Mvc; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using System.Threading.Tasks; - -namespace MEAISentimentAnalysis.Controllers -{ - // This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot - // implementation at runtime. Multiple different IBot implementations running at different endpoints can be - // achieved by specifying a more specific type for the bot constructor argument. - [Route("api/messages")] - [ApiController] - public class BotController : ControllerBase - { - private readonly CloudAdapter Adapter; - private readonly IBot Bot; - - public BotController(CloudAdapter adapter, IBot bot) - { - Adapter = adapter; - Bot = bot; - } - - [HttpPost, HttpGet] - public async Task PostAsync() - { - // Delegate the processing of the HTTP POST to the adapter. - // The adapter will invoke the bot. - await Adapter.ProcessAsync(Request, Response, Bot); - } - } -} diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Controllers/Controller.cs b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Controllers/Controller.cs new file mode 100644 index 0000000000..043f60a0b7 --- /dev/null +++ b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Controllers/Controller.cs @@ -0,0 +1,274 @@ +using Microsoft.Teams.Api.MessageExtensions; +using Microsoft.Teams.Api.TaskModules; +using Microsoft.Teams.Apps.Activities.Invokes; +using Microsoft.Teams.Apps.Annotations; +using Microsoft.Teams.Cards; +using Microsoft.Teams.Common; +using OpenAI.Chat; +using System.Text.RegularExpressions; +using AdaptiveCard = Microsoft.Teams.Cards.AdaptiveCard; +using MessageExtensionResponse = Microsoft.Teams.Api.MessageExtensions.Response; +using TaskModuleSize = Microsoft.Teams.Api.TaskModules.Size; + +namespace MEAISentimentAnalysis.Controllers +{ + [TeamsController] + public class Controller + { + private readonly ConfigOptions _config; + private readonly string _openAIApiKey; + private readonly string _modelName; + private readonly string _applicationBaseUrl; + + public Controller(IConfiguration configuration) + { + _config = configuration.Get() ?? throw new NullReferenceException("ConfigOptions"); + _openAIApiKey = _config.Teams.ApiKey ?? throw new NullReferenceException("OpenAI:ApiKey is not configured"); + _modelName = _config.Teams.ModelName ?? "gpt-4o-mini"; + _applicationBaseUrl = configuration["ApplicationBaseUrl"] ?? "https://localhost"; + } + + // Handle message extension submit action (when action is submitted) + public async Task OnSubmitAction( + [Context] Microsoft.Teams.Api.Activities.Invokes.MessageExtensions.SubmitActionActivity activity, + [Context] Microsoft.Teams.Common.Logging.ILogger log) + { + var commandId = activity.Value?.CommandId; + + if (commandId == "analyzeSentiment") + { + return await HandleAnalyzeSentiment(activity, log); + } + + log.Error($"Unknown command: {commandId}"); + return CreateErrorResponse("Unknown command"); + } + + // Handle message extension fetch task (when task module is opened) + [Invoke("composeExtension/fetchTask")] + public async Task OnMessageExtensionFetchTask( + [Context] Microsoft.Teams.Api.Activities.Invokes.MessageExtensions.FetchTaskActivity activity, + [Context] Microsoft.Teams.Common.Logging.ILogger log) + { + + var commandId = activity.Value?.CommandId; + + if (commandId == "me-sentiment-ai") + { + return await HandleAnalyzeSentimentFetchTask(activity, log); + } + + return CreateErrorTaskResponse("Unknown command"); + } + + private async Task HandleAnalyzeSentiment( + Microsoft.Teams.Api.Activities.Invokes.MessageExtensions.SubmitActionActivity activity, + Microsoft.Teams.Common.Logging.ILogger log) + { + try + { + var textToAnalyze = activity.Value?.MessagePayload?.Body?.Content ?? string.Empty; + + // Remove HTML tags and extra whitespace + textToAnalyze = Regex.Replace(textToAnalyze, @"<[^>]+>| ", "").Trim(); + + if (string.IsNullOrEmpty(textToAnalyze)) + { + return CreateErrorResponse("No text to analyze"); + } + + // Perform sentiment analysis using OpenAI + var sentimentResult = await AnalyzeSentimentWithOpenAI(textToAnalyze, log); + + // Create adaptive card with results + var card = CreateSentimentResultCard(textToAnalyze, sentimentResult); + + var attachment = new Microsoft.Teams.Api.MessageExtensions.Attachment + { + ContentType = Microsoft.Teams.Api.ContentType.AdaptiveCard, + Content = card + }; + + return new MessageExtensionResponse + { + ComposeExtension = new Result + { + Type = ResultType.Result, + AttachmentLayout = Microsoft.Teams.Api.Attachment.Layout.List, + Attachments = new List { attachment } + } + }; + } + catch (Exception ex) + { + log.Error($"Error analyzing sentiment: {ex.Message}"); + return CreateErrorResponse($"Error analyzing sentiment: {ex.Message}"); + } + } + + private async Task HandleAnalyzeSentimentFetchTask( + Microsoft.Teams.Api.Activities.Invokes.MessageExtensions.FetchTaskActivity activity, + Microsoft.Teams.Common.Logging.ILogger log) + { + try + { + var textToAnalyze = activity.Value?.MessagePayload?.Body?.Content ?? string.Empty; + + // Remove HTML tags and extra whitespace + textToAnalyze = Regex.Replace(textToAnalyze, @"<[^>]+>| ", "").Trim(); + + if (string.IsNullOrEmpty(textToAnalyze)) + { + return CreateErrorTaskResponse("No text to analyze"); + } + + // Perform sentiment analysis using OpenAI + var sentimentResult = await AnalyzeSentimentWithOpenAI(textToAnalyze, log); + + // Create adaptive card with results + var card = CreateSentimentResultCard(textToAnalyze, sentimentResult); + + return new ActionResponse + { + Task = new ContinueTask(new TaskInfo + { + Title = "Sentiment Analysis Result", + Height = new Union(TaskModuleSize.Medium), + Width = new Union(TaskModuleSize.Medium), + Card = new Microsoft.Teams.Api.Attachment(card) + }) + }; + } + catch (Exception ex) + { + log.Error($"Error analyzing sentiment: {ex.Message}"); + return CreateErrorTaskResponse($"Error analyzing sentiment: {ex.Message}"); + } + } + + private async Task AnalyzeSentimentWithOpenAI(string text, Microsoft.Teams.Common.Logging.ILogger log) + { + try + { + var client = new ChatClient(_modelName, _openAIApiKey); + + var messages = new List + { + new SystemChatMessage("You will be provided with a text message, and your task is to classify its sentiment as positive, neutral, or negative. Respond with only one word: Positive, Neutral, or Negative."), + new UserChatMessage(text) + }; + + var completion = await client.CompleteChatAsync(messages); + var assistantReply = completion.Value.Content[0].Text.Trim(); + var sentimentLower = assistantReply.ToLower(); + + // Try exact match first (most common case) + if (sentimentLower == "positive") return "Positive"; + if (sentimentLower == "negative") return "Negative"; + if (sentimentLower == "neutral") return "Neutral"; + + // Fallback: Check if response contains sentiment words + if (sentimentLower.Contains("positive")) return "Positive"; + if (sentimentLower.Contains("negative")) return "Negative"; + if (sentimentLower.Contains("neutral")) return "Neutral"; + + // Default to Neutral if unable to determine + return "Neutral"; + } + catch (Exception ex) + { + log.Error($"OpenAI API error: {ex.Message}"); + throw; + } + } + + private AdaptiveCard CreateSentimentResultCard(string actualMessage, string result) + { + var color = result switch + { + "Positive" => TextColor.Good, + "Negative" => TextColor.Attention, + _ => TextColor.Default + }; + + return new AdaptiveCard + { + Schema = "http://adaptivecards.io/schemas/adaptive-card.json", + Body = new List + { + new TextBlock("Sentiment Analysis Result") + { + Weight = TextWeight.Bolder, + Size = TextSize.Large, + Color = TextColor.Accent + }, + new TextBlock("Actual Message") + { + Weight = TextWeight.Bolder, + Size = TextSize.Medium, + Spacing = Spacing.Medium + }, + new TextBlock(actualMessage) + { + Wrap = true, + IsSubtle = true + }, + new TextBlock("Result") + { + Weight = TextWeight.Bolder, + Size = TextSize.Medium, + Spacing = Spacing.Medium + }, + new TextBlock(result) + { + Weight = TextWeight.Bolder, + Size = TextSize.Large, + Color = color + } + } + }; + } + + private MessageExtensionResponse CreateErrorResponse(string message) + { + return new MessageExtensionResponse + { + ComposeExtension = new Result + { + Type = ResultType.Message, + Text = message + } + }; + } + + private ActionResponse CreateErrorTaskResponse(string message) + { + var card = new AdaptiveCard + { + Body = new List + { + new TextBlock("Error") + { + Weight = TextWeight.Bolder, + Color = TextColor.Attention + }, + new TextBlock(message) + { + Wrap = true + } + } + }; + + return new ActionResponse + { + Task = new ContinueTask(new TaskInfo + { + Title = "Error", + Height = new Union(TaskModuleSize.Small), + Width = new Union(TaskModuleSize.Small), + Card = new Microsoft.Teams.Api.Attachment(card) + }) + }; + } + } +} \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/MEAISentimentAnalysis.csproj b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/MEAISentimentAnalysis.csproj index e8be549503..87fb18f43c 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/MEAISentimentAnalysis.csproj +++ b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/MEAISentimentAnalysis.csproj @@ -1,26 +1,30 @@ - + - net6.0 - latest + net10.0 + enable - - - - - + + + + + + - - - Always + + + + + PreserveNewest + None + + + + PreserveNewest + None - - - - - - + \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/MEAISentimentAnalysis.sln b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/MEAISentimentAnalysis.sln deleted file mode 100644 index 201b2bdfc7..0000000000 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/MEAISentimentAnalysis.sln +++ /dev/null @@ -1,39 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.12.35309.182 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MEAISentimentAnalysis", "MEAISentimentAnalysis.csproj", "{2B40B223-275B-49B6-8F13-8B75EC4F1714}" -EndProject -Project("{A9E3F50B-275E-4AF7-ADCE-8BE12D41E305}") = "M365Agent", "..\M365Agent\M365Agent.ttkproj", "{99DC514B-7A11-4E70-9E13-917993C69E17}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{86C7C56B-F160-49A0-8789-727FB8F69ECA}" - ProjectSection(SolutionItems) = preProject - MEAISentimentAnalysis.slnLaunch.user = MEAISentimentAnalysis.slnLaunch.user - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2B40B223-275B-49B6-8F13-8B75EC4F1714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B40B223-275B-49B6-8F13-8B75EC4F1714}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B40B223-275B-49B6-8F13-8B75EC4F1714}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {2B40B223-275B-49B6-8F13-8B75EC4F1714}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B40B223-275B-49B6-8F13-8B75EC4F1714}.Release|Any CPU.Build.0 = Release|Any CPU - {2B40B223-275B-49B6-8F13-8B75EC4F1714}.Release|Any CPU.Deploy.0 = Release|Any CPU - {99DC514B-7A11-4E70-9E13-917993C69E17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {99DC514B-7A11-4E70-9E13-917993C69E17}.Debug|Any CPU.Build.0 = Debug|Any CPU - {99DC514B-7A11-4E70-9E13-917993C69E17}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {99DC514B-7A11-4E70-9E13-917993C69E17}.Release|Any CPU.ActiveCfg = Release|Any CPU - {99DC514B-7A11-4E70-9E13-917993C69E17}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {01318672-B97F-4151-9B04-C067A624220B} - EndGlobalSection -EndGlobal diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/MEAISentimentAnalysis.slnLaunch.user b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/MEAISentimentAnalysis.slnLaunch.user deleted file mode 100644 index 1916f8ec87..0000000000 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/MEAISentimentAnalysis.slnLaunch.user +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "Name": "Microsoft Temas (Browser)", - "Projects": [ - { - "Path": "MEAISentimentAnalysis.csproj", - "Action": "Start", - "DebugTarget": "Start Project" - }, - { - "Path": "..\\M365Agent\\M365Agent.ttkproj", - "Action": "StartWithoutDebugging", - "DebugTarget": "Microsoft Teams (Browser)" - } - ] - }, - { - "Name": "Microsoft Teams (browser) (skip update app)", - "Projects": [ - { - "Path": "MEAISentimentAnalysis.csproj", - "Action": "Start", - "DebugTarget": "Start Project" - }, - { - "Path": "M365Agent\\M365Agent.ttkproj", - "Action": "StartWithoutDebugging", - "DebugTarget": "Microsoft Teams (browser) (skip update app)" - } - ] - } -] \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Pages/SentimentModel.cshtml b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Pages/SentimentModel.cshtml deleted file mode 100644 index 80337f1166..0000000000 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Pages/SentimentModel.cshtml +++ /dev/null @@ -1,48 +0,0 @@ -@page -@model MEAISentimentAnalysis.Pages.SentimentModel -@{ -} - - - - - - - - -
-
-
Sentiment Analysis Result :
-
-
-
-
-
-
- \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Pages/SentimentModel.cshtml.cs b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Pages/SentimentModel.cshtml.cs deleted file mode 100644 index 55d9b4556f..0000000000 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Pages/SentimentModel.cshtml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace MEAISentimentAnalysis.Pages -{ - public class SentimentModel: PageModel - { - public void OnGet() - { - } - } -} diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Program.cs b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Program.cs index dda146c070..ec7c902a3b 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Program.cs +++ b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Program.cs @@ -1,24 +1,46 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.14.0 +using MEAISentimentAnalysis; +using MEAISentimentAnalysis.Controllers; +using Azure.Core; +using Azure.Identity; +using Microsoft.Teams.Api.Auth; +using Microsoft.Teams.Apps; +using Microsoft.Teams.Apps.Extensions; +using Microsoft.Teams.Common.Http; +using Microsoft.Teams.Plugins.AspNetCore.Extensions; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; +var builder = WebApplication.CreateBuilder(args); +var config = builder.Configuration.Get(); -namespace MEAISentimentAnalysis +Func> createTokenFactory = async (string[] scopes, string? tenantId) => { - public class Program + var clientId = config.Teams.ClientId; + + var managedIdentityCredential = new ManagedIdentityCredential(clientId); + var tokenRequestContext = new TokenRequestContext(scopes, tenantId: tenantId); + var accessToken = await managedIdentityCredential.GetTokenAsync(tokenRequestContext); + + return new TokenResponse { - public static void Main(string[] args) + TokenType = "Bearer", + AccessToken = accessToken.Token, + }; +}; +var appBuilder = App.Builder(); + +if (config.Teams.BotType == "UserAssignedMsi") +{ + appBuilder.AddCredentials(new TokenCredentials( + config.Teams.ClientId ?? string.Empty, + async (tenantId, scopes) => { - CreateHostBuilder(args).Build().Run(); + return await createTokenFactory(scopes, tenantId); } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } + )); } + +builder.Services.AddSingleton(); +builder.AddTeams(appBuilder); + +var app = builder.Build(); +app.UseTeams(); +app.Run(); \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Properties/launchSettings.json b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Properties/launchSettings.json index 6ad7a375d7..3572a7a03f 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Properties/launchSettings.json +++ b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Properties/launchSettings.json @@ -1,14 +1,26 @@ -{ +{ "profiles": { + // Debug project within Microsoft 365 Agents Playground + "Microsoft 365 Agents Playground": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Playground", + "TEAMSFX_NOTIFICATION_STORE_FILENAME": ".notification.playgroundstore.json", + "UPDATE_TEAMS_APP": "false" + }, + "hotReloadProfile": "aspnetcore" + }, // Debug project within Teams "Start Project": { "commandName": "Project", "dotnetRunMessages": true, - "applicationUrl": "http://localhost:3978", + "applicationUrl": "http://localhost:5130", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "hotReloadProfile": "aspnetcore" - } + }, } } \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Startup.cs b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Startup.cs deleted file mode 100644 index a17fce10af..0000000000 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/Startup.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// Generated with Bot Builder V4 SDK Template for Visual Studio v4.14.0 - -using MessagingExtensionReminder.Bots; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Bot.Schema; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System.Collections.Concurrent; -using MEAISentimentAnalysis; - -namespace MEAISentimentAnalysis -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers().AddNewtonsoftJson(); - - services.AddHttpClient().AddControllers().AddNewtonsoftJson(); - services.AddRazorPages(); - - // Create the Bot Framework Adapter with error handling enabled. - services.AddSingleton(); - - // Create a global hashset for our ConversationReferences - services.AddSingleton>(); - - // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.) - services.AddSingleton(); - - // Create the Conversation state. (Used by the Dialog system itself.) - services.AddSingleton(); - - // Create the bot as a transient. In this case the ASP Controller is expecting an IBot. - services.AddTransient(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseDefaultFiles(); - app.UseStaticFiles(); - app.UseWebSockets() - .UseRouting() - .UseAuthorization() - .UseEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapRazorPages(); - }); - } - } -} \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/appsettings.Development.json b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/appsettings.Development.json index b49abfc201..84d2d74c35 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/appsettings.Development.json +++ b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/appsettings.Development.json @@ -1,9 +1,22 @@ { - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } - } + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "BotType": "", + "ApiKey": "", + "ModelName": "", + "ApplicationBaseUrl": "", + "TenantId": "" + } +} \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/appsettings.Playground.json b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/appsettings.Playground.json new file mode 100644 index 0000000000..0497106b3c --- /dev/null +++ b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/appsettings.Playground.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "TenantId": "", + "BotType": "" + } +} \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/appsettings.json b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/appsettings.json index 015b151614..9e3379db53 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/appsettings.json +++ b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/appsettings.json @@ -1,6 +1,18 @@ { - "MicrosoftAppId": "{{Microsoft-App-Id}}", - "SECRET_OPENAI_API_KEY": "{{SECRET_OPENAI_API_KEY}}", - "CHAT_COMPLETION_MODEL_NAME": "gpt-3.5-turbo", - "ApplicationBaseUrl": "{{ApplicationBaseUrl}}" + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Microsoft.Teams": { + "Enable": "*", + "Level": "debug" + } + }, + "AllowedHosts": "*", + "Teams": { + "ClientId": "", + "ClientSecret": "", + "BotType": "" + } } \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/wwwroot/default.htm b/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/wwwroot/default.htm deleted file mode 100644 index ec882835b1..0000000000 --- a/samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis/wwwroot/default.htm +++ /dev/null @@ -1,427 +0,0 @@ - - - - - - - MEAISentimentAnalysis - - - - - -
-
-
-
ME AI Sentiment Analysis
-
-
-
-
-
Your app is ready!
-
- You can test your app on Teams messages
- by connecting to http://localhost:3978/api/messages. -
- -
- Visit - Azure - Bot Service - to register your bot and add it to
- various channels. The bot's endpoint URL typically looks - like this: -
-
https://your_bots_hostname/api/messages
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/samples/msgext-ai-sentiment-analysis/csharp/README.md b/samples/msgext-ai-sentiment-analysis/csharp/README.md index ef67bbfcaf..83710e1b4c 100644 --- a/samples/msgext-ai-sentiment-analysis/csharp/README.md +++ b/samples/msgext-ai-sentiment-analysis/csharp/README.md @@ -62,16 +62,16 @@ The simplest way to run this sample in Teams is to use Microsoft 365 Agents Tool > Note these instructions are for running the sample on your local machine, the tunnelling solution is required because the Teams service needs to call into the bot. -1) Run ngrok - point to port 3978 +1) Run ngrok - point to port 5130 ```bash - ngrok http 3978 --host-header="localhost:3978" + ngrok http 5130 --host-header="localhost:5130" ``` Alternatively, you can also use the `dev tunnels`. Please follow [Create and host a dev tunnel](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below: ```bash - devtunnel host -p 3978 --allow-anonymous + devtunnel host -p 5130 --allow-anonymous ``` 2) App Registration @@ -114,7 +114,7 @@ the Teams service needs to call into the bot. - Navigate to folder where repository is cloned then `samples/msgext-ai-sentiment-analysis/csharp/MEAISentimentAnalysis.sln` -1) Update the `appSettings.json` configuration for the bot to use the `MicrosoftAppId`, `SECRET_OPENAI_API_KEY` and `ApplicationBaseUrl` with application base url. For e.g., your ngrok or dev tunnels url. (Note the MicrosoftAppId is the AppId created in step 1 (Setup for Bot), the MicrosoftAppPassword is referred to as the "client secret" in step 1 (Setup for Bot) and you can always create a new client secret anytime.) +1) Update the `appSettings.development.json` configuration for the bot to use the `ClientId`, `ApiKey`, `ModelName` and `ApplicationBaseUrl`, `ClientSecret`, `TenantId` with application base url. For e.g., your ngrok or dev tunnels url. (Note the MicrosoftAppId is the AppId created in step 1 (Setup for Bot), the MicrosoftAppPassword is referred to as the "client secret" in step 1 (Setup for Bot) and you can always create a new client secret anytime.) > Note: If you dont have access to Azure Open Api Key then use `Open Api key`. @@ -122,7 +122,7 @@ the Teams service needs to call into the bot. 5. Setup Manifest for Teams - __*This step is specific to Teams.*__ - - **Edit** the `manifest.json` contained in the ./appPackage folder to replace your Microsoft App Id (that was created when you registered your app registration earlier) *everywhere* you see the place holder string `{{Microsoft-App-Id}}` (depending on the scenario the Microsoft App Id may occur multiple times in the `manifest.json`) + - **Edit** the `manifest.json` contained in the ./appPackage folder to replace your Microsoft App Id (that was created when you registered your app registration earlier) *everywhere* you see the place holder string `{{TEAMS_ID}}` and `BOT_ID` (depending on the scenario the Microsoft App Id may occur multiple times in the `manifest.json`) - **Edit** the `manifest.json` for `validDomains` and replace `{{domain-name}}` with base Url of your domain. E.g. if you are using ngrok it would be `https://1234.ngrok-free.app` then your domain-name will be `1234.ngrok-free.app` and if you are using dev tunnels then your domain will be like: `12345.devtunnels.ms`. - **Zip** up the contents of the `appPackage` folder to create a `manifest.zip` (Make sure that zip file does not contains any subfolder otherwise you will get error while uploading your .zip package)