How We Made BagelBot

We replaced a random-meet-up Slack bot with a slim clone we built during hackday. We changed what we didn’t like about the old one, saved Tails hundreds of $ per month, and rolled it out to the whole company!

Before BagelBot, there was the Donut bot. Donut is a Slack bot that brings people together. If you’re in the Donut channel within your organisation, it pairs you at random with someone else in that channel every two weeks and sends you both a direct message encouraging you to meet up — be it for a coffee, a remote hangout, or a jog (ideally stealing a dog to join!) We started ‘doing donuts’ at Tails last year within Product Engineering. It was a hit. We had our bi-weekly meetings and life was good.

A typical Donut greeting message

We continued using Donut as the UK went into lockdown during the onset of Covid-19. It helped us make those important random connections that happen when you work near other teams and people. As we continued maintaining our connectedness, we started bumping into the free tier limits of Donut. Moving up into the premium tier meant paying a flat fee plus an additional cost for every new person added to the group. Thus, we couldn't expand it to the wider company.

Enter: hackday and three determined engineers. Andrew Healey, Plum Moore, and Sam L.

We booted up our favourite cloud IDE for hacking and started coding. Glitch is a great platform for hackday because it lets you all work in a code space with version control and zero setup. It also lets you host web applications in their free tier. On this tier, apps go to sleep after they haven’t been accessed in a while. This is perfect for a Slack bot that does nothing 99.9% of the time.

Hacking away at the pairing algorithm

All good hacks start with a codename that eventually becomes the real name. 'BagelBot' came from one of the earliest messages in the now dead #donuts Slack channel.

BagelBot is run manually every two weeks. The rough order of events is:

  • Allison Dunnings (our office manager) uses a custom Slack command /bagel-go
  • Slack sends a request from their servers to the BagelBot application hosted on Glitch
  • BagelBot gets a list of everyone currently idling #bagel-time
  • BagelBot runs a pairing algorithm using historical pairing data
  • A direct message is sent to each pair with a random greeting message
  • BagelBot announces to the channel that pairing is complete!

BagelBot is a Flask application. Since we use Flask for the majority of our services here at Tails it’s a common language that we speak. For storage we used an SQLite database. To talk with Slack we used the official library slackapi/python-slackclient.

One of the trickiest parts of the hack was the pairing algorithm. There were a few edge cases that we wanted to grind out to make the bot production ready. Luckily, We made it just in time for hackday presentations.

The pairing algorithm will first try to pair you with someone that you haven’t met before. If such a person isn’t available then it will pair you with someone random. A planned improvement is to pair you with someone that you met a while ago, instead of randomly picking when there is no one new. We are currently tracking when people meet so that when this becomes an issue the solution can be slotted in. We are researching different methods to get 'perfect pairings'. Since the problem is NP-hard, we are looking at iterative ways to get closer to the optimal solution like hill climbing and simulated annealing. When there’s an odd number of bagel-ers, there will be one lucky group of three.

We add in an element of randomness to make this process non-deterministic. Otherwise it’s possible to enter a fixed cycle where you can predict who you will pair with next.

Here’s all that in Python code.

# given a slack id and a bot id, get a list of groups
def get_groups(channel_id, bot_id):
    slack_api = 'https://slack.com/api/conversations.members'
    query = '?token={TOKEN}&channel={channel_id}&pretty=1'
    info = requests.get(f'{slack_api}{query}').text
    users = json.loads(info)['members']
    users.remove(bot_id)

    # ensure that once everyone has already met there isn't a fixed cycle
    random.shuffle(users)

    # query our db and build user history
    user_history = {}
    for pair in get_pairs():
        user = pair[0]
        past_partner = pair[1]
        if user in user_history:
            user_history[user].append(past_partner)
        else:
            user_history[user] = [past_partner]

    # build pairing groups
    groups = []
    for user in users:
        if user == False:
            continue
        potential_partners = users.copy()
        potential_partners.remove(user)
        sanitized_potential_partners = list(filter(None, potential_partners))

        # create a fall-back pairing
        try:
            pair_with = next(u for u in sanitized_potential_partners if u)

        # we're at the end and there's an odd number of users
        # create a trio and stop
        except StopIteration:
            groups[len(groups)-1].append(user)
            break

        # attempt to match users who haven't met
        for person in sanitized_potential_partners:
            if person in user_history.get(user, []):
                continue
            else:
                pair_with = person
                break

        # create a group with the pair and bagelbot
        groups.append([user, pair_with])

        # take them out of the potential pairing pool
        users[users.index(user)] = False
        users[users.index(pair_with)] = False

    return groups

The BagelBot greeting messages that go out to the pairs are randomly picked from a pool that includes lots of internal Tails humour. We want to thank the beta testers that helped us test everything before we fully rolled out the bot. This made sure that every pair (or trio) got one message instead of ... twenty messages.

Goodbye #donuts, hello #bagel-time

Our efforts are captured in January's hackday scrapbook.

Our hack went down so well that we won an award for our project, which meant we each got a coveted hackday sticker for our laptops. More importantly, BagelBot allowed us to be unrestricted by the premium tier costs of Donut. We have since expanded our random-meet-up culture from just Product Engineering to the whole company!

Around 90 people take part in #bagel-time. New joiners are automatically added to the channel and encouraged to participate and foster the community connectedness we’re building at Tails. With most people working from home at the moment, BagelBot has helped combat some of the challenges associated with onboarding people remotely.