During some of my previous posts, I talked about setting up different chat bots and integrating LLM with chat bot like Webex and Discord. In this post, I aim to discuss further on how to develop a more interactive chat bot with adaptive cards.

What is Adaptive Card

Adaptive Card is a platform-agnostic schema for card exchange. It is a simple way to create and share card content in a common and consistent way. Adaptive Cards are supported by a wide range of apps and services, including various bot frameworks, Microsoft Teams, and Outlook.

Both Microsoft and Webex provide a web UI to design adaptive cards, either with drag and drop tools or with JSON schema.

Microsoft adaptive cards designer

png

Webex adaptive card designer

png

In this article, we will walk through the steps to create an adaptive card, and integrating adaptive card to a webex chat bot.

Example Use Case

png

Our example use case is to build a chat bot that can provide the driver’s score in Formula 1. The bot will reply with an adaptive card when a user inputs a command. The card will have an input field for the user to input the driver’s initial, and a button to submit the input.

After the user submits the input, the bot will extract the data to get the driver’s score and reply with the score with an adaptive card.

To follow along, the code is available here

Design an Adaptive Card

We will use the Webex Card Designer to design an adaptive card. The card will have a title, a description, an image, and a button. The button will be used to trigger an action when clicked.

Which the JSON schema of the card is as follows:

{
    "type": "AdaptiveCard",
    "body": [
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "items": [
                        {
                            "type": "Image",
                            "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/F1.svg/1200px-F1.svg.png",
                            "size": "Medium",
                            "height": "20px"
                        }
                    ],
                    "width": "auto"
                },
                {
                    "type": "Column",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Formula1 App",
                            "weight": "Lighter",
                            "color": "Accent"
                        },
                        {
                            "type": "TextBlock",
                            "weight": "Bolder",
                            "text": "Get driver score in 1 click",
                            "horizontalAlignment": "Left",
                            "wrap": true,
                            "color": "Light",
                            "size": "Large",
                            "spacing": "Small"
                        }
                    ],
                    "width": "stretch"
                }
            ]
        },
        {
            "type": "Input.Text",
            "placeholder": "VER",
            "isRequired": true,
            "errorMessage": "You must reply a valid user inital",
            "maxLength": 3,
            "id": "driver",
            "label": "Driver Initial",
            "regex": "[A-Z]"
        },
        {
            "type": "ActionSet",
            "actions": [
                {
                    "type": "Action.Submit",
                    "title": "Submit",
                    "data": {
                        "callback_keyword": "!f1"
                    },
                    "style": "positive"
                }
            ],
            "horizontalAlignment": "Left",
            "spacing": "None"
        }
    ],
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.3"
}

Code Implementation

To extract driver data from the web, we will use the requests and lxml libraries. The F1Scraper class will scrape the data from the F1 website and store the data in a dictionary. The get_driver_points method will return the driver’s score when given the driver’s initials.

from lxml import html
import requests


class F1Scraper:
    def __init__(self):
        self.driver_stat_url = (
            "https://www.formula1.com/en/results.html/2024/drivers.html"
        )
        self.all_driver_data, self.driver_initials_to_full_name = (
            self.get_all_driver_pts()
        )

    @staticmethod
    def scrape_f1(web_url: str) -> list:
        page = requests.get(web_url)
        tree = html.fromstring(page.content)

        table_rows = tree.cssselect("table.resultsarchive-table tr")
        column_headers = [column.text_content().strip() for column in table_rows[0]]

        data_rows = []
        for row in table_rows[1:]:
            data = [column.text_content().strip() for column in row.iterchildren()]
            data_rows.append(dict(zip(column_headers, data)))

        return data_rows

    def get_all_driver_pts(self) -> (dict, dict):
        data_rows = self.scrape_f1(self.driver_stat_url)

        dr_pts = {}
        driver_full_names = {}
        for row in data_rows:
            dr_name_parts = row["Driver"].replace("\n", "").split()
            initials = (
                dr_name_parts[-1]
                if len(dr_name_parts) == 3
                else "".join([name[0] for name in dr_name_parts])
            )
            full_name = (
                " ".join(dr_name_parts[:-1])
                if len(dr_name_parts) == 3
                else " ".join(dr_name_parts)
            )
            pts = row["PTS"]
            dr_pts[initials] = pts
            driver_full_names[initials] = full_name

        print("Driver scores retrieved")
        return dr_pts, driver_full_names

    def get_driver_points(self, initials: str) -> str:
        full_name = self.driver_initials_to_full_name.get(initials)
        pts = self.all_driver_data.get(initials)
        if full_name and pts:
            re = f"{full_name} has {pts} points"
        else:
            re = "Invalid driver initials. Please check the driver initials and try again."
        return re

