From e44b3baa3d21a0972f43a6914aa3a255019498d0 Mon Sep 17 00:00:00 2001 From: Jhonatan Sandoval Velasco <122501764+JhontSouth@users.noreply.github.com> Date: Mon, 24 Feb 2025 12:19:45 -0500 Subject: [PATCH] Add cards bot sample (#68) * add cards bot sample * remove action card --------- Co-authored-by: Rido --- samples/basic/cards/nodejs/.npmrc | 3 + samples/basic/cards/nodejs/README.md | 107 +++++++++ samples/basic/cards/nodejs/env.TEMPLATE | 4 + samples/basic/cards/nodejs/package.json | 28 +++ samples/basic/cards/nodejs/src/bot.ts | 68 ++++++ .../basic/cards/nodejs/src/cardMessages.ts | 171 +++++++++++++++ samples/basic/cards/nodejs/src/index.ts | 27 +++ .../nodejs/src/resources/adaptiveCard.json | 205 ++++++++++++++++++ samples/basic/cards/nodejs/tsconfig.json | 24 ++ 9 files changed, 637 insertions(+) create mode 100644 samples/basic/cards/nodejs/.npmrc create mode 100644 samples/basic/cards/nodejs/README.md create mode 100644 samples/basic/cards/nodejs/env.TEMPLATE create mode 100644 samples/basic/cards/nodejs/package.json create mode 100644 samples/basic/cards/nodejs/src/bot.ts create mode 100644 samples/basic/cards/nodejs/src/cardMessages.ts create mode 100644 samples/basic/cards/nodejs/src/index.ts create mode 100644 samples/basic/cards/nodejs/src/resources/adaptiveCard.json create mode 100644 samples/basic/cards/nodejs/tsconfig.json diff --git a/samples/basic/cards/nodejs/.npmrc b/samples/basic/cards/nodejs/.npmrc new file mode 100644 index 0000000..ccaa7a2 --- /dev/null +++ b/samples/basic/cards/nodejs/.npmrc @@ -0,0 +1,3 @@ +registry=https://registry.npmjs.org/ +@microsoft:registry=https://pkgs.dev.azure.com/ConversationalAI/BotFramework/_packaging/SDK/npm/registry/ +package-lock=false diff --git a/samples/basic/cards/nodejs/README.md b/samples/basic/cards/nodejs/README.md new file mode 100644 index 0000000..732546e --- /dev/null +++ b/samples/basic/cards/nodejs/README.md @@ -0,0 +1,107 @@ +# Cards-bot + +This is a sample of a simple Agent that is hosted on an Node.js web service. This Agent is configured to show how to create a bot that uses rich cards to enhance your bot design. + +## Prerequisites + +- [Node.js](https://nodejs.org) version 20 or higher + + ```bash + # determine node version + node --version + ``` + +## Running this sample + +1. Open this folder from your IDE or Terminal of preference +1. Install dependencies + +```bash +npm install +``` + +### Run in localhost, anonymous mode + +1. Create the `.env` file (or rename env.TEMPLATE) + +```bash +cp env.TEMPLATE .env +``` + +1. Start the application + +```bash +npm start +``` + +At this point you should see the message + +```text +Server listening to port 3978 for appId debug undefined +``` + +The bot is ready to accept messages. + +### Interact with the bot from the Teams App Test Tool + +To interact with the bot you need a chat client, during the install phase we have acquired the `teams-test-app-tool` than can be used to interact with your bot running in `localhost:3978` + +1. Start the test tool with + +```bash +npm run test-tool +``` + +The tool will open a web browser showing the Teams App Test Tool, ready to send messages to your bot. + +Alternatively you can run the next command to start the bot and the test tool with a single command (make sure you stop the bot started previously): + +```bash +npm test +``` + +Refresh the browser to start a new conversation with the Cards bot. + +You should see a message with the list of available cards in Agents: +- Adaptive Card +- Animation Card +- Audio Card +- Hero Card +- Receipt Card +- O365 Connector Card +- Thumbnail Card +- Video Card + +### Interact with the bot from WebChat using Azure Bot Service + +1. [Create an Azure Bot](https://aka.ms/AgentsSDK-CreateBot) + - Record the Application ID, the Tenant ID, and the Client Secret for use below + +2. Configuring the token connection in the Agent settings + 1. Open the `env.TEMPLATE` file in the root of the sample project, rename it to `.env` and configure the following values: + 1. Set the **clientId** to the AppId of the bot identity. + 2. Set the **clientSecret** to the Secret that was created for your identity. + 3. Set the **tenantId** to the Tenant Id where your application is registered. + +3. Install the tool [dev tunnel](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started?tabs=windows) +4. Run `dev tunnels`. See [Create and host a dev tunnel](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below: + + ```bash + devtunnel host -p 3978 --allow-anonymous + ``` + +5. Take note of the url shown after `Connect via browser:` + +6. On the Azure Bot, select **Settings**, then **Configuration**, and update the **Messaging endpoint** to `{tunnel-url}/api/messages` + +7. Start the Agent using `npm start` + +8. Select **Test in WebChat** on the Azure portal. + +### Deploy to Azure + +[TBD] + +## Further reading + +To learn more about building Bots and Agents, see our [Microsoft 365 Agents SDK](https://github.com/microsoft/agents) repo. diff --git a/samples/basic/cards/nodejs/env.TEMPLATE b/samples/basic/cards/nodejs/env.TEMPLATE new file mode 100644 index 0000000..3a8f6e1 --- /dev/null +++ b/samples/basic/cards/nodejs/env.TEMPLATE @@ -0,0 +1,4 @@ +# rename to .env +tenantId= +clientId= +clientSecret= diff --git a/samples/basic/cards/nodejs/package.json b/samples/basic/cards/nodejs/package.json new file mode 100644 index 0000000..d669e21 --- /dev/null +++ b/samples/basic/cards/nodejs/package.json @@ -0,0 +1,28 @@ +{ + "name": "cards-bot", + "version": "1.0.0", + "private": true, + "description": "Agents cards bot sample", + "author": "Microsoft", + "license": "MIT", + "main": "./lib/index.js", + "scripts": { + "build": "tsc --build", + "prestart": "npm run build", + "start": "node --env-file .env ./dist/index.js", + "test-tool": "teamsapptester start", + "test": "npm-run-all -p -r start test-tool" + }, + "dependencies": { + "@microsoft/agents-bot-hosting": "0.1.20", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0" + }, + "devDependencies": { + "@microsoft/teams-app-test-tool": "^0.2.6", + "@types/node": "^22.13.4", + "npm-run-all": "^4.1.5", + "typescript": "^5.7.2" + }, + "keywords": [] +} diff --git a/samples/basic/cards/nodejs/src/bot.ts b/samples/basic/cards/nodejs/src/bot.ts new file mode 100644 index 0000000..c5af6da --- /dev/null +++ b/samples/basic/cards/nodejs/src/bot.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ActivityHandler, Activity, ActivityTypes } from '@microsoft/agents-bot-hosting' +import { CardMessages } from './cardMessages' +import AdaptiveCard from './resources/adaptiveCard.json' + +export class CardFactoryBot extends ActivityHandler { + constructor() { + super() + + this.onMembersAdded(async (context, next) => { + const membersAdded = context.activity.membersAdded + for (let cnt = 0; cnt < membersAdded!.length; cnt++) { + if ((context.activity.recipient != null) && membersAdded![cnt].id !== context.activity.recipient.id) { + await CardMessages.sendIntroCard(context) + + await next() + } + } + }) + + this.onMessage(async (context, next) => { + if (context.activity.text !== undefined) { + switch (context.activity.text.split('.')[0].toLowerCase()) { + case 'display cards options': + await CardMessages.sendIntroCard(context) + break + case '1': + await CardMessages.sendAdaptiveCard(context, AdaptiveCard) + break + case '2': + await CardMessages.sendAnimationCard(context) + break + case '3': + await CardMessages.sendAudioCard(context) + break + case '4': + await CardMessages.sendHeroCard(context) + break + case '5': + await CardMessages.sendReceiptCard(context) + break + case '6': + await CardMessages.sendThumbnailCard(context) + break + case '7': + await CardMessages.sendVideoCard(context) + break + default: { + const reply: Activity = Activity.fromObject( + { + type: ActivityTypes.Message, + text: 'Your input was not recognized, please try again.' + } + ) + await context.sendActivity(reply) + await CardMessages.sendIntroCard(context) + } + } + } else { + await context.sendActivity('This sample is only for testing Cards using CardFactory methods. Please refer to other samples to test out more functionalities.') + } + + await next() + }) + } +} diff --git a/samples/basic/cards/nodejs/src/cardMessages.ts b/samples/basic/cards/nodejs/src/cardMessages.ts new file mode 100644 index 0000000..1ddf9ba --- /dev/null +++ b/samples/basic/cards/nodejs/src/cardMessages.ts @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CardFactory, TurnContext, ActionTypes, Activity, ActivityTypes, Attachment } from '@microsoft/agents-bot-hosting' + +export class CardMessages { + static async sendIntroCard(context: TurnContext): Promise { + // Note that some channels require different values to be used in order to get buttons to display text. + // In this code the web chat is accounted for with the 'title' parameter, but in other channels you may + // need to provide a value for other parameters like 'text' or 'displayText'. + const buttons = [ + { type: ActionTypes.ImBack, title: '1. Adaptive Card', value: '1. Adaptive Card' }, + { type: ActionTypes.ImBack, title: '2. Animation Card', value: '2. Animation Card' }, + { type: ActionTypes.ImBack, title: '3. Audio Card', value: '3. Audio Card' }, + { type: ActionTypes.ImBack, title: '4. Hero Card', value: '4. Hero Card' }, + { type: ActionTypes.ImBack, title: '5. Receipt Card', value: '5. Receipt Card' }, + { type: ActionTypes.ImBack, title: '6. Thumbnail Card', value: '6. Thumbnail Card' }, + { type: ActionTypes.ImBack, title: '7. Video Card', value: '7. Video Card' }, + ] + + const card = CardFactory.heroCard('', undefined, + buttons, { text: 'Select one of the following choices' }) + + await CardMessages.sendActivity(context, card) + } + + static async sendAdaptiveCard(context: TurnContext, adaptiveCard: any): Promise { + const card = CardFactory.adaptiveCard(adaptiveCard) + + await CardMessages.sendActivity(context, card) + } + + static async sendAnimationCard(context: TurnContext): Promise { + const card = CardFactory.animationCard( + 'Microsoft Bot Framework', + [ + { url: 'https://i.giphy.com/Ki55RUbOV5njy.gif' } + ], + [], + { + subtitle: 'Animation Card' + } + ) + + await CardMessages.sendActivity(context, card) + } + + static async sendAudioCard(context: TurnContext): Promise { + const card = CardFactory.audioCard( + 'I am your father', + ['https://www.mediacollege.com/downloads/sound-effects/star-wars/darthvader/darthvader_yourfather.wav'], + CardFactory.actions([ + { + type: ActionTypes.OpenUrl, + title: 'Read more', + value: 'https://en.wikipedia.org/wiki/The_Empire_Strikes_Back' + } + ]), + { + subtitle: 'Star Wars: Episode V - The Empire Strikes Back', + text: 'The Empire Strikes Back (also known as Star Wars: Episode V – The Empire Strikes Back) is a 1980 American epic space opera film directed by Irvin Kershner. Leigh Brackett and Lawrence Kasdan wrote the screenplay, with George Lucas writing the film\'s story and serving as executive producer. The second installment in the original Star Wars trilogy, it was produced by Gary Kurtz for Lucasfilm Ltd. and stars Mark Hamill, Harrison Ford, Carrie Fisher, Billy Dee Williams, Anthony Daniels, David Prowse, Kenny Baker, Peter Mayhew and Frank Oz.', + image: { url: 'https://upload.wikimedia.org/wikipedia/en/3/3c/SW_-_Empire_Strikes_Back.jpg' } + } + ) + + await CardMessages.sendActivity(context, card) + } + + static async sendHeroCard(context: TurnContext): Promise { + const card = CardFactory.heroCard( + 'Copilot Hero Card', + CardFactory.images(['https://blogs.microsoft.com/wp-content/uploads/prod/2023/09/Press-Image_FINAL_16x9-4.jpg']), + CardFactory.actions([ + { + type: ActionTypes.OpenUrl, + title: 'Get started', + value: 'https://docs.microsoft.com/en-us/azure/bot-service/' + } + ]) + ) + + await CardMessages.sendActivity(context, card) + } + + static async sendReceiptCard(context: TurnContext): Promise { + const card = CardFactory.receiptCard({ + title: 'John Doe', + facts: [ + { + key: 'Order Number', + value: '1234' + }, + { + key: 'Payment Method', + value: 'VISA 5555-****' + } + ], + items: [ + { + title: 'Data Transfer', + price: '$38.45', + quantity: 368, + image: { url: 'https://github.com/amido/azure-vector-icons/raw/master/renders/traffic-manager.png' } + }, + { + title: 'App Service', + price: '$45.00', + quantity: 720, + image: { url: 'https://github.com/amido/azure-vector-icons/raw/master/renders/cloud-service.png' } + } + ], + tax: '$7.50', + total: '$90.95', + buttons: CardFactory.actions([ + { + type: ActionTypes.OpenUrl, + title: 'More information', + value: 'https://azure.microsoft.com/en-us/pricing/details/bot-service/' + } + ]) + }) + + await CardMessages.sendActivity(context, card) + } + + static async sendThumbnailCard(context: TurnContext) { + const card = CardFactory.thumbnailCard( + 'Copilot Thumbnail Card', + [{ url: 'https://blogs.microsoft.com/wp-content/uploads/prod/2023/09/Press-Image_FINAL_16x9-4.jpg' }], + [{ + type: ActionTypes.OpenUrl, + title: 'Get started', + value: 'https://docs.microsoft.com/en-us/azure/bot-service/' + }], + { + subtitle: 'Your bots — wherever your users are talking.', + text: 'Build and connect intelligent bots to interact with your users naturally wherever they are, from text/sms to Skype, Slack, Office 365 mail and other popular services.' + } + ) + + await CardMessages.sendActivity(context, card) + } + + static async sendVideoCard(context: TurnContext) { + const card = CardFactory.videoCard( + '2018 Imagine Cup World Championship Intro', + [{ url: 'https://sec.ch9.ms/ch9/783d/d57287a5-185f-4df9-aa08-fcab699a783d/IC18WorldChampionshipIntro2.mp4' }], + [{ + type: ActionTypes.OpenUrl, + title: 'Lean More', + value: 'https://channel9.msdn.com/Events/Imagine-Cup/World-Finals-2018/2018-Imagine-Cup-World-Championship-Intro' + }], + { + subtitle: 'by Microsoft', + text: 'Microsoft\'s Imagine Cup has empowered student developers around the world to create and innovate on the world stage for the past 16 years. These innovations will shape how we live, work and play.' + } + ) + + await CardMessages.sendActivity(context, card) + } + + private static async sendActivity(context: TurnContext, card: Attachment): Promise { + await context.sendActivity(Activity.fromObject( + { + type: ActivityTypes.Message, + attachments: [card] + } + ) + ) + } +} diff --git a/samples/basic/cards/nodejs/src/index.ts b/samples/basic/cards/nodejs/src/index.ts new file mode 100644 index 0000000..d2c7b86 --- /dev/null +++ b/samples/basic/cards/nodejs/src/index.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import express, { Response } from 'express' +import rateLimit from 'express-rate-limit' +import { Request, CloudAdapter, authorizeJWT, AuthConfiguration, loadAuthConfigFromEnv } from '@microsoft/agents-bot-hosting' +import { CardFactoryBot } from './bot' + +const authConfig: AuthConfiguration = loadAuthConfigFromEnv() + +const adapter = new CloudAdapter(authConfig) +const myBot = new CardFactoryBot() + +const app = express() + +app.use(rateLimit({ validate: { xForwardedForHeader: false } })) +app.use(express.json()) +app.use(authorizeJWT(authConfig)) + +app.post('/api/messages', async (req: Request, res: Response) => { + await adapter.process(req, res, async (context) => await myBot.run(context)) +}) + +const port = process.env.PORT || 3978 +app.listen(port, () => { + console.log(`\nServer listening to port ${port} for appId ${authConfig.clientId}`) +}) diff --git a/samples/basic/cards/nodejs/src/resources/adaptiveCard.json b/samples/basic/cards/nodejs/src/resources/adaptiveCard.json new file mode 100644 index 0000000..fcb992a --- /dev/null +++ b/samples/basic/cards/nodejs/src/resources/adaptiveCard.json @@ -0,0 +1,205 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.0", + "type": "AdaptiveCard", + "speak": "Your flight is confirmed for you and 3 other passengers from San Francisco to Amsterdam on Friday, October 10 8:30 AM", + "body": [ + { + "type": "TextBlock", + "text": "Passengers", + "weight": "bolder", + "isSubtle": false + }, + { + "type": "TextBlock", + "text": "Sarah Hum", + "separator": true + }, + { + "type": "TextBlock", + "text": "Jeremy Goldberg", + "spacing": "none" + }, + { + "type": "TextBlock", + "text": "Evan Litvak", + "spacing": "none" + }, + { + "type": "TextBlock", + "text": "2 Stops", + "weight": "bolder", + "spacing": "medium" + }, + { + "type": "TextBlock", + "text": "Fri, October 10 8:30 AM", + "weight": "bolder", + "spacing": "none" + }, + { + "type": "ColumnSet", + "separator": true, + "columns": [ + { + "type": "Column", + "width": 1, + "items": [ + { + "type": "TextBlock", + "text": "San Francisco", + "isSubtle": true + }, + { + "type": "TextBlock", + "size": "extraLarge", + "color": "accent", + "text": "SFO", + "spacing": "none" + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "TextBlock", + "text": " " + }, + { + "type": "Image", + "url": "http://adaptivecards.io/content/airplane.png", + "size": "small", + "spacing": "none" + } + ] + }, + { + "type": "Column", + "width": 1, + "items": [ + { + "type": "TextBlock", + "horizontalAlignment": "right", + "text": "Amsterdam", + "isSubtle": true + }, + { + "type": "TextBlock", + "horizontalAlignment": "right", + "size": "extraLarge", + "color": "accent", + "text": "AMS", + "spacing": "none" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "Non-Stop", + "weight": "bolder", + "spacing": "medium" + }, + { + "type": "TextBlock", + "text": "Fri, October 18 9:50 PM", + "weight": "bolder", + "spacing": "none" + }, + { + "type": "ColumnSet", + "separator": true, + "columns": [ + { + "type": "Column", + "width": 1, + "items": [ + { + "type": "TextBlock", + "text": "Amsterdam", + "isSubtle": true + }, + { + "type": "TextBlock", + "size": "extraLarge", + "color": "accent", + "text": "AMS", + "spacing": "none" + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "TextBlock", + "text": " " + }, + { + "type": "Image", + "url": "http://adaptivecards.io/content/airplane.png", + "size": "small", + "spacing": "none" + } + ] + }, + { + "type": "Column", + "width": 1, + "items": [ + { + "type": "TextBlock", + "horizontalAlignment": "right", + "text": "San Francisco", + "isSubtle": true + }, + { + "type": "TextBlock", + "horizontalAlignment": "right", + "size": "extraLarge", + "color": "accent", + "text": "SFO", + "spacing": "none" + } + ] + } + ] + }, + { + "type": "ColumnSet", + "spacing": "medium", + "columns": [ + { + "type": "Column", + "width": "1", + "items": [ + { + "type": "TextBlock", + "text": "Total", + "size": "medium", + "isSubtle": true + } + ] + }, + { + "type": "Column", + "width": 1, + "items": [ + { + "type": "TextBlock", + "horizontalAlignment": "right", + "text": "$4,032.54", + "size": "medium", + "weight": "bolder" + } + ] + } + ] + } + ] + } + \ No newline at end of file diff --git a/samples/basic/cards/nodejs/tsconfig.json b/samples/basic/cards/nodejs/tsconfig.json new file mode 100644 index 0000000..2036f9c --- /dev/null +++ b/samples/basic/cards/nodejs/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "incremental": true, + "lib": ["ES2021"], + "target": "es2019", + "module": "commonjs", + "declaration": true, + "sourceMap": true, + "composite": true, + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/.tsbuildinfo" + }, + "include": [ + "src", + "src/resources/*.json" + ] +}