Skip to content

Commit

Permalink
Updated copilot handoff successCardHandoff and lab instructions for m… (
Browse files Browse the repository at this point in the history
#1391)

* Updated copilot handoff successCardHandoff and lab instructions for more handoff details

* Update README.md

updated to new agent terminology

* Update Exercise 05 - Code tour.md

updated to new agent terminology

* Update successCardHandoff.json

updated terminology to agents

* Updated Handoff Image

---------

Co-authored-by: Carter <[email protected]>
  • Loading branch information
Carter425 and Carter authored Sep 24, 2024
1 parent 7e69398 commit 8085337
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 26 deletions.
6 changes: 4 additions & 2 deletions samples/msgext-copilot-handoff/ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,16 @@ Here are some ideas for prompts to try. If you don't get the result you expect,
### Copilot handoff to bot

This sample has a copilot agent handoff to enable users to transition from Copilot for Microsoft 365 to a custom engine agent or other Teams bot when needed. To try a copilot agent handoff when running the sample, follow the instructions below. Also, see the copilot agent handoff section at the end of [lab Exercise 05](./lab/Exercise%2005%20-%20Code%20tour.md) - Code tour to explore the copilot handoff code in depth.

- Copilot welcome screen
![Welcome screen for copilot](./lab/images/startScreen.png)

- Select the handoff to bot button
![Handoff action button](./lab/images/action-btn.png)

- Request is handoff to bot
- A new chat opens with the NorthwindProduct bot in Teams, seemlessly continuing the conversation with context
![Bot response](./lab/images/handoff.png)


![](https://m365-visitor-stats.azurewebsites.net/SamplesGallery/officedev-copilot-for-m365-plugins-samples-msgext-northwind-inventory-ts)
![](https://m365-visitor-stats.azurewebsites.net/SamplesGallery/officedev-copilot-for-m365-plugins-samples-msgext-northwind-inventory-ts)
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Please install the following on your computer:

## Step 2 - Download the sample code

Please [clone](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples.git) or [download](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples.git) the sample repository: [https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/).
Please [clone](https://github.com/OfficeDev/Microsoft-Teams-Samples.git) or [download](https://github.com/OfficeDev/Microsoft-Teams-Samples.git) the sample repository: [https://github.com/OfficeDev/Microsoft-Teams-Samples](https://github.com/OfficeDev/Microsoft-Teams-Samples).

Within the cloned or downloaded repository, navigate to the **samples/msgext-northwind-inventory-ts** folder. These labs will refer to this as your "working folder" since this is where you'll be working.
Expand Down
195 changes: 195 additions & 0 deletions samples/msgext-copilot-handoff/ts/lab/Exercise 05 - Code tour.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,201 @@ async function handleTeamsCardActionUpdateStock(context: TurnContext) {

As you can see, the code obtains these two values, updates the database, and then sends a new card that contains a message and the updated data.

## Step 5 - Examine the copilot agent handoff code

When running the Northwind inventory message extension in Copilot for Microsoft 365, users are able to transfer from the Northwind experience in Copilot for Microsoft 365 to a Northwind Products bot in Teams via a copilot agent handoff.

### The copilot agent handoff
Users of the Northwind inventory message extension can transfer from their chat with the copilot plugin in Microsoft for Copilot 365 to a chat with the Northwind Inventory bot in Teams via a copilot agent handoff.

The copilot agent handoff is accomplished in two primary steps by:
1) Creating a deep link URL

2) Handling the `onInvokeActivity` handler in the bot code.

### Creating a deep link URL

The deep link contains the BotID of the Teamsbot the user is transferred to and a continuationtoken to pass the context from the chat with Copilot for Microsoft 365 to the new chat in Teams. Users can access this deeplink via the `Action.OpenURL` added into the adaptive card in the message extension as seen in the **editCard.json** adaptive card.

~~~typescript
{
"type": "Action.OpenUrl",
"title": "Handoff to bot",
"url": "https://teams.microsoft.com/l/chat/0/0?users=${botId}&continuation=${continuationToken}"
}
~~~

This creates the clickablehandoff to bot buttonseen in the adaptive card

![Handoff action button](./images/action-btn.png)

In this example, the botID is automatically populated via `=${botId}`. You can choose to specify a different bot by following the botID format `28:<botId>`. For example if the botId is `68935e91-ff09-4a33-a675-0fe09f015706`, then url above would be:

`https://teams.microsoft.com/l/chat/0/0?users28:68935e91-ff09-4a33-a675-0fe09f015706&continuation=${continuationToken}`

The `Action.OpenUrl` property allows the user to hand off the conversation to the NorthwindProducts bot. When a user selects the action button, the deep link is activated and opens a new chat window with the bot in Teams. The bot receives an invoke call with a payload, which contains the continuation token from the URL and uses the token to maintain the context of the conversation as shown below.

![Handoff action button](./images/handoff.png)

When the bot receives the handoff/action invoke activity, it uses the continuation token to look up any necessary information to continue the conversation seamlessly. This involves retrieving conversation history, user preferences, or any other context needed to provide a consistent experience.

### Handling the invoke Type in the bot code

For the bot to handle the invoke call, override onInvokeActivity method as seen in the **searchApp.ts** file.

~~~typescript
// Handle invoke activities

public async onInvokeActivity(context: TurnContext): Promise<InvokeResponse> {
try {
switch (context.activity.name) {
case "handoff/action": {
this.addOrUpdateContinuationParameters(context);
setTimeout(async () => await this.notifyContinuationActivity(), 10);
return { status: 200 }; // return just the http status
}
case "composeExtension/query":
return {
status: 200,
body: await this.handleTeamsMessagingExtensionQuery(
context,
context.activity.value
),
};

default:
return {
status: 200,
body: `Unknown invoke activity handled as default- ${context.activity.name}`,
};
}
} catch (err) {
console.log(`Error in onInvokeActivity: ${err}`);
return {
status: 500,
body: `Invoke activity received- ${context.activity.name}`,
};
}
}
~~~

The response to this invoke call can only be a http status code 200 for success or anything in the range of 400-500 for error. Any text or card response intended to be delivered to the user based on processing the continuation token received in the invoke call must be asynchronously notified to the user.

Now, when bot receives the invoke call, `context.activity.value.continuation` will contain the continuationToken that was set in the deeplink url.

### Notifying Users in the New Chat (Recommended)

To help manage expectations if there is some delay in returning a continuation response from bot due to network latency and processing time, NorthwindProducts sends a series of activities to keep the user informed as seen in the **index.ts** file.

~~~typescript
await context.sendActivities([
{
type: ActivityTypes.Message,
text: "Continuing conversation from copilot...",
},
{ type: ActivityTypes.Typing },
{ type: "delay", value: 1000 },
{
type: ActivityTypes.Message,
text: `Fetching more details using the continuation token passed: ${continuationToken}`,
},
{ type: ActivityTypes.Typing },
{ type: "delay", value: 4000 },
{
type: ActivityTypes.Message,
text: `Handoff successful!`,
attachments: [(continuationParameter as any).cardAttachment],
},
]);
~~~

The user sees these messages upon entering in the new Teams chat letting them know this chat is being continued from copilot and the user can continue the chat with context when the continuation token is received moments later.

![Handoff action button](./images/handoff.png)

The final message shown in the sequence of messages sent utilizes an adaptive card defined in the **src\adaptiveCards\successCardHandoff.json** file, which can be customized including establishing the context by including the continuationToken in the welcome textblock and further guiding the conversation with the user by utilizing the continuationToken in the action buttons at the bottom of the adaptive card, allowing users to select the best option to address their needs.

### Managing Adapter Cards with the ContinuationToken

As this sample utilizes many adaptive cards, the **cardHandler.ts** file is used to manage the several different cards being utilized including the **successCardHandoff.json** adaptive card.

For each adaptive card in the sample the continuationToken is defined to include the `product.ProductName` as the chosen context to share to the bot during a handoff. You can adjust this to include different data to be shared with the user following the handoff.

~~~typescript
function getEditCard(product: ProductEx, context: TurnContext): any {

var template = new ACData.Template(editCard);
var card = template.expand({
$root: {
productName: product.ProductName,
unitsInStock: product.UnitsInStock,
productId: product.ProductID,
categoryId: product.CategoryID,
imageUrl: product.ImageUrl,
supplierName: product.SupplierName,
supplierCity: product.SupplierCity,
categoryName: product.CategoryName,
inventoryStatus: product.InventoryStatus,
unitPrice: product.UnitPrice,
quantityPerUnit: product.QuantityPerUnit,
unitsOnOrder: product.UnitsOnOrder,
reorderLevel: product.ReorderLevel,
unitSales: product.UnitSales,
inventoryValue: product.InventoryValue,
revenue: product.Revenue,
averageDiscount: product.AverageDiscount,
botId: getBotMri(context),
continuationToken: product.ProductName,
}
});
return CardFactory.adaptiveCard(card);
}
~~~

At the bottom of the **cardHandler.ts** file, the code specifies the successCardHandoff as the adaptive card template to be used when the continuationToken is present, and it populates the adaptive card with the corresponding data from the continuationToken.

~~~typescript
async function handleTeamsCardActionHandOff(context: TurnContext) {
const request = context.activity.value;
const data = request.action.data;
console.log(
`🎬 Handling copilot handoff case, continuationToken=${data.continuationToken}`
);

if (data.continuationToken) {
var template = new ACData.Template(successCardHandoff);
var card = template.expand({
$root: {
continuationToken: data.continuationToken,
},
});

return CreateAdaptiveCardInvokeResponse(200, card);
} else {
return CreateActionErrorResponse(400, 0, "Invalid request");
}
}

function handleTeamsCardActionHandOffWithContinuation(
continuationToken: string
) {
console.log(
`🎬 Handling copilot handoff case, continuationToken=${continuationToken}`
);

var template = new ACData.Template(successCardHandoff);
var card = template.expand({
$root: {
continuationToken,
},
});

return CardFactory.adaptiveCard(card);
}
~~~

Now, the full handoff experience has been set up. Next, you can create your own copilot agent handoff tailored for your users.

## Congratulations

You have completed Exercise 5 and the Copilot for Microsoft 365 Message Extensions plugin lab. Thanks very much for doing these labs!
Binary file modified samples/msgext-copilot-handoff/ts/lab/images/handoff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function getEditCard(product: ProductEx, context: TurnContext): any {
revenue: product.Revenue,
averageDiscount: product.AverageDiscount,
botId: getBotMri(context),
continuationToken: product.ProductName + "-continuation",
continuationToken: product.ProductName,
}
});
return CardFactory.adaptiveCard(card);
Expand Down Expand Up @@ -69,7 +69,7 @@ async function handleTeamsCardActionRefreshCard(context: TurnContext) {
revenue: product.Revenue,
averageDiscount: product.AverageDiscount,
botId: getBotMri(context),
continuationToken: product.ProductName + "-continuation",
continuationToken: product.ProductName,
// Card message
message: `Card refreshed successfully!`,
},
Expand Down Expand Up @@ -114,7 +114,7 @@ async function handleTeamsCardActionUpdateStock(context: TurnContext) {
revenue: product.Revenue,
averageDiscount: product.AverageDiscount,
botId: getBotMri(context),
continuationToken: product.ProductName + "-continuation",
continuationToken: product.ProductName,
// Card message
message: `Stock updated for ${product.ProductName} to ${product.UnitsInStock}!`
}
Expand Down Expand Up @@ -161,7 +161,7 @@ async function handleTeamsCardActionCancelRestock(context: TurnContext) {
revenue: product.Revenue,
averageDiscount: product.AverageDiscount,
botId: getBotMri(context),
continuationToken: product.ProductName + "-continuation",
continuationToken: product.ProductName,
// Card message
message: `Restock cancelled for ${product.ProductName}.`
}
Expand Down Expand Up @@ -203,7 +203,7 @@ async function handleTeamsCardActionRestock(context: TurnContext) {
revenue: product.Revenue,
averageDiscount: product.AverageDiscount,
botId: getBotMri(context),
continuationToken: product.ProductName + "-continuation",
continuationToken: product.ProductName,
// Card message
message: `Restocking ${product.ProductName} placed order for ${data.txtStock ?? 0} units.`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,52 @@
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "Container",
"style": "good",
"separator": true,
"items": [
{
{
"type": "TextBlock",
"text": "Prkare Inventory",
"weight": "Bolder",
"text": "Hello, I am the Northwind Products bot. Copilot shared the history of your conversation. I see that you are working on:",
"wrap": true
},
{
"type": "TextBlock",
"text": "${continuationToken}",
"wrap": true,
"fontType": "Default",
"size": "Medium",
"color": "Good"
}
]
"weight": "Bolder",
"color": "Accent",
"isSubtle": true
},
{
"type": "TextBlock",
"text": "I have some resources that may be able to help you. What would you like to do next?",
"wrap": true
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Tell me more about ${continuationToken}",
"data": {
"msteams": {
"type": "imBack",
"value": "Tell me more about: ${continuationToken} -(Great job on completing the demo! Next, you can create your own fully conversational custom engine agent tailored to your specific user needs and scenarios. To learn more, click 'Build a Custom Engine Agent' above.)"
}
}
},
{
"type": "TextBlock",
"text": "The continuation token received is: ${continuationToken}"
"type": "Action.Submit",
"title": "Speak to a Live Representative",
"data": {
"msteams": {
"type": "imBack",
"value": "Connecting you to a live representative. -(Great job on completing the demo! Next, you can create your own fully conversational custom engine agent tailored to your specific user needs and scenarios. To learn more, click 'Build a Custom Engine Agent' above.)"
}
}
],
"actions": [
},
{
"type": "Action.OpenUrl",
"title": "Handoff Successful!",
"url": "https://microsoft.com"
"type": "Action.OpenUrl",
"title": "Build a Custom Engine Agent",
"url": "https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/overview-custom-engine-agent"
}
]
}

0 comments on commit 8085337

Please sign in to comment.