Luna Architecture

Luna’s Architecture

Overview

Luna has a few moving parts:

  • Node / Express for the back-end

  • API.AI for defining chatbot behavior

  • Twilio for SMS capabilities

  • MongoDB for our database

Application Lifecycle

Startup

Luna starts up from index.js. It pulls in all of our dependencies, makes sure that our Mongo database is running and has the collections we need (UsersMessages, and Jobs), and starts Sisyphus--our cronjob runner--with node-schedule. It also configures our port and defines our webhooks and some middleware, yadda yadda.

User Experience (Behind the Scenes)

Core Pipeline

  • User texts Luna.

  • Twilio receives user SMS.

  • Twilio calls the Luna /sms webhook.

  • Luna /sms webhook saves the Twilio blob to the Messages collection.

  • Luna /sms webhook sends a request to API.AI, with the user's original SMS as the query argument.

  • API.AI plumbs the above request through our defined intents and spits a blob at our Luna /hook webhook.

  • Luna /hook webhook builds an SMS from the API.AI blob and sends it to the user with Twilio.

  • Luna /hook webhook checks the API.AI intent triggered by the user's original SMS. If it is a scheduling intent, relevant details are used to save a new document to the Jobs collection.

Sisyphus Sisyphus is an instance of node-schedule. Every minute it runs through the Jobs collection to see if any documents have an activationTime in the past. If it encounters a document with an activationTime in the past, it uses attributes from the document to build a new request to API.AI that will trigger the core pipeline flow defined above (but starting from the API.AI plumbing part). Sisyphus then deletes the document from the collection to avoid making redundant requests.

Schema(ish)

Messages

  • source (string): where the message came from, either Twilio or API.AI.

  • blob (object): the full-bodied JSON blob from source.

Jobs

  • activationTime (object): year, month, date, hour, and minute that the job should be triggered (and timezone!).

  • requestType (string): whether the request being triggered is an event or a query.

  • requestArg (string): the eventname or querystring value for the above requestType.

  • sessionId (string): the phone number of the user we're going to send the triggered message to.

Users to be defined

API.AI Naming Conventions

To improve the readability of the API.AI components, and to reduce the amount of code we have to write to support changes in conversation flow, we're going to agree to some naming conventions for API.AI intents, contexts, and events.

Examples

The intent that starts the precommit process is called listenForBedtime_delay-30-listenForBedtimeFollowup. The context and event associated with this intent are both listenForBedtime. We use camelCase when naming any component. The intent, however, is a command. Everything before the underscore represents the intent as a unit, and everything after the underscore is communicating some action to the server. When the listenForBedtime_delay-30-listenForBedtimeFollowup intent is triggered, the "delay-30-listenForBedtimeFollowup" part tells the server to write a new document to the Jobs collection with an activationTime thirty minutes from now, and the event it should trigger is called listenForBedtimeFollowup.

So the above example illustrated the use of the delay delimiter applied to a parent-level intent. As the user navigates deeper down the conversation tree, a couple more conventions come up. A good example is smack in the middle of the onboarding tree: the onboarding-thankYou_trigger-onboardingNextSteps intent. The parent-level intent in this conversation tree is onboarding, but the "-thankYou" part of this intent indicates that we're one level deep in the onboarding conversation tree. Hyphens in intent names (before the underscore) indicate the entirety of the conversation tree navigated. The trigger part of the intent name acts just like delay did in the above example, except it invokes an event immediately rather than after a delay. So in this case when the user hits the onboarding-thankYou_trigger-onboardingNextSteps intent, we know they're one level deep in the onboarding tree, and that our server is going to trigger the onboardingNextSteps event right away.

The fallback delimiter does not take any arguments--whenever our server detects "fallback" in the intent name, it lets one of the Luna team members know that Luna can't handle the message it received instead of sending the user a confused message.