Lab E5 - Add adaptive cards

Do these labs if you want to build a Declarative agent where Microsoft 365 provides the AI model and orchestration
- E0 - Setup
- E1 - First declarative agent
- E2 - Build an API
- E3 - Add a declarative agent and API plugin
- E4 - Enhance the API and plugin
- E5 - Add adaptive cards
- E6a - Add Entra ID authentication (Teams Toolkit)
- E6b - Add Entra ID authentication (manual setup)
- E6c - Add Entra ID authentication (Single sign-on)
- Bonus - Add Graph Connector
Table of Contents
In this lab you will further enhance the reponse from Copilot for Microsoft 365 from text to rich cards using Adaptive Cards.
In this lab you will learn:
- What are Adaptive Cards
- How to create and test an Adaptive Card
- How to update Microsoft 365 Copilot responses to use Adaptive Cards for rich content
Note
This lab builds on the previous one, Lab E4. You should be able to continue working in the same folder for labs E2-E6, but solution folders have been provided for your reference. The finished solution for this lab is in the /src/extend-m365-copilot/path-e-lab05-add-adaptive-cards/trey-research-lab05-END folder.
Introduction
What are Adaptive Cards?
Adaptive Cards are platform-independent UI snippets authored in JSON that can be exchanged between apps and services. Once delivered to an app, the JSON transforms into native UI that automatically adapts to its environment. This enables the design and integration of lightweight UI across major platforms and frameworks.Exercise 1: Create and test a simple Adaptive Card
Let's dive in and discover how fun it is to create adaptive cards.
Step 1: Define Your Adaptive Card in JSON
Create a new file named adaptiveCard.json
and add the following JSON content:
{
"type": "AdaptiveCard",
"body": [
{
"type": "TextBlock",
"text": "Hello, Adaptive Cards!",
"size": "large",
"weight": "bolder"
}
],
"actions": [
{
"type": "Action.OpenUrl",
"title": "Click me",
"url":"https://www.contoso.com"
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.3"
}
This JSON defines a simple Adaptive Card with a text block and a button.
Step 2: Test Your Adaptive Card
To test your Adaptive Card, you can use the Adaptive Cards Designer.
- Open the Adaptive Cards Designer .
- Copy the JSON content from your
adaptiveCard.json
file. - Paste the JSON content into the "Card Payload Editor" section on the lower part of the designer.
- You will see a live preview of your Adaptive Card on the upper part of the designer.
Congrats! You are now fully skilled to develop Adaptive cards for your plugin!
Exercise 2: Update the plugin manifest
We are going to update the plugin manifest file called trey-plugin.json in the appPackage folder with a response template using adaptive cards. We will find each function or API call and update the templates.
Step 1: Add an adaptive card for GET /api/consultants requests
- Locate the function getConsultants and after the
properties
node add belowstatic_template
node.
"static_template": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "Container",
"$data": "${$root}",
"items": [
{
"speak": "${name}",
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"text": "${name}",
"weight": "bolder",
"size": "extraLarge",
"spacing": "none",
"wrap": true,
"style": "heading"
},
{
"type": "TextBlock",
"text": "${email}",
"wrap": true,
"spacing": "none"
},
{
"type": "TextBlock",
"text": "${phone}",
"wrap": true,
"spacing": "none"
},
{
"type": "TextBlock",
"text": "${location.city}, ${location.country}",
"wrap": true
}
]
},
{
"type": "Column",
"items": [
{
"type": "Image",
"url": "${consultantPhotoUrl}",
"altText": "${name}"
}
]
}
]
}
]
}
]
}
Step 2: Add an adaptive card for GET /api/me requests:
- Locate the function getUserInformation and after the
properties
node add belowstatic_template
node.
"static_template":{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "Container",
"$data": "${$root}",
"items": [
{
"speak": "${name}",
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"text": "${name}",
"weight": "bolder",
"size": "extraLarge",
"spacing": "none",
"wrap": true,
"style": "heading"
},
{
"type": "TextBlock",
"text": "${email}",
"wrap": true,
"spacing": "none"
},
{
"type": "TextBlock",
"text": "${phone}",
"wrap": true,
"spacing": "none"
},
{
"type": "TextBlock",
"text": "${location.city}, ${location.country}",
"wrap": true
}
]
},
{
"type": "Column",
"items": [
{
"type": "Image",
"url": "${consultantPhotoUrl}",
"altText": "${name}"
}
]
}
]
}
]
}
]
}
Step 3: Add an adaptive card for GET /api/projects requests:
- Locate the function getProjects and after the
properties
node add belowstatic_template
node.
"static_template": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "Container",
"$data": "${$root}",
"items": [
{
"speak": "${description}",
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"text": "${name}",
"weight": "bolder",
"size": "extraLarge",
"spacing": "none",
"wrap": true,
"style": "heading"
},
{
"type": "TextBlock",
"text": "${description}",
"wrap": true,
"spacing": "none"
},
{
"type": "TextBlock",
"text": "${location.city}, ${location.country}",
"wrap": true
},
{
"type": "TextBlock",
"text": "${clientName}",
"weight": "Bolder",
"size": "Large",
"spacing": "Medium",
"wrap": true,
"maxLines": 3
},
{
"type": "TextBlock",
"text": "${clientContact}",
"size": "small",
"wrap": true
},
{
"type": "TextBlock",
"text": "${clientEmail}",
"size": "small",
"wrap": true
}
]
},
{
"type": "Column",
"items": [
{
"type": "Image",
"url": "${location.mapUrl}",
"altText": "${location.street}"
}
]
}
]
}
]
},
{
"type": "TextBlock",
"text": "Project Metrics",
"weight": "Bolder",
"size": "Large",
"spacing": "Medium",
"horizontalAlignment": "Center",
"separator": true
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "Forecast This Month",
"weight": "Bolder",
"spacing": "Small",
"horizontalAlignment": "Center"
},
{
"type": "TextBlock",
"text": "${forecastThisMonth} ",
"size": "ExtraLarge",
"weight": "Bolder",
"horizontalAlignment": "Center"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "Forecast Next Month",
"weight": "Bolder",
"spacing": "Small",
"horizontalAlignment": "Center"
},
{
"type": "TextBlock",
"text": "${forecastNextMonth} ",
"size": "ExtraLarge",
"weight": "Bolder",
"horizontalAlignment": "Center"
}
]
}
]
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "Delivered Last Month",
"weight": "Bolder",
"spacing": "Small",
"horizontalAlignment": "Center"
},
{
"type": "TextBlock",
"text": "${deliveredLastMonth} ",
"size": "ExtraLarge",
"weight": "Bolder",
"horizontalAlignment": "Center"
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "TextBlock",
"text": "Delivered This Month",
"weight": "Bolder",
"spacing": "Small",
"horizontalAlignment": "Center"
},
{
"type": "TextBlock",
"text": "${deliveredThisMonth} ",
"size": "ExtraLarge",
"weight": "Bolder",
"horizontalAlignment": "Center"
}
]
}
]
}
],
"actions": [
{
"type": "Action.OpenUrl",
"title": "View map",
"url": "${location.mapUrl}"
}
]
}
Step 4: Add an adaptive card for POST /api/billHours requests:
- Locate the function postBillhours and after the
properties
node add belowstatic_template
node.
"static_template": {
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5",
"body": [
{
"type": "TextBlock",
"text": "Project Status Summary",
"weight": "Bolder",
"size": "Large",
"separator": true
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Client Name",
"weight": "Bolder",
"spacing": "Small"
},
{
"type": "TextBlock",
"text": "${if(results.clientName, results.clientName, 'N/A')}",
"wrap": true
}
]
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Project Name",
"weight": "Bolder",
"spacing": "Small"
},
{
"type": "TextBlock",
"text": "${if(results.projectName, results.projectName, 'N/A')}",
"wrap": true
}
]
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Remaining Forecast",
"weight": "Bolder",
"spacing": "Small"
},
{
"type": "TextBlock",
"text": "${if(results.remainingForecast, results.remainingForecast, 'N/A')}",
"wrap": true
}
]
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Message",
"weight": "Bolder",
"spacing": "Small"
},
{
"type": "TextBlock",
"text": "${if(results.message, results.message, 'N/A')}",
"wrap": true
}
]
}
]
}
Step 5: Add an adaptive card for POST /api/assignConsultant requests:
- Locate the function postAssignConsultant and after the
properties
node add belowstatic_template
node.
"static_template": {
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5",
"body": [
{
"type": "TextBlock",
"text": "Project Overview",
"weight": "Bolder",
"size": "Large",
"separator": true,
"spacing": "Medium"
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Client Name",
"weight": "Bolder",
"spacing": "Small"
},
{
"type": "TextBlock",
"text": "${if(results.clientName, results.clientName, 'N/A')}",
"wrap": true
}
]
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Project Name",
"weight": "Bolder",
"spacing": "Small"
},
{
"type": "TextBlock",
"text": "${if(results.projectName, results.projectName, 'N/A')}",
"wrap": true
}
]
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Consultant Name",
"weight": "Bolder",
"spacing": "Small"
},
{
"type": "TextBlock",
"text": "${if(results.consultantName, results.consultantName, 'N/A')}",
"wrap": true
}
]
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Remaining Forecast",
"weight": "Bolder",
"spacing": "Small"
},
{
"type": "TextBlock",
"text": "${if(results.remainingForecast, results.remainingForecast, 'N/A')}",
"wrap": true
}
]
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Message",
"weight": "Bolder",
"spacing": "Small"
},
{
"type": "TextBlock",
"text": "${if(results.message, results.message, 'N/A')}",
"wrap": true
}
]
}
]
}
Exercise 3: Test the plugin in Copilot
Before you test the application, update the manifest version of your app package in the appPackage\manifest.json
file, follow these steps:
-
Open the
manifest.json
file located in theappPackage
folder of your project. -
Locate the
version
field in the JSON file. It should look something like this:
json "version": "1.0.0"
-
Increment the version number to a small increment. For example, change it to:
json "version": "1.0.1"
-
Save the file after making the change.
Step 1: Install the plugin
Stop and restart your project to force it to re-deploy the application package. You will be brought into Microsoft Teams. Once you're back in Copilot, open the right flyout 1️⃣ to show your previous chats and declarative agents and select the Trey Genie Local copilot 2️⃣.
Step 2: Display an adaptive card
Now try a prompt such below
what projects are we doing for adatum?
Instead of just the text response you will also get a rich card with information of the project.
Now try a POST operation prompt such below
please charge 1 hour to woodgrove bank in trey research
Since the request requires Copilot to send some data via POST to the API plugin, you need to confirm that you want to allow Copilot to do so selecting the Confirm button.
Once confirmed, instead of just the text response you will also get a rich card with information of the project.
You can now test other prompts as well to see the improved responses from Microsoft 365 Copilot.
CONGRATULATIONS!
You've completed adding adaptive card responses to your first API plugin. You are now ready to proceed to the next lab to add authentication to your API.
You have 3 choices here, all of which show you how to add authentication to your app package files and validate the incoming access token in your web service. The difference is in how the app is registered in Entra ID and Microsoft 365.
-
Use OAuth 2.0 with Teams Toolkit - This is the easiest approach; you will learn to set up Teams Toolkit's automated Entra ID registration for an F5 project start experience
-
Use OAuth 2.0 with Manual Setup - Leads you through all the Entra ID registration details so you can really understand what's happening; this may help adapt your solution to work with another identity provider
-
Use Single Sign-on - New capability for seamless Entra ID authentication, manual setup