diff --git a/.github/workflows/ci-sample-validation.yml b/.github/workflows/ci-sample-validation.yml index 2b86f695..a61e6e67 100644 --- a/.github/workflows/ci-sample-validation.yml +++ b/.github/workflows/ci-sample-validation.yml @@ -80,7 +80,7 @@ jobs: run: | # This script runs the validation tool against changed samples in the pull request. # External samples are excluded from the validation. - exceptions=(".config" ".devcontainer" "assets" "validation-tool" "basic-blazor-tab-app" "incoming-webhook-notification" "stocks-update-notification-bot-dotnet" "whos-next-meeting-app") + exceptions=(".config" ".devcontainer" "assets" "templates" "validation-tool" "basic-blazor-tab-app" "incoming-webhook-notification" "stocks-update-notification-bot-dotnet" "whos-next-meeting-app") samples=`jq -r ".[]" <<< '${{ steps.get_changed_folders.outputs.changed }}'` validationFailed="validation failed" validationResult=true diff --git a/templates/command-and-response-js/.appserviceIgnore b/templates/command-and-response-js/.appserviceIgnore new file mode 100644 index 00000000..2d8a3ad1 --- /dev/null +++ b/templates/command-and-response-js/.appserviceIgnore @@ -0,0 +1,28 @@ +.appserviceIgnore +.fx +.deployment +.localConfigs.testTool +.localConfigs +.notification.localstore.json +.notification.testtoolstore.json +.vscode +*.js.map +*.ts.map +*.ts +.git* +.tsbuildinfo +CHANGELOG.md +readme.md +local.settings.json +test +tsconfig.json +.DS_Store +teamsapp.yml +teamsapp.*.yml +/env/ +/node_modules/.bin +/node_modules/ts-node +/node_modules/typescript +/appPackage/ +/infra/ +/devTools/ \ No newline at end of file diff --git a/templates/command-and-response-js/.gitignore b/templates/command-and-response-js/.gitignore new file mode 100644 index 00000000..dfb975ac --- /dev/null +++ b/templates/command-and-response-js/.gitignore @@ -0,0 +1,26 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +.DS_Store +build +appPackage/build + +# dependencies +node_modules/ + +# misc +.env +.deployment +.DS_Store + +# build +lib/ + +# Local data +.localConfigs.testTool +.localConfigs +.notification.localstore.json +.notification.testtoolstore.json + +# Dev tool directories +/devTools/ \ No newline at end of file diff --git a/templates/command-and-response-js/.vscode/extensions.json b/templates/command-and-response-js/.vscode/extensions.json new file mode 100644 index 00000000..aac0a6e3 --- /dev/null +++ b/templates/command-and-response-js/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/templates/command-and-response-js/.vscode/launch.json b/templates/command-and-response-js/.vscode/launch.json new file mode 100644 index 00000000..ccf40807 --- /dev/null +++ b/templates/command-and-response-js/.vscode/launch.json @@ -0,0 +1,132 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Attach to Local Service", + "type": "node", + "request": "attach", + "port": 9239, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Desktop)", + "type": "node", + "request": "launch", + "preLaunchTask": "Start Teams App in Desktop Client (Remote)", + "presentation": { + "group": "3-remote", + "order": 3 + }, + "internalConsoleOptions": "neverOpen", + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "2-local", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "2-local", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Desktop)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App in Desktop Client", + "presentation": { + "group": "2-local", + "order": 3 + }, + "stopAll": true + }, + { + "name": "Debug in Test Tool", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { + "group": "1-local", + "order": 1 + }, + "stopAll": true + } + ] +} diff --git a/templates/command-and-response-js/.vscode/settings.json b/templates/command-and-response-js/.vscode/settings.json new file mode 100644 index 00000000..42996202 --- /dev/null +++ b/templates/command-and-response-js/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ] +} diff --git a/templates/command-and-response-js/.vscode/tasks.json b/templates/command-and-response-js/.vscode/tasks.json new file mode 100644 index 00000000..9034316c --- /dev/null +++ b/templates/command-and-response-js/.vscode/tasks.json @@ -0,0 +1,232 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool", + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150, // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool", + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}", + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239 // app inspector port for Node.js debugger + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Teams App in Desktop Client", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start application", + "Start desktop client" + ], + "dependsOrder": "sequence" + }, + { + "label": "Start desktop client", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true" + } + }, + { + "label": "Start Teams App in Desktop Client (Remote)", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true" + } + } + ] +} \ No newline at end of file diff --git a/templates/command-and-response-js/README.md b/templates/command-and-response-js/README.md new file mode 100644 index 00000000..8acbdc5b --- /dev/null +++ b/templates/command-and-response-js/README.md @@ -0,0 +1,197 @@ +# Overview of the Command bot template + +This template showcases an app that responds to chat commands by displaying UI using an Adaptive Card. This enables your users to type in simple messages in Teams and your application can provide an appropriate response based on the contents of the message. + +The app template is built using the TeamsFx SDK, which provides a simple set of functions over the Microsoft Bot Framework to implement this scenario. + +## Get Started with the Command bot + +> **Prerequisites** +> +> To run the command bot template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +> +> **Note** +> +> Your app can be installed into a team, or a group chat, or as personal app. See [Installation and Uninstallation](https://aka.ms/teamsfx-command-new#customize-installation). +> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging). + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool`. +3. The browser will pop up to open Teams App Test Tool. +4. Type or select `helloWorld` in the chat to send it to your bot - this is the default command provided by the template. + +The bot will respond to the `helloWorld` command with an Adaptive Card: + +![Command and Response in Test Tool](https://github.com/OfficeDev/TeamsFx/assets/9698542/2636fd91-ec7f-4740-a5ea-b272575b0b7c) + +## What's included in the template + +| Folder / File | Contents | +| - | - | +| `teamsapp.yml` | Main project file describes your application configuration and defines the set of actions to run in each lifecycle stages | +| `teamsapp.local.yml`| This overrides `teamsapp.yml` with actions that enable local execution and debugging | +| `teamsapp.testtool.yml`| This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool | +| `env/`| Name / value pairs are stored in environment files and used by `teamsapp.yml` to customize the provisioning and deployment rules | +| `.vscode/` | VSCode files for debugging | +| `appPackage/` | Templates for the Teams application manifest | +| `infra/` | Templates for provisioning Azure resources | +| `src/` | The source code for the application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| - | - | +| `src/index.js` | Application entry point and `restify` handlers for command and response | +| `src/teamsBot.js` | An empty teams activity handler for bot customization | +| `src/adaptiveCards/helloworldCommand.json` | A generated Adaptive Card that is sent to Teams | +| `src/helloworldCommandHandler.js` | The business logic to handle a command | + +## Extend the command bot template with more commands and responses + +Follow the steps below to add more commands and responses to extend the command bot: + +1. [Step 1: Add a command definition in manifest](#step-1-add-a-command-definition-in-manifest) +2. [Step 2: Respond with an Adaptive Card](#step-2-respond-with-an-adaptive-card) +3. [Step 3: Handle the command](#step-3-handle-the-command) +4. [Step 4: Register the new command](#step-4-register-the-new-command) + +### Step 1: Add a command definition in manifest + +You can edit the manifest template file `appPackage\manifest.json` to include definitions of a `doSomething` command with its title and description in the `commands` array: + +```json +"commandLists": [ + { + "commands": [ + { + "title": "helloWorld", + "description": "A helloworld command to send a welcome message" + }, + { + "title": "doSomething", + "description": "A sample do something command" + } + ] + } +] +``` + +### Step 2: Respond with an Adaptive Card + +To respond with an Adaptive Card, define your card in its JSON format. Create a new file `src/adaptiveCards/doSomethingCommandResponse.json`: + +```json +{ + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": "Your doSomething Command is added!" + }, + { + "type": "TextBlock", + "text": "Congratulations! Your hello world bot now includes a new DoSomething Command", + "wrap": true + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.4" +} +``` + +You can use the [Adaptive Card Designer](https://adaptivecards.io/designer/) to help visually design your Adaptive Card UI. + +> Please note: + +> - Respond with an Adaptive Card is optional, you can simply respond with plain texts. +> - If you'd like to send adaptive card with dynamic data, please refer to [this document](https://aka.ms/teamsfx-command-new#how-to-build-command-response-using-adaptive-card-with-dynamic-content). + +### Step 3: Handle the command + +The TeamsFx SDK provides a convenient class, `TeamsFxBotCommandHandler`, to handle when an command is triggered from Teams conversation message. Create a new file, `src/doSomethingCommandHandler.js`: + +```javascript +const doSomethingCard = require("./adaptiveCards/doSomethingCommandResponse.json"); +const { AdaptiveCards } = require("@microsoft/adaptivecards-tools"); +const { CardFactory, MessageFactory } = require("botbuilder"); + +class DoSomethingCommandHandler { + triggerPatterns = "doSomething"; + + async handleCommandReceived(context, message) { + // verify the command arguments which are received from the client if needed. + console.log(`App received message: ${message.text}`); + + const cardData = { + title: "doSomething command is added", + body: "Congratulations! You have responded to doSomething command", + }; + + const cardJson = AdaptiveCards.declare(doSomethingCard).render(cardData); + return MessageFactory.attachment(CardFactory.adaptiveCard(cardJson)); + } +} + +module.exports = { + DoSomethingCommandHandler, +}; +``` + +You can customize what the command does here, including calling an API, process data, etc. + +### Step 4: Register the new command + +Each new command needs to be configured in the `ConversationBot`, which powers the conversational flow of the command bot template. Navigate to the `src/internal/initialize.js` file and update the `commands` array of the `command` property: + +```javascript +const { BotBuilderCloudAdapter } = require("@microsoft/teamsfx"); +const ConversationBot = BotBuilderCloudAdapter.ConversationBot; +const { HelloWorldCommandHandler } = require("../helloworldCommandHandler"); +const { DoSomethingCommandHandler } = require("../doSomethingCommandHandler"); + +const commandApp = new ConversationBot({ + //... + command: { + enabled: true, + commands: [new HelloWorldCommandHandler(), new DoSomethingCommandHandler()], + }, +}); + +module.exports = { + commandApp, +}; +``` + +Congratulations, you've just created your own command! To learn more about the command bot template, [visit the documentation on GitHub](https://aka.ms/teamsfx-command-new). You can find more scenarios like: + +- [Customize the trigger pattern](https://aka.ms/teamsfx-command-new#customize-the-trigger-pattern) +- [Customize the Adaptive Card with dynamic content](https://aka.ms/teamsfx-command-new#how-to-build-command-response-using-adaptive-card-with-dynamic-content) +- [Change the way to initialize the bot](https://aka.ms/teamsfx-command-new#customize-initialization) +- [Connect to an existing API](https://aka.ms/teamsfx-command-new#connect-to-existing-api) +- [Access Microsoft Graph](https://aka.ms/teamsfx-add-sso-new) + +## Extend command bot with other bot scenarios + +Command bot is compatible with other bot scenarios like notification bot and workflow bot. + +### Add notifications to your command bot + +The notification feature adds the ability for your application to send Adaptive Cards in response to external events. Follow the [steps here](https://aka.ms/teamsfx-command-new#how-to-extend-my-command-and-response-bot-to-support-notification) to add the notification feature to your command bot. Refer [the notification document](https://aka.ms/teamsfx-notification-new) for more information. + +### Add workflow to your command bot + +Adaptive cards can be updated on user action to allow user progress through a series of cards that require user input. Developers can define actions and use a bot to return an Adaptive Cards in response to user action. This can be chained into sequential workflows. Follow the [steps here](https://aka.ms/teamsfx-workflow-new#add-more-card-actions) to add workflow feature to your command bot. Refer [the workflow document](https://aka.ms/teamsfx-workflow-new) for more information. + +## Additional information and references + +- [Manage multiple environments](https://docs.microsoft.com/microsoftteams/platform/toolkit/teamsfx-multi-env) +- [Collaborate with others](https://docs.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) +- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +- [TeamsFx SDK](https://docs.microsoft.com/microsoftteams/platform/toolkit/teamsfx-sdk) +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) diff --git a/templates/command-and-response-js/appPackage/color.png b/templates/command-and-response-js/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/command-and-response-js/appPackage/color.png differ diff --git a/templates/command-and-response-js/appPackage/manifest.json b/templates/command-and-response-js/appPackage/manifest.json new file mode 100644 index 00000000..ac4ff827 --- /dev/null +++ b/templates/command-and-response-js/appPackage/manifest.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "command-and-response-js${{APP_NAME_SUFFIX}}", + "full": "Full name for command-and-response-js" + }, + "description": { + "short": "Short description of command-and-response-js", + "full": "Full description of command-and-response-js" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": false, + "commandLists": [ + { + "scopes": [ + "personal", + "team", + "groupChat" + ], + "commands": [ + { + "title": "helloWorld", + "description": "A helloworld command to send a welcome message" + } + ] + } + ] + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/templates/command-and-response-js/appPackage/outline.png b/templates/command-and-response-js/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/command-and-response-js/appPackage/outline.png differ diff --git a/templates/command-and-response-js/env/.env.dev b/templates/command-and-response-js/env/.env.dev new file mode 100644 index 00000000..4b07861c --- /dev/null +++ b/templates/command-and-response-js/env/.env.dev @@ -0,0 +1,16 @@ +# 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= +BOT_DOMAIN= \ No newline at end of file diff --git a/templates/command-and-response-js/env/.env.testtool b/templates/command-and-response-js/env/.env.testtool new file mode 100644 index 00000000..43ce12aa --- /dev/null +++ b/templates/command-and-response-js/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/command-and-response-js/infra/azure.bicep b/templates/command-and-response-js/infra/azure.bicep new file mode 100644 index 00000000..cca52bf3 --- /dev/null +++ b/templates/command-and-response-js/infra/azure.bicep @@ -0,0 +1,95 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +param webAppSKU string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param identityName string = resourceBaseName +param location string = resourceGroup().location + +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: webAppSKU + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + alwaysOn: true + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure App Service from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x for your site + } + { + name: 'RUNNING_ON_AZURE' + value: '1' + } + { + name: 'BOT_ID' + value: identity.properties.clientId + } + { + name: 'BOT_TENANT_ID' + value: identity.properties.tenantId + } + { + name: 'BOT_TYPE' + 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/templates/command-and-response-js/infra/azure.parameters.json b/templates/command-and-response-js/infra/azure.parameters.json new file mode 100644 index 00000000..75a8ff06 --- /dev/null +++ b/templates/command-and-response-js/infra/azure.parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "commandbot${{RESOURCE_SUFFIX}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "command-and-response-js" + } + } + } \ No newline at end of file diff --git a/templates/command-and-response-js/infra/botRegistration/azurebot.bicep b/templates/command-and-response-js/infra/botRegistration/azurebot.bicep new file mode 100644 index 00000000..a5a27b8f --- /dev/null +++ b/templates/command-and-response-js/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/templates/command-and-response-js/infra/botRegistration/readme.md b/templates/command-and-response-js/infra/botRegistration/readme.md new file mode 100644 index 00000000..d5416243 --- /dev/null +++ b/templates/command-and-response-js/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/templates/command-and-response-js/package.json b/templates/command-and-response-js/package.json new file mode 100644 index 00000000..137ef596 --- /dev/null +++ b/templates/command-and-response-js/package.json @@ -0,0 +1,34 @@ +{ + "name": "commandandresponsejs", + "version": "1.0.0", + "description": "Microsoft Teams Toolkit Command and Response Bot Sample", + "engines": { + "node": "16 || 18" + }, + "author": "Microsoft", + "license": "MIT", + "main": "./src/index.js", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", + "dev": "nodemon --inspect=9239 --signal SIGINT ./src/index.js", + "start": "node ./src/index.js", + "watch": "nodemon ./src/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com" + }, + "dependencies": { + "@microsoft/adaptivecards-tools": "^1.0.0", + "@microsoft/teamsfx": "^2.3.1", + "botbuilder": "^4.20.0", + "restify": "^10.0.0" + }, + "devDependencies": { + "env-cmd": "^10.1.0", + "nodemon": "^2.0.7" + } +} \ No newline at end of file diff --git a/templates/command-and-response-js/src/adaptiveCards/helloworldCommand.json b/templates/command-and-response-js/src/adaptiveCards/helloworldCommand.json new file mode 100644 index 00000000..b8124f61 --- /dev/null +++ b/templates/command-and-response-js/src/adaptiveCards/helloworldCommand.json @@ -0,0 +1,31 @@ +{ + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": "${title}" + }, + { + "type": "TextBlock", + "text": "${body}", + "wrap": true + } + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "Azure Bot Service Docs", + "url": "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0" + }, + { + "type": "Action.OpenUrl", + "title": "Teams Toolkit Docs", + "url": "https://aka.ms/teamsfx-docs" + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.4" + } + \ No newline at end of file diff --git a/templates/command-and-response-js/src/genericCommandHandler.js b/templates/command-and-response-js/src/genericCommandHandler.js new file mode 100644 index 00000000..1eb15c0b --- /dev/null +++ b/templates/command-and-response-js/src/genericCommandHandler.js @@ -0,0 +1,35 @@ +class GenericCommandHandler { + triggerPatterns = new RegExp(/^.+$/); + + async handleCommandReceived(context, message) { + // verify the command arguments which are received from the client if needed. + console.log(`App received message: ${message.text}`); + + let response = ""; + switch (message.text) { + case "hi": + response = + "Hi there! I'm your Command Bot, here to assist you with your tasks. Type 'help' for a list of available commands."; + break; + case "hello": + response = + "Hello! I'm your Command Bot, always ready to help you out. If you need assistance, just type 'help' to see the available commands."; + break; + case "help": + response = + "Here's a list of commands I can help you with:\n" + + "- 'hi' or 'hello': Say hi or hello to me, and I'll greet you back.\n" + + "- 'help': Get a list of available commands.\n" + + "- 'helloworld': See a sample response from me.\n" + + "\nFeel free to ask for help anytime you need it!"; + break; + default: + response = `Sorry, command unknown. Please type 'help' to see the list of available commands.`; + } + return response; + } +} + +module.exports = { + GenericCommandHandler, +}; diff --git a/templates/command-and-response-js/src/helloworldCommandHandler.js b/templates/command-and-response-js/src/helloworldCommandHandler.js new file mode 100644 index 00000000..50a76cf3 --- /dev/null +++ b/templates/command-and-response-js/src/helloworldCommandHandler.js @@ -0,0 +1,27 @@ +const helloWorldCard = require("./adaptiveCards/helloworldCommand.json"); +const { AdaptiveCards } = require("@microsoft/adaptivecards-tools"); +const { CardFactory, MessageFactory } = require("botbuilder"); + +class HelloWorldCommandHandler { + triggerPatterns = "helloWorld"; + + async handleCommandReceived(context, message) { + // verify the command arguments which are received from the client if needed. + console.log(`App received message: ${message.text}`); + + // do something to process your command and return message activity as the response + + // render your adaptive card for reply message + const cardData = { + title: "Your Hello World App is Running", + body: "Congratulations! Your Hello World App is running. Open the documentation below to learn more about how to build applications with the Teams Toolkit.", + }; + + const cardJson = AdaptiveCards.declare(helloWorldCard).render(cardData); + return MessageFactory.attachment(CardFactory.adaptiveCard(cardJson)); + } +} + +module.exports = { + HelloWorldCommandHandler, +}; diff --git a/templates/command-and-response-js/src/index.js b/templates/command-and-response-js/src/index.js new file mode 100644 index 00000000..12ea9234 --- /dev/null +++ b/templates/command-and-response-js/src/index.js @@ -0,0 +1,25 @@ +// Create HTTP server. +const restify = require("restify"); +const { commandApp } = require("./internal/initialize"); +const { TeamsBot } = require("./teamsBot"); + +// This template uses `restify` to serve HTTP responses. +// Create a restify server. +const server = restify.createServer(); +server.use(restify.plugins.bodyParser()); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\nApp Started, ${server.name} listening to ${server.url}`); +}); + +// Register an API endpoint with `restify`. Teams sends messages to your application +// through this endpoint. +// +// The Teams Toolkit bot registration configures the bot with `/api/messages` as the +// Bot Framework endpoint. If you customize this route, update the Bot registration +// in `templates/azure/provision/botservice.bicep`. +const teamsBot = new TeamsBot(); +server.post("/api/messages", async (req, res) => { + await commandApp.requestHandler(req, res, async (context) => { + await teamsBot.run(context); + }); +}); diff --git a/templates/command-and-response-js/src/internal/config.js b/templates/command-and-response-js/src/internal/config.js new file mode 100644 index 00000000..ea6b1a59 --- /dev/null +++ b/templates/command-and-response-js/src/internal/config.js @@ -0,0 +1,8 @@ +const config = { + MicrosoftAppId: process.env.BOT_ID, + MicrosoftAppType: process.env.BOT_TYPE, + MicrosoftAppTenantId: process.env.BOT_TENANT_ID, + MicrosoftAppPassword: process.env.BOT_PASSWORD, +}; + +module.exports = config; diff --git a/templates/command-and-response-js/src/internal/initialize.js b/templates/command-and-response-js/src/internal/initialize.js new file mode 100644 index 00000000..ddf9579a --- /dev/null +++ b/templates/command-and-response-js/src/internal/initialize.js @@ -0,0 +1,22 @@ +const { BotBuilderCloudAdapter } = require("@microsoft/teamsfx"); +const ConversationBot = BotBuilderCloudAdapter.ConversationBot; +const { HelloWorldCommandHandler } = require("../helloworldCommandHandler"); +const { GenericCommandHandler } = require("../genericCommandHandler"); +const config = require("./config"); + +// Create the command bot and register the command handlers for your app. +// You can also use the commandApp.command.registerCommands to register other commands +// if you don't want to register all of them in the constructor +const commandApp = new ConversationBot({ + // The bot id and password to create CloudAdapter. + // See https://aka.ms/about-bot-adapter to learn more about adapters. + adapterConfig: config, + command: { + enabled: true, + commands: [new HelloWorldCommandHandler(), new GenericCommandHandler()], + }, +}); + +module.exports = { + commandApp, +}; diff --git a/templates/command-and-response-js/src/teamsBot.js b/templates/command-and-response-js/src/teamsBot.js new file mode 100644 index 00000000..52e5001b --- /dev/null +++ b/templates/command-and-response-js/src/teamsBot.js @@ -0,0 +1,25 @@ +const { TeamsActivityHandler } = require("botbuilder"); + +// Teams activity handler. +// You can add your customization code here to extend your bot logic if needed. +class TeamsBot extends TeamsActivityHandler { + constructor() { + super(); + + // Listen to MembersAdded event, view https://docs.microsoft.com/en-us/microsoftteams/platform/resources/bot-v3/bots-notifications for more events + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (let cnt = 0; cnt < membersAdded.length; cnt++) { + if (membersAdded[cnt].id) { + await context.sendActivity( + 'Welcome to the Command Bot! I can help you with a few simple commands. Type "helloworld" or "help" to get started.' + ); + break; + } + } + await next(); + }); + } +} + +module.exports.TeamsBot = TeamsBot; diff --git a/templates/command-and-response-js/teamsapp.local.yml b/templates/command-and-response-js/teamsapp.local.yml new file mode 100644 index 00000000..8a81f803 --- /dev/null +++ b/templates/command-and-response-js/teamsapp.local.yml @@ -0,0 +1,83 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: command-and-response-js${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: command-and-response-js${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: command-and-response-js + 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 + # 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 + # Teams 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 + +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + BOT_TYPE: 'MultiTenant' diff --git a/templates/command-and-response-js/teamsapp.testtool.yml b/templates/command-and-response-js/teamsapp.testtool.yml new file mode 100644 index 00000000..44d71fd1 --- /dev/null +++ b/templates/command-and-response-js/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.1 + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/command-and-response-js/teamsapp.yml b/templates/command-and-response-js/teamsapp.yml new file mode 100644 index 00000000..5fd1d101 --- /dev/null +++ b/templates/command-and-response-js/teamsapp.yml @@ -0,0 +1,127 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: command-and-response-js${{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 + # Teams 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 + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # 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 + # Teams 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: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --production + # 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: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .appserviceIgnore + # 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}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: eda605d6-1e6b-433f-873e-f7e04d102fd8 diff --git a/templates/command-and-response-js/web.config b/templates/command-and-response-js/web.config new file mode 100644 index 00000000..c766d733 --- /dev/null +++ b/templates/command-and-response-js/web.config @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/copilot-rag-ai-search/.gitignore b/templates/copilot-rag-ai-search/.gitignore new file mode 100644 index 00000000..75baccc4 --- /dev/null +++ b/templates/copilot-rag-ai-search/.gitignore @@ -0,0 +1,18 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +env/.env.testtool +.env +appPackage/build + +# python virtual environment +.venv/ +__pycache__/ + +# others +.deployment/ +node_modules/ +devTools/*.log + +# Dev tool directories +/devTools/ \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/.vscode/extensions.json b/templates/copilot-rag-ai-search/.vscode/extensions.json new file mode 100644 index 00000000..760a0b1d --- /dev/null +++ b/templates/copilot-rag-ai-search/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension", + "ms-python.python" + ] +} \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/.vscode/launch.json b/templates/copilot-rag-ai-search/.vscode/launch.json new file mode 100644 index 00000000..a0c18a24 --- /dev/null +++ b/templates/copilot-rag-ai-search/.vscode/launch.json @@ -0,0 +1,130 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Desktop)", + "type": "node", + "request": "launch", + "preLaunchTask": "Start Teams App in Desktop Client (Remote)", + "presentation": { + "group": "3-remote", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Start Python", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/src/app.py", + "cwd": "${workspaceFolder}/src", + "console": "integratedTerminal" + }, + { + "name": "Start Test Tool", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/devTools/teamsapptester/node_modules/@microsoft/teams-app-test-tool/cli.js", + "args": [ + "start" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": ["Launch App (Edge)", "Start Python"], + "cascadeTerminateToConfigurations": ["Start Python"], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "1-local", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": ["Launch App (Chrome)", "Start Python"], + "cascadeTerminateToConfigurations": ["Start Python"], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "1-local", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Desktop)", + "configurations": ["Start Python"], + "preLaunchTask": "Start Teams App in Desktop Client", + "presentation": { + "group": "1-local", + "order": 3 + }, + "stopAll": true + }, + { + "name": "Debug in Test Tool", + "configurations": [ + "Start Python", + "Start Test Tool" + ], + "cascadeTerminateToConfigurations": [ + "Start Test Tool" + ], + "preLaunchTask": "Deploy (Test Tool)", + "presentation": { + "group": "2-local", + "order": 1 + }, + "stopAll": true + } + ] +} diff --git a/templates/copilot-rag-ai-search/.vscode/settings.json b/templates/copilot-rag-ai-search/.vscode/settings.json new file mode 100644 index 00000000..0d3ba10b --- /dev/null +++ b/templates/copilot-rag-ai-search/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ] +} \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/.vscode/tasks.json b/templates/copilot-rag-ai-search/.vscode/tasks.json new file mode 100644 index 00000000..a964abf8 --- /dev/null +++ b/templates/copilot-rag-ai-search/.vscode/tasks.json @@ -0,0 +1,135 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Check if Node.js is installed and the version is >= 12. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 56150, // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)" + ], + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool", + } + }, + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978 // app service port + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start Teams App in Desktop Client", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start desktop client" + ], + "dependsOrder": "sequence" + }, + { + "label": "Start desktop client", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true" + } + }, + { + "label": "Start Teams App in Desktop Client (Remote)", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true" + } + } + ] +} \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/.webappignore b/templates/copilot-rag-ai-search/.webappignore new file mode 100644 index 00000000..fc2332c6 --- /dev/null +++ b/templates/copilot-rag-ai-search/.webappignore @@ -0,0 +1,15 @@ +.venv/ +.vscode/ +appPackage/ +devTools/ +infra/ +.env +env/ +__pycache__/ +README.md +teamsapp.yml +teamsapp.local.yml +teamsapp.testtool.yml +.gitignore + +indexers/ \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/README.md b/templates/copilot-rag-ai-search/README.md new file mode 100644 index 00000000..334bc082 --- /dev/null +++ b/templates/copilot-rag-ai-search/README.md @@ -0,0 +1,105 @@ +# Overview of the Chat With Your Data (Using Azure AI Search) template + +This app template showcases how to build one of the most powerful applications enabled by LLM - sophisticated question-answering (Q&A) chat bots that can answer questions about specific source information right in the Microsoft Teams. +This app template also demonstrates usage of techniques like: +- [Retrieval Augmented Generation](https://python.langchain.com/docs/use_cases/question_answering/#what-is-rag), or RAG. +- [Azure AI Search](https://learn.microsoft.com/azure/search/search-what-is-azure-search) +- [Teams AI Library](https://learn.microsoft.com/microsoftteams/platform/bots/how-to/teams%20conversational%20ai/teams-conversation-ai-overview) + +## Get started with the template + +> **Prerequisites** +> +> To run the template in your local dev machine, you will need: +> +> - [Python](https://www.python.org/), version 3.8 to 3.11. +> - [Python extension](https://code.visualstudio.com/docs/languages/python), version v2024.0.1 or higher. +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) latest version or [Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli). +> - An account with [Azure OpenAI](https://aka.ms/oai/access). +> - An [Azure Search service](https://learn.microsoft.com/en-us/azure/search/search-what-is-azure-search). +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). + +### Configurations +1. Open the command box and enter `Python: Create Environment` to create and activate your desired virtual environment. Remember to select `src/requirements.txt` as dependencies to install when creating the virtual environment. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY`, deployment name `AZURE_OPENAI_MODEL_DEPLOYMENT_NAME`, endpoint `AZURE_OPENAI_ENDPOINT` and embedding deployment name `AZURE_OPENAI_EMBEDDING_DEPLOYMENT`. +1. In file *env/.env.local.user*, fill in your Azure Search key `SECRET_AZURE_SEARCH_KEY` and endpoint `AZURE_SEARCH_ENDPOINT`. + +### Setting up index and documents +1. Azure Search key `SECRET_AZURE_SEARCH_KEY` and endpoint `AZURE_SEARCH_ENDPOINT` are loaded from *env/.env.local.user*. Please make sure you have already configured them. +1. Use command `python src/indexers/setup.py` to create index and upload documents in `src/indexers/data`. +1. You will see the following information indicated the success of setup: + ``` + Create index succeeded. If it does not exist, wait for 5 seconds... + Upload new documents succeeded. If they do not exist, wait for several seconds... + setup finished + ``` +1. Once you're done using the sample it's good practice to delete the index. You can do so with the command `python src/indexers/delete.py`. + +### Conversation with bot +1. Select the Teams Toolkit icon on the left in the VS Code toolbar. +1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. +1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. +1. You will receive a welcome message from the bot, or send any message to get a response. + +**Congratulations**! You are running an application that can now interact with users in Teams: + +> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging). + +![alt text](https://github.com/OfficeDev/TeamsFx/assets/109947924/2c17e3e8-09c1-42b6-b47a-ac4234343883) + +## What's included in the template + +| Folder | Contents | +| - | - | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| - | - | +|`src/bot.py`| Handles business logics for the AI Search Bot.| +|`src/config.py`| Defines the environment variables.| +|`src/app.py`| Main module of the AI Search Bot, hosts a aiohttp api server for the app.| +|`src/azure_ai_search_data_source.py.py`| Handles data search logics.| +|`src/prompts/chat/skprompt.txt`| Defines the prompt.| +|`src/prompts/chat/config.json`| Configures the prompt.| + +The following files are scripts and raw texts that help you to prepare or clean data source in Azure Search. + +| File | Contents | +| - | - | +|`src/indexers/get_data.py`| Fetches data and creates embedding vectors.| +|`src/indexers/data/*.md`| Raw text data source.| +|`src/indexers/setup.py`| A script to create index and upload documents.| +|`src/indexers/delete.py`| A script to delete index and documents.| + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| - | - | +|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +|`teamsapp.testtool.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool.| + +## Extend the template + +- Follow [Build a Basic AI Chatbot in Teams](https://aka.ms/teamsfx-basic-ai-chatbot) to extend the template with more AI capabilities. +- Follow [Build a RAG Bot in Teams](https://aka.ms/teamsfx-rag-bot) to extend the template with more RAG capabilities. In this template, we upload raw text data to Azure AI Search. Azure AI Search also allows you to create vectorized data and do vector similarity search. +You can refer to the section [integrate-vectorization](https://github.com/OfficeDev/TeamsFx/wiki/Build-a-RAG-Bot-in-Teams#integrate-vectorization) or the demo [integrated-vectorization](https://github.com/Azure/azure-search-vector-samples/tree/main/demo-python/code/integrated-vectorization) for more details. +- Understand more about [Azure AI Search as data source](https://aka.ms/teamsfx-rag-bot#azure-ai-search-as-data-source). + +## Additional information and references + +- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) + +## Known issue +- If you use `Debug in Test Tool` to local debug, you might get an error `InternalServiceError: connect ECONNREFUSED 127.0.0.1:3978` in Test Tool console log or error message `Error: Cannot connect to your app, +please make sure your app is running or restart your app` in log panel of Test Tool web page. You can wait for Python launch console ready and then refresh the front end web page. +- When you use `Launch Remote in Teams` to remote debug after deployment, you might loose interaction with your bot. This is because the remote service needs to restart. Please wait for several minutes to retry it. \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/appPackage/color.png b/templates/copilot-rag-ai-search/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/copilot-rag-ai-search/appPackage/color.png differ diff --git a/templates/copilot-rag-ai-search/appPackage/manifest.json b/templates/copilot-rag-ai-search/appPackage/manifest.json new file mode 100644 index 00000000..65011dd1 --- /dev/null +++ b/templates/copilot-rag-ai-search/appPackage/manifest.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "copilot-rag-ai-search${{APP_NAME_SUFFIX}}", + "full": "full name for copilot-rag-ai-search" + }, + "description": { + "short": "short description for copilot-rag-ai-search", + "full": "full description for copilot-rag-ai-search" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} diff --git a/templates/copilot-rag-ai-search/appPackage/outline.png b/templates/copilot-rag-ai-search/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/copilot-rag-ai-search/appPackage/outline.png differ diff --git a/templates/copilot-rag-ai-search/env/.env.dev b/templates/copilot-rag-ai-search/env/.env.dev new file mode 100644 index 00000000..8172044c --- /dev/null +++ b/templates/copilot-rag-ai-search/env/.env.dev @@ -0,0 +1,17 @@ +# 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= +TEAMS_APP_TENANT_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= +BOT_DOMAIN= \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/infra/azure.bicep b/templates/copilot-rag-ai-search/infra/azure.bicep new file mode 100644 index 00000000..5ada2424 --- /dev/null +++ b/templates/copilot-rag-ai-search/infra/azure.bicep @@ -0,0 +1,120 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@description('Required when create Azure Bot service') +param botAadAppClientId string + +@secure() +@description('Required by Bot Framework package in your bot project') +param botAadAppClientSecret string + +@secure() +@description('Required in your bot project to access Azure OpenAI service. You can get it from Azure Portal > OpenAI > Keys > Key1 > Resource Management > Endpoint') +param azureOpenaiKey string +param azureOpenaiModelDeploymentName string +param azureOpenaiEndpoint string +param azureOpenaiEmbeddingDeployment string + +@secure() +@description('Required in your bot project to access Azure Search service. You can get it from Azure Portal > Azure Search > Keys > Admin Key') +param azureSearchKey string +param azureSearchEndpoint string + +param webAppSKU string +param linuxFxVersion string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param location string = resourceGroup().location +param pythonVersion string = linuxFxVersion + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app,linux' + location: location + name: serverfarmsName + sku: { + name: webAppSKU + } + properties:{ + reserved: true + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app,linux' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + siteConfig: { + alwaysOn: true + appCommandLine: 'gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:app' + linuxFxVersion: pythonVersion + appSettings: [ + { + name: 'WEBSITES_CONTAINER_START_TIME_LIMIT' + value: '600' + } + { + name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' + value: 'true' + } + { + name: 'BOT_ID' + value: botAadAppClientId + } + { + name: 'BOT_PASSWORD' + value: botAadAppClientSecret + } + { + name: 'AZURE_OPENAI_API_KEY' + value: azureOpenaiKey + } + { + name: 'AZURE_OPENAI_MODEL_DEPLOYMENT_NAME' + value: azureOpenaiModelDeploymentName + } + { + name: 'AZURE_OPENAI_ENDPOINT' + value: azureOpenaiEndpoint + } + { + name: 'AZURE_OPENAI_EMBEDDING_DEPLOYMENT' + value: azureOpenaiEmbeddingDeployment + } + { + name: 'AZURE_SEARCH_KEY' + value: azureSearchKey + } + { + name: 'AZURE_SEARCH_ENDPOINT' + value: azureSearchEndpoint + } + ] + ftpsState: 'FtpsOnly' + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + botAadAppClientId: botAadAppClientId + 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 diff --git a/templates/copilot-rag-ai-search/infra/azure.parameters.json b/templates/copilot-rag-ai-search/infra/azure.parameters.json new file mode 100644 index 00000000..3b97c205 --- /dev/null +++ b/templates/copilot-rag-ai-search/infra/azure.parameters.json @@ -0,0 +1,42 @@ +{ + "$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": "${{BOT_ID}}" + }, + "botAadAppClientSecret": { + "value": "${{SECRET_BOT_PASSWORD}}" + }, + "azureOpenaiKey": { + "value": "${{SECRET_AZURE_OPENAI_API_KEY}}" + }, + "azureOpenaiModelDeploymentName" : { + "value": "${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}}" + }, + "azureOpenaiEndpoint" : { + "value": "${{AZURE_OPENAI_ENDPOINT}}" + }, + "azureOpenaiEmbeddingDeployment" : { + "value": "${{AZURE_OPENAI_EMBEDDING_DEPLOYMENT}}" + }, + "azureSearchKey": { + "value": "${{SECRET_AZURE_SEARCH_KEY}}" + }, + "azureSearchEndpoint": { + "value": "${{AZURE_SEARCH_ENDPOINT}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "AISearch-py" + }, + "linuxFxVersion": { + "value": "PYTHON|3.11" + } + } +} \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/infra/botRegistration/azurebot.bicep b/templates/copilot-rag-ai-search/infra/botRegistration/azurebot.bicep new file mode 100644 index 00000000..ab67c7a5 --- /dev/null +++ b/templates/copilot-rag-ai-search/infra/botRegistration/azurebot.bicep @@ -0,0 +1,37 @@ +@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 botAadAppClientId 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: botAadAppClientId + } + 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/templates/copilot-rag-ai-search/infra/botRegistration/readme.md b/templates/copilot-rag-ai-search/infra/botRegistration/readme.md new file mode 100644 index 00000000..d5416243 --- /dev/null +++ b/templates/copilot-rag-ai-search/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/templates/copilot-rag-ai-search/src/app.py b/templates/copilot-rag-ai-search/src/app.py new file mode 100644 index 00000000..910476d0 --- /dev/null +++ b/templates/copilot-rag-ai-search/src/app.py @@ -0,0 +1,30 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import asyncio +from http import HTTPStatus +from aiohttp import web +from botbuilder.core.integration import aiohttp_error_middleware + +from bot import bot_app + +routes = web.RouteTableDef() + +@routes.post("/api/messages") +async def on_messages(req: web.Request) -> web.Response: + res = await bot_app.process(req) + + if res is not None: + return res + + return web.Response(status=HTTPStatus.OK) + +app = web.Application(middlewares=[aiohttp_error_middleware]) +app.add_routes(routes) + +from config import Config + +if __name__ == "__main__": + web.run_app(app, host="localhost", port=Config.PORT) \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/src/azure_ai_search_data_source.py b/templates/copilot-rag-ai-search/src/azure_ai_search_data_source.py new file mode 100644 index 00000000..24c50940 --- /dev/null +++ b/templates/copilot-rag-ai-search/src/azure_ai_search_data_source.py @@ -0,0 +1,98 @@ +from dataclasses import dataclass +from typing import Optional, List +from azure.search.documents.indexes.models import _edm as EDM +from azure.search.documents.models import VectorQuery, VectorizedQuery +from teams.ai.embeddings import AzureOpenAIEmbeddings, AzureOpenAIEmbeddingsOptions +from teams.state.memory import Memory +from teams.state.state import TurnContext +from teams.ai.tokenizers import Tokenizer +from teams.ai.data_sources import DataSource + +from config import Config + +async def get_embedding_vector(text: str): + embeddings = AzureOpenAIEmbeddings(AzureOpenAIEmbeddingsOptions( + azure_api_key=Config.AZURE_OPENAI_API_KEY, + azure_endpoint=Config.AZURE_OPENAI_ENDPOINT, + azure_deployment=Config.AZURE_OPENAI_EMBEDDING_DEPLOYMENT + )) + + result = await embeddings.create_embeddings(text) + if (result.status != 'success' or not result.output): + raise Exception(f"Failed to generate embeddings for description: {text}") + + return result.output[0] + +@dataclass +class Doc: + docId: Optional[str] = None + docTitle: Optional[str] = None + description: Optional[str] = None + descriptionVector: Optional[List[float]] = None + +@dataclass +class AzureAISearchDataSourceOptions: + name: str + indexName: str + azureAISearchApiKey: str + azureAISearchEndpoint: str + +from azure.core.credentials import AzureKeyCredential +from azure.search.documents import SearchClient +import json + +@dataclass +class Result: + def __init__(self, output, length, too_long): + self.output = output + self.length = length + self.too_long = too_long + +class AzureAISearchDataSource(DataSource): + def __init__(self, options: AzureAISearchDataSourceOptions): + self.name = options.name + self.options = options + self.searchClient = SearchClient( + options.azureAISearchEndpoint, + options.indexName, + AzureKeyCredential(options.azureAISearchApiKey) + ) + + def name(self): + return self.name + + async def render_data(self, _context: TurnContext, memory: Memory, tokenizer: Tokenizer, maxTokens: int): + query = memory.get('temp.input') + embedding = await get_embedding_vector(query) + vector_query = VectorizedQuery(vector=embedding, k_nearest_neighbors=2, fields="descriptionVector") + + if not query: + return Result('', 0, False) + + selectedFields = [ + 'docTitle', + 'description', + 'descriptionVector', + ] + + searchResults = self.searchClient.search( + search_text=query, + select=selectedFields, + vector_queries=[vector_query], + ) + + if not searchResults: + return Result('', 0, False) + + usedTokens = 0 + doc = '' + for result in searchResults: + tokens = len(tokenizer.encode(json.dumps(result["description"]))) + + if usedTokens + tokens > maxTokens: + break + + doc += json.dumps(result["description"]) + usedTokens += tokens + + return Result(doc, usedTokens, usedTokens > maxTokens) \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/src/bot.py b/templates/copilot-rag-ai-search/src/bot.py new file mode 100644 index 00000000..80085b1c --- /dev/null +++ b/templates/copilot-rag-ai-search/src/bot.py @@ -0,0 +1,75 @@ +import asyncio +from dataclasses import dataclass +import json +import os +import sys +import traceback +from typing import Generic, TypeVar + +from botbuilder.core import MemoryStorage, TurnContext +from teams import Application, ApplicationOptions, TeamsAdapter +from teams.ai import AIOptions +from teams.ai.models import AzureOpenAIModelOptions, OpenAIModel, OpenAIModelOptions +from teams.ai.planners import ActionPlanner, ActionPlannerOptions +from teams.ai.prompts import PromptManager, PromptManagerOptions +from teams.ai.actions import ActionTypes +from teams.state import TurnState + +from azure_ai_search_data_source import AzureAISearchDataSource, AzureAISearchDataSourceOptions +from config import Config + +config = Config() + +# Create AI components +model: OpenAIModel + +model = OpenAIModel( + AzureOpenAIModelOptions( + api_key=config.AZURE_OPENAI_API_KEY, + default_model=config.AZURE_OPENAI_MODEL_DEPLOYMENT_NAME, + endpoint=config.AZURE_OPENAI_ENDPOINT, + ) +) + +prompts = PromptManager(PromptManagerOptions(prompts_folder=f"{os.getcwd()}/prompts")) + +prompts.add_data_source( + AzureAISearchDataSource( + AzureAISearchDataSourceOptions( + name='azure-ai-search', + indexName='contoso-electronics', + azureAISearchApiKey=config.AZURE_SEARCH_KEY, + azureAISearchEndpoint=config.AZURE_SEARCH_ENDPOINT, + ) + ) +) + +planner = ActionPlanner( + ActionPlannerOptions(model=model, prompts=prompts, default_prompt="chat") +) + +# Define storage and application +storage = MemoryStorage() +bot_app = Application[TurnState]( + ApplicationOptions( + bot_app_id=config.APP_ID, + storage=storage, + adapter=TeamsAdapter(config), + ai=AIOptions(planner=planner), + ) +) + +@bot_app.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState): + await context.send_activity("How can I help you today?") + +@bot_app.error +async def on_error(context: TurnContext, error: Exception): + # This check writes out errors to console log .vs. app insights. + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/src/config.py b/templates/copilot-rag-ai-search/src/config.py new file mode 100644 index 00000000..c6a6bf9f --- /dev/null +++ b/templates/copilot-rag-ai-search/src/config.py @@ -0,0 +1,24 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import os + +from dotenv import load_dotenv + +load_dotenv() + +class Config: + """Bot Configuration""" + + PORT = 3978 + APP_ID = os.environ.get("BOT_ID", "") + APP_PASSWORD = os.environ.get("BOT_PASSWORD", "") + AZURE_OPENAI_API_KEY = os.environ["AZURE_OPENAI_API_KEY"] # Azure OpenAI API key + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME = os.environ["AZURE_OPENAI_MODEL_DEPLOYMENT_NAME"] # Azure OpenAI model deployment name + AZURE_OPENAI_ENDPOINT = os.environ["AZURE_OPENAI_ENDPOINT"] # Azure OpenAI endpoint + AZURE_OPENAI_EMBEDDING_DEPLOYMENT = os.environ["AZURE_OPENAI_EMBEDDING_DEPLOYMENT"] # Azure OpenAI embedding deployment + AZURE_SEARCH_KEY = os.environ["AZURE_SEARCH_KEY"] # Azure Search key + AZURE_SEARCH_ENDPOINT = os.environ["AZURE_SEARCH_ENDPOINT"] # Azure Search endpoint + diff --git a/templates/copilot-rag-ai-search/src/indexers/data/Contoso_Electronics_Company_Overview.md b/templates/copilot-rag-ai-search/src/indexers/data/Contoso_Electronics_Company_Overview.md new file mode 100644 index 00000000..6878a8e2 --- /dev/null +++ b/templates/copilot-rag-ai-search/src/indexers/data/Contoso_Electronics_Company_Overview.md @@ -0,0 +1,48 @@ +# Contoso Electronics Company Overview + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## History + +Contoso Electronics, a pioneering force in the tech industry, was founded in 1985 by visionary entrepreneurs with a passion for innovation. Over the years, the company has played a pivotal role in shaping the landscape of consumer electronics. + +| Year | Milestone | +|------|-----------| +| 1985 | Company founded with a focus on cutting-edge technology | +| 1990 | Launched the first-ever handheld personal computer | +| 2000 | Introduced groundbreaking advancements in AI and robotics | +| 2015 | Expansion into sustainable and eco-friendly product lines | + +## Company Overview + +At Contoso Electronics, we take pride in fostering a dynamic and inclusive workplace. Our dedicated team of experts collaborates to create innovative solutions that empower and connect people globally. + +### Core Values + +- **Innovation:** Constantly pushing the boundaries of technology. +- **Diversity:** Embracing different perspectives for creative excellence. +- **Sustainability:** Committed to eco-friendly practices in our products. + +## Vacation Perks + +We believe in work-life balance and understand the importance of well-deserved breaks. Our vacation perks are designed to help our employees recharge and return with renewed enthusiasm. + +| Vacation Tier | Duration | Additional Benefits | +|---------------|----------|---------------------| +| Standard | 2 weeks | Health and wellness stipend | +| Senior | 4 weeks | Travel vouchers for a dream destination | +| Executive | 6 weeks | Luxury resort getaway with family | + +## Employee Recognition + +Recognizing the hard work and dedication of our employees is at the core of our culture. Here are some ways we celebrate achievements: + +- Monthly "Innovator of the Month" awards +- Annual gala with awards for outstanding contributions +- Team-building retreats for high-performing departments + +## Join Us! + +Contoso Electronics is always on the lookout for talented individuals who share our passion for innovation. If you're ready to be part of a dynamic team shaping the future of technology, check out our [careers page](http://www.contoso.com) for exciting opportunities. + +[Learn more about Contoso Electronics!](http://www.contoso.com) diff --git a/templates/copilot-rag-ai-search/src/indexers/data/Contoso_Electronics_PerkPlus_Program.md b/templates/copilot-rag-ai-search/src/indexers/data/Contoso_Electronics_PerkPlus_Program.md new file mode 100644 index 00000000..1d97d511 --- /dev/null +++ b/templates/copilot-rag-ai-search/src/indexers/data/Contoso_Electronics_PerkPlus_Program.md @@ -0,0 +1,36 @@ +# Contoso Electronics PerksPlus Program + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## Overview +Introducing PerksPlus - the ultimate benefits program designed to support the health and wellness of employees. With PerksPlus, employees have the opportunity to expense up to $1000 for fitness-related programs, making it easier and more affordable to maintain a healthy lifestyle. PerksPlus is not only designed to support employees' physical health, but also their mental health. Regular exercise has been shown to reduce stress, improve mood, and enhance overall well-being. With PerksPlus, employees can invest in their health and wellness, while enjoying the peace of mind that comes with knowing they are getting the support they need to lead a healthy life. +What is Covered? + +PerksPlus covers a wide range of fitness activities, including but not limited to: +* Gym memberships +* Personal training sessions +* Yoga and Pilates classes +* Fitness equipment purchases +* Sports team fees +* Health retreats and spas +* Outdoor adventure activities (such as rock climbing, hiking, and kayaking) +* Group fitness classes (such as dance, martial arts, and cycling) +* Virtual fitness programs (such as online yoga and workout classes) + +In addition to the wide range of fitness activities covered by PerksPlus, the program also covers a variety of lessons and experiences that promote health and wellness. Some of the lessons covered under PerksPlus include: +* Skiing and snowboarding lessons +* Scuba diving lessons +* Surfing lessons +* Horseback riding lessons + +These lessons provide employees with the opportunity to try new things, challenge themselves, and improve their physical skills. They are also a great way to relieve stress and have fun while staying active. + +With PerksPlus, employees can choose from a variety of fitness programs to suit their individual needs and preferences. Whether you're looking to improve your physical fitness, reduce stress, or just have some fun, PerksPlus has you covered. + +## What is Not Covered? +In addition to the wide range of activities covered by PerksPlus, there is also a list of things that are not +covered under the program. These include but are not limited to: +* Non-fitness related expenses +* Medical treatments and procedures +* Travel expenses (unless related to a fitness program) +* Food and supplements \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/src/indexers/data/Contoso_Electronics_Plan_Benefits.md b/templates/copilot-rag-ai-search/src/indexers/data/Contoso_Electronics_Plan_Benefits.md new file mode 100644 index 00000000..9da5c642 --- /dev/null +++ b/templates/copilot-rag-ai-search/src/indexers/data/Contoso_Electronics_Plan_Benefits.md @@ -0,0 +1,37 @@ +# Contoso Electronics Plan and Benefit Packages + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## Northwind Health Plus + +Northwind Health Plus is a comprehensive plan that provides comprehensive coverage for medical, vision, and dental services. This plan also offers prescription drug coverage, mental health and substance abuse coverage, and coverage for preventive care services. With Northwind Health Plus, you can choose from a variety of in-network providers, including primary care physicians, specialists, hospitals, and pharmacies. + +This plan also offers coverage for emergency services, both in-network and out-of-network. + +## Northwind Standard + +Northwind Standard is a basic plan that provides coverage for medical, vision, and dental services. This plan also offers coverage for preventive care services, as well as prescription drug coverage. With Northwind Standard, you can choose from a variety of in-network providers, including primary care physicians, specialists, hospitals, and pharmacies. This plan does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services. + +## Comparison of Plans + +Both plans offer coverage for routine physicals, well-child visits, immunizations, and other preventive care services. The plans also cover preventive care services such as mammograms, colonoscopies, and other cancer screenings. + +Northwind Health Plus offers more comprehensive coverage than Northwind Standard. This plan offers coverage for emergency services, both in-network and out-of-network, as well as mental health and substance abuse coverage. Northwind Standard does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services. + +Both plans offer coverage for prescription drugs. Northwind Health Plus offers a wider range of prescription drug coverage than Northwind Standard. Northwind Health Plus covers generic, brand-name, and specialty drugs, while Northwind Standard only covers generic and brand-name drugs. + +Both plans offer coverage for vision and dental services. Northwind Health Plus offers coverage for vision exams, glasses, and contact lenses, as well as dental exams, cleanings, and fillings. Northwind Standard only offers coverage for vision exams and glasses. + +Both plans offer coverage for medical services. Northwind Health Plus offers coverage for hospital stays, doctor visits, lab tests, and X-rays. Northwind Standard only offers coverage for doctor visits and lab tests. + +Northwind Health Plus is a comprehensive plan that offers more coverage than Northwind Standard. Northwind Health Plus offers coverage for emergency services, mental health and substance abuse coverage, and out-of-network services, while Northwind Standard does not. Northwind Health Plus also offers a wider range of prescription drug coverage than Northwind Standard. Both plans offer coverage for vision and dental services, as well as medical services. + +## Cost Comparison + +Contoso Electronics deducts the employee's portion of the healthcare cost from each paycheck. This means that the cost of the health insurance will be spread out over the course of the year, rather than being paid in one lump sum. The employee's portion of the cost will be calculated based on the selected health plan and the number of people covered by the insurance. The table below shows a cost comparison between the different health plans offered by Contoso Electronics + +| | Northwind Standard | NorthWind Health Plus | +|---------------|----------|---------------------| +| Employee Only | $45.00 | $55.00 | +| Employee +1 | $65.00 | $71.00 | +| Employee +2 or more | $78.00 | $89.00 | \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/src/indexers/delete.py b/templates/copilot-rag-ai-search/src/indexers/delete.py new file mode 100644 index 00000000..10a6ce8e --- /dev/null +++ b/templates/copilot-rag-ai-search/src/indexers/delete.py @@ -0,0 +1,19 @@ +import os +from azure.core.credentials import AzureKeyCredential +from azure.search.documents.indexes import SearchIndexClient + +from dotenv import load_dotenv + +load_dotenv(f'{os.getcwd()}/env/.env.local.user') + +def delete_index(client: SearchIndexClient, name: str): + client.delete_index(name) + print(f"Index {name} deleted") + +index = 'contoso-electronics' +search_api_key = os.getenv('SECRET_AZURE_SEARCH_KEY') +search_api_endpoint = os.getenv('AZURE_SEARCH_ENDPOINT') +credentials = AzureKeyCredential(search_api_key) + +search_index_client = SearchIndexClient(search_api_endpoint, credentials) +delete_index(search_index_client, index) \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/src/indexers/get_data.py b/templates/copilot-rag-ai-search/src/indexers/get_data.py new file mode 100644 index 00000000..a55656e7 --- /dev/null +++ b/templates/copilot-rag-ai-search/src/indexers/get_data.py @@ -0,0 +1,41 @@ +import os + +async def get_doc_data(embeddings): + with open(f'{os.getcwd()}/src/indexers/data/Contoso_Electronics_PerkPlus_Program.md', 'r') as file: + raw_description1 = file.read() + doc1 = { + "docId": "1", + "docTitle": "Contoso_Electronics_PerkPlus_Program", + "description": raw_description1, + "descriptionVector": await get_embedding_vector(raw_description1, embeddings=embeddings), + } + + with open(f'{os.getcwd()}/src/indexers/data/Contoso_Electronics_Company_Overview.md', 'r') as file: + raw_description2 = file.read() + doc2 = { + "docId": "2", + "docTitle": "Contoso_Electronics_Company_Overview", + "description": raw_description2, + "descriptionVector": await get_embedding_vector(raw_description2, embeddings=embeddings), + } + + with open(f'{os.getcwd()}/src/indexers/data/Contoso_Electronics_Plan_Benefits.md', 'r') as file: + raw_description3 = file.read() + doc3 = { + "docId": "3", + "docTitle": "Contoso_Electronics_Plan_Benefits", + "description": raw_description3, + "descriptionVector": await get_embedding_vector(raw_description3, embeddings=embeddings), + } + + return [doc1, doc2, doc3] + + +async def get_embedding_vector(text: str, embeddings): + result = await embeddings.create_embeddings(text) + if (result.status != 'success' or not result.output): + if result.status == 'error': + raise Exception(f"Failed to generate embeddings for description: <{text[:200]+'...'}>\n\nError: {result.output}") + raise Exception(f"Failed to generate embeddings for description: <{text[:200]+'...'}>") + + return result.output[0] \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/src/indexers/setup.py b/templates/copilot-rag-ai-search/src/indexers/setup.py new file mode 100644 index 00000000..c95417b6 --- /dev/null +++ b/templates/copilot-rag-ai-search/src/indexers/setup.py @@ -0,0 +1,85 @@ +import asyncio +import os +from dataclasses import dataclass +from typing import List, Optional + +from azure.core.credentials import AzureKeyCredential +from azure.search.documents import SearchClient +from azure.search.documents.indexes import SearchIndexClient +from azure.search.documents.indexes.models import ( + SearchIndex, + SimpleField, + SearchableField, + SearchField, + SearchFieldDataType, + ComplexField, + CorsOptions, + VectorSearch, + VectorSearchProfile, + HnswAlgorithmConfiguration +) +from teams.ai.embeddings import AzureOpenAIEmbeddings, AzureOpenAIEmbeddingsOptions + +from get_data import get_doc_data + +from dotenv import load_dotenv + +load_dotenv(f'{os.getcwd()}/env/.env.local.user') + +@dataclass +class Doc: + docId: Optional[str] = None + docTitle: Optional[str] = None + description: Optional[str] = None + descriptionVector: Optional[List[float]] = None + +async def upsert_documents(client: SearchClient, documents: list[Doc]): + return client.merge_or_upload_documents(documents) + +async def create_index_if_not_exists(client: SearchIndexClient, name: str): + doc_index = SearchIndex( + name=name, + fields = [ + SimpleField(name="docId", type=SearchFieldDataType.String, key=True), + SimpleField(name="docTitle", type=SearchFieldDataType.String), + SearchableField(name="description", type=SearchFieldDataType.String, searchable=True), + SearchField(name="descriptionVector", type=SearchFieldDataType.Collection(SearchFieldDataType.Single), searchable=True, vector_search_dimensions=1536, vector_search_profile_name='my-vector-config'), + ], + scoring_profiles=[], + cors_options=CorsOptions(allowed_origins=["*"]), + vector_search = VectorSearch( + profiles=[VectorSearchProfile(name="my-vector-config", algorithm_configuration_name="my-algorithms-config")], + algorithms=[HnswAlgorithmConfiguration(name="my-algorithms-config")], + ) + ) + + client.create_or_update_index(doc_index) + +async def setup(search_api_key, search_api_endpoint): + index = 'contoso-electronics' + + credentials = AzureKeyCredential(search_api_key) + + search_index_client = SearchIndexClient(search_api_endpoint, credentials) + await create_index_if_not_exists(search_index_client, index) + + print("Create index succeeded. If it does not exist, wait for 5 seconds...") + await asyncio.sleep(5) + + search_client = SearchClient(search_api_endpoint, index, credentials) + + embeddings = AzureOpenAIEmbeddings(AzureOpenAIEmbeddingsOptions( + azure_api_key=os.getenv('SECRET_AZURE_OPENAI_API_KEY'), + azure_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT'), + azure_deployment=os.getenv('AZURE_OPENAI_EMBEDDING_DEPLOYMENT') + )) + data = await get_doc_data(embeddings=embeddings) + await upsert_documents(search_client, data) + + print("Upload new documents succeeded. If they do not exist, wait for several seconds...") + +search_api_key = os.getenv('SECRET_AZURE_SEARCH_KEY') +search_api_endpoint = os.getenv('AZURE_SEARCH_ENDPOINT') +asyncio.run(setup(search_api_key, search_api_endpoint)) +print("setup finished") + diff --git a/templates/copilot-rag-ai-search/src/prompts/chat/config.json b/templates/copilot-rag-ai-search/src/prompts/chat/config.json new file mode 100644 index 00000000..1f3e7a7e --- /dev/null +++ b/templates/copilot-rag-ai-search/src/prompts/chat/config.json @@ -0,0 +1,23 @@ +{ + "schema": 1.1, + "description": "Chat with Teams RAG", + "type": "completion", + "completion": { + "completion_type": "chat", + "include_history": true, + "include_input": true, + "max_input_tokens": 4096, + "max_tokens": 1000, + "temperature": 0.9, + "top_p": 0.0, + "presence_penalty": 0.6, + "frequency_penalty": 0.0, + "stop_sequences": [] + }, + "augmentation": { + "augmentation_type": "none", + "data_sources": { + "azure-ai-search": 2500 + } + } +} \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/src/prompts/chat/skprompt.txt b/templates/copilot-rag-ai-search/src/prompts/chat/skprompt.txt new file mode 100644 index 00000000..789155ad --- /dev/null +++ b/templates/copilot-rag-ai-search/src/prompts/chat/skprompt.txt @@ -0,0 +1,3 @@ +The following is a conversation with an AI assistant, who is an expert on answering questions over the given context. +Responses should be in a short journalistic style with no more than 80 words. +Use the context provided in the `` tags as the source for your answers. \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/src/requirements.txt b/templates/copilot-rag-ai-search/src/requirements.txt new file mode 100644 index 00000000..a3d79814 --- /dev/null +++ b/templates/copilot-rag-ai-search/src/requirements.txt @@ -0,0 +1,5 @@ +python-dotenv +aiohttp +azure-search +azure-search-documents +teams-ai~=1.2.0 \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/teamsapp.local.yml b/templates/copilot-rag-ai-search/teamsapp.local.yml new file mode 100644 index 00000000..efb61595 --- /dev/null +++ b/templates/copilot-rag-ai-search/teamsapp.local.yml @@ -0,0 +1,82 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: copilot-rag-ai-search${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: copilot-rag-ai-search${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: copilot-rag-ai-search + 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 + # 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 + # Teams 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 + +deploy: + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} + AZURE_OPENAI_EMBEDDING_DEPLOYMENT: ${{AZURE_OPENAI_EMBEDDING_DEPLOYMENT}} + AZURE_SEARCH_KEY: ${{SECRET_AZURE_SEARCH_KEY}} + AZURE_SEARCH_ENDPOINT: ${{AZURE_SEARCH_ENDPOINT}} diff --git a/templates/copilot-rag-ai-search/teamsapp.testtool.yml b/templates/copilot-rag-ai-search/teamsapp.testtool.yml new file mode 100644 index 00000000..f7e564a7 --- /dev/null +++ b/templates/copilot-rag-ai-search/teamsapp.testtool.yml @@ -0,0 +1,27 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.1 + symlinkDir: ./devTools/teamsapptester + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} + BOT_ID: "" + BOT_PASSWORD: "" + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} + AZURE_OPENAI_EMBEDDING_DEPLOYMENT: ${{AZURE_OPENAI_EMBEDDING_DEPLOYMENT}} + AZURE_SEARCH_KEY: ${{SECRET_AZURE_SEARCH_KEY}} + AZURE_SEARCH_ENDPOINT: ${{AZURE_SEARCH_ENDPOINT}} \ No newline at end of file diff --git a/templates/copilot-rag-ai-search/teamsapp.yml b/templates/copilot-rag-ai-search/teamsapp.yml new file mode 100644 index 00000000..b27b9e38 --- /dev/null +++ b/templates/copilot-rag-ai-search/teamsapp.yml @@ -0,0 +1,137 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsfx provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: copilot-rag-ai-search${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: copilot-rag-ai-search${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + - 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-tab + # Teams 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 + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # 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 + # Teams 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 'teamsfx deploy' is executed +deploy: + # 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: ./src + # Ignore file location, leave blank will ignore nothing + ignoreFile: .webappignore + # 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}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: 30623828-6647-49e6-a040-8953665a1218 diff --git a/templates/copilot-rag-customize/.gitignore b/templates/copilot-rag-customize/.gitignore new file mode 100644 index 00000000..75baccc4 --- /dev/null +++ b/templates/copilot-rag-customize/.gitignore @@ -0,0 +1,18 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +env/.env.testtool +.env +appPackage/build + +# python virtual environment +.venv/ +__pycache__/ + +# others +.deployment/ +node_modules/ +devTools/*.log + +# Dev tool directories +/devTools/ \ No newline at end of file diff --git a/templates/copilot-rag-customize/.vscode/extensions.json b/templates/copilot-rag-customize/.vscode/extensions.json new file mode 100644 index 00000000..760a0b1d --- /dev/null +++ b/templates/copilot-rag-customize/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension", + "ms-python.python" + ] +} \ No newline at end of file diff --git a/templates/copilot-rag-customize/.vscode/launch.json b/templates/copilot-rag-customize/.vscode/launch.json new file mode 100644 index 00000000..a0c18a24 --- /dev/null +++ b/templates/copilot-rag-customize/.vscode/launch.json @@ -0,0 +1,130 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Desktop)", + "type": "node", + "request": "launch", + "preLaunchTask": "Start Teams App in Desktop Client (Remote)", + "presentation": { + "group": "3-remote", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Start Python", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/src/app.py", + "cwd": "${workspaceFolder}/src", + "console": "integratedTerminal" + }, + { + "name": "Start Test Tool", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/devTools/teamsapptester/node_modules/@microsoft/teams-app-test-tool/cli.js", + "args": [ + "start" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": ["Launch App (Edge)", "Start Python"], + "cascadeTerminateToConfigurations": ["Start Python"], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "1-local", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": ["Launch App (Chrome)", "Start Python"], + "cascadeTerminateToConfigurations": ["Start Python"], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "1-local", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Desktop)", + "configurations": ["Start Python"], + "preLaunchTask": "Start Teams App in Desktop Client", + "presentation": { + "group": "1-local", + "order": 3 + }, + "stopAll": true + }, + { + "name": "Debug in Test Tool", + "configurations": [ + "Start Python", + "Start Test Tool" + ], + "cascadeTerminateToConfigurations": [ + "Start Test Tool" + ], + "preLaunchTask": "Deploy (Test Tool)", + "presentation": { + "group": "2-local", + "order": 1 + }, + "stopAll": true + } + ] +} diff --git a/templates/copilot-rag-customize/.vscode/settings.json b/templates/copilot-rag-customize/.vscode/settings.json new file mode 100644 index 00000000..0d3ba10b --- /dev/null +++ b/templates/copilot-rag-customize/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ] +} \ No newline at end of file diff --git a/templates/copilot-rag-customize/.vscode/tasks.json b/templates/copilot-rag-customize/.vscode/tasks.json new file mode 100644 index 00000000..a964abf8 --- /dev/null +++ b/templates/copilot-rag-customize/.vscode/tasks.json @@ -0,0 +1,135 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Check if Node.js is installed and the version is >= 12. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 56150, // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)" + ], + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool", + } + }, + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978 // app service port + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start Teams App in Desktop Client", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start desktop client" + ], + "dependsOrder": "sequence" + }, + { + "label": "Start desktop client", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true" + } + }, + { + "label": "Start Teams App in Desktop Client (Remote)", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true" + } + } + ] +} \ No newline at end of file diff --git a/templates/copilot-rag-customize/.webappignore b/templates/copilot-rag-customize/.webappignore new file mode 100644 index 00000000..0a7d5f28 --- /dev/null +++ b/templates/copilot-rag-customize/.webappignore @@ -0,0 +1,13 @@ +.venv/ +.vscode/ +appPackage/ +devTools/ +infra/ +.env +env/ +__pycache__/ +README.md +teamsapp.yml +teamsapp.local.yml +teamsapp.testtool.yml +.gitignore diff --git a/templates/copilot-rag-customize/README.md b/templates/copilot-rag-customize/README.md new file mode 100644 index 00000000..ebbceba1 --- /dev/null +++ b/templates/copilot-rag-customize/README.md @@ -0,0 +1,81 @@ +# Overview of the Chat With Your Data (Custom Data Source) template + +This app template showcases how to build one of the most powerful applications enabled by LLM - sophisticated question-answering (Q&A) chat bots that can answer questions about specific source information right in the Microsoft Teams. +This app template also demonstrates usage of techniques like: +- [Retrieval Augmented Generation](https://python.langchain.com/docs/use_cases/question_answering/#what-is-rag), or RAG. +- [Teams AI Library](https://learn.microsoft.com/microsoftteams/platform/bots/how-to/teams%20conversational%20ai/teams-conversation-ai-overview) + +## Get started with the template + +> **Prerequisites** +> +> To run the template in your local dev machine, you will need: +> +> - [Python](https://www.python.org/), version 3.8 to 3.11. +> - [Python extension](https://code.visualstudio.com/docs/languages/python), version v2024.0.1 or higher. +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) latest version or [Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli). +> - An account with [Azure OpenAI](https://aka.ms/oai/access). +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). + +### Configurations +1. Open the command box and enter `Python: Create Environment` to create and activate your desired virtual environment. Remember to select `src/requirements.txt` as dependencies to install when creating the virtual environment. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY`, deployment name `AZURE_OPENAI_MODEL_DEPLOYMENT_NAME` and endpoint `AZURE_OPENAI_ENDPOINT`. + +### Conversation with bot +1. Select the Teams Toolkit icon on the left in the VS Code toolbar. +1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. +1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. +1. You will receive a welcome message from the bot, or send any message to get a response. + +**Congratulations**! You are running an application that can now interact with users in Teams: + +> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging). + +![alt text](https://github.com/OfficeDev/TeamsFx/assets/109947924/d4f9b455-dbb0-4e14-8557-59f9be5c1200) + +## What's included in the template + +| Folder | Contents | +| - | - | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| - | - | +|`src/bot.py`| Handles business logics for the Basic RAG Bot.| +|`src/config.py`| Defines the environment variables.| +|`src/app.py`| Main module of the Basic RAG Bot, hosts a aiohttp api server for the app.| +|`src/my_data_source.py`| Handles local customized text data search logics.| +|`src/data/*.md`| Raw text data source.| +|`src/prompts/chat/skprompt.txt`| Defines the prompt.| +|`src/prompts/chat/config.json`| Configures the prompt.| + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| - | - | +|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +|`teamsapp.testtool.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool.| + +## Extend the template + +- Follow [Build a Basic AI Chatbot in Teams](https://aka.ms/teamsfx-basic-ai-chatbot) to extend the template with more AI capabilities. +- Understand more about [build your own data ingestion](https://aka.ms/teamsfx-rag-bot#build-your-own-data-ingestion). + +## Additional information and references + +- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) + +## Known issue +- If you use `Debug in Test Tool` to local debug, you might get an error `InternalServiceError: connect ECONNREFUSED 127.0.0.1:3978` in Test Tool console log or error message `Error: Cannot connect to your app, +please make sure your app is running or restart your app` in log panel of Test Tool web page. You can wait for Python launch console ready and then refresh the front end web page. +- When you use `Launch Remote in Teams` to remote debug after deployment, you might loose interaction with your bot. This is because the remote service needs to restart. Please wait for several minutes to retry it. \ No newline at end of file diff --git a/templates/copilot-rag-customize/appPackage/color.png b/templates/copilot-rag-customize/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/copilot-rag-customize/appPackage/color.png differ diff --git a/templates/copilot-rag-customize/appPackage/manifest.json b/templates/copilot-rag-customize/appPackage/manifest.json new file mode 100644 index 00000000..34958259 --- /dev/null +++ b/templates/copilot-rag-customize/appPackage/manifest.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "copilot-rag-customize${{APP_NAME_SUFFIX}}", + "full": "full name for copilot-rag-customize" + }, + "description": { + "short": "short description for copilot-rag-customize", + "full": "full description for copilot-rag-customize" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} diff --git a/templates/copilot-rag-customize/appPackage/outline.png b/templates/copilot-rag-customize/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/copilot-rag-customize/appPackage/outline.png differ diff --git a/templates/copilot-rag-customize/env/.env.dev b/templates/copilot-rag-customize/env/.env.dev new file mode 100644 index 00000000..8172044c --- /dev/null +++ b/templates/copilot-rag-customize/env/.env.dev @@ -0,0 +1,17 @@ +# 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= +TEAMS_APP_TENANT_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= +BOT_DOMAIN= \ No newline at end of file diff --git a/templates/copilot-rag-customize/infra/azure.bicep b/templates/copilot-rag-customize/infra/azure.bicep new file mode 100644 index 00000000..424a59e6 --- /dev/null +++ b/templates/copilot-rag-customize/infra/azure.bicep @@ -0,0 +1,102 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@description('Required when create Azure Bot service') +param botAadAppClientId string + +@secure() +@description('Required by Bot Framework package in your bot project') +param botAadAppClientSecret string + +@secure() +@description('Required in your bot project to access Azure OpenAI service. You can get it from Azure Portal > OpenAI > Keys > Key1 > Resource Management > Endpoint') +param azureOpenaiKey string +param azureOpenaiModelDeploymentName string +param azureOpenaiEndpoint string + +param webAppSKU string +param linuxFxVersion string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param location string = resourceGroup().location +param pythonVersion string = linuxFxVersion + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app,linux' + location: location + name: serverfarmsName + sku: { + name: webAppSKU + } + properties:{ + reserved: true + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app,linux' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + siteConfig: { + alwaysOn: true + appCommandLine: 'gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:app' + linuxFxVersion: pythonVersion + appSettings: [ + { + name: 'WEBSITES_CONTAINER_START_TIME_LIMIT' + value: '600' + } + { + name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' + value: 'true' + } + { + name: 'BOT_ID' + value: botAadAppClientId + } + { + name: 'BOT_PASSWORD' + value: botAadAppClientSecret + } + { + name: 'AZURE_OPENAI_API_KEY' + value: azureOpenaiKey + } + { + name: 'AZURE_OPENAI_MODEL_DEPLOYMENT_NAME' + value: azureOpenaiModelDeploymentName + } + { + name: 'AZURE_OPENAI_ENDPOINT' + value: azureOpenaiEndpoint + } + ] + ftpsState: 'FtpsOnly' + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + botAadAppClientId: botAadAppClientId + 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 diff --git a/templates/copilot-rag-customize/infra/azure.parameters.json b/templates/copilot-rag-customize/infra/azure.parameters.json new file mode 100644 index 00000000..2529cb47 --- /dev/null +++ b/templates/copilot-rag-customize/infra/azure.parameters.json @@ -0,0 +1,33 @@ +{ + "$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": "${{BOT_ID}}" + }, + "botAadAppClientSecret": { + "value": "${{SECRET_BOT_PASSWORD}}" + }, + "azureOpenaiKey": { + "value": "${{SECRET_AZURE_OPENAI_API_KEY}}" + }, + "azureOpenaiModelDeploymentName" : { + "value": "${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}}" + }, + "azureOpenaiEndpoint" : { + "value": "${{AZURE_OPENAI_ENDPOINT}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "copilot-rag-customize" + }, + "linuxFxVersion": { + "value": "PYTHON|3.11" + } + } +} \ No newline at end of file diff --git a/templates/copilot-rag-customize/infra/botRegistration/azurebot.bicep b/templates/copilot-rag-customize/infra/botRegistration/azurebot.bicep new file mode 100644 index 00000000..ab67c7a5 --- /dev/null +++ b/templates/copilot-rag-customize/infra/botRegistration/azurebot.bicep @@ -0,0 +1,37 @@ +@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 botAadAppClientId 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: botAadAppClientId + } + 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/templates/copilot-rag-customize/infra/botRegistration/readme.md b/templates/copilot-rag-customize/infra/botRegistration/readme.md new file mode 100644 index 00000000..d5416243 --- /dev/null +++ b/templates/copilot-rag-customize/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/templates/copilot-rag-customize/src/app.py b/templates/copilot-rag-customize/src/app.py new file mode 100644 index 00000000..910476d0 --- /dev/null +++ b/templates/copilot-rag-customize/src/app.py @@ -0,0 +1,30 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import asyncio +from http import HTTPStatus +from aiohttp import web +from botbuilder.core.integration import aiohttp_error_middleware + +from bot import bot_app + +routes = web.RouteTableDef() + +@routes.post("/api/messages") +async def on_messages(req: web.Request) -> web.Response: + res = await bot_app.process(req) + + if res is not None: + return res + + return web.Response(status=HTTPStatus.OK) + +app = web.Application(middlewares=[aiohttp_error_middleware]) +app.add_routes(routes) + +from config import Config + +if __name__ == "__main__": + web.run_app(app, host="localhost", port=Config.PORT) \ No newline at end of file diff --git a/templates/copilot-rag-customize/src/bot.py b/templates/copilot-rag-customize/src/bot.py new file mode 100644 index 00000000..6d8893e5 --- /dev/null +++ b/templates/copilot-rag-customize/src/bot.py @@ -0,0 +1,63 @@ +import os +import sys +import traceback + +from botbuilder.core import MemoryStorage, TurnContext +from teams import Application, ApplicationOptions, TeamsAdapter +from teams.ai import AIOptions +from teams.ai.models import AzureOpenAIModelOptions, OpenAIModel, OpenAIModelOptions +from teams.ai.planners import ActionPlanner, ActionPlannerOptions +from teams.ai.prompts import PromptManager, PromptManagerOptions +from teams.state import TurnState + +from my_data_source import MyDataSource + +from config import Config + +config = Config() + +# Create AI components +model: OpenAIModel + +model = OpenAIModel( + AzureOpenAIModelOptions( + api_key=config.AZURE_OPENAI_API_KEY, + default_model=config.AZURE_OPENAI_MODEL_DEPLOYMENT_NAME, + endpoint=config.AZURE_OPENAI_ENDPOINT, + ) +) + +prompts = PromptManager(PromptManagerOptions(prompts_folder=f"{os.getcwd()}/prompts")) + +my_data_source = MyDataSource('local-search') +prompts.add_data_source(my_data_source) + +planner = ActionPlanner( + ActionPlannerOptions(model=model, prompts=prompts, default_prompt="chat") +) + +# Define storage and application +storage = MemoryStorage() +bot_app = Application[TurnState]( + ApplicationOptions( + bot_app_id=config.APP_ID, + storage=storage, + adapter=TeamsAdapter(config), + ai=AIOptions(planner=planner), + ) +) + +@bot_app.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState): + await context.send_activity("How can I help you today?") + +@bot_app.error +async def on_error(context: TurnContext, error: Exception): + # This check writes out errors to console log .vs. app insights. + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") \ No newline at end of file diff --git a/templates/copilot-rag-customize/src/config.py b/templates/copilot-rag-customize/src/config.py new file mode 100644 index 00000000..135d4e58 --- /dev/null +++ b/templates/copilot-rag-customize/src/config.py @@ -0,0 +1,20 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import os + +from dotenv import load_dotenv + +load_dotenv() + +class Config: + """Bot Configuration""" + + PORT = 3978 + APP_ID = os.environ.get("BOT_ID", "") + APP_PASSWORD = os.environ.get("BOT_PASSWORD", "") + AZURE_OPENAI_API_KEY = os.environ["AZURE_OPENAI_API_KEY"] # Azure OpenAI API key + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME = os.environ["AZURE_OPENAI_MODEL_DEPLOYMENT_NAME"] # Azure OpenAI model deployment name + AZURE_OPENAI_ENDPOINT = os.environ["AZURE_OPENAI_ENDPOINT"] # Azure OpenAI endpoint diff --git a/templates/copilot-rag-customize/src/data/Contoso_Electronics_Company_Overview.md b/templates/copilot-rag-customize/src/data/Contoso_Electronics_Company_Overview.md new file mode 100644 index 00000000..6878a8e2 --- /dev/null +++ b/templates/copilot-rag-customize/src/data/Contoso_Electronics_Company_Overview.md @@ -0,0 +1,48 @@ +# Contoso Electronics Company Overview + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## History + +Contoso Electronics, a pioneering force in the tech industry, was founded in 1985 by visionary entrepreneurs with a passion for innovation. Over the years, the company has played a pivotal role in shaping the landscape of consumer electronics. + +| Year | Milestone | +|------|-----------| +| 1985 | Company founded with a focus on cutting-edge technology | +| 1990 | Launched the first-ever handheld personal computer | +| 2000 | Introduced groundbreaking advancements in AI and robotics | +| 2015 | Expansion into sustainable and eco-friendly product lines | + +## Company Overview + +At Contoso Electronics, we take pride in fostering a dynamic and inclusive workplace. Our dedicated team of experts collaborates to create innovative solutions that empower and connect people globally. + +### Core Values + +- **Innovation:** Constantly pushing the boundaries of technology. +- **Diversity:** Embracing different perspectives for creative excellence. +- **Sustainability:** Committed to eco-friendly practices in our products. + +## Vacation Perks + +We believe in work-life balance and understand the importance of well-deserved breaks. Our vacation perks are designed to help our employees recharge and return with renewed enthusiasm. + +| Vacation Tier | Duration | Additional Benefits | +|---------------|----------|---------------------| +| Standard | 2 weeks | Health and wellness stipend | +| Senior | 4 weeks | Travel vouchers for a dream destination | +| Executive | 6 weeks | Luxury resort getaway with family | + +## Employee Recognition + +Recognizing the hard work and dedication of our employees is at the core of our culture. Here are some ways we celebrate achievements: + +- Monthly "Innovator of the Month" awards +- Annual gala with awards for outstanding contributions +- Team-building retreats for high-performing departments + +## Join Us! + +Contoso Electronics is always on the lookout for talented individuals who share our passion for innovation. If you're ready to be part of a dynamic team shaping the future of technology, check out our [careers page](http://www.contoso.com) for exciting opportunities. + +[Learn more about Contoso Electronics!](http://www.contoso.com) diff --git a/templates/copilot-rag-customize/src/data/Contoso_Electronics_PerksPlus_Program.md b/templates/copilot-rag-customize/src/data/Contoso_Electronics_PerksPlus_Program.md new file mode 100644 index 00000000..1d97d511 --- /dev/null +++ b/templates/copilot-rag-customize/src/data/Contoso_Electronics_PerksPlus_Program.md @@ -0,0 +1,36 @@ +# Contoso Electronics PerksPlus Program + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## Overview +Introducing PerksPlus - the ultimate benefits program designed to support the health and wellness of employees. With PerksPlus, employees have the opportunity to expense up to $1000 for fitness-related programs, making it easier and more affordable to maintain a healthy lifestyle. PerksPlus is not only designed to support employees' physical health, but also their mental health. Regular exercise has been shown to reduce stress, improve mood, and enhance overall well-being. With PerksPlus, employees can invest in their health and wellness, while enjoying the peace of mind that comes with knowing they are getting the support they need to lead a healthy life. +What is Covered? + +PerksPlus covers a wide range of fitness activities, including but not limited to: +* Gym memberships +* Personal training sessions +* Yoga and Pilates classes +* Fitness equipment purchases +* Sports team fees +* Health retreats and spas +* Outdoor adventure activities (such as rock climbing, hiking, and kayaking) +* Group fitness classes (such as dance, martial arts, and cycling) +* Virtual fitness programs (such as online yoga and workout classes) + +In addition to the wide range of fitness activities covered by PerksPlus, the program also covers a variety of lessons and experiences that promote health and wellness. Some of the lessons covered under PerksPlus include: +* Skiing and snowboarding lessons +* Scuba diving lessons +* Surfing lessons +* Horseback riding lessons + +These lessons provide employees with the opportunity to try new things, challenge themselves, and improve their physical skills. They are also a great way to relieve stress and have fun while staying active. + +With PerksPlus, employees can choose from a variety of fitness programs to suit their individual needs and preferences. Whether you're looking to improve your physical fitness, reduce stress, or just have some fun, PerksPlus has you covered. + +## What is Not Covered? +In addition to the wide range of activities covered by PerksPlus, there is also a list of things that are not +covered under the program. These include but are not limited to: +* Non-fitness related expenses +* Medical treatments and procedures +* Travel expenses (unless related to a fitness program) +* Food and supplements \ No newline at end of file diff --git a/templates/copilot-rag-customize/src/data/Contoso_Electronics_Plan_Benefits.md b/templates/copilot-rag-customize/src/data/Contoso_Electronics_Plan_Benefits.md new file mode 100644 index 00000000..9da5c642 --- /dev/null +++ b/templates/copilot-rag-customize/src/data/Contoso_Electronics_Plan_Benefits.md @@ -0,0 +1,37 @@ +# Contoso Electronics Plan and Benefit Packages + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## Northwind Health Plus + +Northwind Health Plus is a comprehensive plan that provides comprehensive coverage for medical, vision, and dental services. This plan also offers prescription drug coverage, mental health and substance abuse coverage, and coverage for preventive care services. With Northwind Health Plus, you can choose from a variety of in-network providers, including primary care physicians, specialists, hospitals, and pharmacies. + +This plan also offers coverage for emergency services, both in-network and out-of-network. + +## Northwind Standard + +Northwind Standard is a basic plan that provides coverage for medical, vision, and dental services. This plan also offers coverage for preventive care services, as well as prescription drug coverage. With Northwind Standard, you can choose from a variety of in-network providers, including primary care physicians, specialists, hospitals, and pharmacies. This plan does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services. + +## Comparison of Plans + +Both plans offer coverage for routine physicals, well-child visits, immunizations, and other preventive care services. The plans also cover preventive care services such as mammograms, colonoscopies, and other cancer screenings. + +Northwind Health Plus offers more comprehensive coverage than Northwind Standard. This plan offers coverage for emergency services, both in-network and out-of-network, as well as mental health and substance abuse coverage. Northwind Standard does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services. + +Both plans offer coverage for prescription drugs. Northwind Health Plus offers a wider range of prescription drug coverage than Northwind Standard. Northwind Health Plus covers generic, brand-name, and specialty drugs, while Northwind Standard only covers generic and brand-name drugs. + +Both plans offer coverage for vision and dental services. Northwind Health Plus offers coverage for vision exams, glasses, and contact lenses, as well as dental exams, cleanings, and fillings. Northwind Standard only offers coverage for vision exams and glasses. + +Both plans offer coverage for medical services. Northwind Health Plus offers coverage for hospital stays, doctor visits, lab tests, and X-rays. Northwind Standard only offers coverage for doctor visits and lab tests. + +Northwind Health Plus is a comprehensive plan that offers more coverage than Northwind Standard. Northwind Health Plus offers coverage for emergency services, mental health and substance abuse coverage, and out-of-network services, while Northwind Standard does not. Northwind Health Plus also offers a wider range of prescription drug coverage than Northwind Standard. Both plans offer coverage for vision and dental services, as well as medical services. + +## Cost Comparison + +Contoso Electronics deducts the employee's portion of the healthcare cost from each paycheck. This means that the cost of the health insurance will be spread out over the course of the year, rather than being paid in one lump sum. The employee's portion of the cost will be calculated based on the selected health plan and the number of people covered by the insurance. The table below shows a cost comparison between the different health plans offered by Contoso Electronics + +| | Northwind Standard | NorthWind Health Plus | +|---------------|----------|---------------------| +| Employee Only | $45.00 | $55.00 | +| Employee +1 | $65.00 | $71.00 | +| Employee +2 or more | $78.00 | $89.00 | \ No newline at end of file diff --git a/templates/copilot-rag-customize/src/my_data_source.py b/templates/copilot-rag-customize/src/my_data_source.py new file mode 100644 index 00000000..30ce7687 --- /dev/null +++ b/templates/copilot-rag-customize/src/my_data_source.py @@ -0,0 +1,62 @@ +import os +from dataclasses import dataclass + +from teams.ai.tokenizers import Tokenizer +from teams.ai.data_sources import DataSource +from teams.state.state import TurnContext +from teams.state.memory import Memory + +@dataclass +class Result: + output: str + length: int + too_long: bool + +class MyDataSource(DataSource): + """ + A data source that searches through a local directory of files for a given query. + """ + + def __init__(self, name): + """ + Creates a new instance of the LocalDataSource instance. + Initializes the data source. + """ + self.name = name + + filePath = os.path.join(os.path.dirname(__file__), 'data') + files = os.listdir(filePath) + self._data = [open(os.path.join(filePath, file), 'r').read() for file in files] + + def name(self): + return self.name + + async def render_data(self, context: TurnContext, memory: Memory, tokenizer: Tokenizer, maxTokens: int): + """ + Renders the data source as a string of text. + The returned output should be a string of text that will be injected into the prompt at render time. + """ + query = memory.get('temp.input') + if not query: + return Result('', 0, False) + + result='' + # Text search + for data in self._data: + if query in data: + result += data + # Key word search + if 'history' in query.lower() or 'company' in query.lower(): + result += self._data[0] + if 'perksplus' in query.lower() or 'program' in query.lower(): + result += self._data[1] + if 'northwind' in query.lower() or 'health' in query.lower(): + result += self._data[2] + + return Result(self.formatDocument(result), len(result), False) if result!='' else Result('', 0, False) + + def formatDocument(self, result): + """ + Formats the result string + """ + return f"{result}" \ No newline at end of file diff --git a/templates/copilot-rag-customize/src/prompts/chat/config.json b/templates/copilot-rag-customize/src/prompts/chat/config.json new file mode 100644 index 00000000..8166d24f --- /dev/null +++ b/templates/copilot-rag-customize/src/prompts/chat/config.json @@ -0,0 +1,23 @@ +{ + "schema": 1.1, + "description": "Chat with Teams RAG", + "type": "completion", + "completion": { + "completion_type": "chat", + "include_history": true, + "include_input": true, + "max_input_tokens": 4096, + "max_tokens": 1000, + "temperature": 0.9, + "top_p": 0.0, + "presence_penalty": 0.6, + "frequency_penalty": 0.0, + "stop_sequences": [] + }, + "augmentation": { + "augmentation_type": "none", + "data_sources": { + "local-search": 1200 + } + } +} \ No newline at end of file diff --git a/templates/copilot-rag-customize/src/prompts/chat/skprompt.txt b/templates/copilot-rag-customize/src/prompts/chat/skprompt.txt new file mode 100644 index 00000000..789155ad --- /dev/null +++ b/templates/copilot-rag-customize/src/prompts/chat/skprompt.txt @@ -0,0 +1,3 @@ +The following is a conversation with an AI assistant, who is an expert on answering questions over the given context. +Responses should be in a short journalistic style with no more than 80 words. +Use the context provided in the `` tags as the source for your answers. \ No newline at end of file diff --git a/templates/copilot-rag-customize/src/requirements.txt b/templates/copilot-rag-customize/src/requirements.txt new file mode 100644 index 00000000..3640ac1c --- /dev/null +++ b/templates/copilot-rag-customize/src/requirements.txt @@ -0,0 +1,3 @@ +python-dotenv +aiohttp +teams-ai~=1.2.0 \ No newline at end of file diff --git a/templates/copilot-rag-customize/teamsapp.local.yml b/templates/copilot-rag-customize/teamsapp.local.yml new file mode 100644 index 00000000..46586827 --- /dev/null +++ b/templates/copilot-rag-customize/teamsapp.local.yml @@ -0,0 +1,79 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: copilot-rag-customize${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: copilot-rag-customize${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: basicSearch + 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 + # 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 + # Teams 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 + +deploy: + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} diff --git a/templates/copilot-rag-customize/teamsapp.testtool.yml b/templates/copilot-rag-customize/teamsapp.testtool.yml new file mode 100644 index 00000000..40130167 --- /dev/null +++ b/templates/copilot-rag-customize/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.1 + symlinkDir: ./devTools/teamsapptester + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} + BOT_ID: "" + BOT_PASSWORD: "" + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} diff --git a/templates/copilot-rag-customize/teamsapp.yml b/templates/copilot-rag-customize/teamsapp.yml new file mode 100644 index 00000000..b1c8a020 --- /dev/null +++ b/templates/copilot-rag-customize/teamsapp.yml @@ -0,0 +1,137 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsfx provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: copilot-rag-customize${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: copilot-rag-customize${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + - 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-tab + # Teams 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 + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # 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 + # Teams 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 'teamsfx deploy' is executed +deploy: + # 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: ./src + # Ignore file location, leave blank will ignore nothing + ignoreFile: .webappignore + # 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}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: 6b04cc0c-1196-42fa-836a-e1682cf1f3d2 diff --git a/templates/copilot-rag-m365/.gitignore b/templates/copilot-rag-m365/.gitignore new file mode 100644 index 00000000..bc090d91 --- /dev/null +++ b/templates/copilot-rag-m365/.gitignore @@ -0,0 +1,22 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +.localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +appPackage/build + +# dependencies +node_modules/ + +# misc +.env +.deployment +.DS_Store + +# build +lib/ + +# devTools +devTools/ \ No newline at end of file diff --git a/templates/copilot-rag-m365/.tours/load-data-from-graph-connector.tour b/templates/copilot-rag-m365/.tours/load-data-from-graph-connector.tour new file mode 100644 index 00000000..6a9333c0 --- /dev/null +++ b/templates/copilot-rag-m365/.tours/load-data-from-graph-connector.tour @@ -0,0 +1,82 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "Load data from Graph connector", + "steps": [ + { + "file": "aad.manifest.json", + "description": "### Update Entra app manifest\n\nChange the permission to be able to read external items. This is necessary to grant your app access to external items imported to Microsoft 365 by the Microsoft Graph connector. Change the permission to:\n\n```text\nExternalItem.Read.All\n```", + "line": 24, + "selection": { + "start": { + "line": 24, + "character": 28 + }, + "end": { + "line": 24, + "character": 42 + } + }, + "title": "Update Entra app manifest" + }, + { + "file": "src/app/app.ts", + "description": "### Update default scopes\n\nUpdate the scopes that the Microsoft Graph client should request when connecting to Microsoft Graph. This is necessary for your app to be able to read external items imported by the Graph connector. Change the permission to:\n\n```text\nExternalItem.Read.All\n```", + "line": 41, + "selection": { + "start": { + "line": 41, + "character": 19 + }, + "end": { + "line": 41, + "character": 33 + } + }, + "title": "Update Entra app permissions" + }, + { + "file": "src/app/graphDataSource.ts", + "description": "### Update entity type\n\nUpdate entity type to search for external items. This instructs Microsoft Search to only search for items of type `externalItem` which represents external items ingested by Graph connectors. Change the code to:\n\n```text\nexternalItem\n```", + "line": 61, + "selection": { + "start": { + "line": 61, + "character": 32 + }, + "end": { + "line": 61, + "character": 41 + } + }, + "title": "Update entity type" + }, + { + "file": "src/app/graphDataSource.ts", + "description": "### Define content source\n\nDefine the name of your external connection. This scopes the search result to only include results from the specified external connection. Add the snippet and replace `myconnection` with your connection name:\n\n```json\n contentSources: [\n '/external/connections/myconnection'\n ],\n\n```", + "line": 62, + "title": "Define content source" + }, + { + "file": "src/app/graphDataSource.ts", + "description": "### Update download code\n\nUpdate the code to download external item's contents:\n\n```javascript\n const rawContent = await this\n .downloadExternalContent(result.resource.properties.substrateContentDomainId);\n```\n", + "line": 89, + "selection": { + "start": { + "line": 87, + "character": 1 + }, + "end": { + "line": 89, + "character": 15 + } + }, + "title": "Update code to download external item's contents" + }, + { + "file": "src/app/graphDataSource.ts", + "description": "### Define download content method\n\nDefine new method to retrieve external item's contents. Update `myconnection` with your connection's name:\n\n```javascript\n\n private async downloadExternalContent(externalItemFullId: string) {\n const externalItemId = externalItemFullId.split(',')[1];\n const externalItem = await this.graphClient\n .api(`/external/connections/myconnection/items/${externalItemId}`)\n .get();\n return externalItem.content.value;\n }\n\n```\n", + "line": 105, + "title": "Define method to download external item's contents" + } + ] +} \ No newline at end of file diff --git a/templates/copilot-rag-m365/.vscode/extensions.json b/templates/copilot-rag-m365/.vscode/extensions.json new file mode 100644 index 00000000..086855dc --- /dev/null +++ b/templates/copilot-rag-m365/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension", + "vsls-contrib.codetour" + ] +} \ No newline at end of file diff --git a/templates/copilot-rag-m365/.vscode/launch.json b/templates/copilot-rag-m365/.vscode/launch.json new file mode 100644 index 00000000..0fff774b --- /dev/null +++ b/templates/copilot-rag-m365/.vscode/launch.json @@ -0,0 +1,120 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Attach to Local Service", + "type": "node", + "request": "attach", + "port": 9239, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Desktop)", + "type": "node", + "request": "launch", + "preLaunchTask": "Start Teams App in Desktop Client (Remote)", + "presentation": { + "group": "3-remote", + "order": 3 + }, + "internalConsoleOptions": "neverOpen", + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "1-local", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "1-local", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Desktop)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App in Desktop Client", + "presentation": { + "group": "1-local", + "order": 3 + }, + "stopAll": true + } + ] +} diff --git a/templates/copilot-rag-m365/.vscode/settings.json b/templates/copilot-rag-m365/.vscode/settings.json new file mode 100644 index 00000000..0d3ba10b --- /dev/null +++ b/templates/copilot-rag-m365/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ] +} \ No newline at end of file diff --git a/templates/copilot-rag-m365/.vscode/tasks.json b/templates/copilot-rag-m365/.vscode/tasks.json new file mode 100644 index 00000000..615f0a12 --- /dev/null +++ b/templates/copilot-rag-m365/.vscode/tasks.json @@ -0,0 +1,133 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239 // app inspector port for Node.js debugger + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Teams App in Desktop Client", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start application", + "Start desktop client" + ], + "dependsOrder": "sequence" + }, + { + "label": "Start desktop client", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true" + } + }, + { + "label": "Start Teams App in Desktop Client (Remote)", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true" + } + } + ] +} \ No newline at end of file diff --git a/templates/copilot-rag-m365/.webappignore b/templates/copilot-rag-m365/.webappignore new file mode 100644 index 00000000..18a015a2 --- /dev/null +++ b/templates/copilot-rag-m365/.webappignore @@ -0,0 +1,27 @@ +.webappignore +.fx +.deployment +.localConfigs.testTool +.localConfigs +.notification.localstore.json +.notification.testtoolstore.json +.vscode +*.js.map +*.ts.map +*.ts +.git* +.tsbuildinfo +CHANGELOG.md +readme.md +local.settings.json +test +tsconfig.json +.DS_Store +teamsapp.yml +teamsapp.*.yml +/env/ +/node_modules/.bin +/node_modules/ts-node +/node_modules/typescript +/appPackage/ +/infra/ \ No newline at end of file diff --git a/templates/copilot-rag-m365/README.md b/templates/copilot-rag-m365/README.md new file mode 100644 index 00000000..e3a23a09 --- /dev/null +++ b/templates/copilot-rag-m365/README.md @@ -0,0 +1,74 @@ +# Overview of the Chat With Your Data (Using Microsoft 365 Data) template + +This app template showcases how to build one of the most powerful applications enabled by LLM - sophisticated question-answering (Q&A) chat bots that can answer questions about specific source information right in the Microsoft Teams. +This app template also demonstrates usage of techniques like: +- [Retrieval Augmented Generation](https://python.langchain.com/docs/use_cases/question_answering/#what-is-rag), or RAG. +- [Microsoft Graph Search API](https://learn.microsoft.com/graph/search-concept-overview) +- [Teams AI Library](https://learn.microsoft.com/microsoftteams/platform/bots/how-to/teams%20conversational%20ai/teams-conversation-ai-overview) + +## Get started with the template + +> **Prerequisites** +> +> To run the template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - A Microsoft 365 tenant in which you have permission to upload Teams apps. You can get a free Microsoft 365 developer tenant by joining the [Microsoft 365 developer program](https://developer.microsoft.com/en-us/microsoft-365/dev-program). +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli). +> - Prepare your own [Azure OpenAI](https://aka.ms/oai/access) resource. + +> [!TIP] +> You can adjust this template to use data from a Microsoft Graph connector. Follow the steps in the [CodeTour](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour) included in the project to apply the necessary changes. To use data from a Microsoft Graph connector, you need a Graph connector deployed to your tenant. For testing, we recommend using the [Ingest custom API data using TypeScript, Node.js and Teams Toolkit for Visual Studio Code](https://adoption.microsoft.com/sample-solution-gallery/sample/pnp-graph-connector-nodejs-typescript-food-catalog) sample. + +> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging). + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY=`, endpoint `AZURE_OPENAI_ENDPOINT=` and deployment name `AZURE_OPENAI_DEPLOYMENT_NAME=`. +1. Microsoft Graph Search API is available for searching SharePoint content, thus you just need to ensure your document in *src/data/\*.txt* is [uploaded to SharePoint / OneDrive](https://support.microsoft.com/office/upload-files-and-folders-to-a-library-da549fb1-1fcb-4167-87d0-4693e93cb7a0), no extra data ingestion required. +1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. +1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. +1. You can send any message to get a response from the bot. + +![M365 RAG Bot](https://github.com/OfficeDev/TeamsFx/assets/13211513/c2fff68c-53ce-445a-a101-97f0c127b825) + +## What's included in the template + +| Folder | Contents | +| - | - | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| - | - | +|`src/index.ts`| Sets up the bot app server.| +|`src/adapter.ts`| Sets up the bot adapter.| +|`src/config.ts`| Defines the environment variables.| +|`src/prompts/chat/skprompt.txt`| Defines the prompt.| +|`src/prompts/chat/config.json`| Configures the prompt.| +|`src/app/app.ts`| Handles business logics for the RAG bot.| +|`src/app/m365DataSource.ts`| Defines the m365 data source.| +|`src/data/*.txt`| Raw text data sources.| +|`src/public/*.html`| Auth start page and an auth end page to be used by the user sign in flow.| + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| - | - | +|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| + +## Extend the template + +- Follow [Build a Basic AI Chatbot in Teams](https://aka.ms/teamsfx-basic-ai-chatbot) to extend the template with more AI capabilities. +- Understand more about [how to add additional APIs](https://aka.ms/teamsfx-rag-bot#add-more-api-for-custom-api-as-data-source). + +## Additional information and references + +- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) \ No newline at end of file diff --git a/templates/copilot-rag-m365/aad.manifest.json b/templates/copilot-rag-m365/aad.manifest.json new file mode 100644 index 00000000..e1fe6d07 --- /dev/null +++ b/templates/copilot-rag-m365/aad.manifest.json @@ -0,0 +1,107 @@ +{ + "id": "${{AAD_APP_OBJECT_ID}}", + "appId": "${{AAD_APP_CLIENT_ID}}", + "name": "copilot-rag-m365-aad", + "accessTokenAcceptedVersion": 2, + "signInAudience": "AzureADMyOrg", + "optionalClaims": { + "idToken": [], + "accessToken": [ + { + "name": "idtyp", + "source": null, + "essential": false, + "additionalProperties": [] + } + ], + "saml2Token": [] + }, + "requiredResourceAccess": [ + { + "resourceAppId": "Microsoft Graph", + "resourceAccess": [ + { + "id": "Files.Read.All", + "type": "Scope" + } + ] + } + ], + "oauth2Permissions": [ + { + "adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.", + "adminConsentDisplayName": "Teams can access app's web APIs", + "id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}", + "isEnabled": true, + "type": "User", + "userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have", + "userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf", + "value": "access_as_user" + } + ], + "preAuthorizedApplications": [ + { + "appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "00000002-0000-0ff1-ce00-000000000000", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "0ec893e0-5785-4de6-99da-4ed124e5296c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4765445b-32c6-49b0-83e6-1d93765276ca", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4345a7b9-9a63-4910-a426-35363201d503", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "27922004-5251-4030-b22d-91ecd9a37ea4", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + } + ], + "identifierUris":[ + "api://botid-${{BOT_ID}}" + ], + "replyUrlsWithType":[ + { + "url": "https://${{BOT_DOMAIN}}/auth-end.html", + "type": "Web" + } + ] +} diff --git a/templates/copilot-rag-m365/appPackage/color.png b/templates/copilot-rag-m365/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/copilot-rag-m365/appPackage/color.png differ diff --git a/templates/copilot-rag-m365/appPackage/manifest.json b/templates/copilot-rag-m365/appPackage/manifest.json new file mode 100644 index 00000000..6277275a --- /dev/null +++ b/templates/copilot-rag-m365/appPackage/manifest.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "copilot-rag-m365${{APP_NAME_SUFFIX}}", + "full": "full name for copilot-rag-m365" + }, + "description": { + "short": "short description for copilot-rag-m365", + "full": "full description for copilot-rag-m365" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "${{BOT_DOMAIN}}" + ], + "webApplicationInfo": { + "id": "${{AAD_APP_CLIENT_ID}}", + "resource": "api://botid-${{BOT_ID}}" + } +} \ No newline at end of file diff --git a/templates/copilot-rag-m365/appPackage/outline.png b/templates/copilot-rag-m365/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/copilot-rag-m365/appPackage/outline.png differ diff --git a/templates/copilot-rag-m365/env/.env.dev b/templates/copilot-rag-m365/env/.env.dev new file mode 100644 index 00000000..8cc91bf0 --- /dev/null +++ b/templates/copilot-rag-m365/env/.env.dev @@ -0,0 +1,22 @@ +# 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= +BOT_DOMAIN= +AAD_APP_CLIENT_ID= +AAD_APP_OBJECT_ID= +AAD_APP_TENANT_ID= +AAD_APP_OAUTH_AUTHORITY= +AAD_APP_OAUTH_AUTHORITY_HOST= +AAD_APP_ACCESS_AS_USER_PERMISSION_ID= \ No newline at end of file diff --git a/templates/copilot-rag-m365/infra/azure.bicep b/templates/copilot-rag-m365/infra/azure.bicep new file mode 100644 index 00000000..aeb716b1 --- /dev/null +++ b/templates/copilot-rag-m365/infra/azure.bicep @@ -0,0 +1,141 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@secure() +param azureOpenAIKey string + +@secure() +param azureOpenAIEndpoint string + +@secure() +param azureOpenAIDeploymentName string + +param webAppSKU string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param identityName string = resourceBaseName +param location string = resourceGroup().location +param aadAppClientId string +param aadAppTenantId string +param aadAppOauthAuthorityHost string +@secure() +param aadAppClientSecret string + +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: webAppSKU + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + alwaysOn: true + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure App Service from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x for your site + } + { + name: 'RUNNING_ON_AZURE' + value: '1' + } + { + name: 'BOT_ID' + value: identity.properties.clientId + } + { + name: 'BOT_TENANT_ID' + value: identity.properties.tenantId + } + { + name: 'BOT_TYPE' + value: 'UserAssignedMsi' + } + { + name: 'AZURE_OPENAI_API_KEY' + value: azureOpenAIKey + } + { + name: 'AZURE_OPENAI_ENDPOINT' + value: azureOpenAIEndpoint + } + { + name: 'AZURE_OPENAI_DEPLOYMENT_NAME' + value: azureOpenAIDeploymentName + } + ] + ftpsState: 'FtpsOnly' + } + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${identity.id}': {} + } + } +} + +resource webAppSettings 'Microsoft.Web/sites/config@2021-02-01' = { + name: '${webAppName}/appsettings' + properties: { + WEBSITE_NODE_DEFAULT_VERSION: '~18' + WEBSITE_RUN_FROM_PACKAGE: '1' + BOT_ID: identity.properties.clientId + BOT_TENANT_ID: identity.properties.tenantId + BOT_TYPE: 'UserAssignedMsi' + BOT_DOMAIN: webApp.properties.defaultHostName + AAD_APP_CLIENT_ID: aadAppClientId + AAD_APP_CLIENT_SECRET: aadAppClientSecret + AAD_APP_TENANT_ID: aadAppTenantId + AAD_APP_OAUTH_AUTHORITY_HOST: aadAppOauthAuthorityHost + AZURE_OPENAI_API_KEY: azureOpenAIKey + AZURE_OPENAI_ENDPOINT: azureOpenAIEndpoint + AZURE_OPENAI_DEPLOYMENT_NAME: azureOpenAIDeploymentName + RUNNING_ON_AZURE: '1' + } +} + +// 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/templates/copilot-rag-m365/infra/azure.parameters.json b/templates/copilot-rag-m365/infra/azure.parameters.json new file mode 100644 index 00000000..a2fa8a55 --- /dev/null +++ b/templates/copilot-rag-m365/infra/azure.parameters.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "bot${{RESOURCE_SUFFIX}}" + }, + "azureOpenAIKey": { + "value": "${{SECRET_AZURE_OPENAI_API_KEY}}" + }, + "azureOpenAIEndpoint": { + "value": "${{AZURE_OPENAI_ENDPOINT}}" + }, + "azureOpenAIDeploymentName": { + "value": "${{AZURE_OPENAI_DEPLOYMENT_NAME}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "copilot-rag-m365" + }, + "aadAppClientId": { + "value": "${{AAD_APP_CLIENT_ID}}" + }, + "aadAppClientSecret": { + "value": "${{SECRET_AAD_APP_CLIENT_SECRET}}" + }, + "aadAppTenantId": { + "value": "${{AAD_APP_TENANT_ID}}" + }, + "aadAppOauthAuthorityHost": { + "value": "${{AAD_APP_OAUTH_AUTHORITY_HOST}}" + } + } +} \ No newline at end of file diff --git a/templates/copilot-rag-m365/infra/botRegistration/azurebot.bicep b/templates/copilot-rag-m365/infra/botRegistration/azurebot.bicep new file mode 100644 index 00000000..a5a27b8f --- /dev/null +++ b/templates/copilot-rag-m365/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/templates/copilot-rag-m365/infra/botRegistration/readme.md b/templates/copilot-rag-m365/infra/botRegistration/readme.md new file mode 100644 index 00000000..d5416243 --- /dev/null +++ b/templates/copilot-rag-m365/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/templates/copilot-rag-m365/package.json b/templates/copilot-rag-m365/package.json new file mode 100644 index 00000000..8d093b15 --- /dev/null +++ b/templates/copilot-rag-m365/package.json @@ -0,0 +1,45 @@ +{ + "name": "copilotragm365", + "version": "1.0.0", + "msteams": { + "teamsAppId": null + }, + "description": "Microsoft Teams Toolkit RAG Bot Sample with Graph API and Teams AI Library", + "engines": { + "node": "16 || 18" + }, + "author": "Microsoft", + "license": "MIT", + "main": "./lib/src/index.js", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", + "dev": "nodemon --exec node --inspect=9239 --signal SIGINT -r ts-node/register ./src/index.ts", + "build": "tsc --build && shx cp -r ./src/prompts ./lib/src && shx cp -r ./src/public ./lib/src", + "start": "node ./lib/src/index.js", + "test": "echo \"Error: no test specified\" && exit 1", + "watch": "nodemon --exec \"npm run start\"" + }, + "repository": { + "type": "git", + "url": "https://github.com" + }, + "dependencies": { + "@microsoft/microsoft-graph-client": "^3.0.1", + "@azure/search-documents": "^12.0.0", + "@microsoft/teams-ai": "^1.1.0", + "botbuilder": "^4.20.0", + "openai": "~4.28.4", + "restify": "^10.0.0" + }, + "devDependencies": { + "@types/restify": "^8.5.5", + "@types/node": "^16.0.0", + "env-cmd": "^10.1.0", + "ts-node": "^10.4.0", + "typescript": "^4.4.4", + "nodemon": "^2.0.7", + "shx": "^0.3.3" + } +} \ No newline at end of file diff --git a/templates/copilot-rag-m365/src/adapter.ts b/templates/copilot-rag-m365/src/adapter.ts new file mode 100644 index 00000000..7cc2bd48 --- /dev/null +++ b/templates/copilot-rag-m365/src/adapter.ts @@ -0,0 +1,47 @@ +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { + CloudAdapter, + ConfigurationBotFrameworkAuthentication, + ConfigurationServiceClientCredentialFactory, +} from "botbuilder"; + +// This bot's main dialog. +import config from "./config"; + +const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication( + {}, + new ConfigurationServiceClientCredentialFactory(config) +); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about how bots work. +const adapter = new CloudAdapter(botFrameworkAuthentication); + +// Catch-all for errors. +const onTurnErrorHandler = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + // NOTE: In production environment, you should consider logging this to Azure + // application insights. + console.error(`\n [onTurnError] unhandled error: ${error}`); + + // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. + if (context.activity.type === "message") { + // Send a trace activity, which will be displayed in Bot Framework Emulator + await context.sendTraceActivity( + "OnTurnError Trace", + `${error}`, + "https://www.botframework.com/schemas/error", + "TurnError" + ); + + // Send a message to the user + await context.sendActivity("The bot encountered an error or bug."); + await context.sendActivity("To continue to run this bot, please fix the bot source code."); + } +}; + +// Set the onTurnError for the singleton CloudAdapter. +adapter.onTurnError = onTurnErrorHandler; + +export default adapter; diff --git a/templates/copilot-rag-m365/src/app/app.ts b/templates/copilot-rag-m365/src/app/app.ts new file mode 100644 index 00000000..d627afd3 --- /dev/null +++ b/templates/copilot-rag-m365/src/app/app.ts @@ -0,0 +1,67 @@ +import { MemoryStorage } from "botbuilder"; +import * as path from "path"; +import config from "../config"; + +// See https://aka.ms/teams-ai-library to learn more about the Teams AI library. +import { Application, ActionPlanner, OpenAIModel, PromptManager, TurnState } from "@microsoft/teams-ai"; +import { GraphDataSource } from "./graphDataSource"; + +// Create AI components +const model = new OpenAIModel({ + azureApiKey: config.azureOpenAIKey, + azureDefaultDeployment: config.azureOpenAIDeploymentName, + azureEndpoint: config.azureOpenAIEndpoint, + + useSystemMessages: true, + logRequests: true, +}); +const prompts = new PromptManager({ + promptsFolder: path.join(__dirname, "../prompts"), +}); +const planner = new ActionPlanner({ + model, + prompts, + defaultPrompt: "chat", +}); + +// Register your data source with planner +const graphDataSource = new GraphDataSource("graph-ai-search"); +planner.prompts.addDataSource(graphDataSource); + +// Define storage and application +const storage = new MemoryStorage(); +const app = new Application({ + storage, + ai: { + planner, + }, + authentication: { + settings: { + graph: { + scopes: ["Files.Read.All"], + msalConfig: { + auth: { + clientId: process.env.AAD_APP_CLIENT_ID, + clientSecret: process.env.AAD_APP_CLIENT_SECRET, + authority: `${process.env.AAD_APP_OAUTH_AUTHORITY_HOST}/${process.env.AAD_APP_TENANT_ID}` + } + }, + signInLink: `https://${process.env.BOT_DOMAIN}/auth-start.html`, + } + }, + autoSignIn: true, + } +}); + +app.authentication.get("graph").onUserSignInSuccess(async (context, state) => { + // Successfully logged in + await context.sendActivity("You are successfully logged in. You can send a new message to talk to the bot."); +}); + +app.authentication.get("graph").onUserSignInFailure(async (context, state, error) => { + // Failed to login + await context.sendActivity("Failed to login"); + await context.sendActivity(`Error message: ${error.message}`); +}); + +export default app; diff --git a/templates/copilot-rag-m365/src/app/graphDataSource.ts b/templates/copilot-rag-m365/src/app/graphDataSource.ts new file mode 100644 index 00000000..507efce3 --- /dev/null +++ b/templates/copilot-rag-m365/src/app/graphDataSource.ts @@ -0,0 +1,134 @@ +import { DataSource, Memory, RenderedPromptSection, Tokenizer } from "@microsoft/teams-ai"; +import { TurnContext } from "botbuilder"; +import { Client, ResponseType } from "@microsoft/microsoft-graph-client"; + +/** + * A data source that searches through Graph API. + */ +export class GraphDataSource implements DataSource { + /** + * Name of the data source. + */ + public readonly name: string; + + /** + * Graph client to make requests to Graph API. + */ + private graphClient: Client; + + /** + * Creates a new instance of the Graph DataSource instance. + */ + public constructor(name: string) { + this.name = name; + } + + /** + * Renders the data source as a string of text. + * @remarks + * The returned output should be a string of text that will be injected into the prompt at render time. + * @param context Turn context for the current turn of conversation with the user. + * @param memory An interface for accessing state values. + * @param tokenizer Tokenizer to use when rendering the data source. + * @param maxTokens Maximum number of tokens allowed to be rendered. + * @returns A promise that resolves to the rendered data source. + */ + public async renderData(context: TurnContext, memory: Memory, tokenizer: Tokenizer, maxTokens: number): Promise> { + const query = memory.getValue("temp.input") as string; + if(!query) { + return { output: "", length: 0, tooLong: false }; + } + if (!this.graphClient) { + this.graphClient = Client.init({ + authProvider: (done) => { + done(null, (memory as any).temp.authTokens["graph"]); + } + }); + } + let graphQuery = query; + if (query.toLocaleLowerCase().includes("perksplus")) { + graphQuery = "perksplus program"; + } else if (query.toLocaleLowerCase().includes("company") || query.toLocaleLowerCase().includes("history")) { + graphQuery = "company history"; + } else if (query.toLocaleLowerCase().includes("northwind") || query.toLocaleLowerCase().includes("health")) { + graphQuery = "northwind health"; + } + + const contentResults = []; + const response = await this.graphClient.api("/search/query").post({ + requests: [ + { + entityTypes: ["driveItem"], + query: { + // Search for markdown files in the user's OneDrive and SharePoint + // The supported file types are listed here: + // https://learn.microsoft.com/sharepoint/technical-reference/default-crawled-file-name-extensions-and-parsed-file-types + queryString: `${graphQuery}`, + }, + // This parameter is required only when searching with application permissions + // https://learn.microsoft.com/graph/search-concept-searchall + // region: "US", + }, + ], + }); + for (const value of response?.value ?? []) { + for (const hitsContainer of value?.hitsContainers ?? []) { + contentResults.push(...(hitsContainer?.hits ?? [])); + } + } + + // Add documents until you run out of tokens + let length = 0, + output = ""; + for (const result of contentResults) { + const rawContent = await this.downloadSharepointFile( + result.resource.webUrl + ); + if (!rawContent) { + continue; + } + let doc = `${rawContent}\n\n`; + let docLength = tokenizer.encode(doc).length; + const remainingTokens = maxTokens - (length + docLength); + if (remainingTokens <= 0) { + break; + } + + // Append do to output + output += doc; + length += docLength; + } + return { output: this.formatDocument(output), length: output.length, tooLong: false }; + } + + /** + * Formats the result string + * @param result + * @returns + */ + private formatDocument(result: string): string { + return `${result}`; + } + + // Download the file from SharePoint + // https://docs.microsoft.com/en-us/graph/api/driveitem-get-content + private async downloadSharepointFile( + contentUrl: string + ): Promise { + const encodedUrl = this.encodeSharepointContentUrl(contentUrl); + const fileContentResponse = await this.graphClient + .api(`/shares/${encodedUrl}/driveItem/content`) + .responseType(ResponseType.TEXT) + .get(); + + return fileContentResponse; + } + + private encodeSharepointContentUrl(webUrl: string): string { + const byteData = Buffer.from(webUrl, "utf-8"); + const base64String = byteData.toString("base64"); + return ( + "u!" + base64String.replace("=", "").replace("/", "_").replace("+", "_") + ); + } +} \ No newline at end of file diff --git a/templates/copilot-rag-m365/src/config.ts b/templates/copilot-rag-m365/src/config.ts new file mode 100644 index 00000000..368387d8 --- /dev/null +++ b/templates/copilot-rag-m365/src/config.ts @@ -0,0 +1,11 @@ +const config = { + MicrosoftAppId: process.env.BOT_ID, + MicrosoftAppType: process.env.BOT_TYPE, + MicrosoftAppTenantId: process.env.BOT_TENANT_ID, + MicrosoftAppPassword: process.env.BOT_PASSWORD, + azureOpenAIKey: process.env.AZURE_OPENAI_API_KEY, + azureOpenAIEndpoint: process.env.AZURE_OPENAI_ENDPOINT, + azureOpenAIDeploymentName: process.env.AZURE_OPENAI_DEPLOYMENT_NAME, +}; + +export default config; diff --git a/templates/copilot-rag-m365/src/data/Contoso Electronics_PerkPlus_Program.txt b/templates/copilot-rag-m365/src/data/Contoso Electronics_PerkPlus_Program.txt new file mode 100644 index 00000000..1d97d511 --- /dev/null +++ b/templates/copilot-rag-m365/src/data/Contoso Electronics_PerkPlus_Program.txt @@ -0,0 +1,36 @@ +# Contoso Electronics PerksPlus Program + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## Overview +Introducing PerksPlus - the ultimate benefits program designed to support the health and wellness of employees. With PerksPlus, employees have the opportunity to expense up to $1000 for fitness-related programs, making it easier and more affordable to maintain a healthy lifestyle. PerksPlus is not only designed to support employees' physical health, but also their mental health. Regular exercise has been shown to reduce stress, improve mood, and enhance overall well-being. With PerksPlus, employees can invest in their health and wellness, while enjoying the peace of mind that comes with knowing they are getting the support they need to lead a healthy life. +What is Covered? + +PerksPlus covers a wide range of fitness activities, including but not limited to: +* Gym memberships +* Personal training sessions +* Yoga and Pilates classes +* Fitness equipment purchases +* Sports team fees +* Health retreats and spas +* Outdoor adventure activities (such as rock climbing, hiking, and kayaking) +* Group fitness classes (such as dance, martial arts, and cycling) +* Virtual fitness programs (such as online yoga and workout classes) + +In addition to the wide range of fitness activities covered by PerksPlus, the program also covers a variety of lessons and experiences that promote health and wellness. Some of the lessons covered under PerksPlus include: +* Skiing and snowboarding lessons +* Scuba diving lessons +* Surfing lessons +* Horseback riding lessons + +These lessons provide employees with the opportunity to try new things, challenge themselves, and improve their physical skills. They are also a great way to relieve stress and have fun while staying active. + +With PerksPlus, employees can choose from a variety of fitness programs to suit their individual needs and preferences. Whether you're looking to improve your physical fitness, reduce stress, or just have some fun, PerksPlus has you covered. + +## What is Not Covered? +In addition to the wide range of activities covered by PerksPlus, there is also a list of things that are not +covered under the program. These include but are not limited to: +* Non-fitness related expenses +* Medical treatments and procedures +* Travel expenses (unless related to a fitness program) +* Food and supplements \ No newline at end of file diff --git a/templates/copilot-rag-m365/src/data/Contoso_Electronics_Company_Overview.txt b/templates/copilot-rag-m365/src/data/Contoso_Electronics_Company_Overview.txt new file mode 100644 index 00000000..6878a8e2 --- /dev/null +++ b/templates/copilot-rag-m365/src/data/Contoso_Electronics_Company_Overview.txt @@ -0,0 +1,48 @@ +# Contoso Electronics Company Overview + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## History + +Contoso Electronics, a pioneering force in the tech industry, was founded in 1985 by visionary entrepreneurs with a passion for innovation. Over the years, the company has played a pivotal role in shaping the landscape of consumer electronics. + +| Year | Milestone | +|------|-----------| +| 1985 | Company founded with a focus on cutting-edge technology | +| 1990 | Launched the first-ever handheld personal computer | +| 2000 | Introduced groundbreaking advancements in AI and robotics | +| 2015 | Expansion into sustainable and eco-friendly product lines | + +## Company Overview + +At Contoso Electronics, we take pride in fostering a dynamic and inclusive workplace. Our dedicated team of experts collaborates to create innovative solutions that empower and connect people globally. + +### Core Values + +- **Innovation:** Constantly pushing the boundaries of technology. +- **Diversity:** Embracing different perspectives for creative excellence. +- **Sustainability:** Committed to eco-friendly practices in our products. + +## Vacation Perks + +We believe in work-life balance and understand the importance of well-deserved breaks. Our vacation perks are designed to help our employees recharge and return with renewed enthusiasm. + +| Vacation Tier | Duration | Additional Benefits | +|---------------|----------|---------------------| +| Standard | 2 weeks | Health and wellness stipend | +| Senior | 4 weeks | Travel vouchers for a dream destination | +| Executive | 6 weeks | Luxury resort getaway with family | + +## Employee Recognition + +Recognizing the hard work and dedication of our employees is at the core of our culture. Here are some ways we celebrate achievements: + +- Monthly "Innovator of the Month" awards +- Annual gala with awards for outstanding contributions +- Team-building retreats for high-performing departments + +## Join Us! + +Contoso Electronics is always on the lookout for talented individuals who share our passion for innovation. If you're ready to be part of a dynamic team shaping the future of technology, check out our [careers page](http://www.contoso.com) for exciting opportunities. + +[Learn more about Contoso Electronics!](http://www.contoso.com) diff --git a/templates/copilot-rag-m365/src/data/Contoso_Electronics_Plan_Benefits.txt b/templates/copilot-rag-m365/src/data/Contoso_Electronics_Plan_Benefits.txt new file mode 100644 index 00000000..9da5c642 --- /dev/null +++ b/templates/copilot-rag-m365/src/data/Contoso_Electronics_Plan_Benefits.txt @@ -0,0 +1,37 @@ +# Contoso Electronics Plan and Benefit Packages + +*Disclaimer: This document contains information generated using a language model (Azure OpenAI). The information contained in this document is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft.* + +## Northwind Health Plus + +Northwind Health Plus is a comprehensive plan that provides comprehensive coverage for medical, vision, and dental services. This plan also offers prescription drug coverage, mental health and substance abuse coverage, and coverage for preventive care services. With Northwind Health Plus, you can choose from a variety of in-network providers, including primary care physicians, specialists, hospitals, and pharmacies. + +This plan also offers coverage for emergency services, both in-network and out-of-network. + +## Northwind Standard + +Northwind Standard is a basic plan that provides coverage for medical, vision, and dental services. This plan also offers coverage for preventive care services, as well as prescription drug coverage. With Northwind Standard, you can choose from a variety of in-network providers, including primary care physicians, specialists, hospitals, and pharmacies. This plan does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services. + +## Comparison of Plans + +Both plans offer coverage for routine physicals, well-child visits, immunizations, and other preventive care services. The plans also cover preventive care services such as mammograms, colonoscopies, and other cancer screenings. + +Northwind Health Plus offers more comprehensive coverage than Northwind Standard. This plan offers coverage for emergency services, both in-network and out-of-network, as well as mental health and substance abuse coverage. Northwind Standard does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services. + +Both plans offer coverage for prescription drugs. Northwind Health Plus offers a wider range of prescription drug coverage than Northwind Standard. Northwind Health Plus covers generic, brand-name, and specialty drugs, while Northwind Standard only covers generic and brand-name drugs. + +Both plans offer coverage for vision and dental services. Northwind Health Plus offers coverage for vision exams, glasses, and contact lenses, as well as dental exams, cleanings, and fillings. Northwind Standard only offers coverage for vision exams and glasses. + +Both plans offer coverage for medical services. Northwind Health Plus offers coverage for hospital stays, doctor visits, lab tests, and X-rays. Northwind Standard only offers coverage for doctor visits and lab tests. + +Northwind Health Plus is a comprehensive plan that offers more coverage than Northwind Standard. Northwind Health Plus offers coverage for emergency services, mental health and substance abuse coverage, and out-of-network services, while Northwind Standard does not. Northwind Health Plus also offers a wider range of prescription drug coverage than Northwind Standard. Both plans offer coverage for vision and dental services, as well as medical services. + +## Cost Comparison + +Contoso Electronics deducts the employee's portion of the healthcare cost from each paycheck. This means that the cost of the health insurance will be spread out over the course of the year, rather than being paid in one lump sum. The employee's portion of the cost will be calculated based on the selected health plan and the number of people covered by the insurance. The table below shows a cost comparison between the different health plans offered by Contoso Electronics + +| | Northwind Standard | NorthWind Health Plus | +|---------------|----------|---------------------| +| Employee Only | $45.00 | $55.00 | +| Employee +1 | $65.00 | $71.00 | +| Employee +2 or more | $78.00 | $89.00 | \ No newline at end of file diff --git a/templates/copilot-rag-m365/src/index.ts b/templates/copilot-rag-m365/src/index.ts new file mode 100644 index 00000000..7062c984 --- /dev/null +++ b/templates/copilot-rag-m365/src/index.ts @@ -0,0 +1,33 @@ +// Import required packages +import * as restify from "restify"; + +// This bot's adapter +import adapter from "./adapter"; + +// This bot's main dialog. +import app from "./app/app"; +import path from "path"; + +// Create HTTP server. +const server = restify.createServer(); +server.use(restify.plugins.bodyParser()); + +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\nBot Started, ${server.name} listening to ${server.url}`); +}); + +// Listen for incoming server requests. +server.post("/api/messages", async (req, res) => { + // Route received a request to adapter for processing + await adapter.process(req, res as any, async (context) => { + // Dispatch to application for routing + await app.run(context); + }); +}); + +server.get( + "/auth-:name(start|end).html", + restify.plugins.serveStatic({ + directory: path.join(__dirname, "public"), + }) +); diff --git a/templates/copilot-rag-m365/src/prompts/chat/config.json b/templates/copilot-rag-m365/src/prompts/chat/config.json new file mode 100644 index 00000000..c9e6987a --- /dev/null +++ b/templates/copilot-rag-m365/src/prompts/chat/config.json @@ -0,0 +1,22 @@ +{ + "schema": 1.1, + "description": "Chat with Teams RAG.", + "type": "completion", + "completion": { + "completion_type": "chat", + "include_history": true, + "include_input": true, + "max_input_tokens": 2800, + "max_tokens": 1000, + "temperature": 0.9, + "top_p": 0.0, + "presence_penalty": 0.6, + "frequency_penalty": 0.0, + "stop_sequences": [] + }, + "augmentation": { + "data_sources": { + "graph-ai-search": 1200 + } + } +} \ No newline at end of file diff --git a/templates/copilot-rag-m365/src/prompts/chat/skprompt.txt b/templates/copilot-rag-m365/src/prompts/chat/skprompt.txt new file mode 100644 index 00000000..2a2ebee5 --- /dev/null +++ b/templates/copilot-rag-m365/src/prompts/chat/skprompt.txt @@ -0,0 +1,3 @@ +The following is a conversation with an AI assistant, who is an expert on answering questions over the given context. +Responses should be in a short journalistic style with no more than 80 words. +Use the context provided in the `` tags as the source for your answers. \ No newline at end of file diff --git a/templates/copilot-rag-m365/src/public/auth-end.html b/templates/copilot-rag-m365/src/public/auth-end.html new file mode 100644 index 00000000..41e70ccc --- /dev/null +++ b/templates/copilot-rag-m365/src/public/auth-end.html @@ -0,0 +1,66 @@ + + + Login End Page + + + + + +
+ + + diff --git a/templates/copilot-rag-m365/src/public/auth-start.html b/templates/copilot-rag-m365/src/public/auth-start.html new file mode 100644 index 00000000..4a2d2588 --- /dev/null +++ b/templates/copilot-rag-m365/src/public/auth-start.html @@ -0,0 +1,177 @@ + + + + + Login Start Page + + + + + + + diff --git a/templates/copilot-rag-m365/teamsapp.local.yml b/templates/copilot-rag-m365/teamsapp.local.yml new file mode 100644 index 00000000..f09d1f41 --- /dev/null +++ b/templates/copilot-rag-m365/teamsapp.local.yml @@ -0,0 +1,109 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +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: copilot-rag-m365-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" # Authenticate users with a Microsoft work or school account in your organization's Azure AD tenant (for example, single tenant). + 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: copilot-rag-m365${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: copilot-rag-m365${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: copilot-rag-m365 + 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: + 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 + # 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 + # Teams 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 + +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + BOT_TYPE: 'MultiTenant' + BOT_DOMAIN: ${{BOT_DOMAIN}} + AAD_APP_CLIENT_ID: ${{AAD_APP_CLIENT_ID}} + AAD_APP_CLIENT_SECRET: ${{SECRET_AAD_APP_CLIENT_SECRET}} + AAD_APP_TENANT_ID: ${{AAD_APP_TENANT_ID}} + AAD_APP_OAUTH_AUTHORITY_HOST: ${{AAD_APP_OAUTH_AUTHORITY_HOST}} + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} + AZURE_OPENAI_DEPLOYMENT_NAME: ${{AZURE_OPENAI_DEPLOYMENT_NAME}} diff --git a/templates/copilot-rag-m365/teamsapp.yml b/templates/copilot-rag-m365/teamsapp.yml new file mode 100644 index 00000000..ad979d8f --- /dev/null +++ b/templates/copilot-rag-m365/teamsapp.yml @@ -0,0 +1,150 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +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: copilot-rag-m365-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" # Authenticate users with a Microsoft work or school account in your organization's Azure AD tenant (for example, single tenant). + 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: copilot-rag-m365${{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 + # Teams 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 + + - 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 + + # 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 + # 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 + # Teams 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: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install + - uses: cli/runNpmCommand + name: build app + with: + args: run build --if-present + # 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: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .webappignore + # 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}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: 0347caf7-d838-4f74-a92f-bdb586bd118b diff --git a/templates/copilot-rag-m365/tsconfig.json b/templates/copilot-rag-m365/tsconfig.json new file mode 100644 index 00000000..a68afb21 --- /dev/null +++ b/templates/copilot-rag-m365/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "declaration": true, + "target": "es2017", + "module": "commonjs", + "outDir": "./lib", + "rootDir": "./", + "sourceMap": true, + "incremental": true, + "tsBuildInfoFile": "./lib/.tsbuildinfo", + "esModuleInterop": true + } +} \ No newline at end of file diff --git a/templates/copilot-rag-m365/web.config b/templates/copilot-rag-m365/web.config new file mode 100644 index 00000000..793a3a98 --- /dev/null +++ b/templates/copilot-rag-m365/web.config @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/.gitignore b/templates/custom-copilot-assistant-new/.gitignore new file mode 100644 index 00000000..75baccc4 --- /dev/null +++ b/templates/custom-copilot-assistant-new/.gitignore @@ -0,0 +1,18 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +env/.env.testtool +.env +appPackage/build + +# python virtual environment +.venv/ +__pycache__/ + +# others +.deployment/ +node_modules/ +devTools/*.log + +# Dev tool directories +/devTools/ \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/.vscode/extensions.json b/templates/custom-copilot-assistant-new/.vscode/extensions.json new file mode 100644 index 00000000..760a0b1d --- /dev/null +++ b/templates/custom-copilot-assistant-new/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension", + "ms-python.python" + ] +} \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/.vscode/launch.json b/templates/custom-copilot-assistant-new/.vscode/launch.json new file mode 100644 index 00000000..ec88b372 --- /dev/null +++ b/templates/custom-copilot-assistant-new/.vscode/launch.json @@ -0,0 +1,131 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Desktop)", + "type": "node", + "request": "launch", + "preLaunchTask": "Start Teams App in Desktop Client (Remote)", + "presentation": { + "group": "3-remote", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Start Python", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/src/app.py", + "cwd": "${workspaceFolder}/src", + "console": "integratedTerminal" + }, + { + "name": "Start Test Tool", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/devTools/teamsapptester/node_modules/@microsoft/teams-app-test-tool/cli.js", + "args": [ + "start" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": ["Launch App (Edge)", "Start Python"], + "cascadeTerminateToConfigurations": ["Start Python"], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "1-local", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": ["Launch App (Chrome)", "Start Python"], + "cascadeTerminateToConfigurations": ["Start Python"], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "1-local", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Desktop)", + "configurations": ["Start Python"], + "preLaunchTask": "Start Teams App in Desktop Client", + "presentation": { + "group": "1-local", + "order": 3 + }, + "stopAll": true + }, + { + "name": "Debug in Test Tool", + "configurations": [ + "Start Python", + "Start Test Tool" + ], + "cascadeTerminateToConfigurations": [ + "Start Test Tool" + ], + "preLaunchTask": "Deploy (Test Tool)", + "presentation": { + "group": "2-local", + "order": 1 + }, + "stopAll": true + } + ] + } + \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/.vscode/settings.json b/templates/custom-copilot-assistant-new/.vscode/settings.json new file mode 100644 index 00000000..0d3ba10b --- /dev/null +++ b/templates/custom-copilot-assistant-new/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ] +} \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/.vscode/tasks.json b/templates/custom-copilot-assistant-new/.vscode/tasks.json new file mode 100644 index 00000000..a964abf8 --- /dev/null +++ b/templates/custom-copilot-assistant-new/.vscode/tasks.json @@ -0,0 +1,135 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Check if Node.js is installed and the version is >= 12. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 56150, // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)" + ], + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool", + } + }, + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978 // app service port + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start Teams App in Desktop Client", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start desktop client" + ], + "dependsOrder": "sequence" + }, + { + "label": "Start desktop client", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true" + } + }, + { + "label": "Start Teams App in Desktop Client (Remote)", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true" + } + } + ] +} \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/.webappignore b/templates/custom-copilot-assistant-new/.webappignore new file mode 100644 index 00000000..63b9af10 --- /dev/null +++ b/templates/custom-copilot-assistant-new/.webappignore @@ -0,0 +1,10 @@ +.venv/ +.vscode/ +.env +env/ +__pycache__/ +README.md +teamsapp.yml +teamsapp.local.yml +teamsapp.testtool.yml +/devTools/ \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/README.md b/templates/custom-copilot-assistant-new/README.md new file mode 100644 index 00000000..18fbc725 --- /dev/null +++ b/templates/custom-copilot-assistant-new/README.md @@ -0,0 +1,79 @@ +# Overview of the AI Agent template + +This app template is built on top of [Teams AI library](https://aka.ms/teams-ai-library). +It showcases how to build an AI agent in Teams capable of chatting with users and helping users accomplish a specific task using natural language right in the Teams conversations, such as managing tasks. + +## Get started with the template + +> **Prerequisites** +> +> To run the template in your local dev machine, you will need: +> +> - [Python](https://www.python.org/), version 3.8 to 3.11. +> - [Python extension](https://code.visualstudio.com/docs/languages/python), version v2024.0.1 or higher. +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) latest version or [Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli). +> - An account with [Azure OpenAI](https://aka.ms/oai/access). +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). + +### Configurations +1. Open the command box and enter `Python: Create Environment` to create and activate your desired virtual environment. Remember to select `src/requirements.txt` as dependencies to install when creating the virtual environment. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY`, deployment name `AZURE_OPENAI_MODEL_DEPLOYMENT_NAME` and endpoint `AZURE_OPENAI_ENDPOINT`. + +### Conversation with bot +1. Select the Teams Toolkit icon on the left in the VS Code toolbar. +1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. +1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. +1. You will receive a welcome message from the bot, or send any message to get a response. + +**Congratulations**! You are running an application that can now interact with users in Teams: + +> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging). + +![ai agent](https://github.com/OfficeDev/TeamsFx/assets/109947924/775a0fde-f2ba-4198-a94d-a43c598d6e9b) + +## What's included in the template + +| Folder | Contents | +| - | - | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| - | - | +|`src/app.py`| Hosts an aiohttp api server and exports an app module.| +|`src/bot.py`| Handles business logics for the AI Agent.| +|`src/config.py`| Defines the environment variables.| +|`src/state.py`| Defines the app state of AI Agent.| +|`src/prompts/planner/skprompt.txt`| Defines the prompt.| +|`src/prompts/planner/config.json`| Configures the prompt.| +|`src/prompts/planner/action.json`| Configures the actions.| + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| - | - | +|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +|`teamsapp.testtool.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool.| + +## Extend the template + +You can follow [Build an AI Agent in Teams](https://aka.ms/teamsfx-ai-agent) to extend the AI Agent template with more AI capabilities, like: +- [Add functions](https://aka.ms/teamsfx-ai-agent#add-functions-build-new) + +## Additional information and references + +- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) + +## Known issue +- If you use `Debug in Test Tool` to local debug, you might get an error `InternalServiceError: connect ECONNREFUSED 127.0.0.1:3978` in Test Tool console log or error message `Error: Cannot connect to your app, +please make sure your app is running or restart your app` in log panel of Test Tool web page. You can wait for Python launch console ready and then refresh the front end web page. +- When you use `Launch Remote in Teams` to remote debug after deployment, you might loose interaction with your bot. This is because the remote service needs to restart. Please wait for several minutes to retry it. \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/appPackage/color.png b/templates/custom-copilot-assistant-new/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/custom-copilot-assistant-new/appPackage/color.png differ diff --git a/templates/custom-copilot-assistant-new/appPackage/manifest.json b/templates/custom-copilot-assistant-new/appPackage/manifest.json new file mode 100644 index 00000000..5f086f0c --- /dev/null +++ b/templates/custom-copilot-assistant-new/appPackage/manifest.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "custom-copilot-assistant-new${{APP_NAME_SUFFIX}}", + "full": "full name for custom-copilot-assistant-new" + }, + "description": { + "short": "short description for custom-copilot-assistant-new", + "full": "full description for custom-copilot-assistant-new" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} diff --git a/templates/custom-copilot-assistant-new/appPackage/outline.png b/templates/custom-copilot-assistant-new/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/custom-copilot-assistant-new/appPackage/outline.png differ diff --git a/templates/custom-copilot-assistant-new/env/.env.dev b/templates/custom-copilot-assistant-new/env/.env.dev new file mode 100644 index 00000000..8172044c --- /dev/null +++ b/templates/custom-copilot-assistant-new/env/.env.dev @@ -0,0 +1,17 @@ +# 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= +TEAMS_APP_TENANT_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= +BOT_DOMAIN= \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/infra/azure.bicep b/templates/custom-copilot-assistant-new/infra/azure.bicep new file mode 100644 index 00000000..424a59e6 --- /dev/null +++ b/templates/custom-copilot-assistant-new/infra/azure.bicep @@ -0,0 +1,102 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@description('Required when create Azure Bot service') +param botAadAppClientId string + +@secure() +@description('Required by Bot Framework package in your bot project') +param botAadAppClientSecret string + +@secure() +@description('Required in your bot project to access Azure OpenAI service. You can get it from Azure Portal > OpenAI > Keys > Key1 > Resource Management > Endpoint') +param azureOpenaiKey string +param azureOpenaiModelDeploymentName string +param azureOpenaiEndpoint string + +param webAppSKU string +param linuxFxVersion string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param location string = resourceGroup().location +param pythonVersion string = linuxFxVersion + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app,linux' + location: location + name: serverfarmsName + sku: { + name: webAppSKU + } + properties:{ + reserved: true + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app,linux' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + siteConfig: { + alwaysOn: true + appCommandLine: 'gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:app' + linuxFxVersion: pythonVersion + appSettings: [ + { + name: 'WEBSITES_CONTAINER_START_TIME_LIMIT' + value: '600' + } + { + name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' + value: 'true' + } + { + name: 'BOT_ID' + value: botAadAppClientId + } + { + name: 'BOT_PASSWORD' + value: botAadAppClientSecret + } + { + name: 'AZURE_OPENAI_API_KEY' + value: azureOpenaiKey + } + { + name: 'AZURE_OPENAI_MODEL_DEPLOYMENT_NAME' + value: azureOpenaiModelDeploymentName + } + { + name: 'AZURE_OPENAI_ENDPOINT' + value: azureOpenaiEndpoint + } + ] + ftpsState: 'FtpsOnly' + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + botAadAppClientId: botAadAppClientId + 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 diff --git a/templates/custom-copilot-assistant-new/infra/azure.parameters.json b/templates/custom-copilot-assistant-new/infra/azure.parameters.json new file mode 100644 index 00000000..7f3a4f69 --- /dev/null +++ b/templates/custom-copilot-assistant-new/infra/azure.parameters.json @@ -0,0 +1,33 @@ +{ + "$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": "${{BOT_ID}}" + }, + "botAadAppClientSecret": { + "value": "${{SECRET_BOT_PASSWORD}}" + }, + "azureOpenaiKey": { + "value": "${{SECRET_AZURE_OPENAI_API_KEY}}" + }, + "azureOpenaiModelDeploymentName" : { + "value": "${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}}" + }, + "azureOpenaiEndpoint" : { + "value": "${{AZURE_OPENAI_ENDPOINT}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "custom-copilot-assistant-new" + }, + "linuxFxVersion": { + "value": "PYTHON|3.11" + } + } +} \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/infra/botRegistration/azurebot.bicep b/templates/custom-copilot-assistant-new/infra/botRegistration/azurebot.bicep new file mode 100644 index 00000000..ab67c7a5 --- /dev/null +++ b/templates/custom-copilot-assistant-new/infra/botRegistration/azurebot.bicep @@ -0,0 +1,37 @@ +@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 botAadAppClientId 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: botAadAppClientId + } + 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/templates/custom-copilot-assistant-new/infra/botRegistration/readme.md b/templates/custom-copilot-assistant-new/infra/botRegistration/readme.md new file mode 100644 index 00000000..d5416243 --- /dev/null +++ b/templates/custom-copilot-assistant-new/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/templates/custom-copilot-assistant-new/src/app.py b/templates/custom-copilot-assistant-new/src/app.py new file mode 100644 index 00000000..b0d6b986 --- /dev/null +++ b/templates/custom-copilot-assistant-new/src/app.py @@ -0,0 +1,30 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from http import HTTPStatus + +from aiohttp import web +from botbuilder.core.integration import aiohttp_error_middleware + +from bot import bot_app + +routes = web.RouteTableDef() + +@routes.post("/api/messages") +async def on_messages(req: web.Request) -> web.Response: + res = await bot_app.process(req) + + if res is not None: + return res + + return web.Response(status=HTTPStatus.OK) + +app = web.Application(middlewares=[aiohttp_error_middleware]) +app.add_routes(routes) + +from config import Config + +if __name__ == "__main__": + web.run_app(app, host="localhost", port=Config.PORT) \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/src/bot.py b/templates/custom-copilot-assistant-new/src/bot.py new file mode 100644 index 00000000..a0520ffc --- /dev/null +++ b/templates/custom-copilot-assistant-new/src/bot.py @@ -0,0 +1,84 @@ +import os +import sys +import traceback +from typing import Any, Dict, Optional + +from botbuilder.core import MemoryStorage, TurnContext +from state import AppTurnState +from teams import Application, ApplicationOptions, TeamsAdapter +from teams.ai import AIOptions +from teams.ai.actions import ActionTurnContext +from teams.ai.models import AzureOpenAIModelOptions, OpenAIModel, OpenAIModelOptions +from teams.ai.planners import ActionPlanner, ActionPlannerOptions +from teams.ai.prompts import PromptManager, PromptManagerOptions +from teams.state import TurnState + +from config import Config + +config = Config() + +# Create AI components +model: OpenAIModel + +model = OpenAIModel( + AzureOpenAIModelOptions( + api_key=config.AZURE_OPENAI_API_KEY, + default_model=config.AZURE_OPENAI_MODEL_DEPLOYMENT_NAME, + endpoint=config.AZURE_OPENAI_ENDPOINT, + ) +) + +prompts = PromptManager(PromptManagerOptions(prompts_folder=f"{os.getcwd()}/prompts")) + +planner = ActionPlanner( + ActionPlannerOptions(model=model, prompts=prompts, default_prompt="planner") +) + +# Define storage and application +storage = MemoryStorage() +bot_app = Application[AppTurnState]( + ApplicationOptions( + bot_app_id=config.APP_ID, + storage=storage, + adapter=TeamsAdapter(config), + ai=AIOptions(planner=planner), + ) +) + +@bot_app.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState): + await context.send_activity("How can I help you today?") + +@bot_app.turn_state_factory +async def turn_state_factory(context: TurnContext): + return await AppTurnState.load(context, storage) + +@bot_app.ai.action("createTask") +async def create_task(context: ActionTurnContext[Dict[str, Any]], state: AppTurnState): + if not state.conversation.tasks: + state.conversation.tasks = {} + parameters = state.conversation.planner_history[-1].content.action.parameters + task = {"title": parameters["title"], "description": parameters["description"]} + state.conversation.tasks[parameters["title"]] = task + return f"task created, think about your next action" + +@bot_app.ai.action("deleteTask") +async def delete_task(context: ActionTurnContext[Dict[str, Any]], state: AppTurnState): + if not state.conversation.tasks: + state.conversation.tasks = {} + parameters = state.conversation.planner_history[-1].content.action.parameters + if parameters["title"] not in state.conversation.tasks: + return "task not found, think about your next action" + del state.conversation.tasks[parameters["title"]] + return f"task deleted, think about your next action" + +@bot_app.error +async def on_error(context: TurnContext, error: Exception): + # This check writes out errors to console log .vs. app insights. + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/src/config.py b/templates/custom-copilot-assistant-new/src/config.py new file mode 100644 index 00000000..135d4e58 --- /dev/null +++ b/templates/custom-copilot-assistant-new/src/config.py @@ -0,0 +1,20 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import os + +from dotenv import load_dotenv + +load_dotenv() + +class Config: + """Bot Configuration""" + + PORT = 3978 + APP_ID = os.environ.get("BOT_ID", "") + APP_PASSWORD = os.environ.get("BOT_PASSWORD", "") + AZURE_OPENAI_API_KEY = os.environ["AZURE_OPENAI_API_KEY"] # Azure OpenAI API key + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME = os.environ["AZURE_OPENAI_MODEL_DEPLOYMENT_NAME"] # Azure OpenAI model deployment name + AZURE_OPENAI_ENDPOINT = os.environ["AZURE_OPENAI_ENDPOINT"] # Azure OpenAI endpoint diff --git a/templates/custom-copilot-assistant-new/src/prompts/planner/actions.json b/templates/custom-copilot-assistant-new/src/prompts/planner/actions.json new file mode 100644 index 00000000..a552ec88 --- /dev/null +++ b/templates/custom-copilot-assistant-new/src/prompts/planner/actions.json @@ -0,0 +1,39 @@ +[ + { + "name": "createTask", + "description": "Create a new task with title and description", + "parameters": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "The title of the task to create" + }, + "description": { + "type": "string", + "description": "The detailed description of the task to create" + } + }, + "required": [ + "title", + "description" + ] + } + }, + { + "name": "deleteTask", + "description": "Delete an existing task", + "parameters": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "The title of the task to delete" + } + }, + "required": [ + "title" + ] + } + } +] \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/src/prompts/planner/config.json b/templates/custom-copilot-assistant-new/src/prompts/planner/config.json new file mode 100644 index 00000000..b2823059 --- /dev/null +++ b/templates/custom-copilot-assistant-new/src/prompts/planner/config.json @@ -0,0 +1,20 @@ +{ + "schema": 1.1, + "description": "A bot that can chat or manage tasks.", + "type": "completion", + "completion": { + "completion_type": "chat", + "include_history": true, + "include_input": true, + "max_input_tokens": 2800, + "max_tokens": 1000, + "temperature": 0.2, + "top_p": 0.0, + "presence_penalty": 0.6, + "frequency_penalty": 0.0, + "stop_sequences": [] + }, + "augmentation": { + "augmentation_type": "monologue" + } +} \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/src/prompts/planner/skprompt.txt b/templates/custom-copilot-assistant-new/src/prompts/planner/skprompt.txt new file mode 100644 index 00000000..b5134c30 --- /dev/null +++ b/templates/custom-copilot-assistant-new/src/prompts/planner/skprompt.txt @@ -0,0 +1,10 @@ +You are an AI assistant that can +- chat with user +- manage tasks for user + +instructions: +- only create task user has explicitly asked to create. +- always show the short description of the task after creating or deleting it. + +Current tasks: +{{$conversation.tasks}} \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/src/requirements.txt b/templates/custom-copilot-assistant-new/src/requirements.txt new file mode 100644 index 00000000..3640ac1c --- /dev/null +++ b/templates/custom-copilot-assistant-new/src/requirements.txt @@ -0,0 +1,3 @@ +python-dotenv +aiohttp +teams-ai~=1.2.0 \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/src/state.py b/templates/custom-copilot-assistant-new/src/state.py new file mode 100644 index 00000000..c7407222 --- /dev/null +++ b/templates/custom-copilot-assistant-new/src/state.py @@ -0,0 +1,30 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Any, Dict, Optional + +from botbuilder.core import Storage, TurnContext +from teams.state import TurnState, ConversationState, UserState, TempState + + +class AppConversationState(ConversationState): + tasks: Dict[str, Any]=None + + @classmethod + async def load(cls, context: TurnContext, storage: Optional[Storage] = None) -> "AppConversationState": + state = await super().load(context, storage) + return cls(**state) + + +class AppTurnState(TurnState[AppConversationState, UserState, TempState]): + conversation: AppConversationState + + @classmethod + async def load(cls, context: TurnContext, storage: Optional[Storage] = None) -> "AppTurnState": + return cls( + conversation=await AppConversationState.load(context, storage), + user=await UserState.load(context, storage), + temp=await TempState.load(context, storage), + ) \ No newline at end of file diff --git a/templates/custom-copilot-assistant-new/teamsapp.local.yml b/templates/custom-copilot-assistant-new/teamsapp.local.yml new file mode 100644 index 00000000..81898b0a --- /dev/null +++ b/templates/custom-copilot-assistant-new/teamsapp.local.yml @@ -0,0 +1,79 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: custom-copilot-assistant-new${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: custom-copilot-assistant-new${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: custom-copilot-assistant-new + 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 + # 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 + # Teams 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 + +deploy: + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} diff --git a/templates/custom-copilot-assistant-new/teamsapp.testtool.yml b/templates/custom-copilot-assistant-new/teamsapp.testtool.yml new file mode 100644 index 00000000..40130167 --- /dev/null +++ b/templates/custom-copilot-assistant-new/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.1 + symlinkDir: ./devTools/teamsapptester + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} + BOT_ID: "" + BOT_PASSWORD: "" + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} diff --git a/templates/custom-copilot-assistant-new/teamsapp.yml b/templates/custom-copilot-assistant-new/teamsapp.yml new file mode 100644 index 00000000..477685c0 --- /dev/null +++ b/templates/custom-copilot-assistant-new/teamsapp.yml @@ -0,0 +1,137 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsfx provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: custom-copilot-assistant-new${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: custom-copilot-assistant-new${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + - 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-tab + # Teams 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 + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # 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 + # Teams 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 'teamsfx deploy' is executed +deploy: + # 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: src + # Ignore file location, leave blank will ignore nothing + ignoreFile: .webappignore + # 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}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: 192e46f6-cf4b-4c8a-9075-ec5ad44eb455 diff --git a/templates/custom-copilot-assistant/.gitignore b/templates/custom-copilot-assistant/.gitignore new file mode 100644 index 00000000..75baccc4 --- /dev/null +++ b/templates/custom-copilot-assistant/.gitignore @@ -0,0 +1,18 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +env/.env.testtool +.env +appPackage/build + +# python virtual environment +.venv/ +__pycache__/ + +# others +.deployment/ +node_modules/ +devTools/*.log + +# Dev tool directories +/devTools/ \ No newline at end of file diff --git a/templates/custom-copilot-assistant/.vscode/extensions.json b/templates/custom-copilot-assistant/.vscode/extensions.json new file mode 100644 index 00000000..760a0b1d --- /dev/null +++ b/templates/custom-copilot-assistant/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension", + "ms-python.python" + ] +} \ No newline at end of file diff --git a/templates/custom-copilot-assistant/.vscode/launch.json b/templates/custom-copilot-assistant/.vscode/launch.json new file mode 100644 index 00000000..a0c18a24 --- /dev/null +++ b/templates/custom-copilot-assistant/.vscode/launch.json @@ -0,0 +1,130 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Desktop)", + "type": "node", + "request": "launch", + "preLaunchTask": "Start Teams App in Desktop Client (Remote)", + "presentation": { + "group": "3-remote", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Start Python", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/src/app.py", + "cwd": "${workspaceFolder}/src", + "console": "integratedTerminal" + }, + { + "name": "Start Test Tool", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/devTools/teamsapptester/node_modules/@microsoft/teams-app-test-tool/cli.js", + "args": [ + "start" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": ["Launch App (Edge)", "Start Python"], + "cascadeTerminateToConfigurations": ["Start Python"], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "1-local", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": ["Launch App (Chrome)", "Start Python"], + "cascadeTerminateToConfigurations": ["Start Python"], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "1-local", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Desktop)", + "configurations": ["Start Python"], + "preLaunchTask": "Start Teams App in Desktop Client", + "presentation": { + "group": "1-local", + "order": 3 + }, + "stopAll": true + }, + { + "name": "Debug in Test Tool", + "configurations": [ + "Start Python", + "Start Test Tool" + ], + "cascadeTerminateToConfigurations": [ + "Start Test Tool" + ], + "preLaunchTask": "Deploy (Test Tool)", + "presentation": { + "group": "2-local", + "order": 1 + }, + "stopAll": true + } + ] +} diff --git a/templates/custom-copilot-assistant/.vscode/settings.json b/templates/custom-copilot-assistant/.vscode/settings.json new file mode 100644 index 00000000..0d3ba10b --- /dev/null +++ b/templates/custom-copilot-assistant/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ] +} \ No newline at end of file diff --git a/templates/custom-copilot-assistant/.vscode/tasks.json b/templates/custom-copilot-assistant/.vscode/tasks.json new file mode 100644 index 00000000..a964abf8 --- /dev/null +++ b/templates/custom-copilot-assistant/.vscode/tasks.json @@ -0,0 +1,135 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Check if Node.js is installed and the version is >= 12. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 56150, // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)" + ], + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool", + } + }, + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978 // app service port + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start Teams App in Desktop Client", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start desktop client" + ], + "dependsOrder": "sequence" + }, + { + "label": "Start desktop client", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true" + } + }, + { + "label": "Start Teams App in Desktop Client (Remote)", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true" + } + } + ] +} \ No newline at end of file diff --git a/templates/custom-copilot-assistant/.webappignore b/templates/custom-copilot-assistant/.webappignore new file mode 100644 index 00000000..63b9af10 --- /dev/null +++ b/templates/custom-copilot-assistant/.webappignore @@ -0,0 +1,10 @@ +.venv/ +.vscode/ +.env +env/ +__pycache__/ +README.md +teamsapp.yml +teamsapp.local.yml +teamsapp.testtool.yml +/devTools/ \ No newline at end of file diff --git a/templates/custom-copilot-assistant/README.md b/templates/custom-copilot-assistant/README.md new file mode 100644 index 00000000..241cb3ef --- /dev/null +++ b/templates/custom-copilot-assistant/README.md @@ -0,0 +1,116 @@ +# Overview of the AI Agent template + +This app template is built on top of [Teams AI library](https://aka.ms/teams-ai-library) and [OpenAI Assistants API](https://platform.openai.com/docs/assistants/overview). +It showcases how to build an AI agent in Teams capable of helping users accomplish specific tasks using natural language right in the Teams conversations, such as solving a math problem, call functions to get city weather, etc. + +## Get started with the template + +> **Prerequisites** +> +> To run the template in your local dev machine, you will need: +> +> - [Python](https://www.python.org/), version 3.8 or higher +> - [Python extension](https://code.visualstudio.com/docs/languages/python), version v2024.0.1 or higher +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +> - An account with [Azure OpenAI](https://aka.ms/oai/access). +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). + +> Please make sure you are using model version 0613 or newer (0613, 1106, 0125) or gpt-4 turbo or gpt-35 turbo. Lower versions do NOT support assistants. + +### Configurations +1. Open the command box and enter `Python: Create Environment` to create and activate your desired virtual environment. Remember to select `src/requirements.txt` as dependencies to install when creating the virtual environment. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY`, deployment name `AZURE_OPENAI_MODEL_DEPLOYMENT_NAME` and endpoint `AZURE_OPENAI_ENDPOINT`. + +### Create your own OpenAI Assistant + +Before running or debugging your bot, please follow these steps to setup your own [Azure OpenAI Assistant](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/assistant). + +**If you haven't setup any Assistant yet** + +> This app template provides script `src/utils/creator.py` to help create assistant. You can change the instructions and settings in the script to customize the assistant. +> +> After creation, you can change and manage your assistants on [Azure OpenAI Studio](https://oai.azure.com/). + +1. Run command `python src/utils/creator.py`. Remember to fill in your **Azure OpenAI key** in *env/.env.local.user* first. + ``` + > python src/utils/creator.py + ``` +1. The above command will output something like "*Created a new assistant with an ID of: **asst_xxx...***". +1. Fill in both Azure OpenAI API Key, endpoint, deployment name and the created Assistant ID into `env/.env.local.user`. + ``` + SECRET_AZURE_OPENAI_API_KEY= + AZURE_OPENAI_ENDPOINT= + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME= + AZURE_OPENAI_ASSISTANT_ID= + ``` + +**If you already have an Assistant created** + +1. Fill in both Azure OpenAI API Key, endpoint, deployment name and the created Assistant ID into `env/.env.local.user`. + ``` + SECRET_AZURE_OPENAI_API_KEY= + AZURE_OPENAI_ENDPOINT= + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME= + AZURE_OPENAI_ASSISTANT_ID= + ``` + +### Conversation with bot +1. Select the Teams Toolkit icon on the left in the VS Code toolbar. +1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. +1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. +1. You will receive a welcome message from the bot, or send any message to get a response. + +**Congratulations**! You are running an application that can now interact with users in Teams: + +> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging). + +![AI Agent in Teams](https://github.com/OfficeDev/TeamsFx/assets/37978464/fd1cf673-e7d8-4826-9cac-e9481a74ee1e) + +## What's included in the template + +| Folder | Contents | +| - | - | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| - | - | +|`src/app.py`| Hosts an aiohttp api server and exports an app module.| +|`src/bot.py`| Handles business logics for the AI Agent.| +|`src/config.py`| Defines the environment variables.| + +The following file is a script that helps you to prepare an OpenAI assistant. + +| File | Contents | +| - | - | +|`src/utils/creator.py`| Create an OpenAI assistant with defined functions and prompts.| + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| - | - | +|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +|`teamsapp.testtool.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool.| + +## Extend the template + +You can follow [Build an AI Agent in Teams](https://aka.ms/teamsfx-ai-agent) to extend the AI Agent template with more AI capabilities, like: +- [Add functions](https://aka.ms/teamsfx-ai-agent#add-functions-build-new) + +## Additional information and references + +- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) + +## Known issue +- If you use `Debug in Test Tool` to local debug, you might get an error `InternalServiceError: connect ECONNREFUSED 127.0.0.1:3978` in Test Tool console log or error message `Error: Cannot connect to your app, +please make sure your app is running or restart your app` in log panel of Test Tool web page. You can wait for Python launch console ready and then refresh the front end web page. +- When you use `Launch Remote in Teams` to remote debug after deployment, you might loose interaction with your bot. This is because the remote service needs to restart. Please wait for several minutes to retry it. \ No newline at end of file diff --git a/templates/custom-copilot-assistant/appPackage/color.png b/templates/custom-copilot-assistant/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/custom-copilot-assistant/appPackage/color.png differ diff --git a/templates/custom-copilot-assistant/appPackage/manifest.json b/templates/custom-copilot-assistant/appPackage/manifest.json new file mode 100644 index 00000000..42193f8d --- /dev/null +++ b/templates/custom-copilot-assistant/appPackage/manifest.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "custom-copilot-assistant${{APP_NAME_SUFFIX}}", + "full": "full name for custom-copilot-assistant" + }, + "description": { + "short": "short description for custom-copilot-assistant", + "full": "full description for custom-copilot-assistant" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} diff --git a/templates/custom-copilot-assistant/appPackage/outline.png b/templates/custom-copilot-assistant/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/custom-copilot-assistant/appPackage/outline.png differ diff --git a/templates/custom-copilot-assistant/env/.env.dev b/templates/custom-copilot-assistant/env/.env.dev new file mode 100644 index 00000000..4b07861c --- /dev/null +++ b/templates/custom-copilot-assistant/env/.env.dev @@ -0,0 +1,16 @@ +# 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= +BOT_DOMAIN= \ No newline at end of file diff --git a/templates/custom-copilot-assistant/infra/azure.bicep b/templates/custom-copilot-assistant/infra/azure.bicep new file mode 100644 index 00000000..7d086ac3 --- /dev/null +++ b/templates/custom-copilot-assistant/infra/azure.bicep @@ -0,0 +1,107 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@description('Required when create Azure Bot service') +param botAadAppClientId string + +@secure() +@description('Required by Bot Framework package in your bot project') +param botAadAppClientSecret string + +@secure() +@description('Required in your bot project to access Azure OpenAI service. You can get it from Azure Portal > OpenAI > Keys > Key1 > Resource Management > Endpoint') +param azureOpenaiKey string +param azureOpenaiModelDeploymentName string +param azureOpenaiEndpoint string +param assistantId string + +param webAppSKU string +param linuxFxVersion string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param location string = resourceGroup().location +param pythonVersion string = linuxFxVersion + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app,linux' + location: location + name: serverfarmsName + sku: { + name: webAppSKU + } + properties:{ + reserved: true + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app,linux' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + siteConfig: { + alwaysOn: true + appCommandLine: 'gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:app' + linuxFxVersion: pythonVersion + appSettings: [ + { + name: 'WEBSITES_CONTAINER_START_TIME_LIMIT' + value: '600' + } + { + name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' + value: 'true' + } + { + name: 'BOT_ID' + value: botAadAppClientId + } + { + name: 'BOT_PASSWORD' + value: botAadAppClientSecret + } + { + name: 'AZURE_OPENAI_API_KEY' + value: azureOpenaiKey + } + { + name: 'AZURE_OPENAI_MODEL_DEPLOYMENT_NAME' + value: azureOpenaiModelDeploymentName + } + { + name: 'AZURE_OPENAI_ENDPOINT' + value: azureOpenaiEndpoint + } + { + name: 'AZURE_OPENAI_ASSISTANT_ID' + value: assistantId + } + ] + ftpsState: 'FtpsOnly' + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + botAadAppClientId: botAadAppClientId + 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 diff --git a/templates/custom-copilot-assistant/infra/azure.parameters.json b/templates/custom-copilot-assistant/infra/azure.parameters.json new file mode 100644 index 00000000..f550f4f6 --- /dev/null +++ b/templates/custom-copilot-assistant/infra/azure.parameters.json @@ -0,0 +1,36 @@ +{ + "$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": "${{BOT_ID}}" + }, + "botAadAppClientSecret": { + "value": "${{SECRET_BOT_PASSWORD}}" + }, + "azureOpenaiKey": { + "value": "${{SECRET_AZURE_OPENAI_API_KEY}}" + }, + "azureOpenaiModelDeploymentName" : { + "value": "${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}}" + }, + "azureOpenaiEndpoint" : { + "value": "${{AZURE_OPENAI_ENDPOINT}}" + }, + "assistantId": { + "value": "${{AZURE_OPENAI_ASSISTANT_ID}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "custom-copilot-assistant" + }, + "linuxFxVersion": { + "value": "PYTHON|3.11" + } + } +} \ No newline at end of file diff --git a/templates/custom-copilot-assistant/infra/botRegistration/azurebot.bicep b/templates/custom-copilot-assistant/infra/botRegistration/azurebot.bicep new file mode 100644 index 00000000..ab67c7a5 --- /dev/null +++ b/templates/custom-copilot-assistant/infra/botRegistration/azurebot.bicep @@ -0,0 +1,37 @@ +@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 botAadAppClientId 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: botAadAppClientId + } + 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/templates/custom-copilot-assistant/infra/botRegistration/readme.md b/templates/custom-copilot-assistant/infra/botRegistration/readme.md new file mode 100644 index 00000000..d5416243 --- /dev/null +++ b/templates/custom-copilot-assistant/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/templates/custom-copilot-assistant/src/app.py b/templates/custom-copilot-assistant/src/app.py new file mode 100644 index 00000000..b0d6b986 --- /dev/null +++ b/templates/custom-copilot-assistant/src/app.py @@ -0,0 +1,30 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from http import HTTPStatus + +from aiohttp import web +from botbuilder.core.integration import aiohttp_error_middleware + +from bot import bot_app + +routes = web.RouteTableDef() + +@routes.post("/api/messages") +async def on_messages(req: web.Request) -> web.Response: + res = await bot_app.process(req) + + if res is not None: + return res + + return web.Response(status=HTTPStatus.OK) + +app = web.Application(middlewares=[aiohttp_error_middleware]) +app.add_routes(routes) + +from config import Config + +if __name__ == "__main__": + web.run_app(app, host="localhost", port=Config.PORT) \ No newline at end of file diff --git a/templates/custom-copilot-assistant/src/bot.py b/templates/custom-copilot-assistant/src/bot.py new file mode 100644 index 00000000..eba802f3 --- /dev/null +++ b/templates/custom-copilot-assistant/src/bot.py @@ -0,0 +1,76 @@ +import os +import sys +import traceback +from typing import Any, Dict, Optional + +from botbuilder.core import MemoryStorage, TurnContext +from teams import Application, ApplicationOptions, TeamsAdapter +from teams.ai import AIOptions +from teams.ai.planners import AssistantsPlanner, OpenAIAssistantsOptions, AzureOpenAIAssistantsOptions +from teams.state import TurnState + +from config import Config + +config = Config() + +planner = AssistantsPlanner[TurnState]( + AzureOpenAIAssistantsOptions( + api_key=config.AZURE_OPENAI_API_KEY, + endpoint=config.AZURE_OPENAI_ENDPOINT, + default_model=config.AZURE_OPENAI_MODEL_DEPLOYMENT_NAME, + assistant_id=config.AZURE_OPENAI_ASSISTANT_ID) +) + +# Define storage and application +storage = MemoryStorage() +bot_app = Application[TurnState]( + ApplicationOptions( + bot_app_id=config.APP_ID, + storage=storage, + adapter=TeamsAdapter(config), + ai=AIOptions(planner=planner), + ) +) + +@bot_app.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState): + await context.send_activity("How can I help you today?") + +@bot_app.ai.action("getCurrentWeather") +async def get_current_weather(context: TurnContext, state: TurnState): + weatherData = { + 'San Francisco, CA': { + 'f': '71.6F', + 'c': '22C', + }, + 'Los Angeles': { + 'f': '75.2F', + 'c': '24C', + }, + } + location = context.data.get("location") + if not weatherData.get(location): + return f"No weather data for ${location} found" + + return weatherData[location][context.data.get("unit") if context.data.get("unit") else 'f'] + +@bot_app.ai.action("getNickname") +async def get_nickname(context: TurnContext, state: TurnState): + nicknames = { + 'San Francisco, CA': 'The Golden City', + 'Los Angeles': 'LA', + } + location = context.data.get("location") + + return nicknames.get(location) if nicknames.get(location) else f"No nickname for ${location} found" + +@bot_app.error +async def on_error(context: TurnContext, error: Exception): + # This check writes out errors to console log .vs. app insights. + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") \ No newline at end of file diff --git a/templates/custom-copilot-assistant/src/config.py b/templates/custom-copilot-assistant/src/config.py new file mode 100644 index 00000000..4b1a2d32 --- /dev/null +++ b/templates/custom-copilot-assistant/src/config.py @@ -0,0 +1,21 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import os + +from dotenv import load_dotenv + +load_dotenv() + +class Config: + """Bot Configuration""" + + PORT = 3978 + APP_ID = os.environ.get("BOT_ID", "") + APP_PASSWORD = os.environ.get("BOT_PASSWORD", "") + AZURE_OPENAI_API_KEY = os.environ["AZURE_OPENAI_API_KEY"] # Azure OpenAI API key + AZURE_OPENAI_ENDPOINT = os.environ["AZURE_OPENAI_ENDPOINT"] # Azure OpenAI endpoint + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME = os.environ["AZURE_OPENAI_MODEL_DEPLOYMENT_NAME"] # Azure OpenAI deployment model name + AZURE_OPENAI_ASSISTANT_ID = os.environ["AZURE_OPENAI_ASSISTANT_ID"] # Azure OpenAI Assistant ID diff --git a/templates/custom-copilot-assistant/src/requirements.txt b/templates/custom-copilot-assistant/src/requirements.txt new file mode 100644 index 00000000..3640ac1c --- /dev/null +++ b/templates/custom-copilot-assistant/src/requirements.txt @@ -0,0 +1,3 @@ +python-dotenv +aiohttp +teams-ai~=1.2.0 \ No newline at end of file diff --git a/templates/custom-copilot-assistant/src/utils/creator.py b/templates/custom-copilot-assistant/src/utils/creator.py new file mode 100644 index 00000000..3962e0f9 --- /dev/null +++ b/templates/custom-copilot-assistant/src/utils/creator.py @@ -0,0 +1,75 @@ +import asyncio, os +from teams.ai.planners import AssistantsPlanner +from openai.types.beta import AssistantCreateParams +from openai.types.beta.function_tool_param import FunctionToolParam +from openai.types.shared_params import FunctionDefinition + +from dotenv import load_dotenv + +load_dotenv(f'{os.getcwd()}/env/.env.local.user') + +async def main(): + options = AssistantCreateParams( + name="Assistant", + instructions="\n".join([ + "You are an intelligent bot that can", + "- write and run code to answer math questions", + "- use the provided functions to answer questions" + ]), + tools=[ + { + "type": "code_interpreter", + }, + FunctionToolParam( + type="function", + function=FunctionDefinition( + name="getCurrentWeather", + description="Get the weather in location", + parameters={ + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state e.g. San Francisco, CA", + }, + "unit": { + "type": "string", + "enum": ["c", "f"], + }, + }, + "required": ["location"], + } + ) + ), + FunctionToolParam( + type="function", + function=FunctionDefinition( + name="getNickname", + description="Get the nickname of a city", + parameters={ + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state e.g. San Francisco, CA", + }, + }, + "required": ["location"], + } + ) + ) + ], + model=os.getenv("AZURE_OPENAI_MODEL_DEPLOYMENT_NAME"), + ) + + assistant = await AssistantsPlanner.create_assistant( + api_key=os.getenv("SECRET_AZURE_OPENAI_API_KEY"), + api_version="", + organization="", + endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"), + request=options + ) + print(assistant.tools) + print(f"Created a new assistant with an ID of: {assistant.id}") + +asyncio.run(main()) \ No newline at end of file diff --git a/templates/custom-copilot-assistant/teamsapp.local.yml b/templates/custom-copilot-assistant/teamsapp.local.yml new file mode 100644 index 00000000..a33454f5 --- /dev/null +++ b/templates/custom-copilot-assistant/teamsapp.local.yml @@ -0,0 +1,80 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: custom-copilot-assistant${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: custom-copilot-assistant${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: custom-copilot-assistant + 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 + # 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 + # Teams 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 + +deploy: + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} + AZURE_OPENAI_ASSISTANT_ID: ${{AZURE_OPENAI_ASSISTANT_ID}} diff --git a/templates/custom-copilot-assistant/teamsapp.testtool.yml b/templates/custom-copilot-assistant/teamsapp.testtool.yml new file mode 100644 index 00000000..9bb6737a --- /dev/null +++ b/templates/custom-copilot-assistant/teamsapp.testtool.yml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.1 + symlinkDir: ./devTools/teamsapptester + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} + BOT_ID: "" + BOT_PASSWORD: "" + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} + AZURE_OPENAI_ASSISTANT_ID: ${{AZURE_OPENAI_ASSISTANT_ID}} diff --git a/templates/custom-copilot-assistant/teamsapp.yml b/templates/custom-copilot-assistant/teamsapp.yml new file mode 100644 index 00000000..24b96a8f --- /dev/null +++ b/templates/custom-copilot-assistant/teamsapp.yml @@ -0,0 +1,137 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: custom-copilot-assistant${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: custom-copilot-assistant${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + - 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 + # Teams 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 + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # 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 + # Teams 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: + # 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: src + # Ignore file location, leave blank will ignore nothing + ignoreFile: .webappignore + # 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}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: d226549c-0d16-4863-8e12-4eba120d2d0f diff --git a/templates/custom-copilot-basic-python/.gitignore b/templates/custom-copilot-basic-python/.gitignore new file mode 100644 index 00000000..75baccc4 --- /dev/null +++ b/templates/custom-copilot-basic-python/.gitignore @@ -0,0 +1,18 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +env/.env.testtool +.env +appPackage/build + +# python virtual environment +.venv/ +__pycache__/ + +# others +.deployment/ +node_modules/ +devTools/*.log + +# Dev tool directories +/devTools/ \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/.vscode/extensions.json b/templates/custom-copilot-basic-python/.vscode/extensions.json new file mode 100644 index 00000000..760a0b1d --- /dev/null +++ b/templates/custom-copilot-basic-python/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension", + "ms-python.python" + ] +} \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/.vscode/launch.json b/templates/custom-copilot-basic-python/.vscode/launch.json new file mode 100644 index 00000000..a0c18a24 --- /dev/null +++ b/templates/custom-copilot-basic-python/.vscode/launch.json @@ -0,0 +1,130 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Desktop)", + "type": "node", + "request": "launch", + "preLaunchTask": "Start Teams App in Desktop Client (Remote)", + "presentation": { + "group": "3-remote", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Start Python", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/src/app.py", + "cwd": "${workspaceFolder}/src", + "console": "integratedTerminal" + }, + { + "name": "Start Test Tool", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/devTools/teamsapptester/node_modules/@microsoft/teams-app-test-tool/cli.js", + "args": [ + "start" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": ["Launch App (Edge)", "Start Python"], + "cascadeTerminateToConfigurations": ["Start Python"], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "1-local", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": ["Launch App (Chrome)", "Start Python"], + "cascadeTerminateToConfigurations": ["Start Python"], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "1-local", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Desktop)", + "configurations": ["Start Python"], + "preLaunchTask": "Start Teams App in Desktop Client", + "presentation": { + "group": "1-local", + "order": 3 + }, + "stopAll": true + }, + { + "name": "Debug in Test Tool", + "configurations": [ + "Start Python", + "Start Test Tool" + ], + "cascadeTerminateToConfigurations": [ + "Start Test Tool" + ], + "preLaunchTask": "Deploy (Test Tool)", + "presentation": { + "group": "2-local", + "order": 1 + }, + "stopAll": true + } + ] +} diff --git a/templates/custom-copilot-basic-python/.vscode/settings.json b/templates/custom-copilot-basic-python/.vscode/settings.json new file mode 100644 index 00000000..0d3ba10b --- /dev/null +++ b/templates/custom-copilot-basic-python/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ] +} \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/.vscode/tasks.json b/templates/custom-copilot-basic-python/.vscode/tasks.json new file mode 100644 index 00000000..a964abf8 --- /dev/null +++ b/templates/custom-copilot-basic-python/.vscode/tasks.json @@ -0,0 +1,135 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Check if Node.js is installed and the version is >= 12. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 56150, // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)" + ], + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool", + } + }, + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978 // app service port + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start Teams App in Desktop Client", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start desktop client" + ], + "dependsOrder": "sequence" + }, + { + "label": "Start desktop client", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true" + } + }, + { + "label": "Start Teams App in Desktop Client (Remote)", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true" + } + } + ] +} \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/.webappignore b/templates/custom-copilot-basic-python/.webappignore new file mode 100644 index 00000000..63b9af10 --- /dev/null +++ b/templates/custom-copilot-basic-python/.webappignore @@ -0,0 +1,10 @@ +.venv/ +.vscode/ +.env +env/ +__pycache__/ +README.md +teamsapp.yml +teamsapp.local.yml +teamsapp.testtool.yml +/devTools/ \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/README.md b/templates/custom-copilot-basic-python/README.md new file mode 100644 index 00000000..8b043fdf --- /dev/null +++ b/templates/custom-copilot-basic-python/README.md @@ -0,0 +1,83 @@ +# Overview of the Basic AI Chatbot template + +This app template is built on top of [Teams AI library](https://aka.ms/teams-ai-library). +This template showcases a bot app that responds to user questions like an AI assistant. This enables your users to talk with the AI assistant in Teams to find information. + + +## Get started with the template + +> **Prerequisites** +> +> To run the template in your local dev machine, you will need: +> +> - [Python](https://www.python.org/), version 3.8 to 3.11. +> - [Python extension](https://code.visualstudio.com/docs/languages/python), version v2024.0.1 or higher. +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) latest version or [Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli). +> - An account with [Azure OpenAI](https://aka.ms/oai/access). +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts). + +### Configurations +1. Open the command box and enter `Python: Create Environment` to create and activate your desired virtual environment. Remember to select `src/requirements.txt` as dependencies to install when creating the virtual environment. +1. In file *env/.env.local.user*, fill in your Azure OpenAI key `SECRET_AZURE_OPENAI_API_KEY`, deployment name `AZURE_OPENAI_MODEL_DEPLOYMENT_NAME` and endpoint `AZURE_OPENAI_ENDPOINT`. + +### Conversation with bot +1. Select the Teams Toolkit icon on the left in the VS Code toolbar. +1. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +1. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. +1. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. +1. You will receive a welcome message from the bot, or send any message to get a response. + +**Congratulations**! You are running an application that can now interact with users in Teams: + +> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging). + +![ai chat bot](https://user-images.githubusercontent.com/7642967/258726187-8306610b-579e-4301-872b-1b5e85141eff.png) + +## What's included in the template + +| Folder | Contents | +| - | - | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| - | - | +|`src/app.py`| Hosts an aiohttp api server and exports an app module.| +|`src/bot.py`| Handles business logics for the Basic AI Chatbot.| +|`src/config.py`| Defines the environment variables.| +|`src/prompts/chat/skprompt.txt`| Defines the prompt.| +|`src/prompts/chat/config.json`| Configures the prompt.| + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| - | - | +|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +|`teamsapp.testtool.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool.| + +## Extend the template + +You can follow [Build a Basic AI Chatbot in Teams](https://aka.ms/teamsfx-basic-ai-chatbot) to extend the Basic AI Chatbot template with more AI capabilities, like: +- [Customize prompt](https://aka.ms/teamsfx-basic-ai-chatbot#customize-prompt) +- [Customize user input](https://aka.ms/teamsfx-basic-ai-chatbot#customize-user-input) +- [Customize conversation history](https://aka.ms/teamsfx-basic-ai-chatbot#customize-conversation-history) +- [Customize model type](https://aka.ms/teamsfx-basic-ai-chatbot#customize-model-type) +- [Customize model parameters](https://aka.ms/teamsfx-basic-ai-chatbot#customize-model-parameters) +- [Handle messages with image](https://aka.ms/teamsfx-basic-ai-chatbot#handle-messages-with-image) + +## Additional information and references + +- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) + +## Known issue +- If you use `Debug in Test Tool` to local debug, you might get an error `InternalServiceError: connect ECONNREFUSED 127.0.0.1:3978` in Test Tool console log or error message `Error: Cannot connect to your app, +please make sure your app is running or restart your app` in log panel of Test Tool web page. You can wait for Python launch console ready and then refresh the front end web page. +- When you use `Launch Remote in Teams` to remote debug after deployment, you might loose interaction with your bot. This is because the remote service needs to restart. Please wait for several minutes to retry it. \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/appPackage/color.png b/templates/custom-copilot-basic-python/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/custom-copilot-basic-python/appPackage/color.png differ diff --git a/templates/custom-copilot-basic-python/appPackage/manifest.json b/templates/custom-copilot-basic-python/appPackage/manifest.json new file mode 100644 index 00000000..d4715678 --- /dev/null +++ b/templates/custom-copilot-basic-python/appPackage/manifest.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "custom-copilot-basic-python${{APP_NAME_SUFFIX}}", + "full": "full name for custom-copilot-basic-python" + }, + "description": { + "short": "short description for custom-copilot-basic-python", + "full": "full description for custom-copilot-basic-python" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} diff --git a/templates/custom-copilot-basic-python/appPackage/outline.png b/templates/custom-copilot-basic-python/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/custom-copilot-basic-python/appPackage/outline.png differ diff --git a/templates/custom-copilot-basic-python/env/.env.dev b/templates/custom-copilot-basic-python/env/.env.dev new file mode 100644 index 00000000..8172044c --- /dev/null +++ b/templates/custom-copilot-basic-python/env/.env.dev @@ -0,0 +1,17 @@ +# 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= +TEAMS_APP_TENANT_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= +BOT_DOMAIN= \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/infra/azure.bicep b/templates/custom-copilot-basic-python/infra/azure.bicep new file mode 100644 index 00000000..424a59e6 --- /dev/null +++ b/templates/custom-copilot-basic-python/infra/azure.bicep @@ -0,0 +1,102 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@description('Required when create Azure Bot service') +param botAadAppClientId string + +@secure() +@description('Required by Bot Framework package in your bot project') +param botAadAppClientSecret string + +@secure() +@description('Required in your bot project to access Azure OpenAI service. You can get it from Azure Portal > OpenAI > Keys > Key1 > Resource Management > Endpoint') +param azureOpenaiKey string +param azureOpenaiModelDeploymentName string +param azureOpenaiEndpoint string + +param webAppSKU string +param linuxFxVersion string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param location string = resourceGroup().location +param pythonVersion string = linuxFxVersion + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app,linux' + location: location + name: serverfarmsName + sku: { + name: webAppSKU + } + properties:{ + reserved: true + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app,linux' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + siteConfig: { + alwaysOn: true + appCommandLine: 'gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:app' + linuxFxVersion: pythonVersion + appSettings: [ + { + name: 'WEBSITES_CONTAINER_START_TIME_LIMIT' + value: '600' + } + { + name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' + value: 'true' + } + { + name: 'BOT_ID' + value: botAadAppClientId + } + { + name: 'BOT_PASSWORD' + value: botAadAppClientSecret + } + { + name: 'AZURE_OPENAI_API_KEY' + value: azureOpenaiKey + } + { + name: 'AZURE_OPENAI_MODEL_DEPLOYMENT_NAME' + value: azureOpenaiModelDeploymentName + } + { + name: 'AZURE_OPENAI_ENDPOINT' + value: azureOpenaiEndpoint + } + ] + ftpsState: 'FtpsOnly' + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + botAadAppClientId: botAadAppClientId + 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 diff --git a/templates/custom-copilot-basic-python/infra/azure.parameters.json b/templates/custom-copilot-basic-python/infra/azure.parameters.json new file mode 100644 index 00000000..132cd4c2 --- /dev/null +++ b/templates/custom-copilot-basic-python/infra/azure.parameters.json @@ -0,0 +1,33 @@ +{ + "$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": "${{BOT_ID}}" + }, + "botAadAppClientSecret": { + "value": "${{SECRET_BOT_PASSWORD}}" + }, + "azureOpenaiKey": { + "value": "${{SECRET_AZURE_OPENAI_API_KEY}}" + }, + "azureOpenaiModelDeploymentName" : { + "value": "${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}}" + }, + "azureOpenaiEndpoint" : { + "value": "${{AZURE_OPENAI_ENDPOINT}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "custom-copilot-basic-python" + }, + "linuxFxVersion": { + "value": "PYTHON|3.11" + } + } +} \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/infra/botRegistration/azurebot.bicep b/templates/custom-copilot-basic-python/infra/botRegistration/azurebot.bicep new file mode 100644 index 00000000..ab67c7a5 --- /dev/null +++ b/templates/custom-copilot-basic-python/infra/botRegistration/azurebot.bicep @@ -0,0 +1,37 @@ +@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 botAadAppClientId 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: botAadAppClientId + } + 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/templates/custom-copilot-basic-python/infra/botRegistration/readme.md b/templates/custom-copilot-basic-python/infra/botRegistration/readme.md new file mode 100644 index 00000000..d5416243 --- /dev/null +++ b/templates/custom-copilot-basic-python/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/templates/custom-copilot-basic-python/src/app.py b/templates/custom-copilot-basic-python/src/app.py new file mode 100644 index 00000000..835f655a --- /dev/null +++ b/templates/custom-copilot-basic-python/src/app.py @@ -0,0 +1,29 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" +from http import HTTPStatus + +from aiohttp import web +from botbuilder.core.integration import aiohttp_error_middleware + +from bot import bot_app + +routes = web.RouteTableDef() + +@routes.post("/api/messages") +async def on_messages(req: web.Request) -> web.Response: + res = await bot_app.process(req) + + if res is not None: + return res + + return web.Response(status=HTTPStatus.OK) + +app = web.Application(middlewares=[aiohttp_error_middleware]) +app.add_routes(routes) + +from config import Config + +if __name__ == "__main__": + web.run_app(app, host="localhost", port=Config.PORT) \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/src/bot.py b/templates/custom-copilot-basic-python/src/bot.py new file mode 100644 index 00000000..c56b7a9e --- /dev/null +++ b/templates/custom-copilot-basic-python/src/bot.py @@ -0,0 +1,58 @@ +import os +import sys +import traceback + +from botbuilder.core import MemoryStorage, TurnContext +from teams import Application, ApplicationOptions, TeamsAdapter +from teams.ai import AIOptions +from teams.ai.models import AzureOpenAIModelOptions, OpenAIModel, OpenAIModelOptions +from teams.ai.planners import ActionPlanner, ActionPlannerOptions +from teams.ai.prompts import PromptManager, PromptManagerOptions +from teams.state import TurnState + +from config import Config + +config = Config() + +# Create AI components +model: OpenAIModel + +model = OpenAIModel( + AzureOpenAIModelOptions( + api_key=config.AZURE_OPENAI_API_KEY, + default_model=config.AZURE_OPENAI_MODEL_DEPLOYMENT_NAME, + endpoint=config.AZURE_OPENAI_ENDPOINT, + ) +) + +prompts = PromptManager(PromptManagerOptions(prompts_folder=f"{os.getcwd()}/prompts")) + +planner = ActionPlanner( + ActionPlannerOptions(model=model, prompts=prompts, default_prompt="chat") +) + +# Define storage and application +storage = MemoryStorage() +bot_app = Application[TurnState]( + ApplicationOptions( + bot_app_id=config.APP_ID, + storage=storage, + adapter=TeamsAdapter(config), + ai=AIOptions(planner=planner), + ) +) + +@bot_app.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState): + await context.send_activity("How can I help you today?") + +@bot_app.error +async def on_error(context: TurnContext, error: Exception): + # This check writes out errors to console log .vs. app insights. + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/src/config.py b/templates/custom-copilot-basic-python/src/config.py new file mode 100644 index 00000000..135d4e58 --- /dev/null +++ b/templates/custom-copilot-basic-python/src/config.py @@ -0,0 +1,20 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import os + +from dotenv import load_dotenv + +load_dotenv() + +class Config: + """Bot Configuration""" + + PORT = 3978 + APP_ID = os.environ.get("BOT_ID", "") + APP_PASSWORD = os.environ.get("BOT_PASSWORD", "") + AZURE_OPENAI_API_KEY = os.environ["AZURE_OPENAI_API_KEY"] # Azure OpenAI API key + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME = os.environ["AZURE_OPENAI_MODEL_DEPLOYMENT_NAME"] # Azure OpenAI model deployment name + AZURE_OPENAI_ENDPOINT = os.environ["AZURE_OPENAI_ENDPOINT"] # Azure OpenAI endpoint diff --git a/templates/custom-copilot-basic-python/src/prompts/chat/config.json b/templates/custom-copilot-basic-python/src/prompts/chat/config.json new file mode 100644 index 00000000..7937be72 --- /dev/null +++ b/templates/custom-copilot-basic-python/src/prompts/chat/config.json @@ -0,0 +1,20 @@ +{ + "schema": 1.1, + "description": "Chat with Teams AI Bot", + "type": "completion", + "completion": { + "completion_type": "chat", + "include_history": true, + "include_input": true, + "max_input_tokens": 2800, + "max_tokens": 1000, + "temperature": 0.9, + "top_p": 0.0, + "presence_penalty": 0.6, + "frequency_penalty": 0.0, + "stop_sequences": [] + }, + "augmentation": { + "augmentation_type": "None" + } +} \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/src/prompts/chat/skprompt.txt b/templates/custom-copilot-basic-python/src/prompts/chat/skprompt.txt new file mode 100644 index 00000000..fcc88e21 --- /dev/null +++ b/templates/custom-copilot-basic-python/src/prompts/chat/skprompt.txt @@ -0,0 +1 @@ +You are an AI assistant that helps people find information. \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/src/requirements.txt b/templates/custom-copilot-basic-python/src/requirements.txt new file mode 100644 index 00000000..3640ac1c --- /dev/null +++ b/templates/custom-copilot-basic-python/src/requirements.txt @@ -0,0 +1,3 @@ +python-dotenv +aiohttp +teams-ai~=1.2.0 \ No newline at end of file diff --git a/templates/custom-copilot-basic-python/teamsapp.local.yml b/templates/custom-copilot-basic-python/teamsapp.local.yml new file mode 100644 index 00000000..6b52024c --- /dev/null +++ b/templates/custom-copilot-basic-python/teamsapp.local.yml @@ -0,0 +1,79 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: custom-copilot-basic-python${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: custom-copilot-basic-python${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: custom-copilot-basic-python + 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 + # 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 + # Teams 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 + +deploy: + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} diff --git a/templates/custom-copilot-basic-python/teamsapp.testtool.yml b/templates/custom-copilot-basic-python/teamsapp.testtool.yml new file mode 100644 index 00000000..40130167 --- /dev/null +++ b/templates/custom-copilot-basic-python/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.1 + symlinkDir: ./devTools/teamsapptester + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.env + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} + BOT_ID: "" + BOT_PASSWORD: "" + AZURE_OPENAI_API_KEY: ${{SECRET_AZURE_OPENAI_API_KEY}} + AZURE_OPENAI_MODEL_DEPLOYMENT_NAME: ${{AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}} + AZURE_OPENAI_ENDPOINT: ${{AZURE_OPENAI_ENDPOINT}} diff --git a/templates/custom-copilot-basic-python/teamsapp.yml b/templates/custom-copilot-basic-python/teamsapp.yml new file mode 100644 index 00000000..fbe4249a --- /dev/null +++ b/templates/custom-copilot-basic-python/teamsapp.yml @@ -0,0 +1,137 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsfx provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: custom-copilot-basic-python${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: custom-copilot-basic-python${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + - 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-tab + # Teams 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 + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # 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 + # Teams 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 'teamsfx deploy' is executed +deploy: + # 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: src + # Ignore file location, leave blank will ignore nothing + ignoreFile: .webappignore + # 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}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: 65dd0aed-7b74-4e0d-9964-eb8be9b949f6 diff --git a/templates/dashboard-tab-ts/.gitignore b/templates/dashboard-tab-ts/.gitignore new file mode 100644 index 00000000..7f36769d --- /dev/null +++ b/templates/dashboard-tab-ts/.gitignore @@ -0,0 +1,21 @@ +# TeamsFx files +env/.env.dev.user +env/.env.local +.DS_Store +.localConfigs +build +appPackage/build + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env +.deployment \ No newline at end of file diff --git a/templates/dashboard-tab-ts/.vscode/extensions.json b/templates/dashboard-tab-ts/.vscode/extensions.json new file mode 100644 index 00000000..aac0a6e3 --- /dev/null +++ b/templates/dashboard-tab-ts/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/templates/dashboard-tab-ts/.vscode/launch.json b/templates/dashboard-tab-ts/.vscode/launch.json new file mode 100644 index 00000000..b7527a1a --- /dev/null +++ b/templates/dashboard-tab-ts/.vscode/launch.json @@ -0,0 +1,211 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Outlook (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://outlook.office.com/host/${{M365_APP_ID}}?${account-hint}", + "presentation": { + "group": "group 2: Outlook", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Outlook (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://outlook.office.com/host/${{M365_APP_ID}}?${account-hint}", + "presentation": { + "group": "group 2: Outlook", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in the Microsoft 365 app (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://www.office.com/m365apps/${{M365_APP_ID}}?auth=2&${account-hint}", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in the Microsoft 365 app (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://www.office.com/m365apps/${{M365_APP_ID}}?auth=2&${account-hint}", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Outlook (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://outlook.office.com/host/${{local:M365_APP_ID}}?${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Outlook (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://outlook.office.com/host/${{local:M365_APP_ID}}?${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in the Microsoft 365 app (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://www.office.com/m365apps/${{local:M365_APP_ID}}?auth=2&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in the Microsoft 365 app (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://www.office.com/m365apps/${{local:M365_APP_ID}}?auth=2&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Attach to Frontend in Teams (Edge)" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Attach to Frontend in Teams (Chrome)" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Outlook (Edge)", + "configurations": [ + "Attach to Frontend in Outlook (Edge)" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 2: Outlook", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Outlook (Chrome)", + "configurations": [ + "Attach to Frontend in Outlook (Chrome)" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 2: Outlook", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in the Microsoft 365 app (Edge)", + "configurations": [ + "Attach to Frontend in the Microsoft 365 app (Edge)" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in the Microsoft 365 app (Chrome)", + "configurations": [ + "Attach to Frontend in the Microsoft 365 app (Chrome)" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 2 + }, + "stopAll": true + } + ] +} \ No newline at end of file diff --git a/templates/dashboard-tab-ts/.vscode/settings.json b/templates/dashboard-tab-ts/.vscode/settings.json new file mode 100644 index 00000000..42996202 --- /dev/null +++ b/templates/dashboard-tab-ts/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ] +} diff --git a/templates/dashboard-tab-ts/.vscode/tasks.json b/templates/dashboard-tab-ts/.vscode/tasks.json new file mode 100644 index 00000000..72413ddb --- /dev/null +++ b/templates/dashboard-tab-ts/.vscode/tasks.json @@ -0,0 +1,83 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Provision", + "Deploy", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 53000 // tab service port + ] + } + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "dependsOn": [ + "Start frontend" + ] + }, + { + "label": "Start frontend", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + }, + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Compiled|Failed|compiled|failed" + } + } + } + ] +} \ No newline at end of file diff --git a/templates/dashboard-tab-ts/README.md b/templates/dashboard-tab-ts/README.md new file mode 100644 index 00000000..67a78bb0 --- /dev/null +++ b/templates/dashboard-tab-ts/README.md @@ -0,0 +1,210 @@ +# Overview of the Dashboard template + +This template showcases an app that embeds a canvas containing multiple cards that provide an overview of content in Microsoft Teams. Start with this template you can: + +- Use widgets to display content from apps and services within your dashboard tab. +- Integrate your app with Graph API to visualize details about the implementation of the selected data. +- Create customizable dashboards that allow your business to set specific goals that help you track the information you need to view in multiple areas and across departments + +## Get started with the Dashboard template + +> **Prerequisites** +> To run the dashboard template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) +> - [Set up your dev environment for extending Teams apps across Microsoft 365](https://aka.ms/teamsfx-m365-apps-prerequisites) +> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +3. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. +4. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. + +**Congratulations**! You are running an application that can now show a dashboard in Teams: + +![Dashboard](https://github.com/OfficeDev/TeamsFx/assets/107838226/9d0f4dcc-e216-418f-a947-671957f3dbee) + +## What's included in the template + +| Folder | Contents | +| ------------ | --------------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the dashboard Teams application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| ------------------------------------ | -------------------------------------------------- | +| `src/models/chartModel.ts` | Data model for the chart widget | +| `src/models/listModel.ts` | Data model for the list widget | +| `src/services/chartService.ts` | A data retrive implementation for the chart widget | +| `src/services/listService.ts` | A data retrive implementation for the list widget | +| `src/dashboards/SampleDashboard.tsx` | A sample dashboard layout implementation | +| `src/styles/ChartWidget.css` | The chart widget style file | +| `src/styles/ListWidget.css` | The list widget style file | +| `src/widgets/ChartWidget.tsx` | A widget implementation that can display a chart | +| `src/widgets/ListWidget.tsx` | A widget implementation that can display a list | +| `src/App.css` | The style of application route | +| `src/App.tsx` | Application route | + +The following are project-related files. You generally will not need to customize these files. + +| File | Contents | +| ------------------------- | ------------------------------------ | +| `src/index.css` | The style of application entry point | +| `src/index.tsx` | Application entry point | +| `src/internal/context.ts` | TeamsFx Context | + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | + +## Extend the Dashboard template to add a new widget + +You can use the following steps to add a new widget to the dashboard: + +1. [Step 1: Define a data model](#step-1-define-a-data-model) +2. [Step 2: Create a data retrive service](#step-2-create-a-data-retrive-service) +3. [Step 3: Create a widget file](#step-3-create-a-widget-file) +4. [Step 4: Add the widget to the dashboard](#step-4-add-the-widget-to-the-dashboard) + +### Step 1: Define a data model + +Define a data model based on the business scenario, we recommend that you place the data model under the `src/models` directory. Here is an example of a data model:: + +```typescript +//sampleModel.ts +export interface SampleModel { + content: string; +} +``` + +### Step 2: Create a data retrive service + +Typically, a widget requires a service to retrieve the necessary data for displaying its content. This service can either fetch static data from a predefined source or retrieve dynamic data from a backend service or API. + +For instance, we will implement a service that returns static data and place it under the `src/services` directory. + +Here is a sample service for retrieving static data: + +```typescript +//sampleService.ts +import { SampleModel } from "../models/sampleModel"; + +export const getSampleData = (): SampleModel => { + return { content: "Hello world!" }; +}; +``` + +### Step 3: Create a widget file + +Create a widget file in the `src/widgets` folder. Inherit the `BaseWidget` class from `@microsoft/teamsfx-react`. The following table lists the methods that you can override to customize your widget. + +| Methods | Function | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `getData()` | This method is used to get the data for the widget. You can implement it to get data from the backend service or from the Microsoft Graph API | +| `header()` | Customize the content of the widget header | +| `body()` | Customize the content of the widget body | +| `footer()` | Customize the content of the widget footer | +| `styling()` | Customize the widget style | + +> All method overrides are optional. + +Here's a sample widget implementation: + +```tsx +//SampleWidget.tsx +import { Button, Text } from "@fluentui/react-components"; +import { BaseWidget } from "@microsoft/teamsfx-react"; +import { SampleModel } from "../models/sampleModel"; +import { getSampleData } from "../services/sampleService"; + +interface SampleWidgetState { + data?: SampleModel; +} + +export class SampleWidget extends BaseWidget { + override async getData(): Promise { + return { data: getSampleData() }; + } + + override header(): JSX.Element | undefined { + return Sample Widget; + } + + override body(): JSX.Element | undefined { + return
{this.state.data?.content}
; + } + + override footer(): JSX.Element | undefined { + return ; + } +} +``` + +### Step 4: Add the widget to the dashboard + +Open the `src/dashboards/SampleDashboard.tsx` file and add the widget to the implementation of the `layout` method. If you want to create a new dashboard, please refer to [How to add a new dashboard](https://aka.ms/teamsfx-dashboard-new#how-to-add-a-new-dashboard). + +```tsx +override layout(): JSX.Element | undefined { + return ( + <> + + + + + ); +} +``` + +Optional: If you want to arrange multiple widgets in the same column, you can refer to the following code snippet: + +```css +.one-column { + display: grid; + gap: 20px; + grid-template-rows: 1fr 1fr; +} +``` + +```jsx +override layout(): JSX.Element | undefined { + return ( + <> + +
+ + +
+ + ); +} +``` + +Congratulations, you've just added your own widget! To learn more about the dashboard template, [visit the documentation](https://aka.ms/teamsfx-dashboard-new). You can find more scenarios like: + +- [Customize the widget](https://aka.ms/teamsfx-dashboard-new#customize-the-widget) +- [Customize the dashboard layout](https://aka.ms/teamsfx-dashboard-new#customize-the-dashboard-layout) +- [Create a data loader](https://aka.ms/teamsfx-dashboard-new#how-to-include-a-data-loader) +- [Refresh data based on the schedule](https://aka.ms/teamsfx-dashboard-new#how-to-refresh-data-as-scheduled) +- [Handle empty state](https://aka.ms/teamsfx-dashboard-new#how-to-handle-empty-state) +- [Add a new dashboard](https://aka.ms/teamsfx-dashboard-new#how-to-add-a-new-dashboard) +- [Use Microsoft Graph Toolkit as widget content](https://aka.ms/teamsfx-dashboard-new#how-to-use-microsoft-graph-toolkit-as-widget-content) +- [Embed Power BI to dashboard](https://aka.ms/teamsfx-dashboard-new#how-to-embed-power-bi-to-dashboard) +- [How to add a new Graph API call](https://aka.ms/teamsfx-dashboard-new#how-to-add-a-new-graph-api-call) +- [Enable the app for multi-tenant](https://github.com/OfficeDev/TeamsFx/wiki/Multi-tenancy-Support-for-Azure-AD-app) +- [Preview the app on mobile clients](https://aka.ms/teamsfx-mobile) + +## Additional resources + +- [Fluent UI](https://react.fluentui.dev/?path=/docs/concepts-introduction--page) +- [Fluent UI React Charting Example](https://fluentuipr.z22.web.core.windows.net/heads/master/react-charting/demo/index.html#/) diff --git a/templates/dashboard-tab-ts/appPackage/color.png b/templates/dashboard-tab-ts/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/dashboard-tab-ts/appPackage/color.png differ diff --git a/templates/dashboard-tab-ts/appPackage/manifest.json b/templates/dashboard-tab-ts/appPackage/manifest.json new file mode 100644 index 00000000..5754a0a1 --- /dev/null +++ b/templates/dashboard-tab-ts/appPackage/manifest.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "${{TAB_ENDPOINT}}", + "privacyUrl": "${{TAB_ENDPOINT}}/index.html#/privacy", + "termsOfUseUrl": "${{TAB_ENDPOINT}}/index.html#/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "dashboard-tab-ts${{APP_NAME_SUFFIX}}", + "full": "Full name for dashboard-tab-ts" + }, + "description": { + "short": "Short description of dashboard-tab-ts", + "full": "Full description of dashboard-tab-ts" + }, + "accentColor": "#FFFFFF", + "bots": [], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [ + { + "entityId": "index0", + "name": "Dashboard", + "contentUrl": "${{TAB_ENDPOINT}}/index.html#/tab", + "websiteUrl": "${{TAB_ENDPOINT}}/index.html#/tab", + "scopes": [ + "personal", + "groupChat", + "team" + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "${{TAB_DOMAIN}}" + ] +} \ No newline at end of file diff --git a/templates/dashboard-tab-ts/appPackage/outline.png b/templates/dashboard-tab-ts/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/dashboard-tab-ts/appPackage/outline.png differ diff --git a/templates/dashboard-tab-ts/env/.env.dev b/templates/dashboard-tab-ts/env/.env.dev new file mode 100644 index 00000000..dbd3838d --- /dev/null +++ b/templates/dashboard-tab-ts/env/.env.dev @@ -0,0 +1,14 @@ +# 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. +TEAMS_APP_ID= +TAB_ENDPOINT= diff --git a/templates/dashboard-tab-ts/infra/azure.bicep b/templates/dashboard-tab-ts/infra/azure.bicep new file mode 100644 index 00000000..7c2375e7 --- /dev/null +++ b/templates/dashboard-tab-ts/infra/azure.bicep @@ -0,0 +1,24 @@ +@maxLength(20) +@minLength(4) +param resourceBaseName string +param staticWebAppSku string +param staticWebAppName string = resourceBaseName + +// Azure Static Web Apps that hosts your static web site +resource swa 'Microsoft.Web/staticSites@2022-09-01' = { + name: staticWebAppName + // SWA do not need location setting + location: 'centralus' + sku: { + name: staticWebAppSku + tier: staticWebAppSku + } + properties:{} +} + +var siteDomain = swa.properties.defaultHostname + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output AZURE_STATIC_WEB_APPS_RESOURCE_ID string = swa.id +output TAB_DOMAIN string = siteDomain +output TAB_ENDPOINT string = 'https://${siteDomain}' diff --git a/templates/dashboard-tab-ts/infra/azure.parameters.json b/templates/dashboard-tab-ts/infra/azure.parameters.json new file mode 100644 index 00000000..0a6927bc --- /dev/null +++ b/templates/dashboard-tab-ts/infra/azure.parameters.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "tab${{RESOURCE_SUFFIX}}" + }, + "staticWebAppSku": { + "value": "Free" + } + } + } \ No newline at end of file diff --git a/templates/dashboard-tab-ts/package.json b/templates/dashboard-tab-ts/package.json new file mode 100644 index 00000000..23455026 --- /dev/null +++ b/templates/dashboard-tab-ts/package.json @@ -0,0 +1,54 @@ +{ + "name": "dashboardtabts", + "version": "0.1.0", + "engines": { + "node": "16 || 18" + }, + "private": true, + "dependencies": { + "@fluentui/react-charting": "^5.14.10", + "@fluentui/react-components": "^9.18.0", + "@fluentui/react-icons": "^2.0.186", + "@microsoft/teams-js": "^2.19.0", + "@microsoft/teamsfx": "^2.2.0", + "@microsoft/teamsfx-react": "^3.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.0", + "react-scripts": "^5.0.1" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/react-router-dom": "^5.3.3", + "env-cmd": "^10.1.0", + "typescript": "^4.1.2" + }, + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run start", + "start": "react-scripts start", + "build": "react-scripts build", + "eject": "react-scripts eject", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "homepage": "." +} \ No newline at end of file diff --git a/templates/dashboard-tab-ts/public/index.html b/templates/dashboard-tab-ts/public/index.html new file mode 100644 index 00000000..c61bcb44 --- /dev/null +++ b/templates/dashboard-tab-ts/public/index.html @@ -0,0 +1,17 @@ + + + + + + + Microsoft Teams Tab + + + + +
+ + diff --git a/templates/dashboard-tab-ts/src/App.css b/templates/dashboard-tab-ts/src/App.css new file mode 100644 index 00000000..2575d18e --- /dev/null +++ b/templates/dashboard-tab-ts/src/App.css @@ -0,0 +1,7 @@ +#fluent-provider { + background: var(--colorTransparentBackground); +} + +#spinner { + margin: 100; +} \ No newline at end of file diff --git a/templates/dashboard-tab-ts/src/App.tsx b/templates/dashboard-tab-ts/src/App.tsx new file mode 100644 index 00000000..233c53bb --- /dev/null +++ b/templates/dashboard-tab-ts/src/App.tsx @@ -0,0 +1,52 @@ +import "./App.css"; + +import { HashRouter as Router, Navigate, Route, Routes } from "react-router-dom"; + +import { + FluentProvider, + Spinner, + teamsDarkTheme, + teamsHighContrastTheme, + teamsLightTheme, +} from "@fluentui/react-components"; +import { useTeams } from "@microsoft/teamsfx-react"; + +import SampleDashboard from "./dashboards/SampleDashboard"; +import { TeamsFxContext } from "./internal/context"; +import Privacy from "./Privacy"; +import TermsOfUse from "./TermsOfUse"; + +/** + * The main app which handles the initialization and routing + * of the app. + */ +export default function App() { + const { loading, themeString } = useTeams()[0]; + return ( + + + + {loading ? ( + + ) : ( + + } /> + } /> + } /> + } /> + + )} + + + + ); +} diff --git a/templates/dashboard-tab-ts/src/Privacy.tsx b/templates/dashboard-tab-ts/src/Privacy.tsx new file mode 100644 index 00000000..048cb6f0 --- /dev/null +++ b/templates/dashboard-tab-ts/src/Privacy.tsx @@ -0,0 +1,17 @@ +import React from "react"; +/** + * This component is used to display the required + * privacy statement which can be found in a link in the + * about tab. + */ +class Privacy extends React.Component { + render() { + return ( +
+

Privacy Statement

+
+ ); + } +} + +export default Privacy; diff --git a/templates/dashboard-tab-ts/src/TermsOfUse.tsx b/templates/dashboard-tab-ts/src/TermsOfUse.tsx new file mode 100644 index 00000000..f3a5c10a --- /dev/null +++ b/templates/dashboard-tab-ts/src/TermsOfUse.tsx @@ -0,0 +1,17 @@ +import React from "react"; +/** + * This component is used to display the required + * terms of use statement which can be found in a + * link in the about tab. + */ +class TermsOfUse extends React.Component { + render() { + return ( +
+

Terms of Use

+
+ ); + } +} + +export default TermsOfUse; diff --git a/templates/dashboard-tab-ts/src/dashboards/SampleDashboard.tsx b/templates/dashboard-tab-ts/src/dashboards/SampleDashboard.tsx new file mode 100644 index 00000000..29d55383 --- /dev/null +++ b/templates/dashboard-tab-ts/src/dashboards/SampleDashboard.tsx @@ -0,0 +1,15 @@ +import { BaseDashboard } from "@microsoft/teamsfx-react"; + +import ChartWidget from "../widgets/ChartWidget"; +import ListWidget from "../widgets/ListWidget"; + +export default class SampleDashboard extends BaseDashboard { + override layout(): JSX.Element | undefined { + return ( + <> + + + + ); + } +} diff --git a/templates/dashboard-tab-ts/src/index.css b/templates/dashboard-tab-ts/src/index.css new file mode 100644 index 00000000..e64ea560 --- /dev/null +++ b/templates/dashboard-tab-ts/src/index.css @@ -0,0 +1,16 @@ +* { + box-sizing: border-box; + /* outline: 1px dashed #f00; */ +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow: auto; +} + +::-webkit-scrollbar { + display: none; +} diff --git a/templates/dashboard-tab-ts/src/index.tsx b/templates/dashboard-tab-ts/src/index.tsx new file mode 100644 index 00000000..c329de3f --- /dev/null +++ b/templates/dashboard-tab-ts/src/index.tsx @@ -0,0 +1,10 @@ +import "./index.css"; + +import React from "react"; +import { createRoot } from "react-dom/client"; + +import App from "./App"; + +const container = document.getElementById("root"); +const root = createRoot(container!); +root.render(); diff --git a/templates/dashboard-tab-ts/src/internal/context.ts b/templates/dashboard-tab-ts/src/internal/context.ts new file mode 100644 index 00000000..e78f5ca2 --- /dev/null +++ b/templates/dashboard-tab-ts/src/internal/context.ts @@ -0,0 +1,11 @@ +import { createContext } from "react"; + +import { Theme } from "@fluentui/react-components"; + +export const TeamsFxContext = createContext<{ + theme?: Theme; + themeString: string; +}>({ + theme: undefined, + themeString: "", +}); diff --git a/templates/dashboard-tab-ts/src/models/chartModel.ts b/templates/dashboard-tab-ts/src/models/chartModel.ts new file mode 100644 index 00000000..1dd9ff27 --- /dev/null +++ b/templates/dashboard-tab-ts/src/models/chartModel.ts @@ -0,0 +1,10 @@ +export enum DayRange { + Seven, + Thirty, + Sixty, +} + +export interface TimeModel { + range: DayRange; + name: string; +} diff --git a/templates/dashboard-tab-ts/src/models/listModel.ts b/templates/dashboard-tab-ts/src/models/listModel.ts new file mode 100644 index 00000000..562a468c --- /dev/null +++ b/templates/dashboard-tab-ts/src/models/listModel.ts @@ -0,0 +1,5 @@ +export interface ListModel { + id: string; + title: string; + content: string; +} diff --git a/templates/dashboard-tab-ts/src/services/chartService.ts b/templates/dashboard-tab-ts/src/services/chartService.ts new file mode 100644 index 00000000..1c277924 --- /dev/null +++ b/templates/dashboard-tab-ts/src/services/chartService.ts @@ -0,0 +1,247 @@ +import { DayRange, TimeModel } from "../models/chartModel"; + +export function getTimeRange(): TimeModel[] { + return [ + { range: DayRange.Seven, name: "7 days" }, + { range: DayRange.Thirty, name: "30 days" }, + { range: DayRange.Sixty, name: "60 days" }, + ]; +} + +export function getChart1Points(range: DayRange): any[] { + switch (range) { + case DayRange.Seven: + return chart1Points_7D; + case DayRange.Thirty: + return chart1Points_30D; + case DayRange.Sixty: + return chart1Points_60D; + default: + return []; + } +} + +export function getChart2Points(range: DayRange): any[] { + switch (range) { + case DayRange.Seven: + return chart2Points_7D; + case DayRange.Thirty: + return chart2Points_30D; + case DayRange.Sixty: + return chart2Points_60D; + default: + return []; + } +} + +const chart1Points_7D = [ + { x: new Date("01/01"), y: 18000 }, + { x: new Date("01/06"), y: 14000 }, + { x: new Date("01/11"), y: 19000 }, + { x: new Date("01/16"), y: 13000 }, + { x: new Date("01/21"), y: 21000 }, + { x: new Date("01/26"), y: 18000 }, + { x: new Date("01/31"), y: 23000 }, +]; + +const chart2Points_7D = [ + { x: new Date("01/01"), y: 8000 }, + { x: new Date("01/06"), y: 10000 }, + { x: new Date("01/11"), y: 100 }, + { x: new Date("01/16"), y: 9000 }, + { x: new Date("01/21"), y: 11000 }, + { x: new Date("01/26"), y: 7000 }, + { x: new Date("01/31"), y: 7200 }, +]; + +const chart1Points_30D = [ + { x: new Date("01/01"), y: 18000 }, + { x: new Date("01/02"), y: 14000 }, + { x: new Date("01/03"), y: 19000 }, + { x: new Date("01/04"), y: 13000 }, + { x: new Date("01/05"), y: 21000 }, + { x: new Date("01/06"), y: 18000 }, + { x: new Date("01/07"), y: 18000 }, + { x: new Date("01/08"), y: 14000 }, + { x: new Date("01/09"), y: 19000 }, + { x: new Date("01/10"), y: 13000 }, + { x: new Date("01/11"), y: 21000 }, + { x: new Date("01/12"), y: 18000 }, + { x: new Date("01/13"), y: 23000 }, + { x: new Date("01/14"), y: 18000 }, + { x: new Date("01/15"), y: 14000 }, + { x: new Date("01/16"), y: 19000 }, + { x: new Date("01/17"), y: 13000 }, + { x: new Date("01/18"), y: 21000 }, + { x: new Date("01/19"), y: 18000 }, + { x: new Date("01/20"), y: 23000 }, + { x: new Date("01/21"), y: 18000 }, + { x: new Date("01/22"), y: 14000 }, + { x: new Date("01/23"), y: 19000 }, + { x: new Date("01/24"), y: 13000 }, + { x: new Date("01/25"), y: 21000 }, + { x: new Date("01/26"), y: 18000 }, + { x: new Date("01/27"), y: 23000 }, + { x: new Date("01/28"), y: 13000 }, + { x: new Date("01/29"), y: 21000 }, + { x: new Date("01/30"), y: 18000 }, + { x: new Date("01/31"), y: 23000 }, +]; + +const chart2Points_30D = [ + { x: new Date("01/01"), y: 18000 }, + { x: new Date("01/02"), y: 18000 }, + { x: new Date("01/03"), y: 18000 }, + { x: new Date("01/04"), y: 18000 }, + { x: new Date("01/05"), y: 18000 }, + { x: new Date("01/06"), y: 14000 }, + { x: new Date("01/07"), y: 18000 }, + { x: new Date("01/08"), y: 18000 }, + { x: new Date("01/09"), y: 18000 }, + { x: new Date("01/10"), y: 18000 }, + { x: new Date("01/11"), y: 19000 }, + { x: new Date("01/12"), y: 18000 }, + { x: new Date("01/13"), y: 18000 }, + { x: new Date("01/14"), y: 18000 }, + { x: new Date("01/15"), y: 18000 }, + { x: new Date("01/16"), y: 13000 }, + { x: new Date("01/17"), y: 18000 }, + { x: new Date("01/18"), y: 18000 }, + { x: new Date("01/19"), y: 18000 }, + { x: new Date("01/20"), y: 18000 }, + { x: new Date("01/21"), y: 12000 }, + { x: new Date("01/22"), y: 18000 }, + { x: new Date("01/23"), y: 18000 }, + { x: new Date("01/24"), y: 18000 }, + { x: new Date("01/25"), y: 18000 }, + { x: new Date("01/26"), y: 14000 }, + { x: new Date("01/27"), y: 18000 }, + { x: new Date("01/28"), y: 18000 }, + { x: new Date("01/29"), y: 18000 }, + { x: new Date("01/30"), y: 18000 }, + { x: new Date("01/31"), y: 15000 }, +]; + +const chart1Points_60D = [ + { x: new Date("01/01"), y: 18000 }, + { x: new Date("01/02"), y: 14000 }, + { x: new Date("01/03"), y: 19000 }, + { x: new Date("01/04"), y: 13000 }, + { x: new Date("01/05"), y: 21000 }, + { x: new Date("01/06"), y: 18000 }, + { x: new Date("01/07"), y: 18000 }, + { x: new Date("01/08"), y: 14000 }, + { x: new Date("01/09"), y: 19000 }, + { x: new Date("01/10"), y: 13000 }, + { x: new Date("01/11"), y: 21000 }, + { x: new Date("01/12"), y: 18000 }, + { x: new Date("01/13"), y: 23000 }, + { x: new Date("01/14"), y: 18000 }, + { x: new Date("01/15"), y: 14000 }, + { x: new Date("01/16"), y: 19000 }, + { x: new Date("01/17"), y: 13000 }, + { x: new Date("01/18"), y: 21000 }, + { x: new Date("01/19"), y: 18000 }, + { x: new Date("01/20"), y: 23000 }, + { x: new Date("01/21"), y: 18000 }, + { x: new Date("01/22"), y: 14000 }, + { x: new Date("01/23"), y: 19000 }, + { x: new Date("01/24"), y: 13000 }, + { x: new Date("01/25"), y: 21000 }, + { x: new Date("01/26"), y: 18000 }, + { x: new Date("01/27"), y: 23000 }, + { x: new Date("01/28"), y: 13000 }, + { x: new Date("01/29"), y: 21000 }, + { x: new Date("01/30"), y: 18000 }, + { x: new Date("01/31"), y: 23000 }, + { x: new Date("02/01"), y: 18000 }, + { x: new Date("02/02"), y: 14000 }, + { x: new Date("02/03"), y: 19000 }, + { x: new Date("02/04"), y: 13000 }, + { x: new Date("02/05"), y: 21000 }, + { x: new Date("02/06"), y: 18000 }, + { x: new Date("02/07"), y: 18000 }, + { x: new Date("02/08"), y: 14000 }, + { x: new Date("02/09"), y: 19000 }, + { x: new Date("02/10"), y: 13000 }, + { x: new Date("02/11"), y: 21000 }, + { x: new Date("02/12"), y: 18000 }, + { x: new Date("02/13"), y: 23000 }, + { x: new Date("02/14"), y: 18000 }, + { x: new Date("02/15"), y: 14000 }, + { x: new Date("02/16"), y: 19000 }, + { x: new Date("02/17"), y: 13000 }, + { x: new Date("02/18"), y: 21000 }, + { x: new Date("02/19"), y: 18000 }, + { x: new Date("02/20"), y: 23000 }, + { x: new Date("02/21"), y: 18000 }, + { x: new Date("02/22"), y: 14000 }, + { x: new Date("02/23"), y: 19000 }, + { x: new Date("02/24"), y: 13000 }, + { x: new Date("02/25"), y: 21000 }, + { x: new Date("02/26"), y: 18000 }, + { x: new Date("02/27"), y: 23000 }, + { x: new Date("02/28"), y: 13000 }, +]; + +const chart2Points_60D = [ + { x: new Date("01/01"), y: 18000 }, + { x: new Date("01/02"), y: 18000 }, + { x: new Date("01/03"), y: 18000 }, + { x: new Date("01/04"), y: 18000 }, + { x: new Date("01/05"), y: 18000 }, + { x: new Date("01/06"), y: 14000 }, + { x: new Date("01/07"), y: 18000 }, + { x: new Date("01/08"), y: 18000 }, + { x: new Date("01/09"), y: 18000 }, + { x: new Date("01/10"), y: 18000 }, + { x: new Date("01/11"), y: 19000 }, + { x: new Date("01/12"), y: 18000 }, + { x: new Date("01/13"), y: 18000 }, + { x: new Date("01/14"), y: 18000 }, + { x: new Date("01/15"), y: 18000 }, + { x: new Date("01/16"), y: 13000 }, + { x: new Date("01/17"), y: 18000 }, + { x: new Date("01/18"), y: 18000 }, + { x: new Date("01/19"), y: 18000 }, + { x: new Date("01/20"), y: 18000 }, + { x: new Date("01/21"), y: 12000 }, + { x: new Date("01/22"), y: 18000 }, + { x: new Date("01/23"), y: 18000 }, + { x: new Date("01/24"), y: 18000 }, + { x: new Date("01/25"), y: 18000 }, + { x: new Date("01/26"), y: 14000 }, + { x: new Date("01/27"), y: 18000 }, + { x: new Date("01/28"), y: 18000 }, + { x: new Date("01/29"), y: 18000 }, + { x: new Date("01/30"), y: 18000 }, + { x: new Date("01/31"), y: 15000 }, + { x: new Date("02/01"), y: 15000 }, + { x: new Date("02/02"), y: 15000 }, + { x: new Date("02/03"), y: 15000 }, + { x: new Date("02/04"), y: 15000 }, + { x: new Date("02/05"), y: 15000 }, + { x: new Date("02/06"), y: 15000 }, + { x: new Date("02/07"), y: 15000 }, + { x: new Date("02/08"), y: 15000 }, + { x: new Date("02/09"), y: 15000 }, + { x: new Date("02/10"), y: 15000 }, + { x: new Date("02/11"), y: 15000 }, + { x: new Date("02/12"), y: 15000 }, + { x: new Date("02/13"), y: 15000 }, + { x: new Date("02/14"), y: 15000 }, + { x: new Date("02/15"), y: 15000 }, + { x: new Date("02/16"), y: 15000 }, + { x: new Date("02/17"), y: 15000 }, + { x: new Date("02/18"), y: 15000 }, + { x: new Date("02/19"), y: 15000 }, + { x: new Date("02/20"), y: 15000 }, + { x: new Date("02/21"), y: 15000 }, + { x: new Date("02/22"), y: 15000 }, + { x: new Date("02/23"), y: 15000 }, + { x: new Date("02/24"), y: 15000 }, + { x: new Date("02/25"), y: 15000 }, + { x: new Date("02/26"), y: 15000 }, + { x: new Date("02/27"), y: 15000 }, + { x: new Date("02/28"), y: 15000 }, +]; diff --git a/templates/dashboard-tab-ts/src/services/listService.ts b/templates/dashboard-tab-ts/src/services/listService.ts new file mode 100644 index 00000000..3b45e162 --- /dev/null +++ b/templates/dashboard-tab-ts/src/services/listService.ts @@ -0,0 +1,23 @@ +import { ListModel } from "../models/listModel"; + +/** + * Retrive sample data + * @returns data for list widget + */ +export const getListData = (): ListModel[] => [ + { + id: "id1", + title: "Lorem ipsum", + content: "Lorem ipsum dolor sit amet", + }, + { + id: "id2", + title: "Lorem ipsum", + content: "Lorem ipsum dolor sit amet", + }, + { + id: "id3", + title: "Lorem ipsum", + content: "Lorem ipsum dolor sit amet", + }, +]; diff --git a/templates/dashboard-tab-ts/src/styles/ChartWidget.css b/templates/dashboard-tab-ts/src/styles/ChartWidget.css new file mode 100644 index 00000000..acdb4cd7 --- /dev/null +++ b/templates/dashboard-tab-ts/src/styles/ChartWidget.css @@ -0,0 +1,33 @@ +.time-span { + display: grid; + gap: var(--spacingVerticalXL) var(--spacingHorizontalXL); + grid-template-columns: repeat(3, max-content); +} + +.time-span button { + font-size: var(--fontSizeBase100); + line-height: var(--lineHeightBase100); + min-width: max-content; +} + +.area-chart { + position: relative; + height: 200px; + width: 100%; +} + +.area-chart .tick text { + fill: var(--colorNeutralForeground1); +} + +.area-chart div { + color: var(--colorNeutralForeground1); +} + +.area-chart line { + stroke: var(--colorNeutralStroke2); +} + +#chart-footer { + color: var(--colorBrandForeground1); +} \ No newline at end of file diff --git a/templates/dashboard-tab-ts/src/styles/ListWidget.css b/templates/dashboard-tab-ts/src/styles/ListWidget.css new file mode 100644 index 00000000..f4f93883 --- /dev/null +++ b/templates/dashboard-tab-ts/src/styles/ListWidget.css @@ -0,0 +1,30 @@ +.list-body { + display: grid; + row-gap: var(--spacingVerticalS); + align-content: start; + min-width: 13.5rem; +} + +.list-body div { + display: grid; +} + +.list-body .divider { + margin: 0 -2rem 0.5rem; + height: var(--strokeWidthThin); + background: var(--colorNeutralStroke2); +} + +.list-body .title { + font-weight: var(--fontWeightSemibold); +} + +.list-body .content { + font-size: var(--fontSizeBase200); +} + +.loading { + display: grid; + justify-content: center; + height: 100%; +} diff --git a/templates/dashboard-tab-ts/src/widgets/ChartWidget.tsx b/templates/dashboard-tab-ts/src/widgets/ChartWidget.tsx new file mode 100644 index 00000000..5410d910 --- /dev/null +++ b/templates/dashboard-tab-ts/src/widgets/ChartWidget.tsx @@ -0,0 +1,104 @@ +import "../styles/ChartWidget.css"; + +import { AreaChart, IChartProps } from "@fluentui/react-charting"; +import { Button, Text, ToggleButton } from "@fluentui/react-components"; +import { + ArrowRight16Filled, + DataPie24Regular, + MoreHorizontal32Regular, +} from "@fluentui/react-icons"; +import { BaseWidget } from "@microsoft/teamsfx-react"; + +import { DayRange, TimeModel } from "../models/chartModel"; +import { getChart1Points, getChart2Points, getTimeRange } from "../services/chartService"; + +interface IChartWidgetState { + selectedRange: DayRange; + chartProps: IChartProps; + timeRange: TimeModel[]; +} + +export default class ChartWidget extends BaseWidget { + override async getData(): Promise { + return { + selectedRange: DayRange.Seven, + chartProps: this.retriveChartsData(DayRange.Seven), + timeRange: getTimeRange(), + }; + } + + override header(): JSX.Element | undefined { + return ( +
+ + Your chart +
+ ); + } + + override body(): JSX.Element | undefined { + return ( +
+
+ {this.state.timeRange?.map((t: TimeModel, i: any) => { + return ( + + this.setState({ + chartProps: this.retriveChartsData(t.range), + selectedRange: t.range, + }) + } + > + {t.name} + + ); + })} +
+ + {this.state.chartProps && ( +
+ +
+ )} +
+ ); + } + + override footer(): JSX.Element | undefined { + return ( + + ); + } + + private retriveChartsData(r: DayRange): IChartProps { + const chartPoints = [ + { + legend: "Line 1", + data: getChart1Points(r), + color: "#6264A7", + }, + { + legend: "Line 2", + data: getChart2Points(r), + color: "#D9DBDB", + }, + ]; + const chartData = { + lineChartData: chartPoints, + }; + return chartData; + } +} diff --git a/templates/dashboard-tab-ts/src/widgets/ListWidget.tsx b/templates/dashboard-tab-ts/src/widgets/ListWidget.tsx new file mode 100644 index 00000000..39de6c9e --- /dev/null +++ b/templates/dashboard-tab-ts/src/widgets/ListWidget.tsx @@ -0,0 +1,48 @@ +import "../styles/ListWidget.css"; + +import { Button, Text } from "@fluentui/react-components"; +import { List28Filled, MoreHorizontal32Regular } from "@fluentui/react-icons"; +import { BaseWidget } from "@microsoft/teamsfx-react"; + +import { ListModel } from "../models/listModel"; +import { getListData } from "../services/listService"; + +interface IListWidgetState { + data: ListModel[]; +} + +export default class ListWidget extends BaseWidget { + async getData(): Promise { + return { data: getListData() }; + } + + header(): JSX.Element | undefined { + return ( +
+ + Your List +
+ ); + } + + body(): JSX.Element | undefined { + return ( +
+ {this.state.data?.map((t: ListModel) => { + return ( +
+
+ {t.title} + {t.content} +
+ ); + })} +
+ ); + } + + footer(): JSX.Element | undefined { + return ; + } +} diff --git a/templates/dashboard-tab-ts/teamsapp.local.yml b/templates/dashboard-tab-ts/teamsapp.local.yml new file mode 100644 index 00000000..5d76c42d --- /dev/null +++ b/templates/dashboard-tab-ts/teamsapp.local.yml @@ -0,0 +1,85 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: dashboard-tab-ts${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Set TAB_DOMAIN and TAB_ENDPOINT for local launch + - uses: script + with: + run: + echo "::set-teamsfx-env TAB_DOMAIN=localhost"; + echo "::set-teamsfx-env TAB_ENDPOINT=https://localhost:53000"; + # 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 + # 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 + # Teams 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 + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + devCert: + trust: true + # Write the information of installed development tool(s) into environment + # file for the specified environment variable(s). + writeToEnvironmentFile: + sslCertFile: SSL_CRT_FILE + sslKeyFile: SSL_KEY_FILE + + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs + envs: + BROWSER: none + HTTPS: true + PORT: 53000 + SSL_CRT_FILE: ${{SSL_CRT_FILE}} + SSL_KEY_FILE: ${{SSL_KEY_FILE}} diff --git a/templates/dashboard-tab-ts/teamsapp.yml b/templates/dashboard-tab-ts/teamsapp.yml new file mode 100644 index 00000000..4a77baf0 --- /dev/null +++ b/templates/dashboard-tab-ts/teamsapp.yml @@ -0,0 +1,142 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: dashboard-tab-ts${{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-tab + # Teams 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 + + # Get the deployment token from Azure Static Web Apps + - uses: azureStaticWebApps/getDeploymentToken + with: + resourceId: ${{AZURE_STATIC_WEB_APPS_RESOURCE_ID}} + # Save deployment token to the environment file for the deployment action + writeToEnvironmentFile: + deploymentToken: SECRET_TAB_SWA_DEPLOYMENT_TOKEN + + # 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 + # 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 + # Teams 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 + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +# Triggered when 'teamsapp deploy' is executed +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --production + - uses: cli/runNpmCommand + name: build app + with: + args: run build --if-present + # Deploy bits to Azure Static Web Apps + - uses: cli/runNpxCommand + name: deploy to Azure Static Web Apps + with: + args: '@azure/static-web-apps-cli deploy build -d + ${{SECRET_TAB_SWA_DEPLOYMENT_TOKEN}} --env production' + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: 410df8f9-505b-48b0-8947-9035b16902fc diff --git a/templates/dashboard-tab-ts/tsconfig.json b/templates/dashboard-tab-ts/tsconfig.json new file mode 100644 index 00000000..a9e45eac --- /dev/null +++ b/templates/dashboard-tab-ts/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] + } \ No newline at end of file diff --git a/templates/default-bot-js/.gitignore b/templates/default-bot-js/.gitignore new file mode 100644 index 00000000..07a76d44 --- /dev/null +++ b/templates/default-bot-js/.gitignore @@ -0,0 +1,19 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +.localConfigs.testTool +.localConfigs +.notification.localstore.json +.notification.testtoolstore.json +appPackage/build + +# dependencies +node_modules/ + +# misc +.env +.deployment +.DS_Store + +# Dev tool directories +/devTools/ \ No newline at end of file diff --git a/templates/default-bot-js/.vscode/extensions.json b/templates/default-bot-js/.vscode/extensions.json new file mode 100644 index 00000000..aac0a6e3 --- /dev/null +++ b/templates/default-bot-js/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/templates/default-bot-js/.vscode/launch.json b/templates/default-bot-js/.vscode/launch.json new file mode 100644 index 00000000..ccf40807 --- /dev/null +++ b/templates/default-bot-js/.vscode/launch.json @@ -0,0 +1,132 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Attach to Local Service", + "type": "node", + "request": "attach", + "port": 9239, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Desktop)", + "type": "node", + "request": "launch", + "preLaunchTask": "Start Teams App in Desktop Client (Remote)", + "presentation": { + "group": "3-remote", + "order": 3 + }, + "internalConsoleOptions": "neverOpen", + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "2-local", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "2-local", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Desktop)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App in Desktop Client", + "presentation": { + "group": "2-local", + "order": 3 + }, + "stopAll": true + }, + { + "name": "Debug in Test Tool", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { + "group": "1-local", + "order": 1 + }, + "stopAll": true + } + ] +} diff --git a/templates/default-bot-js/.vscode/settings.json b/templates/default-bot-js/.vscode/settings.json new file mode 100644 index 00000000..42996202 --- /dev/null +++ b/templates/default-bot-js/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ] +} diff --git a/templates/default-bot-js/.vscode/tasks.json b/templates/default-bot-js/.vscode/tasks.json new file mode 100644 index 00000000..9034316c --- /dev/null +++ b/templates/default-bot-js/.vscode/tasks.json @@ -0,0 +1,232 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool", + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150, // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool", + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}", + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239 // app inspector port for Node.js debugger + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Teams App in Desktop Client", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start application", + "Start desktop client" + ], + "dependsOrder": "sequence" + }, + { + "label": "Start desktop client", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true" + } + }, + { + "label": "Start Teams App in Desktop Client (Remote)", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true" + } + } + ] +} \ No newline at end of file diff --git a/templates/default-bot-js/.webappignore b/templates/default-bot-js/.webappignore new file mode 100644 index 00000000..f79d01ac --- /dev/null +++ b/templates/default-bot-js/.webappignore @@ -0,0 +1,28 @@ +.webappignore +.fx +.deployment +.localConfigs.testTool +.localConfigs +.notification.localstore.json +.notification.testtoolstore.json +.vscode +*.js.map +*.ts.map +*.ts +.git* +.tsbuildinfo +CHANGELOG.md +readme.md +local.settings.json +test +tsconfig.json +.DS_Store +teamsapp.yml +teamsapp.*.yml +/env/ +/node_modules/.bin +/node_modules/ts-node +/node_modules/typescript +/appPackage/ +/infra/ +/devTools/ \ No newline at end of file diff --git a/templates/default-bot-js/README.md b/templates/default-bot-js/README.md new file mode 100644 index 00000000..9e83fb71 --- /dev/null +++ b/templates/default-bot-js/README.md @@ -0,0 +1,70 @@ +# Overview of the Basic Bot template + +Examples of Microsoft Teams bots in everyday use include: + +- Bots that notify about build failures. +- Bots that provide information about the weather or bus schedules. +- Bots that provide travel information. + +A bot interaction can be a quick question and answer, or it can be a complex conversation. Being a cloud application, a bot can provide valuable and secure access to cloud services and corporate resources. + +## Get started with the Basic Bot template + +> **Prerequisites** +> +> To run the Basic Bot template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging). + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool`. +3. The browser will pop up to open Teams App Test Tool. +4. You will receive a welcome message from the bot, and you can send anything to the bot to get an echoed response. + +**Congratulations**! You are running an application that can now interact with users in Teams App Test Tool: + +![basic bot](https://github.com/OfficeDev/TeamsFx/assets/9698542/bdf87809-7dd7-4926-bff0-4546ada25e4b) + +## What's included in the template + +| Folder | Contents | +| - | - | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| - | - | +|`teamsBot.js`| Handles business logics for the Basic Bot.| +|`index.js`|`index.js` is used to setup and configure the Basic Bot.| + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| - | - | +|`teamsapp.yml`|This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +|`teamsapp.local.yml`|This overrides `teamsapp.yml` with actions that enable local execution and debugging.| +|`teamsapp.testtool.yml`| This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool.| + +## Extend the Basic Bot template + +Following documentation will help you to extend the Basic Bot template. + +- [Add or manage the environment](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-multi-env) +- [Create multi-capability app](https://learn.microsoft.com/microsoftteams/platform/toolkit/add-capability) +- [Add single sign on to your app](https://learn.microsoft.com/microsoftteams/platform/toolkit/add-single-sign-on) +- [Access data in Microsoft Graph](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-sdk#microsoft-graph-scenarios) +- [Use an existing Microsoft Entra application](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-existing-aad-app) +- [Customize the Teams app manifest](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-preview-and-customize-app-manifest) +- Host your app in Azure by [provision cloud resources](https://learn.microsoft.com/microsoftteams/platform/toolkit/provision) and [deploy the code to cloud](https://learn.microsoft.com/microsoftteams/platform/toolkit/deploy) +- [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) +- [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) +- [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) +- [Develop with Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli/debug) +- [Preview the app on mobile clients](https://aka.ms/teamsfx-mobile) diff --git a/templates/default-bot-js/appPackage/color.png b/templates/default-bot-js/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/default-bot-js/appPackage/color.png differ diff --git a/templates/default-bot-js/appPackage/manifest.json b/templates/default-bot-js/appPackage/manifest.json new file mode 100644 index 00000000..9530092b --- /dev/null +++ b/templates/default-bot-js/appPackage/manifest.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "default-bot-js${{APP_NAME_SUFFIX}}", + "full": "full name for default-bot-js" + }, + "description": { + "short": "short description for default-bot-js", + "full": "full description for default-bot-js" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": false, + "commandLists": [ + { + "scopes": ["personal", "team", "groupChat"], + "commands": [ + { + "title": "Hi", + "description": "Say hi to the bot." + } + ] + } + ] + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/templates/default-bot-js/appPackage/outline.png b/templates/default-bot-js/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/default-bot-js/appPackage/outline.png differ diff --git a/templates/default-bot-js/config.js b/templates/default-bot-js/config.js new file mode 100644 index 00000000..ea6b1a59 --- /dev/null +++ b/templates/default-bot-js/config.js @@ -0,0 +1,8 @@ +const config = { + MicrosoftAppId: process.env.BOT_ID, + MicrosoftAppType: process.env.BOT_TYPE, + MicrosoftAppTenantId: process.env.BOT_TENANT_ID, + MicrosoftAppPassword: process.env.BOT_PASSWORD, +}; + +module.exports = config; diff --git a/templates/default-bot-js/env/.env.dev b/templates/default-bot-js/env/.env.dev new file mode 100644 index 00000000..4a8cb515 --- /dev/null +++ b/templates/default-bot-js/env/.env.dev @@ -0,0 +1,16 @@ +# 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= +BOT_DOMAIN= diff --git a/templates/default-bot-js/env/.env.testtool b/templates/default-bot-js/env/.env.testtool new file mode 100644 index 00000000..43ce12aa --- /dev/null +++ b/templates/default-bot-js/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/default-bot-js/index.js b/templates/default-bot-js/index.js new file mode 100644 index 00000000..67bf7ca6 --- /dev/null +++ b/templates/default-bot-js/index.js @@ -0,0 +1,64 @@ +// index.js is used to setup and configure your bot + +// Import required packages +const restify = require("restify"); + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +const { + CloudAdapter, + ConfigurationServiceClientCredentialFactory, + ConfigurationBotFrameworkAuthentication, +} = require("botbuilder"); +const { TeamsBot } = require("./teamsBot"); +const config = require("./config"); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const credentialsFactory = new ConfigurationServiceClientCredentialFactory(config); + +const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication( + {}, + credentialsFactory +); + +const adapter = new CloudAdapter(botFrameworkAuthentication); + +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + // NOTE: In production environment, you should consider logging this to Azure + // application insights. See https://aka.ms/bottelemetry for telemetry + // configuration instructions. + console.error(`\n [onTurnError] unhandled error: ${error}`); + + // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. + if (context.activity.type === "message") { + // Send a message to the user + await context.sendActivity(`The bot encountered an unhandled error:\n ${error.message}`); + await context.sendActivity("To continue to run this bot, please fix the bot source code."); + } +}; + +// Create the bot that will handle incoming messages. +const bot = new TeamsBot(); + +// Create HTTP server. +const server = restify.createServer(); +server.use(restify.plugins.bodyParser()); +server.listen(process.env.port || process.env.PORT || 3978, function () { + console.log(`\nBot started, ${server.name} listening to ${server.url}`); +}); + +// Listen for incoming requests. +server.post("/api/messages", async (req, res) => { + await adapter.process(req, res, async (context) => { + await bot.run(context); + }); +}); + +// Gracefully shutdown HTTP server +["exit", "uncaughtException", "SIGINT", "SIGTERM", "SIGUSR1", "SIGUSR2"].forEach((event) => { + process.on(event, () => { + server.close(); + }); +}); diff --git a/templates/default-bot-js/infra/azure.bicep b/templates/default-bot-js/infra/azure.bicep new file mode 100644 index 00000000..d0a059a6 --- /dev/null +++ b/templates/default-bot-js/infra/azure.bicep @@ -0,0 +1,95 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +param webAppSKU string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param identityName string = resourceBaseName +param location string = resourceGroup().location + +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: webAppSKU + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + alwaysOn: true + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure App Service from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x for your site + } + { + name: 'RUNNING_ON_AZURE' + value: '1' + } + { + name: 'BOT_ID' + value: identity.properties.clientId + } + { + name: 'BOT_TENANT_ID' + value: identity.properties.tenantId + } + { + name: 'BOT_TYPE' + 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/templates/default-bot-js/infra/azure.parameters.json b/templates/default-bot-js/infra/azure.parameters.json new file mode 100644 index 00000000..3670521e --- /dev/null +++ b/templates/default-bot-js/infra/azure.parameters.json @@ -0,0 +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}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "default-bot-js" + } + } +} \ No newline at end of file diff --git a/templates/default-bot-js/infra/botRegistration/azurebot.bicep b/templates/default-bot-js/infra/botRegistration/azurebot.bicep new file mode 100644 index 00000000..a5a27b8f --- /dev/null +++ b/templates/default-bot-js/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/templates/default-bot-js/infra/botRegistration/readme.md b/templates/default-bot-js/infra/botRegistration/readme.md new file mode 100644 index 00000000..d5416243 --- /dev/null +++ b/templates/default-bot-js/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/templates/default-bot-js/package.json b/templates/default-bot-js/package.json new file mode 100644 index 00000000..a9d3dffe --- /dev/null +++ b/templates/default-bot-js/package.json @@ -0,0 +1,31 @@ +{ + "name": "defaultbotjs", + "version": "1.0.0", + "msteams": { + "teamsAppId": null + }, + "description": "Microsoft Teams Toolkit hello world Bot sample", + "engines": { + "node": "16 || 18" + }, + "author": "Microsoft", + "license": "MIT", + "main": "index.js", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", + "dev": "nodemon --inspect=9239 --signal SIGINT ./index.js", + "start": "node ./index.js", + "watch": "nodemon ./index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "botbuilder": "^4.20.0", + "restify": "^10.0.0" + }, + "devDependencies": { + "env-cmd": "^10.1.0", + "nodemon": "^2.0.7" + } +} diff --git a/templates/default-bot-js/teamsBot.js b/templates/default-bot-js/teamsBot.js new file mode 100644 index 00000000..8af35f3b --- /dev/null +++ b/templates/default-bot-js/teamsBot.js @@ -0,0 +1,32 @@ +const { TeamsActivityHandler, TurnContext } = require("botbuilder"); + +class TeamsBot extends TeamsActivityHandler { + constructor() { + super(); + + this.onMessage(async (context, next) => { + console.log("Running with Message Activity."); + const removedMentionText = TurnContext.removeRecipientMention(context.activity); + const txt = removedMentionText.toLowerCase().replace(/\n|\r/g, "").trim(); + await context.sendActivity(`Echo: ${txt}`); + // By calling next() you ensure that the next BotHandler is run. + await next(); + }); + + // Listen to MembersAdded event, view https://docs.microsoft.com/en-us/microsoftteams/platform/resources/bot-v3/bots-notifications for more events + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (let cnt = 0; cnt < membersAdded.length; cnt++) { + if (membersAdded[cnt].id) { + await context.sendActivity( + `Hi there! I'm a Teams bot that will echo what you said to me.` + ); + break; + } + } + await next(); + }); + } +} + +module.exports.TeamsBot = TeamsBot; diff --git a/templates/default-bot-js/teamsapp.local.yml b/templates/default-bot-js/teamsapp.local.yml new file mode 100644 index 00000000..1c0eb33a --- /dev/null +++ b/templates/default-bot-js/teamsapp.local.yml @@ -0,0 +1,80 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: default-bot-js${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: default-bot-js${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: default-bot-js + 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 + # 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 + # Teams 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 + +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + BOT_TYPE: 'MultiTenant' diff --git a/templates/default-bot-js/teamsapp.testtool.yml b/templates/default-bot-js/teamsapp.testtool.yml new file mode 100644 index 00000000..44d71fd1 --- /dev/null +++ b/templates/default-bot-js/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.1 + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/default-bot-js/teamsapp.yml b/templates/default-bot-js/teamsapp.yml new file mode 100644 index 00000000..5ef8483e --- /dev/null +++ b/templates/default-bot-js/teamsapp.yml @@ -0,0 +1,127 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: default-bot-js${{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 + # Teams 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 + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # 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 + # Teams 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: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --production + # 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: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .webappignore + # 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}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: 08db3837-b6e3-478e-b01e-c921b1641515 diff --git a/templates/default-bot-js/web.config b/templates/default-bot-js/web.config new file mode 100644 index 00000000..9c8703cb --- /dev/null +++ b/templates/default-bot-js/web.config @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/m365-message-extension-js/.funcignore b/templates/m365-message-extension-js/.funcignore new file mode 100644 index 00000000..20a67199 --- /dev/null +++ b/templates/m365-message-extension-js/.funcignore @@ -0,0 +1,18 @@ +.funcignore +*.js.map +.git* +.localConfigs +.vscode +local.settings.json +test +.DS_Store +.deployment +node_modules/.bin +node_modules/azure-functions-core-tools +README.md +teamsapp.yml +teamsapp.*.yml +/env/ +/appPackage/ +/infra/ +/devTools/ \ No newline at end of file diff --git a/templates/m365-message-extension-js/.gitignore b/templates/m365-message-extension-js/.gitignore new file mode 100644 index 00000000..3cda0399 --- /dev/null +++ b/templates/m365-message-extension-js/.gitignore @@ -0,0 +1,26 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# TeamsFx files +env/.env.*.user +env/.env.local +.DS_Store +build +appPackage/build +.deployment + +# dependencies +/node_modules + +# testing +/coverage + +# Dev tool directories +/devTools/ + +# Azure Functions artifacts +bin +obj +appsettings.json +local.settings.json + +# Local data +.localConfigs \ No newline at end of file diff --git a/templates/m365-message-extension-js/.vscode/extensions.json b/templates/m365-message-extension-js/.vscode/extensions.json new file mode 100644 index 00000000..92a389ad --- /dev/null +++ b/templates/m365-message-extension-js/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "teamsdevapp.ms-teams-vscode-extension" + ] +} \ No newline at end of file diff --git a/templates/m365-message-extension-js/.vscode/launch.json b/templates/m365-message-extension-js/.vscode/launch.json new file mode 100644 index 00000000..e43b2f7c --- /dev/null +++ b/templates/m365-message-extension-js/.vscode/launch.json @@ -0,0 +1,120 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch App in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Backend" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Launch App in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Backend" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Preview in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Preview in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Backend", + "type": "node", + "request": "attach", + "port": 9229, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Preview in Teams (Desktop)", + "type": "node", + "request": "launch", + "preLaunchTask": "Start Teams App in Desktop Client (Remote)", + "presentation": { + "group": "remote", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App in Teams (Edge)", + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "all", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App in Teams (Chrome)", + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "all", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Desktop)", + "configurations": [ + "Attach to Backend" + ], + "preLaunchTask": "Start Teams App in Desktop Client", + "presentation": { + "group": "all", + "order": 3 + }, + "stopAll": true + } + ] +} diff --git a/templates/m365-message-extension-js/.vscode/settings.json b/templates/m365-message-extension-js/.vscode/settings.json new file mode 100644 index 00000000..0ed7b2e7 --- /dev/null +++ b/templates/m365-message-extension-js/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ], + "azureFunctions.stopFuncTaskPostDebug": false, + "azureFunctions.showProjectWarning": false, +} diff --git a/templates/m365-message-extension-js/.vscode/tasks.json b/templates/m365-message-extension-js/.vscode/tasks.json new file mode 100644 index 00000000..d719f7f7 --- /dev/null +++ b/templates/m365-message-extension-js/.vscode/tasks.json @@ -0,0 +1,143 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Create resources", + "Build project", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", + "m365Account", + "portOccupancy" + ], + "portOccupancy": [ + 7071, + 9229 + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 7071, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "OPENAPI_SERVER_URL", // output tunnel endpoint as OPENAPI_SERVER_URL + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + "label": "Create resources", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + "label": "Build project", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "dependsOn": [ + "Start backend" + ] + }, + { + "label": "Start backend", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}", + "env": { + "PATH": "${workspaceFolder}/devTools/func:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/func;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^.*(Job host stopped|signaling restart).*$", + "endsPattern": "^.*(Worker process started and initialized|Host lock lease acquired by instance ID).*$" + } + }, + "presentation": { + "reveal": "silent" + } + }, + { + "label": "Start Teams App in Desktop Client", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Create resources", + "Build project", + "Start application", + "Start desktop client" + ], + "dependsOrder": "sequence" + }, + { + "label": "Start desktop client", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true" + } + }, + { + "label": "Start Teams App in Desktop Client (Remote)", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true" + } + } + ] +} \ No newline at end of file diff --git a/templates/m365-message-extension-js/README.md b/templates/m365-message-extension-js/README.md new file mode 100644 index 00000000..8e0fddfe --- /dev/null +++ b/templates/m365-message-extension-js/README.md @@ -0,0 +1,54 @@ +# Overview of Custom Search Results app template + +## Build a message extension from a new API with Azure Functions + +This app template allows Teams to interact directly with third-party data, apps, and services, enhancing its capabilities and broadening its range of capabilities. It allows Teams to: + +- Retrieve real-time information, for example, latest news coverage on a product launch. +- Retrieve knowledge-based information, for example, my team’s design files in Figma. + +## Get started with the template + +> **Prerequisites** +> +> To run this app template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 18, 20 +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +3. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)` from the launch configuration dropdown. +4. To trigger the Message Extension, you can click the `+` under compose message area to find your message extension. + > Note: Please make sure to switch to New Teams when Teams web client has launched + +## What's included in the template + +| Folder | Contents | +| ------------ | ----------------------------------------------------------------------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest, the API specification and response template for API responses | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the repair API | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| -------------------------------------------- | ------------------------------------------------------------------- | +| `src/functions/repair.js` | The main file of a function in Azure Functions. | +| `src/repairsData.json` | The data source for the repair API. | +| `appPackage/apiSpecificationFile/repair.yml` | A file that describes the structure and behavior of the repair API. | +| `appPackage/responseTemplates/repair.json` | A generated Adaptive Card that used to render API response. | + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | + +## Addition information and references + +- [Extend Teams platform with APIs](https://aka.ms/teamsfx-api-plugin) diff --git a/templates/m365-message-extension-js/appPackage/apiSpecificationFile/repair.yml b/templates/m365-message-extension-js/appPackage/apiSpecificationFile/repair.yml new file mode 100644 index 00000000..b1942190 --- /dev/null +++ b/templates/m365-message-extension-js/appPackage/apiSpecificationFile/repair.yml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: Repair Service + description: A simple service to manage repairs + version: 1.0.0 +servers: + - url: ${{OPENAPI_SERVER_URL}}/api + description: The repair api server +paths: + /repair: + get: + operationId: repair + summary: Search for repairs + description: Search for repairs info with its details and image + parameters: + - name: assignedTo + in: query + description: Filter repairs by who they're assigned to + schema: + type: string + required: false + responses: + '200': + description: A list of repairs + content: + application/json: + schema: + type: array + items: + properties: + id: + type: integer + description: The unique identifier of the repair + title: + type: string + description: The short summary of the repair + description: + type: string + description: The detailed description of the repair + assignedTo: + type: string + description: The user who is responsible for the repair + date: + type: string + format: date-time + description: The date and time when the repair is scheduled or completed + image: + type: string + format: uri + description: The URL of the image of the item to be repaired or the repair process \ No newline at end of file diff --git a/templates/m365-message-extension-js/appPackage/color.png b/templates/m365-message-extension-js/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/m365-message-extension-js/appPackage/color.png differ diff --git a/templates/m365-message-extension-js/appPackage/manifest.json b/templates/m365-message-extension-js/appPackage/manifest.json new file mode 100644 index 00000000..7d4f7bd6 --- /dev/null +++ b/templates/m365-message-extension-js/appPackage/manifest.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json", + "manifestVersion": "devPreview", + "id": "${{TEAMS_APP_ID}}", + "version": "1.0.0", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "m365-message-extension-js${{APP_NAME_SUFFIX}}", + "full": "Full name for m365-message-extension-js" + }, + "description": { + "short": "Track and monitor car repair records for stress-free maintenance management.", + "full": "The ultimate solution for hassle-free car maintenance management makes tracking and monitoring your car repair records a breeze." + }, + "accentColor": "#FFFFFF", + "composeExtensions": [ + { + "composeExtensionType": "apiBased", + "apiSpecificationFile": "apiSpecificationFile/repair.yml", + "commands": [ + { + "id": "repair", + "type": "query", + "title": "Search for repairs info", + "context": [ + "compose", + "commandBox" + ], + "apiResponseRenderingTemplateFile": "responseTemplates/repair.json", + "parameters": [ + { + "name": "assignedTo", + "title": "Assigned To", + "description": "Filter repairs by who they're assigned to", + "inputType": "text" + } + ] + } + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ] +} \ No newline at end of file diff --git a/templates/m365-message-extension-js/appPackage/outline.png b/templates/m365-message-extension-js/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/m365-message-extension-js/appPackage/outline.png differ diff --git a/templates/m365-message-extension-js/appPackage/responseTemplates/repair.data.json b/templates/m365-message-extension-js/appPackage/responseTemplates/repair.data.json new file mode 100644 index 00000000..acfa0e3a --- /dev/null +++ b/templates/m365-message-extension-js/appPackage/responseTemplates/repair.data.json @@ -0,0 +1,8 @@ +{ + "id": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" +} diff --git a/templates/m365-message-extension-js/appPackage/responseTemplates/repair.json b/templates/m365-message-extension-js/appPackage/responseTemplates/repair.json new file mode 100644 index 00000000..9be6d812 --- /dev/null +++ b/templates/m365-message-extension-js/appPackage/responseTemplates/repair.json @@ -0,0 +1,76 @@ +{ + "version": "devPreview", + "$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.ResponseRenderingTemplate.schema.json", + "jsonPath": "results", + "responseLayout": "list", + "responseCardTemplate": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "Container", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "Title: ${if(title, title, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Description: ${if(description, description, 'N/A')}", + "wrap": true + }, + { + "type": "TextBlock", + "text": "Assigned To: ${if(assignedTo, assignedTo, 'N/A')}", + "wrap": true + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "${if(image, image, '')}", + "size": "Medium" + } + ] + } + ] + }, + { + "type": "FactSet", + "facts": [ + { + "title": "Repair ID:", + "value": "${if(id, id, 'N/A')}" + }, + { + "title": "Date:", + "value": "${if(date, date, 'N/A')}" + } + ] + } + ] + } + ] + }, + "previewCardTemplate": { + "title": "${if(title, title, 'N/A')}", + "subtitle": "${if(description, description, 'N/A')}", + "image": { + "url": "${if(image, image, '')}", + "alt": "${if(title, title, 'N/A')}" + } + } +} diff --git a/templates/m365-message-extension-js/env/.env.dev b/templates/m365-message-extension-js/env/.env.dev new file mode 100644 index 00000000..342a8af6 --- /dev/null +++ b/templates/m365-message-extension-js/env/.env.dev @@ -0,0 +1,17 @@ +# 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. +TEAMS_APP_ID= +TEAMS_APP_PUBLISHED_APP_ID= +TEAMS_APP_TENANT_ID= +API_FUNCTION_ENDPOINT= +API_FUNCTION_RESOURCE_ID= \ No newline at end of file diff --git a/templates/m365-message-extension-js/host.json b/templates/m365-message-extension-js/host.json new file mode 100644 index 00000000..9df91361 --- /dev/null +++ b/templates/m365-message-extension-js/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/templates/m365-message-extension-js/infra/azure.bicep b/templates/m365-message-extension-js/infra/azure.bicep new file mode 100644 index 00000000..8021c1d2 --- /dev/null +++ b/templates/m365-message-extension-js/infra/azure.bicep @@ -0,0 +1,57 @@ +@maxLength(20) +@minLength(4) +param resourceBaseName string +param functionAppSKU string + +param location string = resourceGroup().location +param serverfarmsName string = resourceBaseName +param functionAppName string = resourceBaseName + +// Compute resources for Azure Functions +resource serverfarms 'Microsoft.Web/serverfarms@2021-02-01' = { + name: serverfarmsName + location: location + sku: { + name: functionAppSKU // You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionServerfarmsSku property to provisionParameters to override the default value "Y1". + } + properties: {} +} + +// Azure Functions that hosts your function code +resource functionApp 'Microsoft.Web/sites@2021-02-01' = { + name: functionAppName + kind: 'functionapp' + location: location + properties: { + serverFarmId: serverfarms.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' // Use Azure Functions runtime v4 + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'node' // Set runtime to NodeJS + } + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure Functions from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x + } + ] + ftpsState: 'FtpsOnly' + } + } +} +var apiEndpoint = 'https://${functionApp.properties.defaultHostName}' + + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output API_FUNCTION_ENDPOINT string = apiEndpoint +output API_FUNCTION_RESOURCE_ID string = functionApp.id +output OPENAPI_SERVER_URL string = apiEndpoint diff --git a/templates/m365-message-extension-js/infra/azure.parameters.json b/templates/m365-message-extension-js/infra/azure.parameters.json new file mode 100644 index 00000000..faab159a --- /dev/null +++ b/templates/m365-message-extension-js/infra/azure.parameters.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "sme${{RESOURCE_SUFFIX}}" + }, + "functionAppSKU": { + "value": "Y1" + } + } +} \ No newline at end of file diff --git a/templates/m365-message-extension-js/package.json b/templates/m365-message-extension-js/package.json new file mode 100644 index 00000000..39ea3d6c --- /dev/null +++ b/templates/m365-message-extension-js/package.json @@ -0,0 +1,17 @@ +{ + "name": "m365messageextensionjs", + "version": "1.0.0", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev": "func start --javascript --language-worker=\"--inspect=9229\" --port \"7071\" --cors \"*\"", + "start": "npx func start", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@azure/functions": "^4.3.0" + }, + "devDependencies": { + "env-cmd": "^10.1.0" + }, + "main": "src/functions/*.js" +} diff --git a/templates/m365-message-extension-js/src/functions/repair.js b/templates/m365-message-extension-js/src/functions/repair.js new file mode 100644 index 00000000..21ad9676 --- /dev/null +++ b/templates/m365-message-extension-js/src/functions/repair.js @@ -0,0 +1,51 @@ +/* This code sample provides a starter kit to implement server side logic for your Teams App in TypeScript, + * refer to https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference for + * complete Azure Functions developer guide. + */ +const { app } = require("@azure/functions"); +/** + * This function handles the HTTP request and returns the repair information. + * + * @param req - The HTTP request. + * @param context - The Azure Functions context object. + * @returns A promise that resolves with the HTTP response containing the repair information. + */ +async function repair(req, context) { + context.log("HTTP trigger function processed a request."); + // Initialize response. + const res = { + status: 200, + jsonBody: { + results: [], + }, + }; + + // Get the assignedTo query parameter. + const assignedTo = req.query.get("assignedTo"); + + // If the assignedTo query parameter is not provided, return all repair records. + if (!assignedTo) { + return res; + } + + // Get the repair records from the data.json file. + const repairRecords = require("../repairsData.json"); + + // Filter the repair records by the assignedTo query parameter. + const repairs = repairRecords.filter((item) => { + const query = assignedTo.trim().toLowerCase(); + const fullName = item.assignedTo.toLowerCase(); + const [firstName, lastName] = fullName.split(" "); + return fullName === query || firstName === query || lastName === query; + }); + + // Return filtered repair records, or an empty array if no records were found. + res.jsonBody.results = repairs ?? []; + return res; +} + +app.http("repair", { + methods: ["GET"], + authLevel: "anonymous", + handler: repair, +}); diff --git a/templates/m365-message-extension-js/src/repairsData.json b/templates/m365-message-extension-js/src/repairsData.json new file mode 100644 index 00000000..428ab008 --- /dev/null +++ b/templates/m365-message-extension-js/src/repairsData.json @@ -0,0 +1,50 @@ +[ + { + "id": "1", + "title": "Oil change", + "description": "Need to drain the old engine oil and replace it with fresh oil to keep the engine lubricated and running smoothly.", + "assignedTo": "Karin Blair", + "date": "2023-05-23", + "image": "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg" + }, + { + "id": "2", + "title": "Brake repairs", + "description": "Conduct brake repairs, including replacing worn brake pads, resurfacing or replacing brake rotors, and repairing or replacing other components of the brake system.", + "assignedTo": "Issac Fielder", + "date": "2023-05-24", + "image": "https://upload.wikimedia.org/wikipedia/commons/7/71/Disk_brake_dsc03680.jpg" + }, + { + "id": "3", + "title": "Tire service", + "description": "Rotate and replace tires, moving them from one position to another on the vehicle to ensure even wear and removing worn tires and installing new ones.", + "assignedTo": "Karin Blair", + "date": "2023-05-24", + "image": "https://th.bing.com/th/id/OIP.N64J4jmqmnbQc5dHvTm-QAHaE8?pid=ImgDet&rs=1" + }, + { + "id": "4", + "title": "Battery replacement", + "description": "Remove the old battery and install a new one to ensure that the vehicle start reliably and the electrical systems function properly.", + "assignedTo": "Ashley McCarthy", + "date": "2023-05-25", + "image": "https://i.stack.imgur.com/4ftuj.jpg" + }, + { + "id": "5", + "title": "Engine tune-up", + "description": "This can include a variety of services such as replacing spark plugs, air filters, and fuel filters to keep the engine running smoothly and efficiently.", + "assignedTo": "Karin Blair", + "date": "2023-05-28", + "image": "https://th.bing.com/th/id/R.e4c01dd9f232947e6a92beb0a36294a5?rik=P076LRx7J6Xnrg&riu=http%3a%2f%2fupload.wikimedia.org%2fwikipedia%2fcommons%2ff%2ff3%2f1990_300zx_engine.jpg&ehk=f8KyT78eO3b%2fBiXzh6BZr7ze7f56TWgPST%2bY%2f%2bHqhXQ%3d&risl=&pid=ImgRaw&r=0" + }, + { + "id": "6", + "title": "Suspension and steering repairs", + "description": "This can include repairing or replacing components of the suspension and steering systems to ensure that the vehicle handles and rides smoothly.", + "assignedTo": "Daisy Phillips", + "date": "2023-05-29", + "image": "https://i.stack.imgur.com/4v5OI.jpg" + } +] \ No newline at end of file diff --git a/templates/m365-message-extension-js/teamsapp.local.yml b/templates/m365-message-extension-js/teamsapp.local.yml new file mode 100644 index 00000000..19ae85f5 --- /dev/null +++ b/templates/m365-message-extension-js/teamsapp.local.yml @@ -0,0 +1,79 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: m365-message-extension-js${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Set required variables for local launch + - uses: script + with: + run: + echo "::set-teamsfx-env FUNC_NAME=repair"; + echo "::set-teamsfx-env FUNC_ENDPOINT=http://localhost:7071"; + + # 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 + + # 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 + # Teams 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 + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + func: + version: ~4.0.5455 + symlinkDir: ./devTools/func + # Write the information of installed development tool(s) into environment + # file for the specified environment variable(s). + writeToEnvironmentFile: + funcPath: FUNC_PATH + + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit diff --git a/templates/m365-message-extension-js/teamsapp.yml b/templates/m365-message-extension-js/teamsapp.yml new file mode 100644 index 00000000..7f5014b3 --- /dev/null +++ b/templates/m365-message-extension-js/teamsapp.yml @@ -0,0 +1,142 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: m365-message-extension-js${{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-sme + # Teams 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 + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # 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 + # Teams 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 + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +# Triggered when 'teamsapp deploy' is executed +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --production + + # Deploy your application to Azure Functions using the zip deploy feature. + # For additional details, see at https://aka.ms/zip-deploy-to-azure-functions + - uses: azureFunctions/zipDeploy + with: + # deploy base folder + artifactFolder: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .funcignore + # 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: ${{API_FUNCTION_RESOURCE_ID}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: 2008d677-f282-47de-b8fe-a0b0b4fe959e diff --git a/templates/message-extension-action-js/.gitignore b/templates/message-extension-action-js/.gitignore new file mode 100644 index 00000000..b891a68c --- /dev/null +++ b/templates/message-extension-action-js/.gitignore @@ -0,0 +1,20 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +.localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools +appPackage/build + +# dependencies +node_modules/ + +# misc +.env +.deployment +.DS_Store + +# build +lib/ diff --git a/templates/message-extension-action-js/.vscode/extensions.json b/templates/message-extension-action-js/.vscode/extensions.json new file mode 100644 index 00000000..aac0a6e3 --- /dev/null +++ b/templates/message-extension-action-js/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/templates/message-extension-action-js/.vscode/launch.json b/templates/message-extension-action-js/.vscode/launch.json new file mode 100644 index 00000000..9170e827 --- /dev/null +++ b/templates/message-extension-action-js/.vscode/launch.json @@ -0,0 +1,132 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Attach to Local Service", + "type": "node", + "request": "attach", + "port": 9239, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Teams (Desktop)", + "type": "node", + "request": "launch", + "preLaunchTask": "Start Teams App in Desktop Client (Remote)", + "presentation": { + "group": "remote", + "order": 3 + }, + "internalConsoleOptions": "neverOpen", + } + ], + "compounds": [ + { + "name": "Debug in Test Tool", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { + "group": "group 0: Teams App Test Tool", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Desktop)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App in Desktop Client", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "stopAll": true + } + ] +} \ No newline at end of file diff --git a/templates/message-extension-action-js/.vscode/settings.json b/templates/message-extension-action-js/.vscode/settings.json new file mode 100644 index 00000000..3014fd9c --- /dev/null +++ b/templates/message-extension-action-js/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "debug.onTaskErrors": "abort" +} diff --git a/templates/message-extension-action-js/.vscode/tasks.json b/templates/message-extension-action-js/.vscode/tasks.json new file mode 100644 index 00000000..fdd3407c --- /dev/null +++ b/templates/message-extension-action-js/.vscode/tasks.json @@ -0,0 +1,232 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150 // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool" + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239 // app inspector port for Node.js debugger + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Teams App in Desktop Client", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start application", + "Start desktop client" + ], + "dependsOrder": "sequence" + }, + { + "label": "Start desktop client", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true" + } + }, + { + "label": "Start Teams App in Desktop Client (Remote)", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true" + } + } + ] +} \ No newline at end of file diff --git a/templates/message-extension-action-js/.webappignore b/templates/message-extension-action-js/.webappignore new file mode 100644 index 00000000..a6ef2018 --- /dev/null +++ b/templates/message-extension-action-js/.webappignore @@ -0,0 +1,28 @@ +.webappignore +.fx +.deployment +.localConfigs +.localConfigs.testTool +.notification.localstore.json +.notification.testtoolstore.json +/devTools +.vscode +*.js.map +*.ts.map +*.ts +.git* +.tsbuildinfo +CHANGELOG.md +readme.md +local.settings.json +test +tsconfig.json +.DS_Store +teamsapp.yml +teamsapp.*.yml +/env/ +/node_modules/.bin +/node_modules/ts-node +/node_modules/typescript +/appPackage/ +/infra/ \ No newline at end of file diff --git a/templates/message-extension-action-js/README.md b/templates/message-extension-action-js/README.md new file mode 100644 index 00000000..f736bb5c --- /dev/null +++ b/templates/message-extension-action-js/README.md @@ -0,0 +1,67 @@ +# Overview of Collect Form Input and Process Data template + +A Message Extension allows users to interact with your web service while composing messages in the Microsoft Teams client. Users can search or initiate actions in an external system from compose message area, the command box, or directly from a message. + +This app template implements action command that allows you to present your users with a modal pop-up called a task module in Teams. The task module collects or displays information, processes the interaction, and sends the information back to Teams. + +## Get started with the template + +> **Prerequisites** +> +> To run the template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging). + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. +3. To trigger the action command, you can click the `+` under compose message area and select `Action Command`. + +**Congratulations**! You are running an application that can share information in rich format by creating an Adaptive Card in Teams App Test Tool. + +![action-ME](https://github.com/OfficeDev/TeamsFx/assets/9698542/c0afbd89-7fbb-4e73-98a2-f018be4ca88c) + +## What's included in the template + +| Folder | Contents | +| ------------- | -------------------------------------------- | +| `.vscode/` | VSCode files for debugging | +| `appPackage/` | Templates for the Teams application manifest | +| `env/` | Environment files | +| `infra/` | Templates for provisioning Azure resources | +| `src/` | The source code for the action application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| ------------------ | ---------------------------------------------------------------------------------------- | +| `src/actionApp.js` | Handles the business logic for this app template to collect form input and process data. | +| `src/index.js` | `index.js` is used to setup and configure the Message Extension. | + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | +| `teamsapp.testtool.yml`| This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool. | + +## Extend the template + +Following documentation will help you to extend the template. + +- [Add or manage the environment](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-multi-env) +- [Create multi-capability app](https://learn.microsoft.com/microsoftteams/platform/toolkit/add-capability) +- [Add single sign on to your app](https://learn.microsoft.com/microsoftteams/platform/toolkit/add-single-sign-on) +- [Access data in Microsoft Graph](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-sdk#microsoft-graph-scenarios) +- [Use an existing Microsoft Entra application](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-existing-aad-app) +- [Customize the Teams app manifest](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-preview-and-customize-app-manifest) +- Host your app in Azure by [provision cloud resources](https://learn.microsoft.com/microsoftteams/platform/toolkit/provision) and [deploy the code to cloud](https://learn.microsoft.com/microsoftteams/platform/toolkit/deploy) +- [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) +- [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) +- [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) +- [Develop with Teams Toolkit CLI](https://aka.ms/teams-toolkit-cli/debug) +- [Preview the app on mobile clients](https://aka.ms/teamsfx-mobile) + diff --git a/templates/message-extension-action-js/appPackage/color.png b/templates/message-extension-action-js/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/message-extension-action-js/appPackage/color.png differ diff --git a/templates/message-extension-action-js/appPackage/manifest.json b/templates/message-extension-action-js/appPackage/manifest.json new file mode 100644 index 00000000..c5e2481d --- /dev/null +++ b/templates/message-extension-action-js/appPackage/manifest.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "message-extension-action-js${{APP_NAME_SUFFIX}}", + "full": "full name for message-extension-action-js" + }, + "description": { + "short": "short description of message-extension-action-js", + "full": "full description of message-extension-action-js" + }, + "accentColor": "#FFFFFF", + "bots": [], + "composeExtensions": [ + { + "botId": "${{BOT_ID}}", + "commands": [ + { + "id": "createCard", + "context": [ + "compose", + "message", + "commandBox" + ], + "description": "Command to run action to create a Card from Compose Box", + "title": "Create Card", + "type": "action", + "parameters": [ + { + "name": "title", + "title": "Card title", + "description": "Title for the card", + "inputType": "text" + }, + { + "name": "subTitle", + "title": "Subtitle", + "description": "Subtitle for the card", + "inputType": "text" + }, + { + "name": "text", + "title": "Text", + "description": "Text for the card", + "inputType": "textarea" + } + ] + } + ] + } + ], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/templates/message-extension-action-js/appPackage/outline.png b/templates/message-extension-action-js/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/message-extension-action-js/appPackage/outline.png differ diff --git a/templates/message-extension-action-js/env/.env.dev b/templates/message-extension-action-js/env/.env.dev new file mode 100644 index 00000000..4b07861c --- /dev/null +++ b/templates/message-extension-action-js/env/.env.dev @@ -0,0 +1,16 @@ +# 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= +BOT_DOMAIN= \ No newline at end of file diff --git a/templates/message-extension-action-js/env/.env.testtool b/templates/message-extension-action-js/env/.env.testtool new file mode 100644 index 00000000..43ce12aa --- /dev/null +++ b/templates/message-extension-action-js/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/message-extension-action-js/infra/azure.bicep b/templates/message-extension-action-js/infra/azure.bicep new file mode 100644 index 00000000..256f1ce8 --- /dev/null +++ b/templates/message-extension-action-js/infra/azure.bicep @@ -0,0 +1,95 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +param webAppSKU string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param identityName string = resourceBaseName +param location string = resourceGroup().location + +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: webAppSKU + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + alwaysOn: true + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure App Service from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x for your site + } + { + name: 'RUNNING_ON_AZURE' + value: '1' + } + { + name: 'BOT_ID' + value: identity.properties.clientId + } + { + name: 'BOT_TENANT_ID' + value: identity.properties.tenantId + } + { + name: 'BOT_TYPE' + 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/templates/message-extension-action-js/infra/azure.parameters.json b/templates/message-extension-action-js/infra/azure.parameters.json new file mode 100644 index 00000000..030a1517 --- /dev/null +++ b/templates/message-extension-action-js/infra/azure.parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "ME${{RESOURCE_SUFFIX}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "message-extension-action-js" + } + } +} \ No newline at end of file diff --git a/templates/message-extension-action-js/infra/botRegistration/azurebot.bicep b/templates/message-extension-action-js/infra/botRegistration/azurebot.bicep new file mode 100644 index 00000000..11b7c449 --- /dev/null +++ b/templates/message-extension-action-js/infra/botRegistration/azurebot.bicep @@ -0,0 +1,52 @@ +@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' + } +} + +// Connect the bot service to Outlook, and other Microsoft 365 applications +resource botServiceM365ExtensionsChannel 'Microsoft.BotService/botServices/channels@2022-06-15-preview' = { + parent: botService + location: 'global' + name: 'M365Extensions' + properties: { + channelName: 'M365Extensions' + } +} diff --git a/templates/message-extension-action-js/infra/botRegistration/readme.md b/templates/message-extension-action-js/infra/botRegistration/readme.md new file mode 100644 index 00000000..d5416243 --- /dev/null +++ b/templates/message-extension-action-js/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/templates/message-extension-action-js/package.json b/templates/message-extension-action-js/package.json new file mode 100644 index 00000000..0ad636ef --- /dev/null +++ b/templates/message-extension-action-js/package.json @@ -0,0 +1,35 @@ +{ + "name": "messageextensionactionjs", + "version": "1.0.0", + "description": "Microsoft Teams Toolkit message extension action sample", + "engines": { + "node": "16 || 18" + }, + "author": "Microsoft", + "license": "MIT", + "main": "./src/index.js", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", + "dev": "nodemon --exec node --inspect=9239 --signal SIGINT ./src/index.js", + "start": "node ./src/index.js", + "test": "echo \"Error: no test specified\" && exit 1", + "watch": "nodemon --exec \"npm run start\"" + }, + "repository": { + "type": "git", + "url": "https://github.com" + }, + "dependencies": { + "adaptive-expressions": "^4.20.0", + "adaptivecards-templating": "^2.3.1", + "adaptivecards": "^3.0.1", + "botbuilder": "^4.20.0", + "restify": "^10.0.0" + }, + "devDependencies": { + "env-cmd": "^10.1.0", + "nodemon": "^2.0.7" + } +} \ No newline at end of file diff --git a/templates/message-extension-action-js/src/actionApp.js b/templates/message-extension-action-js/src/actionApp.js new file mode 100644 index 00000000..ca5a319f --- /dev/null +++ b/templates/message-extension-action-js/src/actionApp.js @@ -0,0 +1,27 @@ +const { TeamsActivityHandler, CardFactory } = require("botbuilder"); +const ACData = require("adaptivecards-templating"); +const helloWorldCard = require("./adaptiveCards/helloWorldCard.json"); + +class ActionApp extends TeamsActivityHandler { + // Action. + handleTeamsMessagingExtensionSubmitAction(context, action) { + // The user has chosen to create a card by choosing the 'Create Card' context menu command. + const template = new ACData.Template(helloWorldCard); + const card = template.expand({ + $root: { + title: action.data.title ?? "", + subTitle: action.data.subTitle ?? "", + text: action.data.text ?? "", + }, + }); + const attachment = CardFactory.adaptiveCard(card); + return { + composeExtension: { + type: "result", + attachmentLayout: "list", + attachments: [attachment], + }, + }; + } +} +module.exports.ActionApp = ActionApp; diff --git a/templates/message-extension-action-js/src/adaptiveCards/helloWorldCard.json b/templates/message-extension-action-js/src/adaptiveCards/helloWorldCard.json new file mode 100644 index 00000000..3abefc6d --- /dev/null +++ b/templates/message-extension-action-js/src/adaptiveCards/helloWorldCard.json @@ -0,0 +1,25 @@ +{ + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "text": "${title}", + "wrap": true, + "size": "Large" + }, + { + "type": "TextBlock", + "text": "${subTitle}", + "wrap": true, + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "${text}", + "wrap": true, + "size": "Small" + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.4" +} \ No newline at end of file diff --git a/templates/message-extension-action-js/src/config.js b/templates/message-extension-action-js/src/config.js new file mode 100644 index 00000000..ea6b1a59 --- /dev/null +++ b/templates/message-extension-action-js/src/config.js @@ -0,0 +1,8 @@ +const config = { + MicrosoftAppId: process.env.BOT_ID, + MicrosoftAppType: process.env.BOT_TYPE, + MicrosoftAppTenantId: process.env.BOT_TENANT_ID, + MicrosoftAppPassword: process.env.BOT_PASSWORD, +}; + +module.exports = config; diff --git a/templates/message-extension-action-js/src/index.js b/templates/message-extension-action-js/src/index.js new file mode 100644 index 00000000..58b6da0d --- /dev/null +++ b/templates/message-extension-action-js/src/index.js @@ -0,0 +1,52 @@ +// Import required packages +const restify = require("restify"); + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +const { + CloudAdapter, + ConfigurationServiceClientCredentialFactory, + ConfigurationBotFrameworkAuthentication, +} = require("botbuilder"); +const { ActionApp } = require("./actionApp"); +const config = require("./config"); + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about adapters. +const credentialsFactory = new ConfigurationServiceClientCredentialFactory(config); + +const botFrameworkAuthentication = new ConfigurationBotFrameworkAuthentication( + {}, + credentialsFactory +); + +const adapter = new CloudAdapter(botFrameworkAuthentication); + +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + // NOTE: In production environment, you should consider logging this to Azure + // application insights. See https://aka.ms/bottelemetry for telemetry + // configuration instructions. + console.error(`\n [onTurnError] unhandled error: ${error}`); + + // Send a message to the user + await context.sendActivity(`The bot encountered an unhandled error:\n ${error.message}`); + await context.sendActivity("To continue to run this bot, please fix the bot source code."); +}; + +// Create the bot that will handle incoming messages. +const actionApp = new ActionApp(); + +// Create HTTP server. +const server = restify.createServer(); +server.use(restify.plugins.bodyParser()); +server.listen(process.env.port || process.env.PORT || 3978, function () { + console.log(`\nBot started, ${server.name} listening to ${server.url}`); +}); + +// Listen for incoming requests. +server.post("/api/messages", async (req, res) => { + await adapter.process(req, res, async (context) => { + await actionApp.run(context); + }); +}); diff --git a/templates/message-extension-action-js/teamsapp.local.yml b/templates/message-extension-action-js/teamsapp.local.yml new file mode 100644 index 00000000..9b90f42f --- /dev/null +++ b/templates/message-extension-action-js/teamsapp.local.yml @@ -0,0 +1,82 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: message-extension-action-js${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: message-extension-action-js${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: message-extension-action-js + 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 + # 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 + # Teams 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 + +deploy: + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + BOT_TYPE: 'MultiTenant' diff --git a/templates/message-extension-action-js/teamsapp.testtool.yml b/templates/message-extension-action-js/teamsapp.testtool.yml new file mode 100644 index 00000000..44d71fd1 --- /dev/null +++ b/templates/message-extension-action-js/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.1 + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/message-extension-action-js/teamsapp.yml b/templates/message-extension-action-js/teamsapp.yml new file mode 100644 index 00000000..0744b231 --- /dev/null +++ b/templates/message-extension-action-js/teamsapp.yml @@ -0,0 +1,130 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: message-extension-action-js${{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-me-action + # Teams 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 + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # 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 + # Teams 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: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install + - uses: cli/runNpmCommand + name: build app + with: + args: run build --if-present + # 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: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .webappignore + # 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}} + +# Triggered when 'teamsapp publish' is executed +publish: + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + - 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: aef0c66d-1dcc-409b-afe3-4394263cdcd4 diff --git a/templates/message-extension-action-js/web.config b/templates/message-extension-action-js/web.config new file mode 100644 index 00000000..e4525ec4 --- /dev/null +++ b/templates/message-extension-action-js/web.config @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/non-sso-tab-js/.gitignore b/templates/non-sso-tab-js/.gitignore new file mode 100644 index 00000000..c080522e --- /dev/null +++ b/templates/non-sso-tab-js/.gitignore @@ -0,0 +1,21 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +.DS_Store +.localConfigs +build +appPackage/build + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env +.deployment \ No newline at end of file diff --git a/templates/non-sso-tab-js/.vscode/extensions.json b/templates/non-sso-tab-js/.vscode/extensions.json new file mode 100644 index 00000000..aac0a6e3 --- /dev/null +++ b/templates/non-sso-tab-js/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/templates/non-sso-tab-js/.vscode/launch.json b/templates/non-sso-tab-js/.vscode/launch.json new file mode 100644 index 00000000..1bc06ea6 --- /dev/null +++ b/templates/non-sso-tab-js/.vscode/launch.json @@ -0,0 +1,247 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Outlook (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://outlook.office.com/host/${{M365_APP_ID}}?${account-hint}", + "presentation": { + "group": "group 2: Outlook", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Outlook (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://outlook.office.com/host/${{M365_APP_ID}}?${account-hint}", + "presentation": { + "group": "group 2: Outlook", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in the Microsoft 365 app (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://www.office.com/m365apps/${{M365_APP_ID}}?auth=2&${account-hint}", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in the Microsoft 365 app (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://www.office.com/m365apps/${{M365_APP_ID}}?auth=2&${account-hint}", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Outlook (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://outlook.office.com/host/${{local:M365_APP_ID}}?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Outlook (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://outlook.office.com/host/${{local:M365_APP_ID}}?${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in the Microsoft 365 app (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://www.office.com/m365apps/${{local:M365_APP_ID}}?auth=2&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in the Microsoft 365 app (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://www.office.com/m365apps/${{local:M365_APP_ID}}?auth=2&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Local Service", + "type": "node", + "request": "attach", + "port": 9239, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Attach to Frontend in Teams (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Attach to Frontend in Teams (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Outlook (Edge)", + "configurations": [ + "Attach to Frontend in Outlook (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 2: Outlook", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Outlook (Chrome)", + "configurations": [ + "Attach to Frontend in Outlook (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 2: Outlook", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in the Microsoft 365 app (Edge)", + "configurations": [ + "Attach to Frontend in the Microsoft 365 app (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in the Microsoft 365 app (Chrome)", + "configurations": [ + "Attach to Frontend in the Microsoft 365 app (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 2 + }, + "stopAll": true + } + ] +} \ No newline at end of file diff --git a/templates/non-sso-tab-js/.vscode/settings.json b/templates/non-sso-tab-js/.vscode/settings.json new file mode 100644 index 00000000..42996202 --- /dev/null +++ b/templates/non-sso-tab-js/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ] +} diff --git a/templates/non-sso-tab-js/.vscode/tasks.json b/templates/non-sso-tab-js/.vscode/tasks.json new file mode 100644 index 00000000..a3055b2a --- /dev/null +++ b/templates/non-sso-tab-js/.vscode/tasks.json @@ -0,0 +1,78 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Provision", + "Deploy", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 53000, // tab service port, + 9239 // app inspector port for Node.js debugger + ] + } + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + }, + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Compiled|Failed|compiled|failed|listening" + } + } + } + ] +} \ No newline at end of file diff --git a/templates/non-sso-tab-js/.webappignore b/templates/non-sso-tab-js/.webappignore new file mode 100644 index 00000000..217473ab --- /dev/null +++ b/templates/non-sso-tab-js/.webappignore @@ -0,0 +1,24 @@ +.webappignore +.fx +.deployment +.localSettings +.vscode +*.js.map +*.ts.map +*.ts +.git* +.tsbuildinfo +CHANGELOG.md +readme.md +local.settings.json +test +tsconfig.json +.DS_Store +teamsapp.yml +teamsapp.*.yml +/env/ +/node_modules/.bin +/node_modules/ts-node +/node_modules/typescript +/appPackage/ +/infra/ diff --git a/templates/non-sso-tab-js/README.md b/templates/non-sso-tab-js/README.md new file mode 100644 index 00000000..4dc09ecb --- /dev/null +++ b/templates/non-sso-tab-js/README.md @@ -0,0 +1,67 @@ +# Overview of the Basic Tab template + +This template showcases how Microsoft Teams supports the ability to run web-based UI inside "custom tabs" that users can install either for just themselves (personal tabs) or within a team or group chat context. + +## Get started with the Basic Tab template + +> **Prerequisites** +> +> To run the basic tab template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) +> - [Set up your dev environment for extending Teams apps across Microsoft 365](https://aka.ms/teamsfx-m365-apps-prerequisites) +> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +3. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. +4. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. + +**Congratulations**! You are running an application that can now show a basic web page in Teams, Outlook and the Microsoft 365 app. + +![Basic Tab](https://github.com/OfficeDev/TeamsFx/assets/139844715/1a7edb41-a471-46da-850c-dc21f5a4d431) + +## What's included in the template + +| Folder | Contents | +| ------------ | -------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the Teams application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| -------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `src/static/scripts/teamsapp.js` | A script that calls `teamsjs` SDK to get the context of on which Microsoft 365 application your app is running. | +| `src/static/styles/custom.css` | css file for the app. | +| `src/static/views/hello.html` | html file for the app. | +| `src/app.js` | Starting a restify server. | + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | + +## Extend the Basic Tab template + +Following documentation will help you to extend the Basic Tab template. + +- [Add or manage the environment](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-multi-env) +- [Create multi-capability app](https://learn.microsoft.com/microsoftteams/platform/toolkit/add-capability) +- [Add single sign on to your app](https://learn.microsoft.com/microsoftteams/platform/toolkit/add-single-sign-on) +- [Access data in Microsoft Graph](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-sdk#microsoft-graph-scenarios) +- [Use an existing Microsoft Entra application](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-existing-aad-app) +- [Customize the Teams app manifest](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-preview-and-customize-app-manifest) +- Host your app in Azure by [provision cloud resources](https://learn.microsoft.com/microsoftteams/platform/toolkit/provision) and [deploy the code to cloud](https://learn.microsoft.com/microsoftteams/platform/toolkit/deploy) +- [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) +- [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) +- [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) +- [Enable the app for multi-tenant](https://github.com/OfficeDev/TeamsFx/wiki/Multi-tenancy-Support-for-Azure-AD-app) +- [Preview the app on mobile clients](https://aka.ms/teamsfx-mobile) diff --git a/templates/non-sso-tab-js/appPackage/color.png b/templates/non-sso-tab-js/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/non-sso-tab-js/appPackage/color.png differ diff --git a/templates/non-sso-tab-js/appPackage/manifest.json b/templates/non-sso-tab-js/appPackage/manifest.json new file mode 100644 index 00000000..b04c0925 --- /dev/null +++ b/templates/non-sso-tab-js/appPackage/manifest.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "non-sso-tab-js${{APP_NAME_SUFFIX}}", + "full": "Full name for non-sso-tab-js" + }, + "description": { + "short": "Short description of non-sso-tab-js", + "full": "Full description of non-sso-tab-js" + }, + "accentColor": "#FFFFFF", + "bots": [], + "composeExtensions": [], + "staticTabs": [ + { + "entityId": "index0", + "name": "Home", + "contentUrl": "${{TAB_ENDPOINT}}/tab", + "websiteUrl": "${{TAB_ENDPOINT}}/tab", + "scopes": [ + "personal", + "groupChat", + "team" + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "${{TAB_DOMAIN}}" + ] +} \ No newline at end of file diff --git a/templates/non-sso-tab-js/appPackage/outline.png b/templates/non-sso-tab-js/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/non-sso-tab-js/appPackage/outline.png differ diff --git a/templates/non-sso-tab-js/env/.env.dev b/templates/non-sso-tab-js/env/.env.dev new file mode 100644 index 00000000..dbd3838d --- /dev/null +++ b/templates/non-sso-tab-js/env/.env.dev @@ -0,0 +1,14 @@ +# 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. +TEAMS_APP_ID= +TAB_ENDPOINT= diff --git a/templates/non-sso-tab-js/infra/azure.bicep b/templates/non-sso-tab-js/infra/azure.bicep new file mode 100644 index 00000000..c3a7589d --- /dev/null +++ b/templates/non-sso-tab-js/infra/azure.bicep @@ -0,0 +1,53 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +param webAppSku string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param location string = resourceGroup().location + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app' + location: location + name: serverfarmsName + sku: { + name: webAppSku + } +} + +// Azure Web App that hosts your website +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure App Service from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x for your site + } + { + name: 'RUNNING_ON_AZURE' + value: '1' + } + ] + ftpsState: 'FtpsOnly' + } + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output TAB_AZURE_APP_SERVICE_RESOURCE_ID string = webApp.id // used in deploy stage +output TAB_DOMAIN string = webApp.properties.defaultHostName +output TAB_ENDPOINT string = 'https://${webApp.properties.defaultHostName}' diff --git a/templates/non-sso-tab-js/infra/azure.parameters.json b/templates/non-sso-tab-js/infra/azure.parameters.json new file mode 100644 index 00000000..414e310c --- /dev/null +++ b/templates/non-sso-tab-js/infra/azure.parameters.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "tab${{RESOURCE_SUFFIX}}" + }, + "webAppSku": { + "value": "F1" + } + } + } \ No newline at end of file diff --git a/templates/non-sso-tab-js/package.json b/templates/non-sso-tab-js/package.json new file mode 100644 index 00000000..a55ec062 --- /dev/null +++ b/templates/non-sso-tab-js/package.json @@ -0,0 +1,22 @@ +{ + "name": "nonssotabjs", + "version": "0.1.0", + "engines": { + "node": "16 || 18" + }, + "private": true, + "dependencies": { + "restify": "^11.1.0", + "send": "^0.18.0" + }, + "devDependencies": { + "env-cmd": "^10.1.0", + "nodemon": "^2.0.21" + }, + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run start", + "start": "nodemon --inspect=9239 --signal SIGINT src/app.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "homepage": "." +} diff --git a/templates/non-sso-tab-js/src/app.js b/templates/non-sso-tab-js/src/app.js new file mode 100644 index 00000000..8eb03c09 --- /dev/null +++ b/templates/non-sso-tab-js/src/app.js @@ -0,0 +1,36 @@ +const restify = require("restify"); +const send = require("send"); +const fs = require("fs"); + +//Create HTTP server. +const server = restify.createServer({ + key: process.env.SSL_KEY_FILE ? fs.readFileSync(process.env.SSL_KEY_FILE) : undefined, + certificate: process.env.SSL_CRT_FILE ? fs.readFileSync(process.env.SSL_CRT_FILE) : undefined, + formatters: { + "text/html": function (req, res, body) { + return body; + }, + }, +}); + +server.get( + "/static/*", + restify.plugins.serveStatic({ + directory: __dirname, + }) +); + +server.listen(process.env.port || process.env.PORT || 3333, function () { + console.log(`\n${server.name} listening to ${server.url}`); +}); + +// Adding tabs to our app. This will setup routes to various views +// Setup home page +server.get("/", (req, res, next) => { + send(req, __dirname + "/views/hello.html").pipe(res); +}); + +// Setup the static tab +server.get("/tab", (req, res, next) => { + send(req, __dirname + "/views/hello.html").pipe(res); +}); diff --git a/templates/non-sso-tab-js/src/static/favicon.ico b/templates/non-sso-tab-js/src/static/favicon.ico new file mode 100644 index 00000000..ef5ef2b4 Binary files /dev/null and b/templates/non-sso-tab-js/src/static/favicon.ico differ diff --git a/templates/non-sso-tab-js/src/static/scripts/teamsapp.js b/templates/non-sso-tab-js/src/static/scripts/teamsapp.js new file mode 100644 index 00000000..15b11de8 --- /dev/null +++ b/templates/non-sso-tab-js/src/static/scripts/teamsapp.js @@ -0,0 +1,18 @@ +(function () { + "use strict"; + + // Call the initialize API first + microsoftTeams.app.initialize().then(function () { + microsoftTeams.app.getContext().then(function (context) { + if (context?.app?.host?.name) { + updateHubState(context.app.host.name); + } + }); + }); + + function updateHubState(hubName) { + if (hubName) { + document.getElementById("hubState").innerHTML = "in " + hubName; + } + } +})(); diff --git a/templates/non-sso-tab-js/src/static/styles/custom.css b/templates/non-sso-tab-js/src/static/styles/custom.css new file mode 100644 index 00000000..5f710c69 --- /dev/null +++ b/templates/non-sso-tab-js/src/static/styles/custom.css @@ -0,0 +1,19 @@ +* { + box-sizing: border-box; +} + +body { + margin: 4em; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +::-webkit-scrollbar { + display: none; +} + +span p { + display: inline; +} diff --git a/templates/non-sso-tab-js/src/views/hello.html b/templates/non-sso-tab-js/src/views/hello.html new file mode 100644 index 00000000..a62a77e4 --- /dev/null +++ b/templates/non-sso-tab-js/src/views/hello.html @@ -0,0 +1,29 @@ + + + + + + + + + Microsoft Teams Tab + + + + +
+

Hello, World

+ +

Your app is running

+

+
+
+ + + diff --git a/templates/non-sso-tab-js/teamsapp.local.yml b/templates/non-sso-tab-js/teamsapp.local.yml new file mode 100644 index 00000000..e20f5be1 --- /dev/null +++ b/templates/non-sso-tab-js/teamsapp.local.yml @@ -0,0 +1,81 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: non-sso-tab-js${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Set TAB_DOMAIN and TAB_ENDPOINT for local launch + - uses: script + with: + run: + echo "::set-teamsfx-env TAB_DOMAIN=localhost"; + echo "::set-teamsfx-env TAB_ENDPOINT=https://localhost:53000"; + # 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 + # 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 + # Teams 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 + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + devCert: + trust: true + # Write the information of installed development tool(s) into environment + # file for the specified environment variable(s). + writeToEnvironmentFile: + sslCertFile: SSL_CRT_FILE + sslKeyFile: SSL_KEY_FILE + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs + envs: + PORT: 53000 + SSL_CRT_FILE: ${{SSL_CRT_FILE}} + SSL_KEY_FILE: ${{SSL_KEY_FILE}} diff --git a/templates/non-sso-tab-js/teamsapp.yml b/templates/non-sso-tab-js/teamsapp.yml new file mode 100644 index 00000000..d14b2f4f --- /dev/null +++ b/templates/non-sso-tab-js/teamsapp.yml @@ -0,0 +1,137 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: non-sso-tab-js${{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-tab + # Teams 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 + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # 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 + # Teams 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 + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +# Triggered when 'teamsapp deploy' is executed +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --production + # 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: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .webappignore + # 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: ${{TAB_AZURE_APP_SERVICE_RESOURCE_ID}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: abe0a12a-5188-4a8e-8426-0c3c601f3b99 diff --git a/templates/non-sso-tab-js/web.config b/templates/non-sso-tab-js/web.config new file mode 100644 index 00000000..51d3ea95 --- /dev/null +++ b/templates/non-sso-tab-js/web.config @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/notification-restify-js/.appserviceignore b/templates/notification-restify-js/.appserviceignore new file mode 100644 index 00000000..2d8a3ad1 --- /dev/null +++ b/templates/notification-restify-js/.appserviceignore @@ -0,0 +1,28 @@ +.appserviceIgnore +.fx +.deployment +.localConfigs.testTool +.localConfigs +.notification.localstore.json +.notification.testtoolstore.json +.vscode +*.js.map +*.ts.map +*.ts +.git* +.tsbuildinfo +CHANGELOG.md +readme.md +local.settings.json +test +tsconfig.json +.DS_Store +teamsapp.yml +teamsapp.*.yml +/env/ +/node_modules/.bin +/node_modules/ts-node +/node_modules/typescript +/appPackage/ +/infra/ +/devTools/ \ No newline at end of file diff --git a/templates/notification-restify-js/.gitignore b/templates/notification-restify-js/.gitignore new file mode 100644 index 00000000..dfb975ac --- /dev/null +++ b/templates/notification-restify-js/.gitignore @@ -0,0 +1,26 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +.DS_Store +build +appPackage/build + +# dependencies +node_modules/ + +# misc +.env +.deployment +.DS_Store + +# build +lib/ + +# Local data +.localConfigs.testTool +.localConfigs +.notification.localstore.json +.notification.testtoolstore.json + +# Dev tool directories +/devTools/ \ No newline at end of file diff --git a/templates/notification-restify-js/.vscode/extensions.json b/templates/notification-restify-js/.vscode/extensions.json new file mode 100644 index 00000000..aac0a6e3 --- /dev/null +++ b/templates/notification-restify-js/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/templates/notification-restify-js/.vscode/launch.json b/templates/notification-restify-js/.vscode/launch.json new file mode 100644 index 00000000..ccf40807 --- /dev/null +++ b/templates/notification-restify-js/.vscode/launch.json @@ -0,0 +1,132 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Attach to Local Service", + "type": "node", + "request": "attach", + "port": 9239, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Desktop)", + "type": "node", + "request": "launch", + "preLaunchTask": "Start Teams App in Desktop Client (Remote)", + "presentation": { + "group": "3-remote", + "order": 3 + }, + "internalConsoleOptions": "neverOpen", + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "2-local", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "2-local", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Desktop)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App in Desktop Client", + "presentation": { + "group": "2-local", + "order": 3 + }, + "stopAll": true + }, + { + "name": "Debug in Test Tool", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { + "group": "1-local", + "order": 1 + }, + "stopAll": true + } + ] +} diff --git a/templates/notification-restify-js/.vscode/settings.json b/templates/notification-restify-js/.vscode/settings.json new file mode 100644 index 00000000..a2833c70 --- /dev/null +++ b/templates/notification-restify-js/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ], + "azureFunctions.stopFuncTaskPostDebug": false, + "azureFunctions.showProjectWarning": false, + "csharp.suppressDotnetRestoreNotification": true +} diff --git a/templates/notification-restify-js/.vscode/tasks.json b/templates/notification-restify-js/.vscode/tasks.json new file mode 100644 index 00000000..9034316c --- /dev/null +++ b/templates/notification-restify-js/.vscode/tasks.json @@ -0,0 +1,232 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool", + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150, // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool", + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}", + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239 // app inspector port for Node.js debugger + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Teams App in Desktop Client", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start application", + "Start desktop client" + ], + "dependsOrder": "sequence" + }, + { + "label": "Start desktop client", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true" + } + }, + { + "label": "Start Teams App in Desktop Client (Remote)", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true" + } + } + ] +} \ No newline at end of file diff --git a/templates/notification-restify-js/README.md b/templates/notification-restify-js/README.md new file mode 100644 index 00000000..813a048c --- /dev/null +++ b/templates/notification-restify-js/README.md @@ -0,0 +1,119 @@ +# Overview of the Notification bot template + +This template showcases an app that send a message to Teams with Adaptive Cards triggered by a HTTP post request. You can further extend the template to consume, transform and post events to individual, chat or channel in Teams. + +The app template is built using the TeamsFx SDK, which provides a simple set of functions over the Microsoft Bot Framework to implement this scenario. + +## Get Started with the Notification bot + +> +> **Prerequisites** +> +> To run the notification bot template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +> +> **Note** +> +> Your app can be installed into a team, or a group chat, or as personal app. See [Installation and Uninstallation](https://aka.ms/teamsfx-notification-new#customize-installation). +> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging). + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool`. +3. The browser will pop up to open Teams App Test Tool. +4. Send a POST request to `http:///api/notification` with your favorite tool (like `Postman`) + + - When your project is running locally, replace `` with `localhost:3978` + - When your project is deployed to Azure App Service, replace `` with the url from Azure App Service + +The bot will send an Adaptive Card to Teams App Test Tool: + +![Notification Message in Test Tool](https://github.com/OfficeDev/TeamsFx/assets/9698542/43ee64f4-5554-4e0b-854f-f7e20672cb25) + +## What's included in the template + +| Folder / File| Contents | +| - | - | +| `teamsapp.yml` | Main project file describes your application configuration and defines the set of actions to run in each lifecycle stages | +| `teamsapp.local.yml`| This overrides `teamsapp.yml` with actions that enable local execution and debugging | +| `teamsapp.testtool.yml`| This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool | +| `env/`| Name / value pairs are stored in environment files and used by `teamsapp.yml` to customize the provisioning and deployment rules | +| `.vscode/` | VSCode files for debugging | +| `src/` | The source code for the notification Teams application | +| `appPackage/` | Templates for the Teams application manifest | +| `infra/` | Templates for provisioning Azure resources | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| - | - | +| `src/index.js` | Application entry point and `restify` handlers for notifications | +| `src/teamsBot.js`| An empty teams activity handler for bot customization | +| `src/adaptiveCards/notification-default.json` | A generated Adaptive Card that is sent to Teams | + +## Extend the notification bot template + +There are few customizations you can make to extend the template to fit your business requirements. + +1. [Step 1: Customize the trigger point from event source](#step-1-customize-the-trigger-point-from-event-source) +2. [Step 2: Customize the notification content](#step-2-customize-the-notification-content) +3. [Step 3: Customize where notifications are sent](#step-3-customize-where-notifications-are-sent) + +### Step 1: Customize the trigger point from event source + +By default Teams Toolkit scaffolds a single `restify` entry point in `src/index.js`. When a HTTP request is sent to this entry point, the default implementation sends a hard-coded Adaptive Card to Teams. You can customize this behavior by customizing `src/index.js`. A typical implementation might make an API call to retrieve some events and/or data, and then send an Adaptive Card as appropriate. + +You can also add additional triggers by: + +- Creating new routing: `server.post("/api/new-trigger", ...);` +- Add Timer trigger(s) via widely-used npm packages such as [cron](https://www.npmjs.com/package/cron), [node-schedule](https://www.npmjs.com/package/node-schedule), etc. Or add other trigger(s) via other packages. + +### Step 2: Customize the notification content + +`src/adaptiveCards/notification-default.json` defines the default Adaptive Card. You can add, edit, or remove properties and their bindings (e.g., `${title}`) to customize the Adaptive Card to your needs. You can use the [Adaptive Card Designer](https://adaptivecards.io/designer/) to help visually design your Adaptive Card UI. + +You can also add new cards if needed. Follow this [sample](https://aka.ms/teamsfx-adaptive-card-sample-new) to see how to build different types of adaptive cards with a list or a table of dynamic contents using `ColumnSet` and `FactSet`. + +### Step 3: Customize where notifications are sent + +Notifications can be sent to where the bot is installed: + +- [Send notifications to a channel](https://aka.ms/teamsfx-notification-new#send-notifications-to-a-channel) +- [Send notifications to a group chat](https://aka.ms/teamsfx-notification-new#send-notifications-to-a-group-chat) +- [Send notifications to a personal chat](https://aka.ms/teamsfx-notification-new#send-notifications-to-a-personal-chat) + +You can also send the notifications to a specific receiver: + +- [Send notifications to a specific channel](https://aka.ms/teamsfx-notification-new#send-notifications-to-a-specific-channel) +- [Send notifications to a specific person](https://aka.ms/teamsfx-notification-new#send-notifications-to-a-specific-person) + +Congratulations, you've just created your own notification! To learn more about extending the notification bot template, [visit the documentation on Github](https://aka.ms/teamsfx-notification-new). You can find more scenarios like: + +- [Customize storage](https://aka.ms/teamsfx-notification-new#customize-storage) +- [Customize adapter](https://aka.ms/teamsfx-notification-new#customize-adapter) +- [Customize the way to initialize the bot](https://aka.ms/teamsfx-notification-new#customize-initialization) +- [Add authentication for your notification API](https://aka.ms/teamsfx-notification-new#add-authentication-for-your-notification-api) +- [Connect to existing APIs](https://aka.ms/teamsfx-notification-new#connect-to-existing-api) +- [Frequently asked questions](https://aka.ms/teamsfx-notification-new#frequently-asked-questions) + +## Extend notification bot with other bot scenarios + +Notification bot is compatible with other bot scenarios like command bot and workflow bot. + +### Add command to your application + +The command and response feature adds the ability for your application to "listen" to commands sent to it via a Teams message and respond to commands with Adaptive Cards. Follow the [steps here](https://aka.ms/teamsfx-command-new#How-to-add-more-command-and-response) to add the command response feature to your workflow bot. Refer [the command bot document](https://aka.ms/teamsfx-command-new) for more information. + +### Add workflow to your notification bot + +Adaptive cards can be updated on user action to allow user progress through a series of cards that require user input. Developers can define actions and use a bot to return an Adaptive Cards in response to user action. This can be chained into sequential workflows. Follow the [steps here](https://aka.ms/teamsfx-workflow-new#add-more-card-actions) to add workflow feature to your command bot. Refer [the workflow document](https://aka.ms/teamsfx-workflow-new) for more information. + +## Additional information and references + +- [Manage multiple environments](https://docs.microsoft.com/microsoftteams/platform/toolkit/teamsfx-multi-env) +- [Collaborate with others](https://docs.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) +- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +- [TeamsFx SDK](https://docs.microsoft.com/microsoftteams/platform/toolkit/teamsfx-sdk) +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) diff --git a/templates/notification-restify-js/appPackage/color.png b/templates/notification-restify-js/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/notification-restify-js/appPackage/color.png differ diff --git a/templates/notification-restify-js/appPackage/manifest.json b/templates/notification-restify-js/appPackage/manifest.json new file mode 100644 index 00000000..769b6e91 --- /dev/null +++ b/templates/notification-restify-js/appPackage/manifest.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "notification-restify-js${{APP_NAME_SUFFIX}}", + "full": "Full name for notification-restify-js" + }, + "description": { + "short": "Short description of notification-restify-js", + "full": "Full description of notification-restify-js" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/templates/notification-restify-js/appPackage/outline.png b/templates/notification-restify-js/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/notification-restify-js/appPackage/outline.png differ diff --git a/templates/notification-restify-js/env/.env.dev b/templates/notification-restify-js/env/.env.dev new file mode 100644 index 00000000..4b07861c --- /dev/null +++ b/templates/notification-restify-js/env/.env.dev @@ -0,0 +1,16 @@ +# 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= +BOT_DOMAIN= \ No newline at end of file diff --git a/templates/notification-restify-js/env/.env.testtool b/templates/notification-restify-js/env/.env.testtool new file mode 100644 index 00000000..43ce12aa --- /dev/null +++ b/templates/notification-restify-js/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/notification-restify-js/infra/azure.bicep b/templates/notification-restify-js/infra/azure.bicep new file mode 100644 index 00000000..256f1ce8 --- /dev/null +++ b/templates/notification-restify-js/infra/azure.bicep @@ -0,0 +1,95 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +param webAppSKU string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param identityName string = resourceBaseName +param location string = resourceGroup().location + +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: webAppSKU + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + alwaysOn: true + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure App Service from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x for your site + } + { + name: 'RUNNING_ON_AZURE' + value: '1' + } + { + name: 'BOT_ID' + value: identity.properties.clientId + } + { + name: 'BOT_TENANT_ID' + value: identity.properties.tenantId + } + { + name: 'BOT_TYPE' + 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/templates/notification-restify-js/infra/azure.parameters.json b/templates/notification-restify-js/infra/azure.parameters.json new file mode 100644 index 00000000..4e986289 --- /dev/null +++ b/templates/notification-restify-js/infra/azure.parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "notification${{RESOURCE_SUFFIX}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "notification-restify-js" + } + } + } \ No newline at end of file diff --git a/templates/notification-restify-js/infra/botRegistration/azurebot.bicep b/templates/notification-restify-js/infra/botRegistration/azurebot.bicep new file mode 100644 index 00000000..a5a27b8f --- /dev/null +++ b/templates/notification-restify-js/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/templates/notification-restify-js/infra/botRegistration/readme.md b/templates/notification-restify-js/infra/botRegistration/readme.md new file mode 100644 index 00000000..d5416243 --- /dev/null +++ b/templates/notification-restify-js/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/templates/notification-restify-js/package.json b/templates/notification-restify-js/package.json new file mode 100644 index 00000000..369cf8dd --- /dev/null +++ b/templates/notification-restify-js/package.json @@ -0,0 +1,34 @@ +{ + "name": "notificationrestifyjs", + "version": "1.0.0", + "description": "Microsoft Teams Toolkit Notification Bot Sample (Restify)", + "engines": { + "node": "16 || 18" + }, + "author": "Microsoft", + "license": "MIT", + "main": "./src/index.js", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", + "dev": "nodemon --inspect=9239 --signal SIGINT ./src/index.js", + "start": "node ./src/index.js", + "watch": "nodemon ./src/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com" + }, + "dependencies": { + "@microsoft/adaptivecards-tools": "^1.0.0", + "@microsoft/teamsfx": "^2.3.1", + "botbuilder": "^4.20.0", + "restify": "^10.0.0" + }, + "devDependencies": { + "nodemon": "^2.0.7", + "env-cmd": "^10.1.0" + } +} diff --git a/templates/notification-restify-js/src/adaptiveCards/notification-default.json b/templates/notification-restify-js/src/adaptiveCards/notification-default.json new file mode 100644 index 00000000..47a6af15 --- /dev/null +++ b/templates/notification-restify-js/src/adaptiveCards/notification-default.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "TextBlock", + "text": "${title}", + "size": "Large", + "weight": "Bolder", + "wrap": true + }, + { + "type": "TextBlock", + "text": "${appName}", + "isSubtle": true, + "color": "Accent", + "weight": "Bolder", + "size": "Small", + "spacing": "None" + }, + { + "type": "TextBlock", + "text": "${description}", + "isSubtle": true, + "wrap": true + } + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "View Documentation", + "url": "${notificationUrl}" + } + ] +} \ No newline at end of file diff --git a/templates/notification-restify-js/src/index.js b/templates/notification-restify-js/src/index.js new file mode 100644 index 00000000..57afc912 --- /dev/null +++ b/templates/notification-restify-js/src/index.js @@ -0,0 +1,122 @@ +const notificationTemplate = require("./adaptiveCards/notification-default.json"); +const { notificationApp } = require("./internal/initialize"); +const { AdaptiveCards } = require("@microsoft/adaptivecards-tools"); +const { TeamsBot } = require("./teamsBot"); +const restify = require("restify"); + +// Create HTTP server. +const server = restify.createServer(); +server.use(restify.plugins.bodyParser()); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\nApp Started, ${server.name} listening to ${server.url}`); +}); + +// HTTP trigger to send notification. You need to add authentication / authorization for this API. Refer https://aka.ms/teamsfx-notification for more details. +server.post( + "/api/notification", + restify.plugins.queryParser(), + restify.plugins.bodyParser(), // Add more parsers if needed + async (req, res) => { + const pageSize = 100; + let continuationToken = undefined; + do { + const pagedData = await notificationApp.notification.getPagedInstallations( + pageSize, + continuationToken + ); + const installations = pagedData.data; + continuationToken = pagedData.continuationToken; + + for (const target of installations) { + await target.sendAdaptiveCard( + AdaptiveCards.declare(notificationTemplate).render({ + title: "New Event Occurred!", + appName: "Contoso App Notification", + description: `This is a sample http-triggered notification to ${target.type}`, + notificationUrl: "https://aka.ms/teamsfx-notification-new", + }) + ); + + /****** To distinguish different target types ******/ + /** "Channel" means this bot is installed to a Team (default to notify General channel) + if (target.type === NotificationTargetType.Channel) { + // Directly notify the Team (to the default General channel) + await target.sendAdaptiveCard(...); + + // List all channels in the Team then notify each channel + const channels = await target.channels(); + for (const channel of channels) { + await channel.sendAdaptiveCard(...); + } + + // List all members in the Team then notify each member + const pageSize = 100; + let continuationToken = undefined; + do { + const pagedData = await target.getPagedMembers(pageSize, continuationToken); + const members = pagedData.data; + continuationToken = pagedData.continuationToken; + + for (const member of members) { + await member.sendAdaptiveCard(...); + } + } while (continuationToken); + } + **/ + + /** "Group" means this bot is installed to a Group Chat + if (target.type === NotificationTargetType.Group) { + // Directly notify the Group Chat + await target.sendAdaptiveCard(...); + + // List all members in the Group Chat then notify each member + const pageSize = 100; + let continuationToken = undefined; + do { + const pagedData = await target.getPagedMembers(pageSize, continuationToken); + const members = pagedData.data; + continuationToken = pagedData.continuationToken; + + for (const member of members) { + await member.sendAdaptiveCard(...); + } + } while (continuationToken); + } + **/ + + /** "Person" means this bot is installed as a Personal app + if (target.type === NotificationTargetType.Person) { + // Directly notify the individual person + await target.sendAdaptiveCard(...); + } + **/ + } + } while (continuationToken); + + /** You can also find someone and notify the individual person + const member = await notificationApp.notification.findMember( + async (m) => m.account.email === "someone@contoso.com" + ); + await member?.sendAdaptiveCard(...); + **/ + + /** Or find multiple people and notify them + const members = await notificationApp.notification.findAllMembers( + async (m) => m.account.email?.startsWith("test") + ); + for (const member of members) { + await member.sendAdaptiveCard(...); + } + **/ + + res.json({}); + } +); + +// Bot Framework message handler. +const teamsBot = new TeamsBot(); +server.post("/api/messages", async (req, res) => { + await notificationApp.requestHandler(req, res, async (context) => { + await teamsBot.run(context); + }); +}); diff --git a/templates/notification-restify-js/src/internal/config.js b/templates/notification-restify-js/src/internal/config.js new file mode 100644 index 00000000..ea6b1a59 --- /dev/null +++ b/templates/notification-restify-js/src/internal/config.js @@ -0,0 +1,8 @@ +const config = { + MicrosoftAppId: process.env.BOT_ID, + MicrosoftAppType: process.env.BOT_TYPE, + MicrosoftAppTenantId: process.env.BOT_TENANT_ID, + MicrosoftAppPassword: process.env.BOT_PASSWORD, +}; + +module.exports = config; diff --git a/templates/notification-restify-js/src/internal/initialize.js b/templates/notification-restify-js/src/internal/initialize.js new file mode 100644 index 00000000..45590ee5 --- /dev/null +++ b/templates/notification-restify-js/src/internal/initialize.js @@ -0,0 +1,18 @@ +const { BotBuilderCloudAdapter } = require("@microsoft/teamsfx"); +const ConversationBot = BotBuilderCloudAdapter.ConversationBot; +const config = require("./config"); + +// Create bot. +const notificationApp = new ConversationBot({ + // The bot id and password to create CloudAdapter. + // See https://aka.ms/about-bot-adapter to learn more about adapters. + adapterConfig: config, + // Enable notification + notification: { + enabled: true, + }, +}); + +module.exports = { + notificationApp, +}; diff --git a/templates/notification-restify-js/src/teamsBot.js b/templates/notification-restify-js/src/teamsBot.js new file mode 100644 index 00000000..c4cd2cc5 --- /dev/null +++ b/templates/notification-restify-js/src/teamsBot.js @@ -0,0 +1,26 @@ +const { TeamsActivityHandler } = require("botbuilder"); + +// Teams activity handler. +// You can add your customization code here to extend your bot logic if needed. +class TeamsBot extends TeamsActivityHandler { + constructor() { + super(); + + // Listen to MembersAdded event, view https://docs.microsoft.com/en-us/microsoftteams/platform/resources/bot-v3/bots-notifications for more events + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (let cnt = 0; cnt < membersAdded.length; cnt++) { + if (membersAdded[cnt].id) { + await context.sendActivity( + "Welcome to the Notification Bot! I am designed to send you updates and alerts using Adaptive Cards triggered by HTTP post requests. " + + "Please note that I am a notification-only bot and you can't interact with me. Follow the README in the project and stay tuned for notifications!" + ); + break; + } + } + await next(); + }); + } +} + +module.exports.TeamsBot = TeamsBot; diff --git a/templates/notification-restify-js/teamsapp.local.yml b/templates/notification-restify-js/teamsapp.local.yml new file mode 100644 index 00000000..f092aad6 --- /dev/null +++ b/templates/notification-restify-js/teamsapp.local.yml @@ -0,0 +1,82 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: notification-restify-js${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: notification-restify-js${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: notification-restify-js + 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 + # 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 + # Teams 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 + +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + BOT_TYPE: 'MultiTenant' diff --git a/templates/notification-restify-js/teamsapp.testtool.yml b/templates/notification-restify-js/teamsapp.testtool.yml new file mode 100644 index 00000000..44d71fd1 --- /dev/null +++ b/templates/notification-restify-js/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.1 + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/notification-restify-js/teamsapp.yml b/templates/notification-restify-js/teamsapp.yml new file mode 100644 index 00000000..4f047753 --- /dev/null +++ b/templates/notification-restify-js/teamsapp.yml @@ -0,0 +1,127 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: notification-restify-js${{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 + # Teams 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 + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # 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 + # Teams 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: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --production + # 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: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .appserviceignore + # 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}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: bf7a5169-6674-40c0-906b-bf117cb9f58b diff --git a/templates/notification-restify-js/web.config b/templates/notification-restify-js/web.config new file mode 100644 index 00000000..c766d733 --- /dev/null +++ b/templates/notification-restify-js/web.config @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/spfx-tab-ts/.gitignore b/templates/spfx-tab-ts/.gitignore new file mode 100644 index 00000000..b4b8e876 --- /dev/null +++ b/templates/spfx-tab-ts/.gitignore @@ -0,0 +1,20 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +.DS_Store +build +appPackage/build + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env +.deployment \ No newline at end of file diff --git a/templates/spfx-tab-ts/.vscode/extensions.json b/templates/spfx-tab-ts/.vscode/extensions.json new file mode 100644 index 00000000..aac0a6e3 --- /dev/null +++ b/templates/spfx-tab-ts/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/templates/spfx-tab-ts/.vscode/launch.json b/templates/spfx-tab-ts/.vscode/launch.json new file mode 100644 index 00000000..ddd2552d --- /dev/null +++ b/templates/spfx-tab-ts/.vscode/launch.json @@ -0,0 +1,311 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Outlook (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://outlook.office.com/host/${{M365_APP_ID}}?${account-hint}", + "presentation": { + "group": "group 2: Outlook", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Outlook (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://outlook.office.com/host/${{M365_APP_ID}}?${account-hint}", + "presentation": { + "group": "group 2: Outlook", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in the Microsoft 365 app (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://www.office.com/m365apps/${{M365_APP_ID}}?auth=2&${account-hint}", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in the Microsoft 365 app (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://www.office.com/m365apps/${{M365_APP_ID}}?auth=2&${account-hint}", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "SharePoint workbench (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx", + "webRoot": "${workspaceRoot}/src", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "runtimeArgs": [ + "--remote-debugging-port=9222", + "-incognito" + ], + "preLaunchTask": "gulp serve", + "postDebugTask": "Terminate All Tasks", + "presentation": { + "group": "remote", + "order": 1 + } + }, + { + "name": "SharePoint workbench (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx", + "webRoot": "${workspaceRoot}/src", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "runtimeArgs": [ + "--remote-debugging-port=9222", + "-incognito" + ], + "preLaunchTask": "gulp serve", + "postDebugTask": "Terminate All Tasks", + "presentation": { + "group": "remote", + "order": 2 + } + }, + { + "name": "Start Teams workbench (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "webRoot": "${workspaceRoot}/src", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "postDebugTask": "Terminate All Tasks", + "presentation": { + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Start Teams workbench (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "webRoot": "${workspaceRoot}/src", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "postDebugTask": "Terminate All Tasks", + "presentation": { + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Outlook (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://outlook.office.com/host/${{local:M365_APP_ID}}?${account-hint}", + "webRoot": "${workspaceRoot}/src", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "postDebugTask": "Terminate All Tasks", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Outlook (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://outlook.office.com/host/${{local:M365_APP_ID}}?${account-hint}", + "webRoot": "${workspaceRoot}/src", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "postDebugTask": "Terminate All Tasks", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in the Microsoft 365 app (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://www.office.com/m365apps/${{local:M365_APP_ID}}?auth=2&${account-hint}", + "webRoot": "${workspaceRoot}/src", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "postDebugTask": "Terminate All Tasks", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in the Microsoft 365 app (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://www.office.com/m365apps/${{local:M365_APP_ID}}?auth=2&${account-hint}", + "webRoot": "${workspaceRoot}/src", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "postDebugTask": "Terminate All Tasks", + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Teams workbench (Edge)", + "configurations": [ + "Start Teams workbench (Edge)" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Teams workbench (Chrome)", + "configurations": [ + "Start Teams workbench (Chrome)" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Outlook workbench (Edge)", + "configurations": [ + "Attach to Frontend in Outlook (Edge)" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 2: Outlook", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Outlook workbench (Chrome)", + "configurations": [ + "Attach to Frontend in Outlook (Chrome)" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 2: Outlook", + "order": 2 + }, + "stopAll": true + }, + { + "name": "The Microsoft 365 app workbench (Edge)", + "configurations": [ + "Attach to Frontend in the Microsoft 365 app (Edge)" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 1 + }, + "stopAll": true + }, + { + "name": "The Microsoft 365 app workbench (Chrome)", + "configurations": [ + "Attach to Frontend in the Microsoft 365 app (Chrome)" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 2 + }, + "stopAll": true + } + ] +} diff --git a/templates/spfx-tab-ts/.vscode/settings.json b/templates/spfx-tab-ts/.vscode/settings.json new file mode 100644 index 00000000..3014fd9c --- /dev/null +++ b/templates/spfx-tab-ts/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "debug.onTaskErrors": "abort" +} diff --git a/templates/spfx-tab-ts/.vscode/tasks.json b/templates/spfx-tab-ts/.vscode/tasks.json new file mode 100644 index 00000000..a73ca0f0 --- /dev/null +++ b/templates/spfx-tab-ts/.vscode/tasks.json @@ -0,0 +1,113 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Provision", + "gulp serve" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 4321 // SPFx service port + ] + } + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "gulp trust-dev-cert", + "type": "process", + "command": "node", + "args": [ + "${workspaceFolder}/src/node_modules/gulp/bin/gulp.js", + "trust-dev-cert" + ], + "options": { + "cwd": "${workspaceFolder}/src" + }, + "dependsOn": "Deploy" + }, + { + "label": "gulp serve", + "type": "process", + "command": "node", + "args": [ + "${workspaceFolder}/src/node_modules/gulp/bin/gulp.js", + "serve", + "--nobrowser" + ], + "problemMatcher": [ + { + "pattern": [ + { + "regexp": ".", + "file": 1, + "location": 2, + "message": 3 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "^.*Starting gulp.*", + "endsPattern": "^.*Finished subtask 'reload'.*" + } + } + ], + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}/src" + }, + "dependsOn": "gulp trust-dev-cert" + }, + { + "label": "Terminate All Tasks", + "command": "echo ${input:terminate}", + "type": "shell", + "problemMatcher": [] + } + ], + "inputs": [ + { + "id": "terminate", + "type": "command", + "command": "workbench.action.tasks.terminate", + "args": "terminateAll" + } + ] +} \ No newline at end of file diff --git a/templates/spfx-tab-ts/appPackage/color.png b/templates/spfx-tab-ts/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/spfx-tab-ts/appPackage/color.png differ diff --git a/templates/spfx-tab-ts/appPackage/manifest.json b/templates/spfx-tab-ts/appPackage/manifest.json new file mode 100644 index 00000000..9be1c9e4 --- /dev/null +++ b/templates/spfx-tab-ts/appPackage/manifest.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "id": "${{TEAMS_APP_ID}}", + "version": "1.0.0", + "developer": { + "name": "SPFx + Teams Dev", + "websiteUrl": "https://products.office.com/en-us/sharepoint/collaboration", + "privacyUrl": "https://privacy.microsoft.com/en-us/privacystatement", + "termsOfUseUrl": "https://www.microsoft.com/en-us/servicesagreement" + }, + "name": { + "short": "spfx-tab-ts${{APP_NAME_SUFFIX}}", + "full": "Full name for spfx-tab-ts" + }, + "description": { + "short": "Short description of spfx-tab-ts", + "full": "Full description of spfx-tab-ts" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "accentColor": "#004578", + "staticTabs": [ + { + "entityId": "d901749f-b3b7-47ed-817b-b567fc84ed97", + "name": "helloworld", + "contentUrl": "https://{teamSiteDomain}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest=/_layouts/15/teamshostedapp.aspx%3Fteams%26personal%26componentId=d901749f-b3b7-47ed-817b-b567fc84ed97%26forceLocale={locale}", + "websiteUrl": "https://products.office.com/en-us/sharepoint/collaboration", + "scopes": [ + "personal" + ] + } + ], + "configurableTabs": [ + { + "configurationUrl": "https://{teamSiteDomain}{teamSitePath}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/teamshostedapp.aspx%3FopenPropertyPane=true%26teams%26componentId=d901749f-b3b7-47ed-817b-b567fc84ed97%26forceLocale={locale}", + "canUpdateConfiguration": true, + "scopes": [ + "team" + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "*.login.microsoftonline.com", + "*.sharepoint.com", + "*.sharepoint-df.com", + "spoppe-a.akamaihd.net", + "spoprod-a.akamaihd.net", + "resourceseng.blob.core.windows.net", + "msft.spoppe.com" + ], + "webApplicationInfo": { + "resource": "https://{teamSiteDomain}", + "id": "00000003-0000-0ff1-ce00-000000000000" + } +} \ No newline at end of file diff --git a/templates/spfx-tab-ts/appPackage/manifest.local.json b/templates/spfx-tab-ts/appPackage/manifest.local.json new file mode 100644 index 00000000..695a4d00 --- /dev/null +++ b/templates/spfx-tab-ts/appPackage/manifest.local.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "id": "${{TEAMS_APP_ID}}", + "version": "1.0.0", + "developer": { + "name": "SPFx + Teams Dev", + "websiteUrl": "https://products.office.com/en-us/sharepoint/collaboration", + "privacyUrl": "https://privacy.microsoft.com/en-us/privacystatement", + "termsOfUseUrl": "https://www.microsoft.com/en-us/servicesagreement" + }, + "name": { + "short": "spfx-tab-ts${{APP_NAME_SUFFIX}}", + "full": "Full name for spfx-tab-ts" + }, + "description": { + "short": "Short description of spfx-tab-ts", + "full": "Full description of spfx-tab-ts" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "accentColor": "#004578", + "staticTabs": [ + { + "entityId": "d901749f-b3b7-47ed-817b-b567fc84ed97", + "name": "helloworld", + "contentUrl": "https://{teamSiteDomain}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/TeamsWorkBench.aspx%3FcomponentId=d901749f-b3b7-47ed-817b-b567fc84ed97%26teams%26personal%26forceLocale={locale}%26loadSPFX%3Dtrue%26debugManifestsFile%3Dhttps%3A%2F%2Flocalhost%3A4321%2Ftemp%2Fmanifests.js", + "websiteUrl": "https://products.office.com/en-us/sharepoint/collaboration", + "scopes": [ + "personal" + ] + } + ], + "configurableTabs": [ + { + "configurationUrl": "https://{teamSiteDomain}{teamSitePath}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/TeamsWorkBench.aspx%3FcomponentId=d901749f-b3b7-47ed-817b-b567fc84ed97%26openPropertyPane=true%26teams%26forceLocale={locale}%26loadSPFX%3Dtrue%26debugManifestsFile%3Dhttps%3A%2F%2Flocalhost%3A4321%2Ftemp%2Fmanifests.js", + "canUpdateConfiguration": true, + "scopes": [ + "team" + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "*.login.microsoftonline.com", + "*.sharepoint.com", + "*.sharepoint-df.com", + "spoppe-a.akamaihd.net", + "spoprod-a.akamaihd.net", + "resourceseng.blob.core.windows.net", + "msft.spoppe.com" + ], + "webApplicationInfo": { + "resource": "https://{teamSiteDomain}", + "id": "00000003-0000-0ff1-ce00-000000000000" + } +} \ No newline at end of file diff --git a/templates/spfx-tab-ts/appPackage/outline.png b/templates/spfx-tab-ts/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/spfx-tab-ts/appPackage/outline.png differ diff --git a/templates/spfx-tab-ts/env/.env.dev b/templates/spfx-tab-ts/env/.env.dev new file mode 100644 index 00000000..22133309 --- /dev/null +++ b/templates/spfx-tab-ts/env/.env.dev @@ -0,0 +1,8 @@ +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Generated during provision, you can also add your own variables. +TEAMS_APP_ID= + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. \ No newline at end of file diff --git a/templates/spfx-tab-ts/package.json b/templates/spfx-tab-ts/package.json new file mode 100644 index 00000000..4d5d6859 --- /dev/null +++ b/templates/spfx-tab-ts/package.json @@ -0,0 +1,14 @@ +{ + "name": "teamsfx-template-spfx-tab", + "version": "0.0.1", + "description": "", + "engines": { + "node": ">=18.17.1 <19.0.0" + }, + "author": "", + "scripts": { + "dev:teamsfx": "cd ./src && npx gulp serve --nobrowser", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "MIT" +} \ No newline at end of file diff --git a/templates/spfx-tab-ts/src/.eslintrc.js b/templates/spfx-tab-ts/src/.eslintrc.js new file mode 100644 index 00000000..473df80c --- /dev/null +++ b/templates/spfx-tab-ts/src/.eslintrc.js @@ -0,0 +1,352 @@ +require('@rushstack/eslint-config/patch/modern-module-resolution'); +module.exports = { + extends: ['@microsoft/eslint-config-spfx/lib/profiles/react'], + parserOptions: { tsconfigRootDir: __dirname }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + parser: '@typescript-eslint/parser', + 'parserOptions': { + 'project': './tsconfig.json', + 'ecmaVersion': 2018, + 'sourceType': 'module' + }, + rules: { + // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/no-new-null': 1, + // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/hoist-jest-mock': 1, + // Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security + '@rushstack/security/no-unsafe-regexp': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/adjacent-overload-signatures': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // CONFIGURATION: By default, these are banned: String, Boolean, Number, Object, Symbol + '@typescript-eslint/ban-types': [ + 1, + { + 'extendDefaults': false, + 'types': { + 'String': { + 'message': 'Use \'string\' instead', + 'fixWith': 'string' + }, + 'Boolean': { + 'message': 'Use \'boolean\' instead', + 'fixWith': 'boolean' + }, + 'Number': { + 'message': 'Use \'number\' instead', + 'fixWith': 'number' + }, + 'Object': { + 'message': 'Use \'object\' instead, or else define a proper TypeScript type:' + }, + 'Symbol': { + 'message': 'Use \'symbol\' instead', + 'fixWith': 'symbol' + }, + 'Function': { + 'message': 'The \'Function\' type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with \'new\'.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape.' + } + } + } + ], + // RATIONALE: Code is more readable when the type of every variable is immediately obvious. + // Even if the compiler may be able to infer a type, this inference will be unavailable + // to a person who is reviewing a GitHub diff. This rule makes writing code harder, + // but writing code is a much less important activity than reading it. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/explicit-function-return-type': [ + 1, + { + 'allowExpressions': true, + 'allowTypedFunctionExpressions': true, + 'allowHigherOrderFunctions': false + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: although this is a recommended rule, it is up to dev to select coding style. + // Set to 1 (warning) or 2 (error) to enable. + '@typescript-eslint/explicit-member-accessibility': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-array-constructor': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript. + // This rule should be suppressed only in very special cases such as JSON.stringify() + // where the type really can be anything. Even if the type is flexible, another type + // may be more appropriate such as "unknown", "{}", or "Record". + '@typescript-eslint/no-explicit-any': 1, + // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() + // handler. Thus wherever a Promise arises, the code must either append a catch handler, + // or else return the object to a caller (who assumes this responsibility). Unterminated + // promise chains are a serious issue. Besides causing errors to be silently ignored, + // they can also cause a NodeJS process to terminate unexpectedly. + '@typescript-eslint/no-floating-promises': 2, + // RATIONALE: Catches a common coding mistake. + '@typescript-eslint/no-for-in-array': 2, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-misused-new': 2, + // RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks + // a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler + // optimizations. If you are declaring loose functions/variables, it's better to make them + // static members of a class, since classes support property getters and their private + // members are accessible by unit tests. Also, the exercise of choosing a meaningful + // class name tends to produce more discoverable APIs: for example, search+replacing + // the function "reverse()" is likely to return many false matches, whereas if we always + // write "Text.reverse()" is more unique. For large scale organization, it's recommended + // to decompose your code into separate NPM packages, which ensures that component + // dependencies are tracked more conscientiously. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-namespace': [ + 1, + { + 'allowDeclarations': false, + 'allowDefinitionFiles': false + } + ], + // RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)" + // that avoids the effort of declaring "title" as a field. This TypeScript feature makes + // code easier to write, but arguably sacrifices readability: In the notes for + // "@typescript-eslint/member-ordering" we pointed out that fields are central to + // a class's design, so we wouldn't want to bury them in a constructor signature + // just to save some typing. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/parameter-properties': 0, + // RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code + // may impact performance. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-unused-vars': [ + 1, + { + 'vars': 'all', + // Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code, + // the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures + // that are overriding a base class method or implementing an interface. + 'args': 'none' + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-use-before-define': [ + 2, + { + 'functions': false, + 'classes': true, + 'variables': true, + 'enums': true, + 'typedefs': true + } + ], + // Disallows require statements except in import statements. + // In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports. + '@typescript-eslint/no-var-requires': 'error', + // RATIONALE: The "module" keyword is deprecated except when describing legacy libraries. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/prefer-namespace-keyword': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: it's up to developer to decide if he wants to add type annotations + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/no-inferrable-types': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios + '@typescript-eslint/no-empty-interface': 0, + // RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake. + 'accessor-pairs': 1, + // RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking. + 'dot-notation': [ + 1, + { + 'allowPattern': '^_' + } + ], + // RATIONALE: Catches code that is likely to be incorrect + 'eqeqeq': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'for-direction': 1, + // RATIONALE: Catches a common coding mistake. + 'guard-for-in': 2, + // RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time + // to split up your code. + 'max-lines': ['warn', { max: 2000 }], + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-async-promise-executor': 2, + // RATIONALE: Deprecated language feature. + 'no-caller': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-compare-neg-zero': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-cond-assign': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-constant-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-control-regex': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-debugger': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-delete-var': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-duplicate-case': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-character-class': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-pattern': 1, + // RATIONALE: Eval is a security concern and a performance concern. + 'no-eval': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-ex-assign': 2, + // RATIONALE: System types are global and should not be tampered with in a scalable code base. + // If two different libraries (or two versions of the same library) both try to modify + // a type, only one of them can win. Polyfills are acceptable because they implement + // a standardized interoperable contract, but polyfills are generally coded in plain + // JavaScript. + 'no-extend-native': 1, + // Disallow unnecessary labels + 'no-extra-label': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-fallthrough': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-func-assign': 1, + // RATIONALE: Catches a common coding mistake. + 'no-implied-eval': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-invalid-regexp': 2, + // RATIONALE: Catches a common coding mistake. + 'no-label-var': 2, + // RATIONALE: Eliminates redundant code. + 'no-lone-blocks': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-misleading-character-class': 2, + // RATIONALE: Catches a common coding mistake. + 'no-multi-str': 2, + // RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to + // a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()", + // or else implies that the constructor is doing nontrivial computations, which is often + // a poor class design. + 'no-new': 1, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-func': 2, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-object': 2, + // RATIONALE: Obsolete notation. + 'no-new-wrappers': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-octal': 2, + // RATIONALE: Catches code that is likely to be incorrect + 'no-octal-escape': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-regex-spaces': 2, + // RATIONALE: Catches a common coding mistake. + 'no-return-assign': 2, + // RATIONALE: Security risk. + 'no-script-url': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-self-assign': 2, + // RATIONALE: Catches a common coding mistake. + 'no-self-compare': 2, + // RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use + // commas to create compound expressions. In general code is more readable if each + // step is split onto a separate line. This also makes it easier to set breakpoints + // in the debugger. + 'no-sequences': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-shadow-restricted-names': 2, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-sparse-arrays': 2, + // RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception, + // such flexibility adds pointless complexity, by requiring every catch block to test + // the type of the object that it receives. Whereas if catch blocks can always assume + // that their object implements the "Error" contract, then the code is simpler, and + // we generally get useful additional information like a call stack. + 'no-throw-literal': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unmodified-loop-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unsafe-finally': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unused-expressions': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unused-labels': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-useless-catch': 1, + // RATIONALE: Avoids a potential performance problem. + 'no-useless-concat': 1, + // RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior. + // Always use "let" or "const" instead. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + 'no-var': 2, + // RATIONALE: Generally not needed in modern code. + 'no-void': 1, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-with': 2, + // RATIONALE: Makes logic easier to understand, since constants always have a known value + // @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js + 'prefer-const': 1, + // RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused. + 'promise/param-names': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-atomic-updates': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-yield': 1, + // "Use strict" is redundant when using the TypeScript compiler. + 'strict': [ + 2, + 'never' + ], + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'use-isnan': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + // Set to 1 (warning) or 2 (error) to enable. + // Rationale to disable: !!{} + 'no-extra-boolean-cast': 0, + // ==================================================================== + // @microsoft/eslint-plugin-spfx + // ==================================================================== + '@microsoft/spfx/import-requires-chunk-name': 1, + '@microsoft/spfx/no-require-ensure': 2, + '@microsoft/spfx/pair-react-dom-render-unmount': 1 + } + }, + { + // For unit tests, we can be a little bit less strict. The settings below revise the + // defaults specified in the extended configurations, as well as above. + files: [ + // Test files + '*.test.ts', + '*.test.tsx', + '*.spec.ts', + '*.spec.tsx', + + // Facebook convention + '**/__mocks__/*.ts', + '**/__mocks__/*.tsx', + '**/__tests__/*.ts', + '**/__tests__/*.tsx', + + // Microsoft convention + '**/test/*.ts', + '**/test/*.tsx' + ], + rules: {} + } + ] +}; \ No newline at end of file diff --git a/templates/spfx-tab-ts/src/.gitignore b/templates/spfx-tab-ts/src/.gitignore new file mode 100644 index 00000000..51ca7b9e --- /dev/null +++ b/templates/spfx-tab-ts/src/.gitignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules + +# Build generated files +dist +lib +release +solution +temp +*.sppkg +.heft + +# Coverage directory used by tools like istanbul +coverage + +# OSX +.DS_Store + +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj + +# Resx Generated Code +*.resx.ts + +# Styles Generated Code +*.scss.ts diff --git a/templates/spfx-tab-ts/src/.npmignore b/templates/spfx-tab-ts/src/.npmignore new file mode 100644 index 00000000..ae0b487c --- /dev/null +++ b/templates/spfx-tab-ts/src/.npmignore @@ -0,0 +1,16 @@ +!dist +config + +gulpfile.js + +release +src +temp + +tsconfig.json +tslint.json + +*.log + +.yo-rc.json +.vscode diff --git a/templates/spfx-tab-ts/src/.yo-rc.json b/templates/spfx-tab-ts/src/.yo-rc.json new file mode 100644 index 00000000..62390e62 --- /dev/null +++ b/templates/spfx-tab-ts/src/.yo-rc.json @@ -0,0 +1,24 @@ +{ + "@microsoft/generator-sharepoint": { + "whichFolder": "subdir", + "solutionName": "spfx-tab-ts", + "environment": "spo", + "skipFeatureDeployment": true, + "isDomainIsolated": false, + "componentType": "webpart", + "template": "react", + "componentName": "helloworld", + "plusBeta": false, + "isCreatingSolution": true, + "nodeVersion": "18.20.3", + "sdksVersions": { + "@microsoft/microsoft-graph-client": "3.0.2", + "@microsoft/teams-js": "2.12.0" + }, + "version": "1.19.0", + "libraryName": "spfx-tab-ts", + "libraryId": "4038e84d-c177-4de6-a4e6-254c247e6df5", + "packageManager": "npm", + "solutionShortDescription": "spfx-tab-ts description" + } +} diff --git a/templates/spfx-tab-ts/src/README.md b/templates/spfx-tab-ts/src/README.md new file mode 100644 index 00000000..af3eac3b --- /dev/null +++ b/templates/spfx-tab-ts/src/README.md @@ -0,0 +1,79 @@ +# SPFx (SharePoint Framework) App + +## Summary + +The SharePoint Framework (SPFx) is a page and web part model that provides full support for client-side SharePoint development, easy integration with SharePoint data, and extending Microsoft Teams. This project applies SPFx to Teams personal tab and group tab support. + +## Applies to + +- [SharePoint Framework](https://aka.ms/spfx) +- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) + +## Prerequisites + +> - [Set up SharePoint Framework development environment](https://aka.ms/teamsfx-spfx-dev-environment-setup) +> - An Microsoft 365 account. Get your own free Microsoft 365 tenant from [Microsoft 365 developer program](https://developer.microsoft.com/en-us/microsoft-365/dev-program) +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +## Solution + +| Solution | Author(s) | +| ----------- | ------------------------------------------------------- | +| folder name | Author details (name, company, twitter alias with link) | + +## Version history + +| Version | Date | Comments | +| ------- | ---------------- | --------------- | +| 1.1 | March 10, 2021 | Update comment | +| 1.0 | January 29, 2021 | Initial release | + +## Disclaimer + +**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +--- + +## Minimal Path to Awesome + +1. Open the project with VSCode, click `Provision` in LIFECYCLE panel of Teams Toolkit extension. + + Or you can use Teams Toolkit CLI with running this cmd under your project path: + `teamsapp provision` + + It will provision an app in Teams App Studio. You may need to login with your Microsoft 365 tenant admin account. + +2. Build and Deploy your SharePoint Package. + + - Click `Deploy` in LIFECYCLE panel of Teams Toolkit extension, or run `Teams: Deploy` from command palette. This will generate a SharePoint package (\*.sppkg) under sharepoint/solution folder. + + Or you can use Teams Toolkit CLI with running this cmd under your project path: + `teamsapp deploy` + + - After building the \*.sppkg, the Teams Toolkit extension will upload and deploy it to your tenant App Catalog. Only tenant App Catalog site admin has permission to do it. You can create your test tenant following [Setup your Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant). + +3. Go back to Teams Toolkit extension, click `Teams: Publish` in LIFECYCLE panel. + + Or you can use Teams Toolkit CLI with running this cmd under your project path: + `teamsapp publish` + + You will find your app in [Microsoft Teams admin center](https://admin.teams.microsoft.com/policies/manage-apps). Enter your app name in the search box. Click the item and select `Publish` in the Publishing status. + +4. You may need to wait for a few minutes after publishing your teams app. And then login to Teams, and you will find your app in the `Apps - Built for {your-tenant-name}` category. + +5. Click "Add" to use the app as a personal tab. Click "Add to a team" to use the app as a group tab. + +## Debug + +Start debugging the project by hitting the `F5` key in Visual Studio Code. Alternatively use the `Run and Debug Activity Panel` in Visual Studio Code and click the `Start Debugging` green arrow button. + +- `Teams workbench` is the default debug configuration. Using this configuration, you can install the SPFx app within Teams context as a Teams app. +- `SharePoint workbench`. You need to navigate to [launch.json](../.vscode/launch.json), replace `enter-your-SharePoint-site` with your SharePoint site, eg. `https://{your-tenant-name}.sharepoint.com/sites/{your-team-name}/_layouts/workbench.aspx`. You can also use root site if you haven't created one, eg. `https://{your-tenant-name}.sharepoint.com/_layouts/workbench.aspx`. + +## References + +- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) +- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview) +- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis) +- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview) +- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development diff --git a/templates/spfx-tab-ts/src/config/config.json b/templates/spfx-tab-ts/src/config/config.json new file mode 100644 index 00000000..6f9cd619 --- /dev/null +++ b/templates/spfx-tab-ts/src/config/config.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "helloworld-web-part": { + "components": [ + { + "entrypoint": "./lib/webparts/helloworld/HelloworldWebPart.js", + "manifest": "./src/webparts/helloworld/HelloworldWebPart.manifest.json" + } + ] + } + }, + "externals": {}, + "localizedResources": { + "HelloworldWebPartStrings": "lib/webparts/helloworld/loc/{locale}.js" + } +} diff --git a/templates/spfx-tab-ts/src/config/deploy-azure-storage.json b/templates/spfx-tab-ts/src/config/deploy-azure-storage.json new file mode 100644 index 00000000..094854fb --- /dev/null +++ b/templates/spfx-tab-ts/src/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./release/assets/", + "account": "", + "container": "spfx-tab-ts", + "accessKey": "" +} \ No newline at end of file diff --git a/templates/spfx-tab-ts/src/config/package-solution.json b/templates/spfx-tab-ts/src/config/package-solution.json new file mode 100644 index 00000000..83b68ffe --- /dev/null +++ b/templates/spfx-tab-ts/src/config/package-solution.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", + "solution": { + "name": "spfx-tab-ts-client-side-solution", + "id": "4038e84d-c177-4de6-a4e6-254c247e6df5", + "version": "1.0.0.0", + "includeClientSideAssets": true, + "skipFeatureDeployment": true, + "isDomainIsolated": false, + "developer": { + "name": "", + "websiteUrl": "", + "privacyUrl": "", + "termsOfUseUrl": "", + "mpnId": "Undefined-1.19.0" + }, + "metadata": { + "shortDescription": { + "default": "spfx-tab-ts description" + }, + "longDescription": { + "default": "spfx-tab-ts description" + }, + "screenshotPaths": [], + "videoUrl": "", + "categories": [] + }, + "features": [ + { + "title": "spfx-tab-ts Feature", + "description": "The feature that activates elements of the spfx-tab-ts solution.", + "id": "bd2199f1-6003-4095-a8b2-709712c7ab72", + "version": "1.0.0.0" + } + ] + }, + "paths": { + "zippedPackage": "solution/spfx-tab-ts.sppkg" + } +} diff --git a/templates/spfx-tab-ts/src/config/sass.json b/templates/spfx-tab-ts/src/config/sass.json new file mode 100644 index 00000000..5e78c982 --- /dev/null +++ b/templates/spfx-tab-ts/src/config/sass.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json" +} \ No newline at end of file diff --git a/templates/spfx-tab-ts/src/config/serve.json b/templates/spfx-tab-ts/src/config/serve.json new file mode 100644 index 00000000..a4c03e28 --- /dev/null +++ b/templates/spfx-tab-ts/src/config/serve.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json", + "port": 4321, + "https": true, + "initialPage": "https://{tenantDomain}/_layouts/workbench.aspx" +} diff --git a/templates/spfx-tab-ts/src/config/write-manifests.json b/templates/spfx-tab-ts/src/config/write-manifests.json new file mode 100644 index 00000000..bad35260 --- /dev/null +++ b/templates/spfx-tab-ts/src/config/write-manifests.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", + "cdnBasePath": "" +} \ No newline at end of file diff --git a/templates/spfx-tab-ts/src/gulpfile.js b/templates/spfx-tab-ts/src/gulpfile.js new file mode 100644 index 00000000..be291870 --- /dev/null +++ b/templates/spfx-tab-ts/src/gulpfile.js @@ -0,0 +1,16 @@ +'use strict'; + +const build = require('@microsoft/sp-build-web'); + +build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); + +var getTasks = build.rig.getTasks; +build.rig.getTasks = function () { + var result = getTasks.call(build.rig); + + result.set('serve', result.get('serve-deprecated')); + + return result; +}; + +build.initialize(require('gulp')); diff --git a/templates/spfx-tab-ts/src/package.json b/templates/spfx-tab-ts/src/package.json new file mode 100644 index 00000000..afced420 --- /dev/null +++ b/templates/spfx-tab-ts/src/package.json @@ -0,0 +1,42 @@ +{ + "name": "spfx-tab-ts", + "version": "0.0.1", + "private": true, + "engines": { + "node": ">=18.17.1 <19.0.0" + }, + "main": "lib/index.js", + "scripts": { + "build": "gulp bundle", + "clean": "gulp clean", + "test": "gulp test" + }, + "dependencies": { + "tslib": "2.3.1", + "react": "17.0.1", + "react-dom": "17.0.1", + "@fluentui/react": "^8.106.4", + "@microsoft/sp-core-library": "1.19.0", + "@microsoft/sp-component-base": "1.19.0", + "@microsoft/sp-property-pane": "1.19.0", + "@microsoft/sp-webpart-base": "1.19.0", + "@microsoft/sp-lodash-subset": "1.19.0", + "@microsoft/sp-office-ui-fabric-core": "1.19.0" + }, + "devDependencies": { + "@microsoft/rush-stack-compiler-4.7": "0.1.0", + "@rushstack/eslint-config": "2.5.1", + "@microsoft/eslint-plugin-spfx": "1.20.1", + "@microsoft/eslint-config-spfx": "1.20.1", + "@microsoft/sp-build-web": "1.20.1", + "@types/webpack-env": "~1.15.2", + "ajv": "^6.12.5", + "eslint": "8.7.0", + "gulp": "4.0.2", + "typescript": "4.7.4", + "@types/react": "17.0.45", + "@types/react-dom": "17.0.17", + "eslint-plugin-react-hooks": "4.3.0", + "@microsoft/sp-module-interfaces": "1.20.1" + } +} diff --git a/templates/spfx-tab-ts/src/src/index.ts b/templates/spfx-tab-ts/src/src/index.ts new file mode 100644 index 00000000..fb81db1e --- /dev/null +++ b/templates/spfx-tab-ts/src/src/index.ts @@ -0,0 +1 @@ +// A file is required to be in the root of the /src directory by the TypeScript compiler diff --git a/templates/spfx-tab-ts/src/src/webparts/helloworld/HelloworldWebPart.manifest.json b/templates/spfx-tab-ts/src/src/webparts/helloworld/HelloworldWebPart.manifest.json new file mode 100644 index 00000000..7c77b149 --- /dev/null +++ b/templates/spfx-tab-ts/src/src/webparts/helloworld/HelloworldWebPart.manifest.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json", + "id": "d901749f-b3b7-47ed-817b-b567fc84ed97", + "alias": "HelloworldWebPart", + "componentType": "WebPart", + + // The "*" signifies that the version should be taken from the package.json + "version": "*", + "manifestVersion": 2, + + // If true, the component can only be installed on sites where Custom Script is allowed. + // Components that allow authors to embed arbitrary script code should set this to true. + // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f + "requiresCustomScript": false, + "supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"], + "supportsThemeVariants": true, + + "preconfiguredEntries": [{ + "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Advanced + "group": { "default": "Advanced" }, + "title": { "default": "helloworld" }, + "description": { "default": "helloworld description" }, + "officeFabricIconFontName": "Page", + "properties": { + "description": "helloworld" + } + }] +} diff --git a/templates/spfx-tab-ts/src/src/webparts/helloworld/HelloworldWebPart.ts b/templates/spfx-tab-ts/src/src/webparts/helloworld/HelloworldWebPart.ts new file mode 100644 index 00000000..15112750 --- /dev/null +++ b/templates/spfx-tab-ts/src/src/webparts/helloworld/HelloworldWebPart.ts @@ -0,0 +1,121 @@ +import * as React from 'react'; +import * as ReactDom from 'react-dom'; +import { Version } from '@microsoft/sp-core-library'; +import { + type IPropertyPaneConfiguration, + PropertyPaneTextField +} from '@microsoft/sp-property-pane'; +import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base'; +import { IReadonlyTheme } from '@microsoft/sp-component-base'; + +import * as strings from 'HelloworldWebPartStrings'; +import Helloworld from './components/Helloworld'; +import { IHelloworldProps } from './components/IHelloworldProps'; + +export interface IHelloworldWebPartProps { + description: string; +} + +export default class HelloworldWebPart extends BaseClientSideWebPart { + + private _isDarkTheme: boolean = false; + private _environmentMessage: string = ''; + + public render(): void { + const element: React.ReactElement = React.createElement( + Helloworld, + { + description: this.properties.description, + isDarkTheme: this._isDarkTheme, + environmentMessage: this._environmentMessage, + hasTeamsContext: !!this.context.sdks.microsoftTeams, + userDisplayName: this.context.pageContext.user.displayName + } + ); + + ReactDom.render(element, this.domElement); + } + + protected onInit(): Promise { + return this._getEnvironmentMessage().then(message => { + this._environmentMessage = message; + }); + } + + + + private _getEnvironmentMessage(): Promise { + if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook + return this.context.sdks.microsoftTeams.teamsJs.app.getContext() + .then(context => { + let environmentMessage: string = ''; + switch (context.app.host.name) { + case 'Office': // running in Office + environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment; + break; + case 'Outlook': // running in Outlook + environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment; + break; + case 'Teams': // running in Teams + case 'TeamsModern': + environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment; + break; + default: + environmentMessage = strings.UnknownEnvironment; + } + + return environmentMessage; + }); + } + + return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment); + } + + protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void { + if (!currentTheme) { + return; + } + + this._isDarkTheme = !!currentTheme.isInverted; + const { + semanticColors + } = currentTheme; + + if (semanticColors) { + this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null); + this.domElement.style.setProperty('--link', semanticColors.link || null); + this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null); + } + + } + + protected onDispose(): void { + ReactDom.unmountComponentAtNode(this.domElement); + } + + protected get dataVersion(): Version { + return Version.parse('1.0'); + } + + protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { + return { + pages: [ + { + header: { + description: strings.PropertyPaneDescription + }, + groups: [ + { + groupName: strings.BasicGroupName, + groupFields: [ + PropertyPaneTextField('description', { + label: strings.DescriptionFieldLabel + }) + ] + } + ] + } + ] + }; + } +} diff --git a/templates/spfx-tab-ts/src/src/webparts/helloworld/assets/welcome-dark.png b/templates/spfx-tab-ts/src/src/webparts/helloworld/assets/welcome-dark.png new file mode 100644 index 00000000..42f0b8d2 Binary files /dev/null and b/templates/spfx-tab-ts/src/src/webparts/helloworld/assets/welcome-dark.png differ diff --git a/templates/spfx-tab-ts/src/src/webparts/helloworld/assets/welcome-light.png b/templates/spfx-tab-ts/src/src/webparts/helloworld/assets/welcome-light.png new file mode 100644 index 00000000..69eb3b48 Binary files /dev/null and b/templates/spfx-tab-ts/src/src/webparts/helloworld/assets/welcome-light.png differ diff --git a/templates/spfx-tab-ts/src/src/webparts/helloworld/components/Helloworld.module.scss b/templates/spfx-tab-ts/src/src/webparts/helloworld/components/Helloworld.module.scss new file mode 100644 index 00000000..e7241ac6 --- /dev/null +++ b/templates/spfx-tab-ts/src/src/webparts/helloworld/components/Helloworld.module.scss @@ -0,0 +1,34 @@ +@import '~@fluentui/react/dist/sass/References.scss'; + +.helloworld { + overflow: hidden; + padding: 1em; + color: "[theme:bodyText, default: #323130]"; + color: var(--bodyText); + &.teams { + font-family: $ms-font-family-fallbacks; + } +} + +.welcome { + text-align: center; +} + +.welcomeImage { + width: 100%; + max-width: 420px; +} + +.links { + a { + text-decoration: none; + color: "[theme:link, default:#03787c]"; + color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only + + &:hover { + text-decoration: underline; + color: "[theme:linkHovered, default: #014446]"; + color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only + } + } +} \ No newline at end of file diff --git a/templates/spfx-tab-ts/src/src/webparts/helloworld/components/Helloworld.tsx b/templates/spfx-tab-ts/src/src/webparts/helloworld/components/Helloworld.tsx new file mode 100644 index 00000000..f53ddcd9 --- /dev/null +++ b/templates/spfx-tab-ts/src/src/webparts/helloworld/components/Helloworld.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import styles from './Helloworld.module.scss'; +import type { IHelloworldProps } from './IHelloworldProps'; +import { escape } from '@microsoft/sp-lodash-subset'; + +export default class Helloworld extends React.Component { + public render(): React.ReactElement { + const { + description, + isDarkTheme, + environmentMessage, + hasTeamsContext, + userDisplayName + } = this.props; + + return ( +
+
+ +

Well done, {escape(userDisplayName)}!

+
{environmentMessage}
+
Web part property value: {escape(description)}
+
+
+

Welcome to SharePoint Framework!

+

+ The SharePoint Framework (SPFx) is a extensibility model for Microsoft Viva, Microsoft Teams and SharePoint. It's the easiest way to extend Microsoft 365 with automatic Single Sign On, automatic hosting and industry standard tooling. +

+

Learn more about SPFx development:

+ +
+
+ ); + } +} diff --git a/templates/spfx-tab-ts/src/src/webparts/helloworld/components/IHelloworldProps.ts b/templates/spfx-tab-ts/src/src/webparts/helloworld/components/IHelloworldProps.ts new file mode 100644 index 00000000..fd9af008 --- /dev/null +++ b/templates/spfx-tab-ts/src/src/webparts/helloworld/components/IHelloworldProps.ts @@ -0,0 +1,7 @@ +export interface IHelloworldProps { + description: string; + isDarkTheme: boolean; + environmentMessage: string; + hasTeamsContext: boolean; + userDisplayName: string; +} diff --git a/templates/spfx-tab-ts/src/src/webparts/helloworld/loc/en-us.js b/templates/spfx-tab-ts/src/src/webparts/helloworld/loc/en-us.js new file mode 100644 index 00000000..3b25e74a --- /dev/null +++ b/templates/spfx-tab-ts/src/src/webparts/helloworld/loc/en-us.js @@ -0,0 +1,16 @@ +define([], function() { + return { + "PropertyPaneDescription": "Description", + "BasicGroupName": "Group Name", + "DescriptionFieldLabel": "Description Field", + "AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part", + "AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app", + "AppLocalEnvironmentOffice": "The app is running on your local environment in office.com", + "AppLocalEnvironmentOutlook": "The app is running on your local environment in Outlook", + "AppSharePointEnvironment": "The app is running on SharePoint page", + "AppTeamsTabEnvironment": "The app is running in Microsoft Teams", + "AppOfficeEnvironment": "The app is running in office.com", + "AppOutlookEnvironment": "The app is running in Outlook", + "UnknownEnvironment": "The app is running in an unknown environment" + } +}); \ No newline at end of file diff --git a/templates/spfx-tab-ts/src/src/webparts/helloworld/loc/mystrings.d.ts b/templates/spfx-tab-ts/src/src/webparts/helloworld/loc/mystrings.d.ts new file mode 100644 index 00000000..dfcaee45 --- /dev/null +++ b/templates/spfx-tab-ts/src/src/webparts/helloworld/loc/mystrings.d.ts @@ -0,0 +1,19 @@ +declare interface IHelloworldWebPartStrings { + PropertyPaneDescription: string; + BasicGroupName: string; + DescriptionFieldLabel: string; + AppLocalEnvironmentSharePoint: string; + AppLocalEnvironmentTeams: string; + AppLocalEnvironmentOffice: string; + AppLocalEnvironmentOutlook: string; + AppSharePointEnvironment: string; + AppTeamsTabEnvironment: string; + AppOfficeEnvironment: string; + AppOutlookEnvironment: string; + UnknownEnvironment: string; +} + +declare module 'HelloworldWebPartStrings' { + const strings: IHelloworldWebPartStrings; + export = strings; +} diff --git a/templates/spfx-tab-ts/src/teams/d901749f-b3b7-47ed-817b-b567fc84ed97_color.png b/templates/spfx-tab-ts/src/teams/d901749f-b3b7-47ed-817b-b567fc84ed97_color.png new file mode 100644 index 00000000..0e1f764f Binary files /dev/null and b/templates/spfx-tab-ts/src/teams/d901749f-b3b7-47ed-817b-b567fc84ed97_color.png differ diff --git a/templates/spfx-tab-ts/src/teams/d901749f-b3b7-47ed-817b-b567fc84ed97_outline.png b/templates/spfx-tab-ts/src/teams/d901749f-b3b7-47ed-817b-b567fc84ed97_outline.png new file mode 100644 index 00000000..e8cb4b6b Binary files /dev/null and b/templates/spfx-tab-ts/src/teams/d901749f-b3b7-47ed-817b-b567fc84ed97_outline.png differ diff --git a/templates/spfx-tab-ts/src/tsconfig.json b/templates/spfx-tab-ts/src/tsconfig.json new file mode 100644 index 00000000..c4cd392a --- /dev/null +++ b/templates/spfx-tab-ts/src/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "./node_modules/@microsoft/rush-stack-compiler-4.7/includes/tsconfig-web.json", + "compilerOptions": { + "target": "es5", + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "outDir": "lib", + "inlineSources": false, + "noImplicitAny": true, + + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@microsoft" + ], + "types": [ + "webpack-env" + ], + "lib": [ + "es5", + "dom", + "es2015.collection", + "es2015.promise" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/templates/spfx-tab-ts/teamsapp.local.yml b/templates/spfx-tab-ts/teamsapp.local.yml new file mode 100644 index 00000000..b49965f6 --- /dev/null +++ b/templates/spfx-tab-ts/teamsapp.local.yml @@ -0,0 +1,60 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: spfx-tab-ts${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.local.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.local.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # 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 + # Teams 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 + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + workingDirectory: src + args: install --no-audit diff --git a/templates/spfx-tab-ts/teamsapp.yml b/templates/spfx-tab-ts/teamsapp.yml new file mode 100644 index 00000000..01190f37 --- /dev/null +++ b/templates/spfx-tab-ts/teamsapp.yml @@ -0,0 +1,116 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp deploy' is executed +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + workingDirectory: src + args: install + - uses: cli/runNpxCommand + with: + workingDirectory: src + args: gulp bundle --ship --no-color + - uses: cli/runNpxCommand + with: + workingDirectory: src + args: gulp package-solution --ship --no-color + - uses: spfx/deploy + with: + createAppCatalogIfNotExist: false + packageSolutionPath: ./src/config/package-solution.json + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: spfx-tab-ts${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # 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 + # 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 + # Teams 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 + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + - uses: teamsApp/copyAppPackageToSPFx + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + spfxFolder: ./src + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: 79a42297-0a10-4c19-afff-f72d7f3e2677 diff --git a/templates/sso-tab-with-obo-flow-ts/.gitignore b/templates/sso-tab-with-obo-flow-ts/.gitignore new file mode 100644 index 00000000..4f392ca6 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/.gitignore @@ -0,0 +1,18 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# TeamsFx files +env/.env.*.user +env/.env.local +.DS_Store +build +appPackage/build +.deployment +.localConfigs + +# dependencies +/node_modules + +# testing +/coverage + +# Dev tool directories +/devTools/ \ No newline at end of file diff --git a/templates/sso-tab-with-obo-flow-ts/.vscode/extensions.json b/templates/sso-tab-with-obo-flow-ts/.vscode/extensions.json new file mode 100644 index 00000000..aac0a6e3 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/templates/sso-tab-with-obo-flow-ts/.vscode/launch.json b/templates/sso-tab-with-obo-flow-ts/.vscode/launch.json new file mode 100644 index 00000000..e7a3add1 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/.vscode/launch.json @@ -0,0 +1,247 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "group 1: Teams", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Outlook (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://outlook.office.com/host/${{M365_APP_ID}}?${account-hint}", + "presentation": { + "group": "group 2: Outlook", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in Outlook (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://outlook.office.com/host/${{M365_APP_ID}}?${account-hint}", + "presentation": { + "group": "group 2: Outlook", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in the Microsoft 365 app (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://www.office.com/m365apps/${{M365_APP_ID}}?auth=2&${account-hint}", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote in the Microsoft 365 app (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://www.office.com/m365apps/${{M365_APP_ID}}?auth=2&${account-hint}", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 3 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Teams (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Teams (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Outlook (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://outlook.office.com/host/${{local:M365_APP_ID}}?${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in Outlook (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://outlook.office.com/host/${{local:M365_APP_ID}}?${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in the Microsoft 365 app (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://www.office.com/m365apps/${{local:M365_APP_ID}}?auth=2&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Frontend in the Microsoft 365 app (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://www.office.com/m365apps/${{local:M365_APP_ID}}?auth=2&${account-hint}", + "presentation": { + "group": "all", + "hidden": true + }, + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Attach to Local Service", + "type": "node", + "request": "attach", + "port": 9229, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Attach to Frontend in Teams (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Attach to Frontend in Teams (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 1: Teams", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Outlook (Edge)", + "configurations": [ + "Attach to Frontend in Outlook (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 2: Outlook", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Outlook (Chrome)", + "configurations": [ + "Attach to Frontend in Outlook (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 2: Outlook", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in the Microsoft 365 app (Edge)", + "configurations": [ + "Attach to Frontend in the Microsoft 365 app (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in the Microsoft 365 app (Chrome)", + "configurations": [ + "Attach to Frontend in the Microsoft 365 app (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "group 3: the Microsoft 365 app", + "order": 2 + }, + "stopAll": true + } + ] +} diff --git a/templates/sso-tab-with-obo-flow-ts/.vscode/settings.json b/templates/sso-tab-with-obo-flow-ts/.vscode/settings.json new file mode 100644 index 00000000..a2833c70 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ], + "azureFunctions.stopFuncTaskPostDebug": false, + "azureFunctions.showProjectWarning": false, + "csharp.suppressDotnetRestoreNotification": true +} diff --git a/templates/sso-tab-with-obo-flow-ts/.vscode/tasks.json b/templates/sso-tab-with-obo-flow-ts/.vscode/tasks.json new file mode 100644 index 00000000..0cad6d4d --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/.vscode/tasks.json @@ -0,0 +1,135 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Provision", + "Deploy", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + // Check if all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 53000, // tab service port + 7071, // backend service port + 9229 // backend inspector port for Node.js debugger + ] + } + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "dependsOn": [ + "Start frontend", + "Start backend" + ] + }, + { + "label": "Start frontend", + "type": "shell", + "command": "npm run dev-tab:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + }, + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Compiled|Failed|compiled|failed" + } + } + }, + { + "label": "Start backend", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}/api", + "env": { + "PATH": "${workspaceFolder}/devTools/func:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/func;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^.*(Job host stopped|signaling restart).*$", + "endsPattern": "^.*(Worker process started and initialized|Host lock lease acquired by instance ID).*$" + } + }, + "presentation": { + "reveal": "silent" + }, + "dependsOn": "Watch backend" + }, + { + "label": "Watch backend", + "type": "shell", + "command": "npm run watch:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}/api" + }, + "problemMatcher": "$tsc-watch", + "presentation": { + "reveal": "silent" + } + } + ] +} diff --git a/templates/sso-tab-with-obo-flow-ts/README.md b/templates/sso-tab-with-obo-flow-ts/README.md new file mode 100644 index 00000000..fb3ec35b --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/README.md @@ -0,0 +1,60 @@ +# Overview of the React with Fluent UI template + +This app showcases how to craft a visually appealing web page that can be embedded in Microsoft Teams, Outlook and the Microsoft 365 app with React and Fluent UI. The app also enhances the end-user experiences with built-in single sign-on and data from Microsoft Graph. + +This app has adopted [On-Behalf-Of flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow) to implement SSO, and uses Azure Functions as middle-tier service, and make authenticated requests to call Graph from Azure Functions. + +## Get started with the React with Fluent UI template + +> **Prerequisites** +> +> To run the command bot template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 18, 20 +> - A [Microsoft 365 account for development](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) +> - [Set up your dev environment for extending Teams apps across Microsoft 365](https://aka.ms/teamsfx-m365-apps-prerequisites) +> Please note that after you enrolled your developer tenant in Office 365 Target Release, it may take couple days for the enrollment to take effect. +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. In the Account section, sign in with your [Microsoft 365 account](https://docs.microsoft.com/microsoftteams/platform/toolkit/accounts) if you haven't already. +3. Press F5 to start debugging which launches your app in Teams using a web browser. Select `Debug in Teams (Edge)` or `Debug in Teams (Chrome)`. +4. When Teams launches in the browser, select the Add button in the dialog to install your app to Teams. + +**Congratulations**! You are running an application that can now show a beautiful web page in Teams, Outlook and the Microsoft 365 app. + +![Personal tab demo](https://github.com/OfficeDev/TeamsFx/assets/63089166/9599b53c-8f89-493f-9f7e-9edae1f9be54) + +## What's included in the template + +| Folder | Contents | +| ------------ | ---------------------------------------------------------------------------------------------------------------------- | +| `.vscode` | VSCode files for debugging | +| `appPackage` | Templates for the Teams application manifest | +| `env` | Environment files | +| `infra` | Templates for provisioning Azure resources | +| `src` | The source code for the frontend of the Tab application. Implemented with Fluent UI Framework. | +| `api` | The source code for the backend of the Tab application. Implemented single-sign-on with OBO flow using Azure Functions. | + +The following are Teams Toolkit specific project files. You can [visit a complete guide on Github](https://github.com/OfficeDev/TeamsFx/wiki/Teams-Toolkit-Visual-Studio-Code-v5-Guide#overview) to understand how Teams Toolkit works. + +| File | Contents | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `teamsapp.yml` | This is the main Teams Toolkit project file. The project file defines two primary things: Properties and configuration Stage definitions. | +| `teamsapp.local.yml` | This overrides `teamsapp.yml` with actions that enable local execution and debugging. | +| `aad.manifest.json` | This file defines the configuration of Microsoft Entra app. This template will only provision [single tenant](https://learn.microsoft.com/azure/active-directory/develop/single-and-multi-tenant-apps#who-can-sign-in-to-your-app) Microsoft Entra app. | + +## Extend the React with Fluent UI template + +Following documentation will help you to extend the React with Fluent UI template. + +- [Add or manage the environment](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-multi-env) +- [Create multi-capability app](https://learn.microsoft.com/microsoftteams/platform/toolkit/add-capability) +- [Use an existing Microsoft Entra application](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-existing-aad-app) +- [Customize the Teams app manifest](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-preview-and-customize-app-manifest) +- Host your app in Azure by [provision cloud resources](https://learn.microsoft.com/microsoftteams/platform/toolkit/provision) and [deploy the code to cloud](https://learn.microsoft.com/microsoftteams/platform/toolkit/deploy) +- [Collaborate on app development](https://learn.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) +- [Set up the CI/CD pipeline](https://learn.microsoft.com/microsoftteams/platform/toolkit/use-cicd-template) +- [Publish the app to your organization or the Microsoft Teams app store](https://learn.microsoft.com/microsoftteams/platform/toolkit/publish) +- [Enable the app for multi-tenant](https://github.com/OfficeDev/TeamsFx/wiki/Multi-tenancy-Support-for-Azure-AD-app) +- [Preview the app on mobile clients](https://aka.ms/teamsfx-mobile) diff --git a/templates/sso-tab-with-obo-flow-ts/aad.manifest.json b/templates/sso-tab-with-obo-flow-ts/aad.manifest.json new file mode 100644 index 00000000..f0c029f3 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/aad.manifest.json @@ -0,0 +1,115 @@ +{ + "id": "${{AAD_APP_OBJECT_ID}}", + "appId": "${{AAD_APP_CLIENT_ID}}", + "name": "sso-tab-with-obo-flow-ts-aad", + "accessTokenAcceptedVersion": 2, + "signInAudience": "AzureADMyOrg", + "optionalClaims": { + "idToken": [], + "accessToken": [ + { + "name": "idtyp", + "source": null, + "essential": false, + "additionalProperties": [] + } + ], + "saml2Token": [] + }, + "requiredResourceAccess": [ + { + "resourceAppId": "Microsoft Graph", + "resourceAccess": [ + { + "id": "User.Read", + "type": "Scope" + } + ] + } + ], + "oauth2Permissions": [ + { + "adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.", + "adminConsentDisplayName": "Teams can access app's web APIs", + "id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}", + "isEnabled": true, + "type": "User", + "userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have", + "userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf", + "value": "access_as_user" + } + ], + "preAuthorizedApplications": [ + { + "appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "00000002-0000-0ff1-ce00-000000000000", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "0ec893e0-5785-4de6-99da-4ed124e5296c", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4765445b-32c6-49b0-83e6-1d93765276ca", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "4345a7b9-9a63-4910-a426-35363201d503", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + }, + { + "appId": "27922004-5251-4030-b22d-91ecd9a37ea4", + "permissionIds": [ + "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}" + ] + } + ], + "identifierUris": [ + "api://${{TAB_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + ], + "replyUrlsWithType": [ + { + "url": "${{TAB_ENDPOINT}}/auth-end.html", + "type": "Web" + }, + { + "url": "${{TAB_ENDPOINT}}/auth-end.html?clientId=${{AAD_APP_CLIENT_ID}}", + "type": "Spa" + }, + { + "url": "${{TAB_ENDPOINT}}/blank-auth-end.html", + "type": "Spa" + } + ] +} \ No newline at end of file diff --git a/templates/sso-tab-with-obo-flow-ts/api/.funcignore b/templates/sso-tab-with-obo-flow-ts/api/.funcignore new file mode 100644 index 00000000..e470fa3a --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/api/.funcignore @@ -0,0 +1,12 @@ +*.js.map +*.ts +.git* +.localConfigs +.vscode +local.settings.json +test +tsconfig.json +.DS_Store +.deployment +node_modules/.bin +node_modules/azure-functions-core-tools diff --git a/templates/sso-tab-with-obo-flow-ts/api/.gitignore b/templates/sso-tab-with-obo-flow-ts/api/.gitignore new file mode 100644 index 00000000..fc4e33b7 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/api/.gitignore @@ -0,0 +1,17 @@ +# TypeScript output +dist +out + +# Dependency directories +node_modules + +# Azure Functions artifacts +bin +obj +appsettings.json + +# misc +.DS_Store + +# Local data +.localConfigs diff --git a/templates/sso-tab-with-obo-flow-ts/api/README.md b/templates/sso-tab-with-obo-flow-ts/api/README.md new file mode 100644 index 00000000..db2b2adf --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/api/README.md @@ -0,0 +1,108 @@ +# Server-side code in Teams applications + +Azure Functions are a great way to add server-side behaviors to any Teams application. + +## Prerequisites + +- [Node.js](https://nodejs.org/), supported versions: 16, 18 +- A Microsoft 365 account. If you do not have Microsoft 365 account, apply one from [Microsoft 365 developer program](https://developer.microsoft.com/en-us/microsoft-365/dev-program) +- [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) + +## Develop + +The Teams Toolkit IDE Extension and Teams Toolkit CLI provide template code for you to get started with Azure Functions for your Teams application. Microsoft Teams Framework simplifies the task of establishing the user's identity within the Azure Functions. + +The template handles calls from your Teams "custom tab" (client-side of your app), initializes the TeamsFx SDK to access the current user context, and demonstrates how to obtain a pre-authenticated Microsoft Graph Client. Microsoft Graph is the "data plane" of Microsoft 365 - you can use it to access content within Microsoft 365 in your company. With it you can read and write documents, SharePoint collections, Teams channels, and many other entities within Microsoft 365. Read more about [Microsoft Graph](https://docs.microsoft.com/en-us/graph/overview). + +You can add your logic to the single Azure Functions created by this template, as well as add more functions as necessary. See [Azure Functions developer guide](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference) for more information. + +### Call the Function + +To call your Azure Functions, the client sends an HTTP request with an SSO token in the `Authorization` header. Here is an example: + +```ts +import { TeamsUserCredentialAuthConfig, TeamsUserCredential } from "@microsoft/teamsfx"; + +const authConfig: TeamsUserCredentialAuthConfig = { + clientId: process.env.REACT_APP_CLIENT_ID, + initiateLoginEndpoint: process.env.REACT_APP_START_LOGIN_PAGE_URL, +}; +const teamsUserCredential = new TeamsUserCredential(authConfig); +const accessToken = await teamsUserCredential.getToken(""); // Get SSO token +const endpoint = "https://YOUR_API_ENDPOINT"; +const response = await axios.default.get(endpoint + "/api/" + functionName, { + headers: { + Authorization: `Bearer ${accessToken.token}`, + }, +}); +``` + +### Add More Functions + +- From Visual Studio Code, open the command palette, select `Teams: Add Resources` and select `Azure Functions App`. + +## Change Node.js runtime version + +By default, Teams Toolkit and Teams Toolkit CLI will provision an Azure functions app with function runtime version 3, and node runtime version 12. You can change the node version through Azure Portal. + +- Sign in to [Azure Portal](https://azure.microsoft.com/). +- Find your application's resource group and Azure Functions app resource. The resource group name and the Azure functions app name are stored in your project configuration file `.fx/env.*.json`. You can find them by searching the key `resourceGroupName` and `functionAppName` in that file. +- After enter the home page of the Azure Functions app, you can find a navigation item called `Configuration` under `settings` group. +- Click `Configuration`, you would see a list of settings. Then click `WEBSITE_NODE_DEFAULT_VERSION` and update the value to `~16` or `~18` according to your requirement. +- After Click `OK` button, don't forget to click `Save` button on the top of the page. + +Then following requests sent to the Azure Functions app will be handled by new node runtime version. + +## Debug + +- From Visual Studio Code: Start debugging the project by hitting the `F5` key in Visual Studio Code. Alternatively use the `Run and Debug Activity Panel` in Visual Studio Code and click the `Start Debugging` green arrow button. +- From Teams Toolkit CLI: Start debugging the project by executing the command `teamsapp preview --local` in your project directory. + +## Edit the manifest + +You can find the Teams app manifest in `./appPackage` folder. The folder contains one manifest file: + +- `manifest.template.json`: Manifest file for Teams app running locally or running remotely (After deployed to Azure). + +This file contains template arguments with `${{...}}` statements which will be replaced at build time. You may add any extra properties or permissions you require to this file. See the [schema reference](https://docs.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) for more information. + +## Deploy to Azure + +Deploy your project to Azure by following these steps: + +| From Visual Studio Code | From Teams Toolkit CLI | +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- | +|
  • Open Teams Toolkit, and sign into Azure by clicking the `Sign in to Azure` under the `ACCOUNTS` section from sidebar.
  • After you signed in, select a subscription under your account.
  • Open the command palette and select: `Teams: Provision`.
  • Open the command palette and select: `Teams: Deploy`.
|
  • Run command `teamsapp auth login azure`.
  • Run command `teamsapp provision`.
  • Run command `teamsapp deploy`.
| + +> Note: Provisioning and deployment may incur charges to your Azure Subscription. + +## Preview + +Once the provisioning and deployment steps are finished, you can preview your app: + +- From Visual Studio Code + + 1. Open the `Run and Debug Activity Panel`. + 1. Select `Launch Remote (Edge)` or `Launch Remote (Chrome)` from the launch configuration drop-down. + 1. Press the Play (green arrow) button to launch your app - now running remotely from Azure. + +- From Teams Toolkit CLI: execute `teamsapp preview --remote` in your project directory to launch your application. + +## Validate manifest file + +To check that your manifest file is valid: + +- From Visual Studio Code: open the command palette and select: `Teams: Validate manifest file`. +- From Teams Toolkit CLI: run command `teamsapp validate` in your project directory. + +## Package + +- From Visual Studio Code: open the command palette and select `Teams: Zip Teams metadata package`. +- Alternatively, from the command line run `teamsapp package` in the project directory. + +## Publish to Teams + +Once deployed, you may want to distribute your application to your organization's internal app store in Teams. Your app will be submitted for admin approval. + +- From Visual Studio Code: open the command palette and select: `Teams: Publish to Teams`. +- From Teams Toolkit CLI: run command `teamsapp publish` in your project directory. diff --git a/templates/sso-tab-with-obo-flow-ts/api/host.json b/templates/sso-tab-with-obo-flow-ts/api/host.json new file mode 100644 index 00000000..06d01bda --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/api/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} diff --git a/templates/sso-tab-with-obo-flow-ts/api/local.settings.json b/templates/sso-tab-with-obo-flow-ts/api/local.settings.json new file mode 100644 index 00000000..0077370a --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/api/local.settings.json @@ -0,0 +1,11 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node", + "AzureWebJobsFeatureFlags": "EnableWorkerIndexing", + "AzureWebJobsStorage": "UseDevelopmentStorage=true" + }, + "Host": { + "CORS": "*" + } +} \ No newline at end of file diff --git a/templates/sso-tab-with-obo-flow-ts/api/package.json b/templates/sso-tab-with-obo-flow-ts/api/package.json new file mode 100644 index 00000000..f01b580a --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/api/package.json @@ -0,0 +1,30 @@ +{ + "name": "teamsfx-template-api", + "version": "1.0.0", + "engines": { + "node": "18 || 20" + }, + "main": "dist/src/functions/*.js", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev": "func start --typescript --language-worker=\"--inspect=9229\" --port \"7071\" --cors \"*\"", + "build": "tsc", + "watch:teamsfx": "tsc -w", + "clean": "rimraf dist", + "prestart": "npm run clean && npm run build", + "start": "npx func start", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@azure/functions": "^4.0.0", + "@microsoft/teamsfx": "^2.0.0", + "@microsoft/microsoft-graph-client": "^3.0.1", + "isomorphic-fetch": "^3.0.0" + }, + "devDependencies": { + "@types/node": "^18.x", + "env-cmd": "^10.1.0", + "typescript": "^4.4.4", + "rimraf": "^5.0.0" + } +} diff --git a/templates/sso-tab-with-obo-flow-ts/api/src/config.ts b/templates/sso-tab-with-obo-flow-ts/api/src/config.ts new file mode 100644 index 00000000..d7e47e00 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/api/src/config.ts @@ -0,0 +1,8 @@ +const config = { + authorityHost: process.env.M365_AUTHORITY_HOST, + tenantId: process.env.M365_TENANT_ID, + clientId: process.env.M365_CLIENT_ID, + clientSecret: process.env.M365_CLIENT_SECRET, +}; + +export default config; diff --git a/templates/sso-tab-with-obo-flow-ts/api/src/functions/getUserProfile.ts b/templates/sso-tab-with-obo-flow-ts/api/src/functions/getUserProfile.ts new file mode 100644 index 00000000..c72379e7 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/api/src/functions/getUserProfile.ts @@ -0,0 +1,204 @@ +/* This code sample provides a starter kit to implement server side logic for your Teams App in TypeScript, + * refer to https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference for complete Azure Functions + * developer guide. + */ + +// Import polyfills for fetch required by msgraph-sdk-javascript. +import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; +import { + OnBehalfOfCredentialAuthConfig, + OnBehalfOfUserCredential, + UserInfo, +} from "@microsoft/teamsfx"; +import config from "../config"; +import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials"; +import { Client } from "@microsoft/microsoft-graph-client"; + +/** + * This function handles requests from teamsapp client. + * The HTTP request should contain an SSO token queried from Teams in the header. + * + * This function initializes the teamsapp SDK with the configuration and calls these APIs: + * - new OnBehalfOfUserCredential(accessToken, oboAuthConfig) - Construct OnBehalfOfUserCredential instance with the received SSO token and initialized configuration. + * - getUserInfo() - Get the user's information from the received SSO token. + * + * The response contains multiple message blocks constructed into a JSON object, including: + * - An echo of the request body. + * - The display name encoded in the SSO token. + * - Current user's Microsoft 365 profile if the user has consented. + * + * @param {InvocationContext} context - The Azure Functions context object. + * @param {HttpRequest} req - The HTTP request. + */ +export async function getUserProfile( + req: HttpRequest, + context: InvocationContext +): Promise { + context.log("HTTP trigger function processed a request."); + + // Initialize response. + const res: HttpResponseInit = { + status: 200, + }; + const body = Object(); + + // Put an echo into response body. + body.receivedHTTPRequestBody = (await req.text()) || ""; + + // Prepare access token. + const accessToken: string = req.headers.get("Authorization")?.replace("Bearer ", "").trim(); + if (!accessToken) { + return { + status: 400, + body: JSON.stringify({ + error: "No access token was found in request header.", + }), + }; + } + + const oboAuthConfig: OnBehalfOfCredentialAuthConfig = { + authorityHost: config.authorityHost, + clientId: config.clientId, + tenantId: config.tenantId, + clientSecret: config.clientSecret, + }; + + let oboCredential: OnBehalfOfUserCredential; + try { + oboCredential = new OnBehalfOfUserCredential(accessToken, oboAuthConfig); + } catch (e) { + context.error(e); + return { + status: 500, + body: JSON.stringify({ + error: + "Failed to construct OnBehalfOfUserCredential using your accessToken. " + + "Ensure your function app is configured with the right Microsoft Entra App registration.", + }), + }; + } + + // Query user's information from the access token. + try { + const currentUser: UserInfo = await oboCredential.getUserInfo(); + if (currentUser && currentUser.displayName) { + body.userInfoMessage = `User display name is ${currentUser.displayName}.`; + } else { + body.userInfoMessage = "No user information was found in access token."; + } + } catch (e) { + context.error(e); + return { + status: 400, + body: JSON.stringify({ + error: "Access token is invalid.", + }), + }; + } + + // Create a graph client with default scope to access user's Microsoft 365 data after user has consented. + try { + // Create an instance of the TokenCredentialAuthenticationProvider by passing the tokenCredential instance and options to the constructor + const authProvider = new TokenCredentialAuthenticationProvider(oboCredential, { + scopes: ["https://graph.microsoft.com/.default"], + }); + + // Initialize Graph client instance with authProvider + const graphClient = Client.initWithMiddleware({ + authProvider: authProvider, + }); + + body.graphClientMessage = await graphClient.api("/me").get(); + } catch (e) { + context.error(e); + return { + status: 500, + body: JSON.stringify({ + error: + "Failed to retrieve user profile from Microsoft Graph. The application may not be authorized.", + }), + }; + } + res.body = JSON.stringify(body); + + return res; +} + +app.http("getUserProfile", { + methods: ["GET", "POST"], + authLevel: "anonymous", + handler: getUserProfile, +}); + +// You can replace the codes above from the function body with comment "Query user's information from the access token." to the end +// with the following codes to use application permission to get user profiles. +// Remember to get admin consent of application permission "User.Read.All". +/* +// Query user's information from the access token. + let userName: string; + try { + const currentUser: UserInfo = await teamsfx.getUserInfo(); + console.log(currentUser); + userName = currentUser.preferredUserName; // Will be used in app credential flow + if (currentUser && currentUser.displayName) { + res.body.userInfoMessage = `User display name is ${currentUser.displayName}.`; + } else { + res.body.userInfoMessage = "No user information was found in access token."; + } + } catch (e) { + context.error(e); + return { + status: 400, + body: { + error: "Access token is invalid.", + }, + }; + } + + // Use IdentityType.App + client secret to create a teamsfx + const appAuthConfig: AppCredentialAuthConfig = { + clientId: process.env.M365_CLIENT_ID, + clientSecret: process.env.M365_CLIENT_SECRET, + authorityHost: process.env.M365_AUTHORITY_HOST, + tenantId: process.env.M365_TENANT_ID, + }; + try { + const appCredential = new AppCredential(appAuthConfig); + } catch (e) { + context.error(e); + return { + status: 500, + body: { + error: + "App credential error:" + + "Failed to construct TeamsFx using your accessToken. " + + "Ensure your function app is configured with the right Microsoft Entra App registration.", + }, + }; + } + + // Create a graph client with default scope to access user's Microsoft 365 data after user has consented. + try { + // Create an instance of the TokenCredentialAuthenticationProvider by passing the tokenCredential instance and options to the constructor + const authProvider = new TokenCredentialAuthenticationProvider(appCredential, { + scopes: ["https://graph.microsoft.com/.default"], + }); + + // Initialize the Graph client + const graphClient = Client.initWithMiddleware({ + authProvider: authProvider, + }); + + const profile: any = await graphClient.api("/users/"+userName).get(); + res.body.graphClientMessage = profile; + } catch (e) { + context.error(e); + return { + status: 500, + body: { + error: + "Failed to retrieve user profile from Microsoft Graph. The application may not be authorized.", + }, + }; + } +*/ diff --git a/templates/sso-tab-with-obo-flow-ts/api/tsconfig.json b/templates/sso-tab-with-obo-flow-ts/api/tsconfig.json new file mode 100644 index 00000000..cca0ca95 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/api/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "dist", + "rootDir": ".", + "sourceMap": true, + "strict": false, + "typeRoots": ["./node_modules/@types"] + } +} diff --git a/templates/sso-tab-with-obo-flow-ts/appPackage/color.png b/templates/sso-tab-with-obo-flow-ts/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/sso-tab-with-obo-flow-ts/appPackage/color.png differ diff --git a/templates/sso-tab-with-obo-flow-ts/appPackage/manifest.json b/templates/sso-tab-with-obo-flow-ts/appPackage/manifest.json new file mode 100644 index 00000000..4d94f61c --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/appPackage/manifest.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "${{TAB_ENDPOINT}}", + "privacyUrl": "${{TAB_ENDPOINT}}/index.html#/privacy", + "termsOfUseUrl": "${{TAB_ENDPOINT}}/index.html#/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "sso-tab-with-obo-flow-ts${{APP_NAME_SUFFIX}}", + "full": "Full name for sso-tab-with-obo-flow-ts" + }, + "description": { + "short": "Short description of sso-tab-with-obo-flow-ts", + "full": "Full description of sso-tab-with-obo-flow-ts" + }, + "accentColor": "#FFFFFF", + "bots": [], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [ + { + "entityId": "index", + "name": "Home", + "contentUrl": "${{TAB_ENDPOINT}}/index.html#/tab", + "websiteUrl": "${{TAB_ENDPOINT}}/index.html#/tab", + "scopes": [ + "personal", + "groupChat", + "team" + ] + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "${{TAB_HOSTNAME}}" + ], + "webApplicationInfo": { + "id": "${{AAD_APP_CLIENT_ID}}", + "resource": "api://${{TAB_DOMAIN}}/${{AAD_APP_CLIENT_ID}}" + } +} \ No newline at end of file diff --git a/templates/sso-tab-with-obo-flow-ts/appPackage/outline.png b/templates/sso-tab-with-obo-flow-ts/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/sso-tab-with-obo-flow-ts/appPackage/outline.png differ diff --git a/templates/sso-tab-with-obo-flow-ts/env/.env.dev b/templates/sso-tab-with-obo-flow-ts/env/.env.dev new file mode 100644 index 00000000..6c364bf2 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/env/.env.dev @@ -0,0 +1,23 @@ +# 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. If you're adding a secret value, add SECRET_ prefix to the name so Teams Toolkit can handle them properly +TEAMS_APP_ID= +TEAMS_APP_TENANT_ID= +AAD_APP_CLIENT_ID= +AAD_APP_OBJECT_ID= +AAD_APP_ACCESS_AS_USER_PERMISSION_ID= +AAD_APP_TENANT_ID= +AAD_APP_OAUTH_AUTHORITY_HOST= +AAD_APP_OAUTH_AUTHORITY= +TAB_ENDPOINT= +TAB_DOMAIN= +API_FUNCTION_ENDPOINT= +API_FUNCTION_RESOURCE_ID= \ No newline at end of file diff --git a/templates/sso-tab-with-obo-flow-ts/images/helloWorld-tab-with-backend.gif b/templates/sso-tab-with-obo-flow-ts/images/helloWorld-tab-with-backend.gif new file mode 100644 index 00000000..558c7cac Binary files /dev/null and b/templates/sso-tab-with-obo-flow-ts/images/helloWorld-tab-with-backend.gif differ diff --git a/templates/sso-tab-with-obo-flow-ts/infra/azure.bicep b/templates/sso-tab-with-obo-flow-ts/infra/azure.bicep new file mode 100644 index 00000000..1c9e696d --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/infra/azure.bicep @@ -0,0 +1,134 @@ +param resourceBaseName string +param functionAppSKU string +param aadAppClientId string +param aadAppTenantId string +param aadAppOauthAuthorityHost string +@secure() +param aadAppClientSecret string +param location string = resourceGroup().location +param serverfarmsName string = resourceBaseName +param functionAppName string = resourceBaseName +param staticWebAppName string = resourceBaseName +param staticWebAppSku string +var teamsMobileOrDesktopAppClientId = '1fec8e78-bce4-4aaf-ab1b-5451cc387264' +var teamsWebAppClientId = '5e3ce6c0-2b1f-4285-8d4b-75ee78787346' +var officeWebAppClientId1 = '4345a7b9-9a63-4910-a426-35363201d503' +var officeWebAppClientId2 = '4765445b-32c6-49b0-83e6-1d93765276ca' +var outlookDesktopAppClientId = 'd3590ed6-52b3-4102-aeff-aad2292ab01c' +var outlookWebAppClientId = '00000002-0000-0ff1-ce00-000000000000' +var officeUwpPwaClientId = '0ec893e0-5785-4de6-99da-4ed124e5296c' +var outlookOnlineAddInAppClientId = 'bc59ab01-8403-45c6-8796-ac3ef710b3e3' +var outlookMobileAppClientId = '27922004-5251-4030-b22d-91ecd9a37ea4' +var allowedClientApplications = '"${teamsMobileOrDesktopAppClientId}","${teamsWebAppClientId}","${officeWebAppClientId1}","${officeWebAppClientId2}","${outlookDesktopAppClientId}","${outlookWebAppClientId}","${officeUwpPwaClientId}","${outlookOnlineAddInAppClientId}","${outlookMobileAppClientId}"' + +// Azure Static Web Apps that hosts your static web site +resource swa 'Microsoft.Web/staticSites@2022-09-01' = { + name: staticWebAppName + // SWA do not need location setting + location: 'centralus' + sku: { + name: staticWebAppSku + tier: staticWebAppSku + } + properties:{} +} + +var siteDomain = swa.properties.defaultHostname +var tabEndpoint = 'https://${siteDomain}' +var aadApplicationIdUri = 'api://${siteDomain}/${aadAppClientId}' +var oauthAuthority = uri(aadAppOauthAuthorityHost, aadAppTenantId) + +// Compute resources for Azure Functions +resource serverfarms 'Microsoft.Web/serverfarms@2021-02-01' = { + name: serverfarmsName + kind: 'functionapp' + location: location + sku: { + name: functionAppSKU // You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionServerfarmsSku property to provisionParameters to override the default value "Y1". + } + properties: {} +} + +// Azure Functions that hosts your function code +resource functionApp 'Microsoft.Web/sites@2021-02-01' = { + name: functionAppName + kind: 'functionapp' + location: location + properties: { + serverFarmId: serverfarms.id + httpsOnly: true + siteConfig: { + alwaysOn: true + cors: { + allowedOrigins: [ tabEndpoint ] + } + appSettings: [ + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' // Use Azure Functions runtime v4 + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'node' // Set runtime to NodeJS + } + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure Functions from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x + } + { + name: 'M365_CLIENT_ID' + value: aadAppClientId + } + { + name: 'M365_CLIENT_SECRET' + value: aadAppClientSecret + } + { + name: 'M365_TENANT_ID' + value: aadAppTenantId + } + { + name: 'M365_AUTHORITY_HOST' + value: aadAppOauthAuthorityHost + } + { + name: 'M365_APPLICATION_ID_URI' + value: aadApplicationIdUri + } + { + name: 'WEBSITE_AUTH_AAD_ACL' + value: '{"allowed_client_applications": [${allowedClientApplications}]}' + } + ] + ftpsState: 'FtpsOnly' + } + } +} +var apiEndpoint = 'https://${functionApp.properties.defaultHostName}' + +resource authSettings 'Microsoft.Web/sites/config@2021-02-01' = { + parent: functionApp + name: 'authsettings' + properties: { + enabled: true + defaultProvider: 'AzureActiveDirectory' + clientId: aadAppClientId + issuer: '${oauthAuthority}/v2.0' + allowedAudiences: [ + aadAppClientId + aadApplicationIdUri + ] + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output TAB_DOMAIN string = siteDomain +output TAB_HOSTNAME string = siteDomain +output TAB_ENDPOINT string = 'https://${siteDomain}' +output API_FUNCTION_ENDPOINT string = apiEndpoint +output AZURE_STATIC_WEB_APPS_RESOURCE_ID string = swa.id +output API_FUNCTION_RESOURCE_ID string = functionApp.id diff --git a/templates/sso-tab-with-obo-flow-ts/infra/azure.parameters.json b/templates/sso-tab-with-obo-flow-ts/infra/azure.parameters.json new file mode 100644 index 00000000..43024dc6 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/infra/azure.parameters.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "helloworld${{RESOURCE_SUFFIX}}" + }, + "aadAppClientId": { + "value": "${{AAD_APP_CLIENT_ID}}" + }, + "aadAppClientSecret": { + "value": "${{SECRET_AAD_APP_CLIENT_SECRET}}" + }, + "aadAppTenantId": { + "value": "${{AAD_APP_TENANT_ID}}" + }, + "aadAppOauthAuthorityHost": { + "value": "${{AAD_APP_OAUTH_AUTHORITY_HOST}}" + }, + "functionAppSKU": { + "value": "B1" + }, + "staticWebAppSku": { + "value": "Free" + } + } +} diff --git a/templates/sso-tab-with-obo-flow-ts/package.json b/templates/sso-tab-with-obo-flow-ts/package.json new file mode 100644 index 00000000..5ba02bf6 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/package.json @@ -0,0 +1,56 @@ +{ + "name": "ssotabwithoboflowts", + "version": "0.1.0", + "engines": { + "node": "18 || 20" + }, + "private": true, + "dependencies": { + "@fluentui/react-components": "^9.18.0", + "@microsoft/teams-js": "^2.22.0", + "@microsoft/teamsfx": "^2.2.0", + "@microsoft/teamsfx-react": "^3.0.0", + "axios": "^0.21.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.0", + "react-scripts": "^5.0.1" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/react-router-dom": "^5.3.3", + "concurrently": "^8.2.2", + "env-cmd": "^10.1.0", + "typescript": "^4.1.2" + }, + "scripts": { + "dev:teamsfx": "concurrently \"npm run dev-tab:teamsfx\" \"npm run dev-api:teamsfx\"", + "dev-tab:teamsfx": "env-cmd --silent -f .localConfigs npm run start", + "dev-api:teamsfx": "cd api && npm run dev:teamsfx", + "start": "react-scripts start", + "build": "react-scripts build", + "test": "echo \"Error: no test specified\" && exit 1", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "homepage": "." +} diff --git a/templates/sso-tab-with-obo-flow-ts/public/auth-end.html b/templates/sso-tab-with-obo-flow-ts/public/auth-end.html new file mode 100644 index 00000000..3fc148ba --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/public/auth-end.html @@ -0,0 +1,59 @@ + + + + + Login End Page + + + + + + + + + diff --git a/templates/sso-tab-with-obo-flow-ts/public/auth-start.html b/templates/sso-tab-with-obo-flow-ts/public/auth-start.html new file mode 100644 index 00000000..8e47a013 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/public/auth-start.html @@ -0,0 +1,53 @@ + + + + + Login Start Page + + + + + + + + + diff --git a/templates/sso-tab-with-obo-flow-ts/public/deploy.png b/templates/sso-tab-with-obo-flow-ts/public/deploy.png new file mode 100644 index 00000000..130a2ae4 Binary files /dev/null and b/templates/sso-tab-with-obo-flow-ts/public/deploy.png differ diff --git a/templates/sso-tab-with-obo-flow-ts/public/favicon.ico b/templates/sso-tab-with-obo-flow-ts/public/favicon.ico new file mode 100644 index 00000000..ef5ef2b4 Binary files /dev/null and b/templates/sso-tab-with-obo-flow-ts/public/favicon.ico differ diff --git a/templates/sso-tab-with-obo-flow-ts/public/hello.png b/templates/sso-tab-with-obo-flow-ts/public/hello.png new file mode 100644 index 00000000..8654be8a Binary files /dev/null and b/templates/sso-tab-with-obo-flow-ts/public/hello.png differ diff --git a/templates/sso-tab-with-obo-flow-ts/public/index.html b/templates/sso-tab-with-obo-flow-ts/public/index.html new file mode 100644 index 00000000..c61bcb44 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/public/index.html @@ -0,0 +1,17 @@ + + + + + + + Microsoft Teams Tab + + + + +
+ + diff --git a/templates/sso-tab-with-obo-flow-ts/public/publish.png b/templates/sso-tab-with-obo-flow-ts/public/publish.png new file mode 100644 index 00000000..1ba94af3 Binary files /dev/null and b/templates/sso-tab-with-obo-flow-ts/public/publish.png differ diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/App.tsx b/templates/sso-tab-with-obo-flow-ts/src/components/App.tsx new file mode 100644 index 00000000..b4663313 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/App.tsx @@ -0,0 +1,57 @@ +// https://fluentsite.z22.web.core.windows.net/quick-start +import { + FluentProvider, + teamsLightTheme, + teamsDarkTheme, + teamsHighContrastTheme, + Spinner, + tokens, +} from "@fluentui/react-components"; +import { HashRouter as Router, Navigate, Route, Routes } from "react-router-dom"; +import { useTeamsUserCredential } from "@microsoft/teamsfx-react"; +import Privacy from "./Privacy"; +import TermsOfUse from "./TermsOfUse"; +import Tab from "./Tab"; +import { TeamsFxContext } from "./Context"; +import config from "./sample/lib/config"; + +/** + * The main app which handles the initialization and routing + * of the app. + */ +export default function App() { + const { loading, theme, themeString, teamsUserCredential } = useTeamsUserCredential({ + initiateLoginEndpoint: config.initiateLoginEndpoint!, + clientId: config.clientId!, + }); + return ( + + + + {loading ? ( + + ) : ( + + } /> + } /> + } /> + }> + + )} + + + + ); +} diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/Context.tsx b/templates/sso-tab-with-obo-flow-ts/src/components/Context.tsx new file mode 100644 index 00000000..89224df4 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/Context.tsx @@ -0,0 +1,13 @@ +import { TeamsUserCredential } from "@microsoft/teamsfx"; +import { createContext } from "react"; +import { Theme } from "@fluentui/react-components"; + +export const TeamsFxContext = createContext<{ + theme?: Theme; + themeString: string; + teamsUserCredential?: TeamsUserCredential; +}>({ + theme: undefined, + themeString: "", + teamsUserCredential: undefined, +}); diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/Privacy.tsx b/templates/sso-tab-with-obo-flow-ts/src/components/Privacy.tsx new file mode 100644 index 00000000..048cb6f0 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/Privacy.tsx @@ -0,0 +1,17 @@ +import React from "react"; +/** + * This component is used to display the required + * privacy statement which can be found in a link in the + * about tab. + */ +class Privacy extends React.Component { + render() { + return ( +
+

Privacy Statement

+
+ ); + } +} + +export default Privacy; diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/Tab.tsx b/templates/sso-tab-with-obo-flow-ts/src/components/Tab.tsx new file mode 100644 index 00000000..40eb111d --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/Tab.tsx @@ -0,0 +1,17 @@ +import { useContext } from "react"; +import { Welcome } from "./sample/Welcome"; +import { TeamsFxContext } from "./Context"; +import config from "./sample/lib/config"; + +const showFunction = Boolean(config.apiName); + +export default function Tab() { + const { themeString } = useContext(TeamsFxContext); + return ( +
+ +
+ ); +} diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/TermsOfUse.tsx b/templates/sso-tab-with-obo-flow-ts/src/components/TermsOfUse.tsx new file mode 100644 index 00000000..f3a5c10a --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/TermsOfUse.tsx @@ -0,0 +1,17 @@ +import React from "react"; +/** + * This component is used to display the required + * terms of use statement which can be found in a + * link in the about tab. + */ +class TermsOfUse extends React.Component { + render() { + return ( +
+

Terms of Use

+
+ ); + } +} + +export default TermsOfUse; diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/sample/AzureFunctions.tsx b/templates/sso-tab-with-obo-flow-ts/src/components/sample/AzureFunctions.tsx new file mode 100644 index 00000000..94e6b395 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/sample/AzureFunctions.tsx @@ -0,0 +1,108 @@ +import { useContext, useState } from "react"; +import { Button, Spinner } from "@fluentui/react-components"; +import { useData } from "@microsoft/teamsfx-react"; +import * as axios from "axios"; +import { BearerTokenAuthProvider, createApiClient, TeamsUserCredential } from "@microsoft/teamsfx"; +import { TeamsFxContext } from "../Context"; +import config from "./lib/config"; + +const functionName = config.apiName || "myFunc"; + +async function callFunction(teamsUserCredential: TeamsUserCredential) { + try { + const apiBaseUrl = config.apiEndpoint + "/api/"; + // createApiClient(...) creates an Axios instance which uses BearerTokenAuthProvider to inject token to request header + const apiClient = createApiClient( + apiBaseUrl, + new BearerTokenAuthProvider(async () => (await teamsUserCredential.getToken(""))!.token) + ); + const response = await apiClient.get(functionName); + return response.data; + } catch (err: unknown) { + if (axios.default.isAxiosError(err)) { + let funcErrorMsg = ""; + + if (err?.response?.status === 404) { + funcErrorMsg = `There may be a problem with the deployment of Azure Functions App, please deploy Azure Functions (Run command palette "Teams: Deploy") first before running this App`; + } else if (err.message === "Network Error") { + funcErrorMsg = + "Cannot call Azure Functions due to network error, please check your network connection status and "; + if (err.config?.url && err.config.url.indexOf("localhost") >= 0) { + funcErrorMsg += `make sure to start Azure Functions locally (Run "npm run start" command inside api folder from terminal) first before running this App`; + } else { + funcErrorMsg += `make sure to provision and deploy Azure Functions (Run command palette "Teams: Provision" and "Teams: Deploy") first before running this App`; + } + } else { + funcErrorMsg = err.message; + if (err.response?.data?.error) { + funcErrorMsg += ": " + err.response.data.error; + } + } + + throw new Error(funcErrorMsg); + } + throw err; + } +} + +export function AzureFunctions(props: { codePath?: string; docsUrl?: string }) { + const [needConsent, setNeedConsent] = useState(false); + const { codePath, docsUrl } = { + codePath: `api/src/functions/${functionName}.ts`, + docsUrl: "https://aka.ms/teamsfx-azure-functions", + ...props, + }; + const teamsUserCredential = useContext(TeamsFxContext).teamsUserCredential; + const { loading, data, error, reload } = useData(async () => { + if (!teamsUserCredential) { + throw new Error("TeamsFx SDK is not initialized."); + } + if (needConsent) { + await teamsUserCredential!.login(["User.Read"]); + setNeedConsent(false); + } + try { + const functionRes = await callFunction(teamsUserCredential); + return functionRes; + } catch (error: any) { + if (error.message.includes("The application may not be authorized.")) { + setNeedConsent(true); + } + } + }); + return ( +
+

Call your Azure Functions

+

+ An Azure Functions app is running. Authorize this app and click below to call it for a + response: +

+ {!loading && ( + + )} + {loading && ( +
+          
+        
+ )} + {!loading && !!data && !error &&
{JSON.stringify(data, null, 2)}
} + {!loading && !data && !error &&
}
+      {!loading && !!error && 
{(error as any).toString()}
} +

How to edit the Azure Functions

+

+ See the code in {codePath} to add your business logic. +

+ {!!docsUrl && ( +

+ For more information, see the{" "} + + docs + + . +

+ )} +
+ ); +} diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/sample/CurrentUser.tsx b/templates/sso-tab-with-obo-flow-ts/src/components/sample/CurrentUser.tsx new file mode 100644 index 00000000..ca6a044d --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/sample/CurrentUser.tsx @@ -0,0 +1,22 @@ +import React from "react"; + +export function CurrentUser(props: { userName?: string }) { + const { userName } = { + userName: "", + ...props, + }; + return ( +
+

Get the current user

+

Access basic information about the user like this:

+
+        {`const authConfig: TeamsUserCredentialAuthConfig = {\n  clientId: process.env.REACT_APP_CLIENT_ID,\n  initiateLoginEndpoint: process.env.REACT_APP_START_LOGIN_PAGE_URL,\n};\n\nconst credential = new TeamsUserCredential(authConfig);\nconst user = await credential.getUserInfo();`}
+      
+ {!!userName && ( +

+ The currently logged in user's name is {userName} +

+ )} +
+ ); +} diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/sample/Deploy.css b/templates/sso-tab-with-obo-flow-ts/src/components/sample/Deploy.css new file mode 100644 index 00000000..2891c455 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/sample/Deploy.css @@ -0,0 +1,5 @@ +.deploy.page > img { + margin: 0 auto; + display: block; + width: 100%; +} diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/sample/Deploy.tsx b/templates/sso-tab-with-obo-flow-ts/src/components/sample/Deploy.tsx new file mode 100644 index 00000000..a37a0605 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/sample/Deploy.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import "./Deploy.css"; +import { Image } from "@fluentui/react-components"; + +export function Deploy(props: { docsUrl?: string }) { + const { docsUrl } = { + docsUrl: "https://aka.ms/teamsfx-docs", + ...props, + }; + return ( +
+

Deploy to the Cloud

+

+ Before publishing your app to Teams App Catalog, you may want to provision and deploy your + app's resources to the cloud to make sure your app will be running smoothly! +

+

+ To provision your resources, you can either use our CLI command "teamsapp provision" or + apply "Teams: Provision" in Command palette. +

+

+ To deploy your app, you can either use our CLI command "teamsapp deploy" or apply "Teams: + Deploy" in Command palette. +

+ +

+ For more information, see the{" "} + + docs + + . +

+
+ ); +} diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/sample/EditCode.tsx b/templates/sso-tab-with-obo-flow-ts/src/components/sample/EditCode.tsx new file mode 100644 index 00000000..738bc35f --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/sample/EditCode.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import config from "./lib/config"; + +const functionName = config.apiName || "myFunc"; + +export function EditCode(props: { + showFunction?: boolean; + tabCodeEntry?: string; + functionCodePath?: string; +}) { + const { showFunction, tabCodeEntry, functionCodePath } = { + showFunction: true, + tabCodeEntry: "src/index.tsx", + functionCodePath: `api/src/functions/${functionName}.ts`, + ...props, + }; + return ( +
+

Change this code

+

+ The front end is a create-react-app. The entry point is{" "} + {tabCodeEntry}. Just save any file and this page will reload automatically. +

+ {showFunction && ( +

+ This app contains an Azure Functions backend. Find the code in{" "} + {functionCodePath} +

+ )} +
+ ); +} diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/sample/Publish.css b/templates/sso-tab-with-obo-flow-ts/src/components/sample/Publish.css new file mode 100644 index 00000000..6b8c5f46 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/sample/Publish.css @@ -0,0 +1,5 @@ +.publish.page > img { + margin: 0 auto; + display: block; + width: 100%; +} diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/sample/Publish.tsx b/templates/sso-tab-with-obo-flow-ts/src/components/sample/Publish.tsx new file mode 100644 index 00000000..493acb6e --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/sample/Publish.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import "./Publish.css"; +import { Image } from "@fluentui/react-components"; + +export function Publish(props: { docsUrl?: string }) { + const { docsUrl } = { + docsUrl: "https://aka.ms/teamsfx-docs", + ...props, + }; + return ( +
+

Publish to Teams

+

+ Your app's resources and infrastructure are deployed and ready? Publish and register your + app to Teams app catalog to share your app with others in your organization! +

+ +

+ For more information, see the{" "} + + docs + + . +

+
+ ); +} diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/sample/Welcome.css b/templates/sso-tab-with-obo-flow-ts/src/components/sample/Welcome.css new file mode 100644 index 00000000..2010d621 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/sample/Welcome.css @@ -0,0 +1,99 @@ +.narrow { + width: 900px; + margin: 0 auto; +} + +.page-padding { + padding: 4rem; +} + +.welcome.page > .narrow > img { + margin: 0 auto; + display: block; + width: 200px; +} + +.welcome.page > .narrow > ul { + width: 80%; + justify-content: space-between; + margin: 4rem auto; + border-bottom: none; +} + +.welcome.page > .narrow > ul > li { + background-color: inherit; + margin: auto; +} + +.welcome.page > .narrow > ul > li > a { + font-size: 14px; + min-height: 32px; + border-bottom-color: rgb(98, 100, 167); +} + +.center { + text-align: center; +} + +.sections > * { + margin: 4rem auto; +} + +.tabList { + align-items: center; + display: flex; + flex-direction: column; + justify-content: flex-start; + padding: 30px 20px; + row-gap: 30px; +} + +pre, +div.error { + background-color: #e5e5e5; + padding: 1rem; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2); + border-radius: 3px; + margin: 1rem 0; + max-height: 200px; + overflow-x: scroll; + overflow-y: scroll; + max-width: 732px; +} + +pre.fixed, +div.error.fixed { + height: 200px; +} + +code { + background-color: #e5e5e5; + display: inline-block; + padding: 0px 6px; + border-radius: 3px; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2); +} + +code::-webkit-scrollbar, +pre::-webkit-scrollbar { + display: none; +} + +.contrast pre, +.contrast code, +.contrast div.error { + background-color: #000000; + border-color: #ffffff; + border-width: thin; + border-style: solid; +} + +.dark pre, +.dark code, +.dark div.error { + background-color: #1b1b1b; +} + +.error { + color: red; +} diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/sample/Welcome.tsx b/templates/sso-tab-with-obo-flow-ts/src/components/sample/Welcome.tsx new file mode 100644 index 00000000..f14390ca --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/sample/Welcome.tsx @@ -0,0 +1,93 @@ +import { useContext, useState } from "react"; +import { + Image, + TabList, + Tab, + SelectTabEvent, + SelectTabData, + TabValue, +} from "@fluentui/react-components"; +import "./Welcome.css"; +import { EditCode } from "./EditCode"; +import { AzureFunctions } from "./AzureFunctions"; +import { CurrentUser } from "./CurrentUser"; +import { useData } from "@microsoft/teamsfx-react"; +import { Deploy } from "./Deploy"; +import { Publish } from "./Publish"; +import { TeamsFxContext } from "../Context"; +import { app } from "@microsoft/teams-js"; + +export function Welcome(props: { showFunction?: boolean; environment?: string }) { + const { showFunction, environment } = { + showFunction: true, + environment: window.location.hostname === "localhost" ? "local" : "azure", + ...props, + }; + const friendlyEnvironmentName = + { + local: "local environment", + azure: "Azure environment", + }[environment] || "local environment"; + + const [selectedValue, setSelectedValue] = useState("local"); + + const onTabSelect = (event: SelectTabEvent, data: SelectTabData) => { + setSelectedValue(data.value); + }; + const { teamsUserCredential } = useContext(TeamsFxContext); + const { loading, data, error } = useData(async () => { + if (teamsUserCredential) { + const userInfo = await teamsUserCredential.getUserInfo(); + return userInfo; + } + }); + const userName = loading || error ? "" : data!.displayName; + const hubName = useData(async () => { + await app.initialize(); + const context = await app.getContext(); + return context.app.host.name; + })?.data; + return ( +
+
+ +

Congratulations{userName ? ", " + userName : ""}!

+ {hubName &&

Your app is running in {hubName}

} +

Your app is running in your {friendlyEnvironmentName}

+ +
+ + + 1. Build your app locally + + + 2. Provision and Deploy to the Cloud + + + 3. Publish to Teams + + +
+ {selectedValue === "local" && ( +
+ + + {showFunction && } +
+ )} + {selectedValue === "azure" && ( +
+ +
+ )} + {selectedValue === "publish" && ( +
+ +
+ )} +
+
+
+
+ ); +} diff --git a/templates/sso-tab-with-obo-flow-ts/src/components/sample/lib/config.ts b/templates/sso-tab-with-obo-flow-ts/src/components/sample/lib/config.ts new file mode 100644 index 00000000..1e43cf3f --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/components/sample/lib/config.ts @@ -0,0 +1,8 @@ +const config = { + initiateLoginEndpoint: process.env.REACT_APP_START_LOGIN_PAGE_URL, + clientId: process.env.REACT_APP_CLIENT_ID, + apiEndpoint: process.env.REACT_APP_FUNC_ENDPOINT, + apiName: process.env.REACT_APP_FUNC_NAME, +}; + +export default config; diff --git a/templates/sso-tab-with-obo-flow-ts/src/index.css b/templates/sso-tab-with-obo-flow-ts/src/index.css new file mode 100644 index 00000000..eed3a419 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/index.css @@ -0,0 +1,18 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +@media only screen and (max-width: 768px) { + body { + width: fit-content; + } +} diff --git a/templates/sso-tab-with-obo-flow-ts/src/index.tsx b/templates/sso-tab-with-obo-flow-ts/src/index.tsx new file mode 100644 index 00000000..c13a3063 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/src/index.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./components/App"; +import "./index.css"; + +const container = document.getElementById("root"); +const root = createRoot(container!); +root.render(); diff --git a/templates/sso-tab-with-obo-flow-ts/teamsapp.local.yml b/templates/sso-tab-with-obo-flow-ts/teamsapp.local.yml new file mode 100644 index 00000000..41a18edc --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/teamsapp.local.yml @@ -0,0 +1,154 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra 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 Microsoft Entra manifest is the same with the name + # defined here. + name: sso-tab-with-obo-flow-ts + # If the value is false, the action will not generate client secret for you + generateClientSecret: true + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + # Environment variable that starts with `SECRET_` will be stored to the + # .env.{envName}.user environment file + clientSecret: SECRET_AAD_APP_CLIENT_SECRET + 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: sso-tab-with-obo-flow-ts${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Set required variables for local launch + - uses: script + with: + run: + echo "::set-teamsfx-env TAB_HOSTNAME=localhost"; + echo "::set-teamsfx-env TAB_DOMAIN=localhost:53000"; + echo "::set-teamsfx-env TAB_ENDPOINT=https://localhost:53000"; + echo "::set-teamsfx-env FUNC_NAME=getUserProfile"; + echo "::set-teamsfx-env FUNC_ENDPOINT=http://localhost:7071"; + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + 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 + + # 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 + # Teams 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 + + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + devCert: + trust: true + func: + version: ~4.0.5455 + symlinkDir: ./devTools/func + # Write the information of installed development tool(s) into environment + # file for the specified environment variable(s). + writeToEnvironmentFile: + sslCertFile: SSL_CRT_FILE + sslKeyFile: SSL_KEY_FILE + funcPath: FUNC_PATH + + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit + + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + workingDirectory: api + args: install --no-audit + + # Generate runtime environment variables for tab + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs + envs: + BROWSER: none + HTTPS: true + PORT: 53000 + SSL_CRT_FILE: ${{SSL_CRT_FILE}} + SSL_KEY_FILE: ${{SSL_KEY_FILE}} + REACT_APP_CLIENT_ID: ${{AAD_APP_CLIENT_ID}} + REACT_APP_START_LOGIN_PAGE_URL: ${{TAB_ENDPOINT}}/auth-start.html + REACT_APP_FUNC_NAME: ${{FUNC_NAME}} + REACT_APP_FUNC_ENDPOINT: ${{FUNC_ENDPOINT}} + + # Generate runtime environment variables for backend + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./api/.localConfigs + envs: + M365_CLIENT_ID: ${{AAD_APP_CLIENT_ID}} + M365_CLIENT_SECRET: ${{SECRET_AAD_APP_CLIENT_SECRET}} + M365_TENANT_ID: ${{AAD_APP_TENANT_ID}} + M365_AUTHORITY_HOST: ${{AAD_APP_OAUTH_AUTHORITY_HOST}} + ALLOWED_APP_IDS: 1fec8e78-bce4-4aaf-ab1b-5451cc387264;5e3ce6c0-2b1f-4285-8d4b-75ee78787346;0ec893e0-5785-4de6-99da-4ed124e5296c;4345a7b9-9a63-4910-a426-35363201d503;4765445b-32c6-49b0-83e6-1d93765276ca;d3590ed6-52b3-4102-aeff-aad2292ab01c;00000002-0000-0ff1-ce00-000000000000;bc59ab01-8403-45c6-8796-ac3ef710b3e3;27922004-5251-4030-b22d-91ecd9a37ea4 diff --git a/templates/sso-tab-with-obo-flow-ts/teamsapp.yml b/templates/sso-tab-with-obo-flow-ts/teamsapp.yml new file mode 100644 index 00000000..d86428b6 --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/teamsapp.yml @@ -0,0 +1,207 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a new Microsoft Entra app to authenticate users if + # the environment variable that stores clientId is empty + - uses: aadApp/create + with: + # Note: when you run aadApp/update, the Microsoft Entra 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 Microsoft Entra manifest is the same with the name + # defined here. + name: sso-tab-with-obo-flow-ts + # If the value is false, the action will not generate client secret for you + generateClientSecret: true + # Authenticate users with a Microsoft work or school account in your + # organization's Microsoft Entra tenant (for example, single tenant). + signInAudience: AzureADMyOrg + # Write the information of created resources into environment file for the + # specified environment variable(s). + writeToEnvironmentFile: + clientId: AAD_APP_CLIENT_ID + # Environment variable that starts with `SECRET_` will be stored to the + # .env.{envName}.user environment file + clientSecret: SECRET_AAD_APP_CLIENT_SECRET + 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: sso-tab-with-obo-flow-ts${{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-tab + # Teams 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 + + # Get the deployment token from Azure Static Web Apps + - uses: azureStaticWebApps/getDeploymentToken + with: + resourceId: ${{AZURE_STATIC_WEB_APPS_RESOURCE_ID}} + # Save deployment token to the environment file for the deployment action + writeToEnvironmentFile: + deploymentToken: SECRET_TAB_SWA_DEPLOYMENT_TOKEN + + # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in + # manifest file to determine which Microsoft Entra app to update. + - uses: aadApp/update + with: + # Relative path to this file. Environment variables in manifest will + # be replaced before apply to Microsoft Entra app + manifestPath: ./aad.manifest.json + 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 + # 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 + # Teams 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 + # Extend your Teams app to Outlook and the Microsoft 365 app + - uses: teamsApp/extendToM365 + with: + # Relative path to the build app package. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + titleId: M365_TITLE_ID + appId: M365_APP_ID + +# Triggered when 'teamsapp deploy' is executed +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install + - uses: cli/runNpmCommand + name: build app + with: + args: run build --if-present + env: + REACT_APP_CLIENT_ID: ${{AAD_APP_CLIENT_ID}} + REACT_APP_START_LOGIN_PAGE_URL: ${{TAB_ENDPOINT}}/auth-start.html + REACT_APP_FUNC_NAME: getUserProfile + REACT_APP_FUNC_ENDPOINT: ${{API_FUNCTION_ENDPOINT}} + # Deploy bits to Azure Static Web Apps + - uses: cli/runNpxCommand + name: deploy to Azure Static Web Apps + with: + args: '@azure/static-web-apps-cli deploy ./build -d + ${{SECRET_TAB_SWA_DEPLOYMENT_TOKEN}} --env production' + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + workingDirectory: api + args: install + - uses: cli/runNpmCommand + name: build app + with: + workingDirectory: api + args: run build --if-present + # Deploy your application to Azure Functions using the zip deploy feature. + # For additional details, see at https://aka.ms/zip-deploy-to-azure-functions + - uses: azureFunctions/zipDeploy + with: + workingDirectory: api + # deploy base folder + artifactFolder: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .funcignore + # 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: ${{API_FUNCTION_RESOURCE_ID}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: 2f292205-b3dc-4d85-9af7-f18874173583 diff --git a/templates/sso-tab-with-obo-flow-ts/tsconfig.json b/templates/sso-tab-with-obo-flow-ts/tsconfig.json new file mode 100644 index 00000000..9d379a3c --- /dev/null +++ b/templates/sso-tab-with-obo-flow-ts/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/templates/workflow-ts/.appserviceignore b/templates/workflow-ts/.appserviceignore new file mode 100644 index 00000000..d3ef2d0d --- /dev/null +++ b/templates/workflow-ts/.appserviceignore @@ -0,0 +1,29 @@ +.webappignore +.fx +.deployment +.localConfigs.testTool +.localConfigs +.notification.localstore.json +.notification.testtoolstore.json +.vscode +*.js.map +*.ts.map +*.ts +.git* +.tsbuildinfo +CHANGELOG.md +readme.md +local.settings.json +test +tsconfig.json +.DS_Store +teamsapp.yml +teamsapp.*.yml +/env/ +/node_modules/.bin +/node_modules/ts-node +/node_modules/typescript +/appPackage/ +/infra/ +/templates/ +/devTools/ \ No newline at end of file diff --git a/templates/workflow-ts/.gitignore b/templates/workflow-ts/.gitignore new file mode 100644 index 00000000..dfb975ac --- /dev/null +++ b/templates/workflow-ts/.gitignore @@ -0,0 +1,26 @@ +# TeamsFx files +env/.env.*.user +env/.env.local +.DS_Store +build +appPackage/build + +# dependencies +node_modules/ + +# misc +.env +.deployment +.DS_Store + +# build +lib/ + +# Local data +.localConfigs.testTool +.localConfigs +.notification.localstore.json +.notification.testtoolstore.json + +# Dev tool directories +/devTools/ \ No newline at end of file diff --git a/templates/workflow-ts/.vscode/extensions.json b/templates/workflow-ts/.vscode/extensions.json new file mode 100644 index 00000000..aac0a6e3 --- /dev/null +++ b/templates/workflow-ts/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/templates/workflow-ts/.vscode/launch.json b/templates/workflow-ts/.vscode/launch.json new file mode 100644 index 00000000..ccf40807 --- /dev/null +++ b/templates/workflow-ts/.vscode/launch.json @@ -0,0 +1,132 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Remote (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "presentation": { + "group": "3-remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch App (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Launch App (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}", + "cascadeTerminateToConfigurations": [ + "Attach to Local Service" + ], + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen", + "perScriptSourcemaps": "yes" + }, + { + "name": "Attach to Local Service", + "type": "node", + "request": "attach", + "port": 9239, + "restart": true, + "presentation": { + "group": "all", + "hidden": true + }, + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Launch Remote (Desktop)", + "type": "node", + "request": "launch", + "preLaunchTask": "Start Teams App in Desktop Client (Remote)", + "presentation": { + "group": "3-remote", + "order": 3 + }, + "internalConsoleOptions": "neverOpen", + } + ], + "compounds": [ + { + "name": "Debug in Teams (Edge)", + "configurations": [ + "Launch App (Edge)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "2-local", + "order": 1 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Chrome)", + "configurations": [ + "Launch App (Chrome)", + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App Locally", + "presentation": { + "group": "2-local", + "order": 2 + }, + "stopAll": true + }, + { + "name": "Debug in Teams (Desktop)", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App in Desktop Client", + "presentation": { + "group": "2-local", + "order": 3 + }, + "stopAll": true + }, + { + "name": "Debug in Test Tool", + "configurations": [ + "Attach to Local Service" + ], + "preLaunchTask": "Start Teams App (Test Tool)", + "presentation": { + "group": "1-local", + "order": 1 + }, + "stopAll": true + } + ] +} diff --git a/templates/workflow-ts/.vscode/settings.json b/templates/workflow-ts/.vscode/settings.json new file mode 100644 index 00000000..a2833c70 --- /dev/null +++ b/templates/workflow-ts/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": [ + "/aad.*.json" + ], + "schema": {} + } + ], + "azureFunctions.stopFuncTaskPostDebug": false, + "azureFunctions.showProjectWarning": false, + "csharp.suppressDotnetRestoreNotification": true +} diff --git a/templates/workflow-ts/.vscode/tasks.json b/templates/workflow-ts/.vscode/tasks.json new file mode 100644 index 00000000..9034316c --- /dev/null +++ b/templates/workflow-ts/.vscode/tasks.json @@ -0,0 +1,232 @@ +// This file is automatically generated by Teams Toolkit. +// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0. +// See https://aka.ms/teamsfx-tasks for details on how to customize each task. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Teams App (Test Tool)", + "dependsOn": [ + "Validate prerequisites (Test Tool)", + "Deploy (Test Tool)", + "Start application (Test Tool)", + "Start Test Tool", + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites (Test Tool)", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239, // app inspector port for Node.js debugger + 56150, // test tool port + ] + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy (Test Tool)", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "testtool", + } + }, + { + "label": "Start application (Test Tool)", + "type": "shell", + "command": "npm run dev:teamsfx:testtool", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}", + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Test Tool", + "type": "shell", + "command": "npm run dev:teamsfx:launch-testtool", + "isBackground": true, + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin:${env:PATH}" + } + }, + "windows": { + "options": { + "env": { + "PATH": "${workspaceFolder}/devTools/teamsapptester/node_modules/.bin;${env:PATH}" + } + } + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "Listening on" + } + }, + "presentation": { + "panel": "dedicated", + "reveal": "silent" + } + }, + { + "label": "Start Teams App Locally", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start application" + ], + "dependsOrder": "sequence" + }, + { + // Check all required prerequisites. + // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args. + "label": "Validate prerequisites", + "type": "teamsfx", + "command": "debug-check-prerequisites", + "args": { + "prerequisites": [ + "nodejs", // Validate if Node.js is installed. + "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission. + "portOccupancy" // Validate available ports to ensure those debug ones are not occupied. + ], + "portOccupancy": [ + 3978, // app service port + 9239 // app inspector port for Node.js debugger + ] + } + }, + { + // Start the local tunnel service to forward public URL to local port and inspect traffic. + // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions. + "label": "Start local tunnel", + "type": "teamsfx", + "command": "debug-start-local-tunnel", + "args": { + "type": "dev-tunnel", + "ports": [ + { + "portNumber": 3978, + "protocol": "http", + "access": "public", + "writeToEnvironmentFile": { + "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT + "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN + } + } + ], + "env": "local" + }, + "isBackground": true, + "problemMatcher": "$teamsfx-local-tunnel-watch" + }, + { + // Create the debug resources. + // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args. + "label": "Provision", + "type": "teamsfx", + "command": "provision", + "args": { + "env": "local" + } + }, + { + // Build project. + // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args. + "label": "Deploy", + "type": "teamsfx", + "command": "deploy", + "args": { + "env": "local" + } + }, + { + "label": "Start application", + "type": "shell", + "command": "npm run dev:teamsfx", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": { + "pattern": [ + { + "regexp": "^.*$", + "file": 0, + "location": 1, + "message": 2 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "[nodemon] starting", + "endsPattern": "restify listening to|Bot/ME service listening at|[nodemon] app crashed" + } + } + }, + { + "label": "Start Teams App in Desktop Client", + "dependsOn": [ + "Validate prerequisites", + "Start local tunnel", + "Provision", + "Deploy", + "Start application", + "Start desktop client" + ], + "dependsOrder": "sequence" + }, + { + "label": "Start desktop client", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true" + } + }, + { + "label": "Start Teams App in Desktop Client (Remote)", + "type": "teamsfx", + "command": "launch-desktop-client", + "args": { + "url": "teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true" + } + } + ] +} \ No newline at end of file diff --git a/templates/workflow-ts/README.md b/templates/workflow-ts/README.md new file mode 100644 index 00000000..9d319dcc --- /dev/null +++ b/templates/workflow-ts/README.md @@ -0,0 +1,202 @@ +# Overview of the Workflow bot template + +This template showcases an app that responds to chat commands by displaying UI using an Adaptive Card. The card has a button that demonstrates how to receive user input on the card, do something like call an API, and update the UI of that card. This can be further customized to create richer, more complex sequence of steps which forms a complete workflow. + +The app template is built using the TeamsFx SDK, which provides a simple set of functions over the Microsoft Bot Framework to implement this scenario. + +## Get Started with the Workflow bot + +> **Prerequisites** +> +> To run the workflow bot template in your local dev machine, you will need: +> +> - [Node.js](https://nodejs.org/), supported versions: 16, 18 +> - [Teams Toolkit Visual Studio Code Extension](https://aka.ms/teams-toolkit) version 5.0.0 and higher or [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +> +> **Note** +> +> Your app can be installed into a team, or a group chat, or as personal app. See [Installation and Uninstallation](https://aka.ms/teamsfx-command-response#customize-installation). +> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging). + +1. First, select the Teams Toolkit icon on the left in the VS Code toolbar. +2. Press F5 to start debugging which launches your app in Teams App Test Tool using a web browser. Select `Debug in Test Tool`. +3. The browser will pop up to open Teams App Test Tool. +4. Type or select `helloWorld` in the chat to send it to your bot - this is the default command provided by the template. +5. In the response from the bot, select the **DoStuff** button. + +The bot will respond by updating the existing Adaptive Card to show the workflow is now complete! Continue reading to learn more about what's included in the template and how to customize it. + +Here is a screen shot of the application running: + +![Responds to command](https://github.com/OfficeDev/TeamsFx/assets/9698542/94173beb-2fff-44fa-87dc-f686677da631) + +When you click the `DoStuff` button, the above adaptive card will be updated to a new card as shown below: + +![Responds to card action](https://github.com/OfficeDev/TeamsFx/assets/9698542/521ff12d-726c-4087-825a-112547cad836) + +## What's included in the template + +| Folder / File | Contents | +| - | - | +| `teamsapp.yml` | Main project file describes your application configuration and defines the set of actions to run in each lifecycle stages | +| `teamsapp.local.yml`| This overrides `teamsapp.yml` with actions that enable local execution and debugging | +| `teamsapp.testtool.yml`| This overrides `teamsapp.yml` with actions that enable local execution and debugging in Teams App Test Tool | +| `env/`| Name / value pairs are stored in environment files and used by `teamsapp.yml` to customize the provisioning and deployment rules | +| `.vscode/` | VSCode files for debugging | +| `appPackage/` | Templates for the Teams application manifest | +| `infra/` | Templates for provisioning Azure resources | +| `src/` | The source code for the application | + +The following files can be customized and demonstrate an example implementation to get you started. + +| File | Contents | +| - | - | +| `src/index.ts`| Application entry point and `restify` handlers for the Workflow bot | +| `src/teamsBot.ts` | An empty teams activity handler for bot customization | +| `src/commands/helloworldCommandHandler.ts` | Implementation that handles responding to a chat command | +| `src/adaptiveCards/helloworldCommandResponse.json` | Defines the Adaptive Card (UI) that is displayed in response to a chat command | +| `src/adaptiveCards/doStuffActionResponse.json` | A generated Adaptive Card that is sent to Teams for the response of "doStuff" action | +| `src/cardActions/doStuffActionHandler.ts` | Implements the handler for the `doStuff` button displayed in the Adaptive Card | + +## Extend the workflow bot template with more actions and responses + +Follow steps below to add more actions and responses to extend the workflow bot: + +1. [Step 1: Add an action to your Adaptive Card](#step-1-add-an-action-to-your-adaptive-card) +2. [Step 2: Respond with a new Adaptive Card](#step-2-respond-with-a-new-adaptive-card) +3. [Step 3: Handle the new action](#step-3-handle-the-new-action) +4. [Step 4: Register the new handler](#step-4-register-the-new-handler) + +### Step 1: Add an action to your Adaptive Card + +Adding new actions (buttons) to an Adaptive Card is as simple as defining them in the JSON file. Add a new `DoSomething` action to the `src/adaptiveCards/helloworldCommandResponse.json` file: + +Here's a sample action with type `Action.Execute`: + +```json +{ + "type": "AdaptiveCard", + "body": [ + ... + { + "type": "ActionSet", + "actions": [ + { + "type": "Action.Execute", + "title": "DoSomething", + "verb": "doSomething" + } + ] + }, + ... + ] +} +``` + +Specifying the `type` as `Action.Execute` to define an universal action in the base card. User can click the button to perform some business task in Teams chat. Learn more about [Adaptive Card Universal Actions in the documentation](https://learn.microsoft.com/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/overview?tabs=mobile#universal-actions). + +> **_NOTE:_** the `verb` property is required here so that the TeamsFx conversation SDK can invoke the corresponding action handler when the action is invoked in Teams. You should provide a global unique string for the `verb` property, otherwise you may experience unexpected behavior if you're using a general string that might cause a collision with other bot actions. + +### Step 2: Respond with a new Adaptive Card + +For each action, you can display a new Adaptive Card as a response to the user. Create a new file, `src/adaptiveCards/doSomethingResponse.json` to use as a response for the `DoSomething` action created in the previous step: + +```json +{ + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": "A sample response to DoSomething." + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.4" +} +``` + +You can use the [Adaptive Card Designer](https://adaptivecards.io/designer/) to help visually design your Adaptive Card UI. + +### Step 3: Handle the new action + +The TeamsFx SDK provides a convenient class, `TeamsFxAdaptiveCardActionHandler`, to handle when an action from an Adaptive Card is invoked. Create a new file, `src/cardActions/doSomethingActionHandler.ts`: + +```typescript +const { AdaptiveCards } = require("@microsoft/adaptivecards-tools"); +const { AdaptiveCardResponse, InvokeResponseFactory } = require("@microsoft/teamsfx"); +const responseCard = require("../adaptiveCards/doSomethingResponse.json"); + +export class DoSomethingActionHandler { + triggerVerb = "doSomething"; + + async handleActionInvoked(context, message) { + const responseCardJson = AdaptiveCards.declare(responseCard).render(actionData); + return InvokeResponseFactory.adaptiveCard(responseCardJson); + } +} +``` + +> Please note: +> +> - The `triggerVerb` is the `verb` property of your action. +> +> - The `actionData` is the data associated with the action, which may include dynamic user input or some contextual data provided in the `data` property of your action. +> +> - If an Adaptive Card is returned, then the existing card will be replaced with it by default. + +You can customize what the action does here, including calling an API, processing data, etc. + +### Step 4: Register the new handler + +Each new card action needs to be configured in the `ConversationBot`, which powers the conversational flow of the workflow bot template. Navigate to the `src/internal/initialize.ts` file and update the `actions` array of the `cardAction` property. + +1. Go to `src/internal/initialize.ts`; +2. Update your `conversationBot` initialization to enable cardAction feature and add the handler to `actions` array: + +```typescript +import { BotBuilderCloudAdapter } from "@microsoft/teamsfx"; +import ConversationBot = BotBuilderCloudAdapter.ConversationBot; + +const conversationBot = new ConversationBot({ + ... + cardAction: { + enabled: true, + actions: [ + new DoStuffActionHandler(), + new DoSomethingActionHandler() + ], + } +}); +``` + +Congratulations, you've just created your own workflow! To learn more about extending the Workflow bot template, [visit the documentation on GitHub](https://aka.ms/teamsfx-workflow-new). You can find more scenarios like: + +- [Customize the way to respond to an action](https://aka.ms/teamsfx-workflow-new#customize-the-action-response) +- [Customize the Adaptive Card content](https://aka.ms/teamsfx-workflow-new#customize-the-adaptive-card-content) +- [Create a user specific view](https://aka.ms/teamsfx-workflow-new#auto-refresh-to-user-specific-view) +- [Access Microsoft Graph](https://aka.ms/teamsfx-workflow-new#access-microsoft-graph) +- [Connect to existing APIs](https://aka.ms/teamsfx-workflow-new#connect-to-existing-apis) +- [Change the way to initialize the bot](https://aka.ms/teamsfx-workflow-new#customize-the-initialization) + +## Extend workflow bot with other bot scenarios + +Workflow bot is compatible with other bot scenarios like notification bot and command bot. + +### Add notifications to your workflow bot + +The notification feature adds the ability for your application to send Adaptive Cards in response to external events. Follow the [steps here](https://aka.ms/teamsfx-workflow-new#how-to-extend-workflow-bot-with-notification-feature) to add the notification feature to your workflow bot. Refer [the notification document](https://aka.ms/teamsfx-notification-new) for more information. + +### Add command and responses to your workflow bot + +The command and response feature adds the ability for your application to "listen" to commands sent to it via a Teams message and respond to commands with Adaptive Cards. Follow the [steps here](https://aka.ms/teamsfx-command-new#How-to-add-more-command-and-response) to add the command response feature to your workflow bot. Refer [the command bot document](https://aka.ms/teamsfx-command-new) for more information. + +## Additional information and references + +- [Manage multiple environments](https://docs.microsoft.com/microsoftteams/platform/toolkit/teamsfx-multi-env) +- [Collaborate with others](https://docs.microsoft.com/microsoftteams/platform/toolkit/teamsfx-collaboration) +- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +- [TeamsFx SDK](https://docs.microsoft.com/microsoftteams/platform/toolkit/teamsfx-sdk) +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) diff --git a/templates/workflow-ts/appPackage/color.png b/templates/workflow-ts/appPackage/color.png new file mode 100644 index 00000000..01aa37e3 Binary files /dev/null and b/templates/workflow-ts/appPackage/color.png differ diff --git a/templates/workflow-ts/appPackage/manifest.json b/templates/workflow-ts/appPackage/manifest.json new file mode 100644 index 00000000..4ad61026 --- /dev/null +++ b/templates/workflow-ts/appPackage/manifest.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "workflow-ts${{APP_NAME_SUFFIX}}", + "full": "Full name for workflow-ts" + }, + "description": { + "short": "Short description of workflow-ts", + "full": "Full description of workflow-ts" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": false, + "commandLists": [ + { + "scopes": [ + "personal", + "team", + "groupChat" + ], + "commands": [ + { + "title": "helloWorld", + "description": "A helloworld command to send a welcome message" + } + ] + } + ] + } + ], + "composeExtensions": [], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/templates/workflow-ts/appPackage/outline.png b/templates/workflow-ts/appPackage/outline.png new file mode 100644 index 00000000..f7a4c864 Binary files /dev/null and b/templates/workflow-ts/appPackage/outline.png differ diff --git a/templates/workflow-ts/env/.env.dev b/templates/workflow-ts/env/.env.dev new file mode 100644 index 00000000..4b07861c --- /dev/null +++ b/templates/workflow-ts/env/.env.dev @@ -0,0 +1,16 @@ +# 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= +BOT_DOMAIN= \ No newline at end of file diff --git a/templates/workflow-ts/env/.env.testtool b/templates/workflow-ts/env/.env.testtool new file mode 100644 index 00000000..43ce12aa --- /dev/null +++ b/templates/workflow-ts/env/.env.testtool @@ -0,0 +1,8 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=testtool + +# Environment variables used by test tool +TEAMSAPPTESTER_PORT=56150 +TEAMSFX_NOTIFICATION_STORE_FILENAME=.notification.testtoolstore.json diff --git a/templates/workflow-ts/infra/azure.bicep b/templates/workflow-ts/infra/azure.bicep new file mode 100644 index 00000000..cca52bf3 --- /dev/null +++ b/templates/workflow-ts/infra/azure.bicep @@ -0,0 +1,95 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +param webAppSKU string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param identityName string = resourceBaseName +param location string = resourceGroup().location + +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: webAppSKU + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + alwaysOn: true + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure App Service from a package file + } + { + name: 'WEBSITE_NODE_DEFAULT_VERSION' + value: '~18' // Set NodeJS version to 18.x for your site + } + { + name: 'RUNNING_ON_AZURE' + value: '1' + } + { + name: 'BOT_ID' + value: identity.properties.clientId + } + { + name: 'BOT_TENANT_ID' + value: identity.properties.tenantId + } + { + name: 'BOT_TYPE' + 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/templates/workflow-ts/infra/azure.parameters.json b/templates/workflow-ts/infra/azure.parameters.json new file mode 100644 index 00000000..0217ffaf --- /dev/null +++ b/templates/workflow-ts/infra/azure.parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "workflow${{RESOURCE_SUFFIX}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "workflow-ts" + } + } + } \ No newline at end of file diff --git a/templates/workflow-ts/infra/botRegistration/azurebot.bicep b/templates/workflow-ts/infra/botRegistration/azurebot.bicep new file mode 100644 index 00000000..a5a27b8f --- /dev/null +++ b/templates/workflow-ts/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/templates/workflow-ts/infra/botRegistration/readme.md b/templates/workflow-ts/infra/botRegistration/readme.md new file mode 100644 index 00000000..d5416243 --- /dev/null +++ b/templates/workflow-ts/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/templates/workflow-ts/package.json b/templates/workflow-ts/package.json new file mode 100644 index 00000000..a3c5c63c --- /dev/null +++ b/templates/workflow-ts/package.json @@ -0,0 +1,40 @@ +{ + "name": "workflowts", + "version": "1.0.0", + "description": "Microsoft Teams Toolkit Workflow Bot Sample", + "engines": { + "node": "16 || 18" + }, + "author": "Microsoft", + "license": "MIT", + "main": "./src/index.js", + "scripts": { + "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", + "dev:teamsfx:testtool": "env-cmd --silent -f .localConfigs.testTool npm run dev", + "dev:teamsfx:launch-testtool": "env-cmd --silent -f env/.env.testtool teamsapptester start", + "dev": "nodemon --watch ./src --exec node --inspect=9239 --signal SIGINT -r ts-node/register ./src/index.ts", + "build": "tsc --build && shx cp -r ./src/adaptiveCards ./lib/src", + "start": "node ./lib/src/index.js", + "watch": "nodemon --watch ./src --exec \"npm run start\"", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com" + }, + "dependencies": { + "@microsoft/adaptivecards-tools": "^1.0.0", + "@microsoft/teamsfx": "^2.3.1", + "botbuilder": "^4.20.0", + "restify": "^10.0.0" + }, + "devDependencies": { + "@types/restify": "^8.5.5", + "@types/node": "^18.0.0", + "env-cmd": "^10.1.0", + "nodemon": "^2.0.7", + "shx": "^0.3.4", + "ts-node": "^10.4.0", + "typescript": "^4.4.4" + } +} \ No newline at end of file diff --git a/templates/workflow-ts/src/adaptiveCards/doStuffActionResponse.json b/templates/workflow-ts/src/adaptiveCards/doStuffActionResponse.json new file mode 100644 index 00000000..fbc7dd5b --- /dev/null +++ b/templates/workflow-ts/src/adaptiveCards/doStuffActionResponse.json @@ -0,0 +1,18 @@ +{ + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": "✅[ACK] ${title}" + }, + { + "type": "TextBlock", + "text": "${body}", + "wrap": true + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.4" +} \ No newline at end of file diff --git a/templates/workflow-ts/src/adaptiveCards/helloworldCommandResponse.json b/templates/workflow-ts/src/adaptiveCards/helloworldCommandResponse.json new file mode 100644 index 00000000..f08fc068 --- /dev/null +++ b/templates/workflow-ts/src/adaptiveCards/helloworldCommandResponse.json @@ -0,0 +1,28 @@ +{ + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": "${title}" + }, + { + "type": "TextBlock", + "text": "${body}", + "wrap": true + }, + { + "type": "ActionSet", + "actions": [ + { + "type": "Action.Execute", + "verb": "doStuff", + "title": "DoStuff" + } + ] + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.4" +} \ No newline at end of file diff --git a/templates/workflow-ts/src/cardActions/doStuffActionHandler.ts b/templates/workflow-ts/src/cardActions/doStuffActionHandler.ts new file mode 100644 index 00000000..1cdadd95 --- /dev/null +++ b/templates/workflow-ts/src/cardActions/doStuffActionHandler.ts @@ -0,0 +1,42 @@ +import { AdaptiveCards } from "@microsoft/adaptivecards-tools"; +import { TurnContext, InvokeResponse } from "botbuilder"; +import { TeamsFxAdaptiveCardActionHandler, InvokeResponseFactory } from "@microsoft/teamsfx"; +import responseCard from "../adaptiveCards/doStuffActionResponse.json"; +import { CardData } from "../cardModels"; + +/** + * The `DoStuffActionHandler` registers an action with the `TeamsFxBotActionHandler` and responds + * with an Adaptive Card if the user clicks the Adaptive Card action with `triggerVerb`. + */ +export class DoStuffActionHandler implements TeamsFxAdaptiveCardActionHandler { + /** + * A global unique string associated with the `Action.Execute` action. + * The value should be the same as the `verb` property which you define in your adaptive card JSON. + */ + triggerVerb = "doStuff"; + + async handleActionInvoked(context: TurnContext, actionData: any): Promise { + /** + * You can send an adaptive card to respond to the card action invoke. + */ + const cardData: CardData = { + title: "Hello World Bot", + body: "Congratulations! Your task is processed successfully.", + }; + + const cardJson = AdaptiveCards.declare(responseCard).render(cardData); + return InvokeResponseFactory.adaptiveCard(cardJson); + + /** + * If you want to send invoke response with text message, you can: + * + return InvokeResponseFactory.textMessage("[ACK] Successfully!"); + */ + + /** + * If you want to send invoke response with error message, you can: + * + * return InvokeResponseFactory.errorResponse(InvokeResponseErrorCode.BadRequest, "The incoming request is invalid."); + */ + } +} diff --git a/templates/workflow-ts/src/cardModels.ts b/templates/workflow-ts/src/cardModels.ts new file mode 100644 index 00000000..024955f4 --- /dev/null +++ b/templates/workflow-ts/src/cardModels.ts @@ -0,0 +1,8 @@ +/** + * Adaptive Card data model. Properties can be referenced in an adaptive card via the `${var}` + * Adaptive Card syntax. + */ +export interface CardData { + title: string; + body: string; +} diff --git a/templates/workflow-ts/src/commands/genericCommandHandler.ts b/templates/workflow-ts/src/commands/genericCommandHandler.ts new file mode 100644 index 00000000..861fd669 --- /dev/null +++ b/templates/workflow-ts/src/commands/genericCommandHandler.ts @@ -0,0 +1,41 @@ +import { Activity, TurnContext } from "botbuilder"; +import { CommandMessage, TeamsFxBotCommandHandler, TriggerPatterns } from "@microsoft/teamsfx"; + +/** + * The `GenericCommandHandler` registers patterns with the `TeamsFxBotCommandHandler` and responds + * with appropriate messages if the user types general command inputs, such as "hi", "hello", and "help". + */ +export class GenericCommandHandler implements TeamsFxBotCommandHandler { + triggerPatterns: TriggerPatterns = new RegExp(/^.+$/); + + async handleCommandReceived( + context: TurnContext, + message: CommandMessage + ): Promise | void> { + console.log(`App received message: ${message.text}`); + + let response = ""; + switch (message.text) { + case "hi": + response = + "Hi there! I'm your Workflow Bot, here to assist you with your tasks. Type 'help' for a list of available commands."; + break; + case "hello": + response = + "Hello! I'm your Workflow Bot, always ready to help you out. If you need assistance, just type 'help' to see the available commands."; + break; + case "help": + response = + "Here's a list of commands I can help you with:\n" + + "- 'hi' or 'hello': Say hi or hello to me, and I'll greet you back.\n" + + "- 'help': Get a list of available commands.\n" + + "- 'helloworld': See a sample workflow from me.\n" + + "\nFeel free to ask for help anytime you need it!"; + break; + default: + response = `Sorry, command unknown. Please type 'help' to see the list of available commands.`; + } + + return response; + } +} diff --git a/templates/workflow-ts/src/commands/helloworldCommandHandler.ts b/templates/workflow-ts/src/commands/helloworldCommandHandler.ts new file mode 100644 index 00000000..8b2c6fab --- /dev/null +++ b/templates/workflow-ts/src/commands/helloworldCommandHandler.ts @@ -0,0 +1,29 @@ +import { Activity, CardFactory, MessageFactory, TurnContext } from "botbuilder"; +import { CommandMessage, TeamsFxBotCommandHandler, TriggerPatterns } from "@microsoft/teamsfx"; +import { AdaptiveCards } from "@microsoft/adaptivecards-tools"; +import helloWorldCard from "../adaptiveCards/helloworldCommandResponse.json"; +import { CardData } from "../cardModels"; + +/** + * The `HelloWorldCommandHandler` registers a pattern with the `TeamsFxBotCommandHandler` and responds + * with an Adaptive Card if the user types the `triggerPatterns`. + */ +export class HelloWorldCommandHandler implements TeamsFxBotCommandHandler { + triggerPatterns: TriggerPatterns = "helloWorld"; + + async handleCommandReceived( + context: TurnContext, + message: CommandMessage + ): Promise | void> { + console.log(`Bot received message: ${message.text}`); + + // Render your adaptive card for reply message + const cardData: CardData = { + title: "Your Hello World Bot is Running", + body: "Congratulations! Your hello world bot is running. Click the button below to trigger an action.", + }; + + const cardJson = AdaptiveCards.declare(helloWorldCard).render(cardData); + return MessageFactory.attachment(CardFactory.adaptiveCard(cardJson)); + } +} diff --git a/templates/workflow-ts/src/index.ts b/templates/workflow-ts/src/index.ts new file mode 100644 index 00000000..0d474c1a --- /dev/null +++ b/templates/workflow-ts/src/index.ts @@ -0,0 +1,24 @@ +import * as restify from "restify"; +import { workflowApp } from "./internal/initialize"; +import { TeamsBot } from "./teamsBot"; + +// This template uses `restify` to serve HTTP responses. +// Create a restify server. +const server = restify.createServer(); +server.use(restify.plugins.bodyParser()); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\nBot Started, ${server.name} listening to ${server.url}`); +}); + +// Register an API endpoint with `restify`. Teams sends messages to your application +// through this endpoint. +// +// The Teams Toolkit bot registration configures the bot with `/api/messages` as the +// Bot Framework endpoint. If you customize this route, update the Bot registration +// in `/templates/provision/bot.bicep`. +const teamsBot = new TeamsBot(); +server.post("/api/messages", async (req, res) => { + await workflowApp.requestHandler(req, res, async (context) => { + await teamsBot.run(context); + }); +}); diff --git a/templates/workflow-ts/src/internal/config.ts b/templates/workflow-ts/src/internal/config.ts new file mode 100644 index 00000000..4e6c184a --- /dev/null +++ b/templates/workflow-ts/src/internal/config.ts @@ -0,0 +1,8 @@ +const config = { + MicrosoftAppId: process.env.BOT_ID, + MicrosoftAppType: process.env.BOT_TYPE, + MicrosoftAppTenantId: process.env.BOT_TENANT_ID, + MicrosoftAppPassword: process.env.BOT_PASSWORD, +}; + +export default config; diff --git a/templates/workflow-ts/src/internal/initialize.ts b/templates/workflow-ts/src/internal/initialize.ts new file mode 100644 index 00000000..ab518b56 --- /dev/null +++ b/templates/workflow-ts/src/internal/initialize.ts @@ -0,0 +1,21 @@ +import { DoStuffActionHandler } from "../cardActions/doStuffActionHandler"; +import { HelloWorldCommandHandler } from "../commands/helloworldCommandHandler"; +import { GenericCommandHandler } from "../commands/genericCommandHandler"; +import { BotBuilderCloudAdapter } from "@microsoft/teamsfx"; +import ConversationBot = BotBuilderCloudAdapter.ConversationBot; +import config from "./config"; + +// Create the conversation bot and register the command and card action handlers for your app. +export const workflowApp = new ConversationBot({ + // The bot id and password to create CloudAdapter. + // See https://aka.ms/about-bot-adapter to learn more about adapters. + adapterConfig: config, + command: { + enabled: true, + commands: [new HelloWorldCommandHandler(), new GenericCommandHandler()], + }, + cardAction: { + enabled: true, + actions: [new DoStuffActionHandler()], + }, +}); diff --git a/templates/workflow-ts/src/teamsBot.ts b/templates/workflow-ts/src/teamsBot.ts new file mode 100644 index 00000000..d3cb860b --- /dev/null +++ b/templates/workflow-ts/src/teamsBot.ts @@ -0,0 +1,23 @@ +import { TeamsActivityHandler } from "botbuilder"; + +// Teams activity handler. +// You can add your customization code here to extend your bot logic if needed. +export class TeamsBot extends TeamsActivityHandler { + constructor() { + super(); + + // Listen to MembersAdded event, view https://docs.microsoft.com/en-us/microsoftteams/platform/resources/bot-v3/bots-notifications for more events + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded; + for (let cnt = 0; cnt < membersAdded.length; cnt++) { + if (membersAdded[cnt].id) { + await context.sendActivity( + 'Welcome to the Workflow Bot! I can help you work through the Adaptive Card and perform various tasks. Type "helloworld" or "help" to get started.' + ); + break; + } + } + await next(); + }); + } +} diff --git a/templates/workflow-ts/teamsapp.local.yml b/templates/workflow-ts/teamsapp.local.yml new file mode 100644 index 00000000..a5783a85 --- /dev/null +++ b/templates/workflow-ts/teamsapp.local.yml @@ -0,0 +1,83 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: workflow-ts${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: workflow-ts${{APP_NAME_SUFFIX}} + generateClientSecret: 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 + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: workflow-ts + 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 + # 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 + # Teams 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 + +deploy: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs + envs: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + BOT_TYPE: 'MultiTenant' \ No newline at end of file diff --git a/templates/workflow-ts/teamsapp.testtool.yml b/templates/workflow-ts/teamsapp.testtool.yml new file mode 100644 index 00000000..44d71fd1 --- /dev/null +++ b/templates/workflow-ts/teamsapp.testtool.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +deploy: + # Install development tool(s) + - uses: devTool/install + with: + testTool: + version: ~0.2.1 + symlinkDir: ./devTools/teamsapptester + + # Run npm command + - uses: cli/runNpmCommand + with: + args: install --no-audit + + # Generate runtime environment variables + - uses: file/createOrUpdateEnvironmentFile + with: + target: ./.localConfigs.testTool + envs: + TEAMSFX_NOTIFICATION_STORE_FILENAME: ${{TEAMSFX_NOTIFICATION_STORE_FILENAME}} \ No newline at end of file diff --git a/templates/workflow-ts/teamsapp.yml b/templates/workflow-ts/teamsapp.yml new file mode 100644 index 00000000..e14c3cea --- /dev/null +++ b/templates/workflow-ts/teamsapp.yml @@ -0,0 +1,131 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.5/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.5 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: workflow-ts${{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 + # Teams 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 + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + # 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 + # Teams 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: + # Run npm command + - uses: cli/runNpmCommand + name: install dependencies + with: + args: install + - uses: cli/runNpmCommand + name: build app + with: + args: run build --if-present + # 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: . + # Ignore file location, leave blank will ignore nothing + ignoreFile: .appserviceignore + # 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}} + +# Triggered when 'teamsapp publish' is executed +publish: + # 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 + # 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 + # Teams 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 + # Publish the app to + # Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps) + # for review and approval + - uses: teamsApp/publishAppPackage + with: + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + publishedAppId: TEAMS_APP_PUBLISHED_APP_ID +projectId: 4915f454-89a9-4b4c-80cf-00a5152aed7e diff --git a/templates/workflow-ts/tsconfig.json b/templates/workflow-ts/tsconfig.json new file mode 100644 index 00000000..77972584 --- /dev/null +++ b/templates/workflow-ts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "declaration": true, + "target": "es2017", + "module": "commonjs", + "outDir": "./lib", + "rootDir": "./", + "sourceMap": true, + "incremental": true, + "tsBuildInfoFile": "./lib/.tsbuildinfo", + "resolveJsonModule": true, + "esModuleInterop": true, + } +} diff --git a/templates/workflow-ts/web.config b/templates/workflow-ts/web.config new file mode 100644 index 00000000..28463f64 --- /dev/null +++ b/templates/workflow-ts/web.config @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +