|
| 1 | +--- |
| 2 | +title: Extending an existing Teams Bot to become a Bot Powered Adaptive Card Extension |
| 3 | +description: Learn how to extend a Teams Bot to become a Bot Powered Adaptive Card Extension for Microsoft Viva Connections. |
| 4 | +ms.date: 12/12/2024 |
| 5 | +ms.localizationpriority: high |
| 6 | +--- |
| 7 | +# Extending an existing Teams Bot to become a Bot Powered Adaptive Card Extension |
| 8 | + |
| 9 | +The main purpose of having the new Bot Powered Adaptive Card Extensions (ACEs) for Microsoft Viva Connections is to make it possible to reuse already existing bots, enriching their user experience with the support for Microsoft Viva Connections Dashboard. In fact, the return on investment that you can achieve by using and extending an already existing effort is a fundamental benefit of the Bot Powered ACEs. |
| 10 | + |
| 11 | +In this article, you'll learn how to upgrade an already existing Teams Bot into a Bot Powered Adaptive Card Extension (ACE) experience. |
| 12 | + |
| 13 | +## Updating the code of an already existing bot |
| 14 | + |
| 15 | +So, let's assume that you created a bot for Microsoft Teams, using the Microsoft Teams Toolkit, using the **Bot | Basic Bot** template. The first thing to do is to upgrade the **botbuilder** reference in **package.json** to version 4.23.1 or higher. You can use the following command. |
| 16 | + |
| 17 | +```console |
| 18 | +npm i botbuilder --save |
| 19 | +``` |
| 20 | + |
| 21 | +You also need to import the **adaptivecards** package with version 1.2.3, executing the following command. |
| 22 | + |
| 23 | +```console |
| 24 | + |
| 25 | +``` |
| 26 | + |
| 27 | +Then, open the **teamsBot.ts** file (or whatever name you gave to the main bot source code file) and add an import statement to import all the types needed by Bot Powered ACEs. The updated source code file should look like the following one. |
| 28 | + |
| 29 | +```TypeScript |
| 30 | +import { |
| 31 | + AceData, |
| 32 | + AceRequest, |
| 33 | + ActivityHandler, |
| 34 | + CardViewResponse, |
| 35 | + PrimaryTextCardView, |
| 36 | + GetPropertyPaneConfigurationResponse, |
| 37 | + HandleActionResponse, |
| 38 | + InvokeResponse, |
| 39 | + QuickViewResponse, |
| 40 | + SetPropertyPaneConfigurationResponse, |
| 41 | + TeamsActivityHandler, |
| 42 | + TurnContext } |
| 43 | +from "botbuilder"; |
| 44 | +import * as AdaptiveCards from 'adaptivecards'; |
| 45 | +``` |
| 46 | + |
| 47 | +Then, you need to initialize some infrastructural data in the bot constructor, to support the rendering of the Bot Powered ACE. It's a common habit to define a set of Card Views and Quick Views in the constructor of the Bot Powered ACE. You can then reuse them while rendering the actual user experience of the ACE. You can learn more about implementing Bot Powered ACEs by reading the article [Building your first Bot Powered Adaptive Card Extension with Microsoft Teams Toolkit and TypeScript](./Building-Your-First-Bot-Powered-ACE-TTK-TS.md). |
| 48 | + |
| 49 | +In the following code excerpt, you can see the updated constructor of the bot. |
| 50 | + |
| 51 | +```TypeScript |
| 52 | +export class TeamsBot extends TeamsActivityHandler { |
| 53 | + |
| 54 | + private readonly _botId: string = 'AceFromExistingBot'; |
| 55 | + private _cardViews: { [key: string]: CardViewResponse } = {}; |
| 56 | + |
| 57 | + private WELCOME_CARD_VIEW_ID: string = 'WELCOME_CARD_VIEW'; |
| 58 | + private WELCOME_QUICK_VIEW_ID: string = 'WELCOME_QUICK_VIEW'; |
| 59 | + |
| 60 | + constructor() { |
| 61 | + super(); |
| 62 | + |
| 63 | + // Prepare the ACE data for all the card views and quick views. |
| 64 | + const aceData: AceData = { |
| 65 | + id: this._botId, |
| 66 | + title: 'Your extended bot!', |
| 67 | + description: 'Welcome to your extended bot.', |
| 68 | + cardSize: 'Large', |
| 69 | + iconProperty: 'Robot', |
| 70 | + properties: {}, |
| 71 | + dataVersion: '1.0', |
| 72 | + }; |
| 73 | + |
| 74 | + // Welcome Card View (Primary Text Card View) |
| 75 | + const welcomeCardViewResponse: CardViewResponse = { |
| 76 | + aceData: aceData, |
| 77 | + cardViewParameters: PrimaryTextCardView( |
| 78 | + { |
| 79 | + componentName: 'cardBar', |
| 80 | + title: 'Welcome!' |
| 81 | + }, |
| 82 | + { |
| 83 | + componentName: 'text', |
| 84 | + text: 'Welcome!' |
| 85 | + }, |
| 86 | + { |
| 87 | + componentName: 'text', |
| 88 | + text: 'Here is your Teams bot available also in Microsoft Viva Connections!' |
| 89 | + }, |
| 90 | + [ |
| 91 | + { |
| 92 | + componentName: 'cardButton', |
| 93 | + title: 'Show Quick View', |
| 94 | + id: 'ShowQuickView', |
| 95 | + action: { |
| 96 | + type: 'QuickView', |
| 97 | + parameters: { |
| 98 | + view: this.WELCOME_QUICK_VIEW_ID |
| 99 | + } |
| 100 | + } |
| 101 | + } |
| 102 | + ] |
| 103 | + ), |
| 104 | + viewId: this.WELCOME_CARD_VIEW_ID, |
| 105 | + onCardSelection: { |
| 106 | + type: 'QuickView', |
| 107 | + parameters: { |
| 108 | + view: this.WELCOME_QUICK_VIEW_ID |
| 109 | + } |
| 110 | + } |
| 111 | + }; |
| 112 | + this._cardViews[this.WELCOME_CARD_VIEW_ID] = welcomeCardViewResponse; |
| 113 | + |
| 114 | + this.onMessage(async (context, next) => { |
| 115 | + console.log("Running with Message Activity."); |
| 116 | + const removedMentionText = TurnContext.removeRecipientMention(context.activity); |
| 117 | + const txt = removedMentionText.toLowerCase().replace(/\n|\r/g, "").trim(); |
| 118 | + await context.sendActivity(`Echo: ${txt}`); |
| 119 | + // By calling next() you ensure that the next BotHandler is run. |
| 120 | + await next(); |
| 121 | + }); |
| 122 | + |
| 123 | + this.onMembersAdded(async (context, next) => { |
| 124 | + const membersAdded = context.activity.membersAdded; |
| 125 | + for (let cnt = 0; cnt < membersAdded.length; cnt++) { |
| 126 | + if (membersAdded[cnt].id) { |
| 127 | + await context.sendActivity( |
| 128 | + `Hi there! I'm a Teams bot that will echo what you said to me.` |
| 129 | + ); |
| 130 | + break; |
| 131 | + } |
| 132 | + } |
| 133 | + await next(); |
| 134 | + }); |
| 135 | + } |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +Then, you need to add some new custom logic to the bot to support rendering as a Bot Powered ACE. Override the `onInvokeActivity` method of the base class and provide support for any of the following actions: |
| 140 | + |
| 141 | +- **cardExtension/getCardView**: handles the rendering of a Card View. |
| 142 | +- **cardExtension/getQuickView**: handles the rendering of a Quick View. |
| 143 | +- **cardExtension/getPropertyPaneConfiguration**: allows rendering the Property Pane of the ACE. |
| 144 | +- **cardExtension/setPropertyPaneConfiguration**: allows saving the settings configured using the Property Pane of the ACE. |
| 145 | +- **cardExtension/handleAction**: handles a custom action in the ACE like the select on a button in a Card View or any custom action in the UI of a Quick View. |
| 146 | + |
| 147 | +Here follows a sample implementation of the **onInvokeActivity** method. |
| 148 | + |
| 149 | +```TypeScript |
| 150 | +/** |
| 151 | + * Invoked when an invoke activity is received from the connector. |
| 152 | + * Invoke activities can be used to communicate many different things. |
| 153 | + * * Invoke activities communicate programmatic commands from a client or channel to a bot. |
| 154 | + * |
| 155 | + * @param context A strongly-typed context object for this turn |
| 156 | + * @returns A task that represents the work queued to execute |
| 157 | + */ |
| 158 | +protected async onInvokeActivity(context: TurnContext): Promise<InvokeResponse> { |
| 159 | + try { |
| 160 | + switch (context.activity.name) { |
| 161 | + case 'cardExtension/getCardView': |
| 162 | + return ActivityHandler.createInvokeResponse( |
| 163 | + await this.onSharePointTaskGetCardViewAsync(context, context.activity.value as AceRequest) |
| 164 | + ); |
| 165 | + case 'cardExtension/getQuickView': |
| 166 | + return ActivityHandler.createInvokeResponse( |
| 167 | + await this.onSharePointTaskGetQuickViewAsync(context, context.activity.value as AceRequest) |
| 168 | + ); |
| 169 | + case 'cardExtension/getPropertyPaneConfiguration': |
| 170 | + return ActivityHandler.createInvokeResponse( |
| 171 | + await this.onSharePointTaskGetPropertyPaneConfigurationAsync( |
| 172 | + context, |
| 173 | + context.activity.value as AceRequest |
| 174 | + ) |
| 175 | + ); |
| 176 | + case 'cardExtension/setPropertyPaneConfiguration': |
| 177 | + return ActivityHandler.createInvokeResponse( |
| 178 | + await this.onSharePointTaskSetPropertyPaneConfigurationAsync( |
| 179 | + context, |
| 180 | + context.activity.value as AceRequest |
| 181 | + ) |
| 182 | + ); |
| 183 | + case 'cardExtension/handleAction': |
| 184 | + return ActivityHandler.createInvokeResponse( |
| 185 | + await this.onSharePointTaskHandleActionAsync(context, context.activity.value as AceRequest) |
| 186 | + ); |
| 187 | + default: |
| 188 | + return super.onInvokeActivity(context); |
| 189 | + } |
| 190 | + } catch (err) { |
| 191 | + if (err.message === 'NotImplemented') { |
| 192 | + return { status: 501 }; |
| 193 | + } else if (err.message === 'BadRequest') { |
| 194 | + return { status: 400 }; |
| 195 | + } |
| 196 | + throw err; |
| 197 | + } |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +You can read the article [Overview of Bot Powered Adaptive Card Extensions](Overview-Bot-Powered-ACEs.md), to learn about the basic requirement for a Bot Powered ACE to provide at least one Card View. You can optionally provide more Card Views and one or more Quick Views, in case you want to have a better and more personalized user experience. |
| 202 | + |
| 203 | +In this article, you'll add both a Card View and a Quick View. As such, you're going to implement the actions **cardExtension/getCardView** and **cardExtension/getQuickView**, respectively with methods `onSharePointTaskGetCardViewAsync` and `onSharePointTaskGetQuickViewAsync`. While you don't need to implement all the other methods. |
| 204 | + |
| 205 | +```TypeScript |
| 206 | +/** |
| 207 | + * Override this in a derived class to provide logic for when a card view is fetched |
| 208 | + * |
| 209 | + * @param _context - A strongly-typed context object for this turn |
| 210 | + * @param _aceRequest - The Ace invoke request value payload |
| 211 | + * @returns A Card View Response for the request |
| 212 | + */ |
| 213 | +protected async onSharePointTaskGetCardViewAsync( |
| 214 | + _context: TurnContext, |
| 215 | + _aceRequest: AceRequest |
| 216 | +): Promise<CardViewResponse> { |
| 217 | + return this._cardViews[this.WELCOME_CARD_VIEW_ID]; |
| 218 | +} |
| 219 | + |
| 220 | +/** |
| 221 | + * Override this in a derived class to provide logic for when a quick view is fetched |
| 222 | + * |
| 223 | + * @param _context - A strongly-typed context object for this turn |
| 224 | + * @param _aceRequest - The Ace invoke request value payload |
| 225 | + * @returns A Quick View Response for the request |
| 226 | + */ |
| 227 | +protected async onSharePointTaskGetQuickViewAsync(_context: TurnContext, _aceRequest: AceRequest): Promise<QuickViewResponse> { |
| 228 | + |
| 229 | + // Prepare the AdaptiveCard for the Quick View |
| 230 | + const card = new AdaptiveCards.AdaptiveCard(); |
| 231 | + card.version = new AdaptiveCards.Version(1, 5); |
| 232 | + const cardPayload = { |
| 233 | + type: 'AdaptiveCard', |
| 234 | + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", |
| 235 | + body: [ |
| 236 | + { |
| 237 | + type: 'TextBlock', |
| 238 | + text: 'Welcome!', |
| 239 | + weight: 'Bolder', |
| 240 | + size: 'Large', |
| 241 | + wrap: true, |
| 242 | + maxLines: 1, |
| 243 | + spacing: 'None', |
| 244 | + color: 'Dark' |
| 245 | + }, |
| 246 | + { |
| 247 | + type: 'TextBlock', |
| 248 | + text: 'We are happy to see that you are consuming your extended Teams Bot in Microsoft Viva Connections! Thanks!', |
| 249 | + weight: 'Normal', |
| 250 | + size: 'Medium', |
| 251 | + wrap: true, |
| 252 | + maxLines: 3, |
| 253 | + spacing: 'None', |
| 254 | + color: 'Dark' |
| 255 | + } |
| 256 | + ] |
| 257 | + }; |
| 258 | + card.parse(cardPayload); |
| 259 | + |
| 260 | + // Add the Feedback QuickViews |
| 261 | + const welcomeQuickViewResponse: QuickViewResponse = { |
| 262 | + viewId: this.WELCOME_QUICK_VIEW_ID, |
| 263 | + title: 'Welcome!', |
| 264 | + template: card, |
| 265 | + data: {}, |
| 266 | + externalLink: null, |
| 267 | + focusParameters: null |
| 268 | + }; |
| 269 | + |
| 270 | + return Promise.resolve(welcomeQuickViewResponse); |
| 271 | +} |
| 272 | + |
| 273 | +/** |
| 274 | + * Override this in a derived class to provide logic for getting configuration pane properties. |
| 275 | + * |
| 276 | + * @param _context - A strongly-typed context object for this turn |
| 277 | + * @param _aceRequest - The Ace invoke request value payload |
| 278 | + * @returns A Property Pane Configuration Response for the request |
| 279 | + */ |
| 280 | +protected async onSharePointTaskGetPropertyPaneConfigurationAsync( |
| 281 | + _context: TurnContext, |
| 282 | + _aceRequest: AceRequest |
| 283 | +): Promise<GetPropertyPaneConfigurationResponse> { |
| 284 | + throw new Error('NotImplemented'); |
| 285 | +} |
| 286 | + |
| 287 | +/** |
| 288 | + * Override this in a derived class to provide logic for setting configuration pane properties. |
| 289 | + * |
| 290 | + * @param _context - A strongly-typed context object for this turn |
| 291 | + * @param _aceRequest - The Ace invoke request value payload |
| 292 | + * @returns A Card view or no-op action response |
| 293 | + */ |
| 294 | +protected async onSharePointTaskSetPropertyPaneConfigurationAsync( |
| 295 | + _context: TurnContext, |
| 296 | + _aceRequest: AceRequest |
| 297 | +): Promise<SetPropertyPaneConfigurationResponse> { |
| 298 | + throw new Error('NotImplemented'); |
| 299 | +} |
| 300 | + |
| 301 | +/** |
| 302 | + * Override this in a derived class to provide logic for setting configuration pane properties. |
| 303 | + * |
| 304 | + * @param _context - A strongly-typed context object for this turn |
| 305 | + * @param _aceRequest - The Ace invoke request value payload |
| 306 | + * @returns A handle action response |
| 307 | + */ |
| 308 | +protected async onSharePointTaskHandleActionAsync( |
| 309 | + _context: TurnContext, |
| 310 | + _aceRequest: AceRequest |
| 311 | +): Promise<HandleActionResponse> { |
| 312 | + throw new Error('NotImplemented'); |
| 313 | +} |
| 314 | +``` |
| 315 | +
|
| 316 | +The `onSharePointTaskGetCardViewAsync` method returns the Card View defined in the constructor as the only element in the dictionary of Card Views. The `onSharePointTaskGetQuickViewAsync` method creates an Adaptive Card using the **adaptivecards** package that you imported previously and then returns the card into a Quick View object. |
| 317 | +
|
| 318 | +## Updating the manifest of an already existing bot |
| 319 | +
|
| 320 | +Your bot is now ready to support rendering as a Bot Powered ACE in Microsoft Viva Connection. However, to make it available as a new ACE in the Viva Connections Dashboard, you need to update the **manifest.json** file of the solution to declare this new capability. Specifically, you need to add a section `dashboardCards`, for example right after the `bots` configured as follows. Here you can see an excerpt of the **manifest.json** file. |
| 321 | +
|
| 322 | +```JSON |
| 323 | +"dashboardCards": [ |
| 324 | + { |
| 325 | + "id": "${{BOT_ID}}", |
| 326 | + "pickerGroupId": "8cd406cc-7a66-42b5-bda5-9576abe7a818", |
| 327 | + "displayName": "Teams Bot Extended", |
| 328 | + "description": "Bot Powered ACE created from an already existing Teams bot", |
| 329 | + "icon": { |
| 330 | + "officeUIFabricIconName": "Robot" |
| 331 | + }, |
| 332 | + "contentSource": { |
| 333 | + "sourceType": "bot", |
| 334 | + "botConfiguration": { |
| 335 | + "botId": "${{BOT_ID}}" |
| 336 | + } |
| 337 | + }, |
| 338 | + "defaultSize": "large" |
| 339 | + } |
| 340 | +] |
| 341 | +``` |
| 342 | +
|
| 343 | +Then, update the `validDomains` section according to the following excerpt. |
| 344 | +
|
| 345 | +```JSON |
| 346 | +"validDomains": [ |
| 347 | + "${{BOT_DOMAIN}}" |
| 348 | +] |
| 349 | +``` |
| 350 | +
|
| 351 | +The setting includes the ___domain of the bot in the list of valid domains so that Microsoft 365 can trust any resource published by the Bot web application. |
| 352 | +
|
| 353 | +## Deploying the updated bot |
| 354 | +
|
| 355 | +You can now package and deploy the solution using the out-of-the-box capabilities of Microsoft Teams Toolkit. You simply need to trigger the actions to **Provision**, **Deploy**, and **Publish** the bot in Microsoft Teams. After no more than 24 hours, your bot becomes available in Microsoft Viva Connections as a new Bot Powered ACE. |
| 356 | +
|
| 357 | +## Configure the Bot in Azure |
| 358 | +
|
| 359 | +While waiting for the ACE to become available in the Dashboard, you need to slightly update the configuration of the Azure Bot. Open a web browser and navigate to the [Azure Management Portal](https://portal.azure.com/). From the Azure Management Portal home page, select "Azure Bot" and choose the Bot that you provisioned with the Microsoft Teams Toolkit. |
| 360 | +
|
| 361 | +Open the **Configuration** panel of the Bot. Notice that the Microsoft Teams Toolkit configured the Bot to run with a **User-Assigned Managed Identity**. Here you can see what the available options are: |
| 362 | +
|
| 363 | +- **User-Assigned Managed Identity**: if your Bot app doesn't need to access resources outside of its home tenant and if your Bot app is hosted on an Azure resource that supports Managed Identities. |
| 364 | +- **Single Tenant**: if your Bot app doesn't need to access resources outside of its home tenant, but your Bot app isn't hosted on an Azure resource that supports Managed Identities. |
| 365 | +- **Multi-Tenant**: if your Bot app needs to access resources outside its home tenant or serves multiple tenants. |
| 366 | +
|
| 367 | +Notice also that the "Messaging endpoint" URL for your Bot targets the URL of the Web App provisioned on Microsoft Azure by the Microsoft Teams Toolkit. |
| 368 | +
|
| 369 | + |
| 370 | +
|
| 371 | +### Configuring the Microsoft 365 Channel |
| 372 | +
|
| 373 | +You can now select the panel **Channels** in the Azure Bot to configure a new channel for Microsoft 365. In the **Available Channels** section of the page, you need to select the channel with the name **Microsoft 365** to enable it. |
| 374 | +
|
| 375 | + |
| 376 | +
|
| 377 | +A new page shows up, explaining the purpose of the **Microsoft 365** channel. Select the **Apply** button to enable the new channel and go back to the list of channels configured for your Azure Bot. |
| 378 | +
|
| 379 | + |
| 380 | +
|
| 381 | +Right after that, your Azure Bot is fully configured and ready to be used. |
| 382 | +
|
| 383 | + |
| 384 | +
|
| 385 | +You're now ready to run and test your Bot Powered ACE built with Microsoft Teams Toolkit and TypeScript. |
| 386 | +In the following screenshot, you can see the output of the Bot Powered ACE in the Microsoft Viva Connections Dashboard. |
| 387 | +
|
| 388 | + |
0 commit comments