Now we have the adaptive card and the data extraction method ready, the next step is to build the webex bot so that when a user input the command !f1, the bot will reply with the adaptive card that we created above. When the user submits the driver’s initials, the bot will extract the data and reply with the driver’s score.

from webex_bot.models.command import Command

f1api = F1Scraper()

class Driver(Command):
    def __init__(self):
        super().__init__(
            command_keyword="!f1",
            help_message="!f1 - get f1 driver points",
            card=adaptive_cards.DRIVER_CARD,
        )

    def execute(self, message, attachment_actions, activity):
        driver = attachment_actions.inputs["driver"]
        details = f1api.get_driver_points(driver)
        
        return details
    

png

We can also have the bot to reply with an adaptive card instead of a text message. There are various adaptive card libraries available. In this case, we will use the adaptivecardbuilder library to create the adaptive card.

The card will have the driver’s name and points and a button to expand the card for more details.

The code implementation example could look something like this

from webex_bot.models.response import Response
import adaptivecardbuilder as adcb

card = adcb.AdaptiveCard()
card.add(adcb.TextBlock(text="Driver Score", size="Large", weight="Bolder"))
# ... Rest of the logic

card_data = adcb.json.loads(adcb.asyncio.run(card.to_json()))

card_payload = {
    "contentType": "application/vnd.microsoft.card.adaptive",
    "content": card_data,
}
response = Response()
response.text = "F1 Card"
response.attachments = card_payload

To put everything together, in the commands.py file we can have the following.


class Driver(Command):
    def __init__(self):
        super().__init__(
            command_keyword="!f1",
            help_message="!f1 - get f1 driver points",
            card=adaptive_cards.DRIVER_CARD,
        )

    def execute(self, message, attachment_actions, activity):
        driver = attachment_actions.inputs["driver"]
        logger.info(f"{activity['actor']['emailAddress']} enquiring driver: {driver}")

        details = f1api.get_driver_points(driver)
        logger.info(f"Resposne: {details}")

        match = re.compile(r"(.*) has (\d+) points").search(details)

        card = adcb.AdaptiveCard()

        card.add(
            [
                adcb.ColumnSet(),
                    adcb.Column(width="auto"),
                        adcb.Image(
                            url="https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/F1.svg/1200px-F1.svg.png",
                            size="Medium",
                            height="20px",
                        ),
                    "<",
                    adcb.Column(width="stretch"),
                        adcb.TextBlock(text="Formula1 App", color="Accent", weight="Lighter"),
                        adcb.TextBlock(
                            text=f"Driver Statistics: {driver}",
                            size="Large",
                            weight="Bolder",
                            color="Light",
                        ),
                    "<",
                "<",
                adcb.ColumnSet(),
                    adcb.Column(width="stretch"),
                        adcb.FactSet(),
                            adcb.Fact(title="Driver:", value=f"{match.group(1)}"),
                            adcb.Fact(title="Points: ", value=f"{match.group(2)}"),
                "^",
                adcb.ActionSet(),
                    adcb.ActionShowCard(title="More Details", style="positive"),
                        adcb.ColumnSet(),
                            adcb.Column(width="stretch"),
                                adcb.FactSet(),
                                    adcb.Fact(
                                        title="Visit official website",
                                        value="[Click here](https://www.formula1.com/)",
                                    ),
            ]
        )

        card_data = adcb.json.loads(adcb.asyncio.run(card.to_json()))

        card_payload = {
            "contentType": "application/vnd.microsoft.card.adaptive",
            "content": card_data,
        }
        response = Response()
        response.text = "Test Card"
        response.attachments = card_payload

        return response

And to run the bot, we can have the following in the main.py file.

from webex_bot.webex_bot import WebexBot

from commands import *
from settings import app_config

bot = WebexBot(app_config.WEBEX_TOKEN)

bot.add_command(Driver())


if __name__ == "__main__":
    bot.run()

And that’s it! We have successfully built a chat bot that can provide the driver’s score in Formula 1. The bot will reply with an adaptive card when a user inputs a command. The card will have an input field for the user to input the driver’s initial, and a button to submit the input. After the user submits the input, the bot will extract the data to get the driver’s score and reply with the score with an adaptive card.

png

Thank you for reading and have a nice day!

If you want to support my work,

Buy me a coffee

Honestly, if you made it this far you already made my day :)