Skip to content

Instantly share code, notes, and snippets.

@tmikeschu
Last active October 23, 2018 17:55
Show Gist options
  • Save tmikeschu/1aa63ff513ecc47646cd9d5907706320 to your computer and use it in GitHub Desktop.
Save tmikeschu/1aa63ff513ecc47646cd9d5907706320 to your computer and use it in GitHub Desktop.

Business Logic

All code examples are written in JavaScript based on this app. The location of the file is commented at the top. Feel free to explore this repo if you prefer to look at some actual code before getting into the concepts.

Configuring the Webhook

Once you have the get_balance and account_transfer competencies built and trained, you are ready to integrate custom logic to fit your use case. All of the information the AI extracts from an utterance in the platform is mapped to a JSON payload that is then posted to a configured webhook endpoint.

To receive this payload in your application, you must expose an endpoint for the Clinc Platform to POST to. For example, https://your_domain.com/api/v1/clinc. For a simple solution for spinning up a server, check out Heroku.

Once you have a deployed endpoint, paste its URL in the WEBHOOK URL field under your user settings (hover over your username on the top right of the nav bar and click Settings). Be sure to include https://.

You must also check the competencies that you want to be enabled for business logic. The platform will only POST to the webhook endpoint for competencies that are enabled.

Set up your endpoint to just log the request body so you can get a sense of what information is coming through. The platform will ignore any 500 responses.

// ./server.js

app.post("/api/v1/clinc", (req, res) => {
  console.log(
    "XXXXXXXXXXXXXXXXX REQUEST DATA XXXXXXXXXXXXXXXXXXXX",
    JSON.stringify(req.body)
  );
  res.sendStatus(500);
});

Once you see the request data in your server logs, you're ready to implement some business logic.

The Request Body

The request body coming from Clinc will look something like this:

{
  "qid": "6d090a7e-ba91-4b49-b9d5-441f179ccbbe",
  "lat": 42.2730207,
  "lon": -83.7517747,
  "state": "transfer",
  "dialog": "lore36ho5l4pi9mh2avwgqmu5mv6rpxz/98FJ",
  "device": "web",
  "query": "I want to transfer $400 from John's checking account to my credit card account.",
  "time_offset": 300,
  "slots": {
    "_ACCOUNT_FROM_": {
      "type": "string",
      "values": [
        {
          "tokens": "John's checking account",
          "resolved": -1
        }
      ]
    },
    "_ACCOUNT_TO_": {
      "type": "string",
      "values": [
        {
          "tokens": "credit card",
          "resolved": -1
        }
      ]
    },
    "_TRANSFER_AMOUNT_": {
      "type": "string",
      "values": [
        {
          "tokens": "$400",
          "resolved": -1
        }
      ]
    }
  }
}

All types are string for now (dates, money, etc. coming).

The Response Body

The POST endpoint has to respond with a JSON payload of the same shape. Only the state and slots properties can be manipulated. Everything else is read-only and should remain the same in the response body. For this example, we will mutate the object in place (If you are familiar with function composition and immutable transformation patterns, that is the recommended way to go to effectively scale your business logic).

Tools for simpler manipulation are in the works, but for now, it's important to note how to traverse the JSON tree. Common tasks in business logic involve "resolving" a slot value and accessing the tokens value:

Set _ACCOUNT_FROM_ to resolved: 1:

  1. Access the slots object
  2. Access the _ACCOUNT_FROM_ object
  3. Access the values array
  4. Access the first element in the values array
  5. Set the resolved to 1.

Here are some examples using JavaScript:

// using 'dot' notation
body.slots["_ACCOUNT_FROM_"].values[0].resolved = 1;

// using Object.assign (merges objects by mutating first object, second object overwrites)
Object.assign(body.slots["_ACCOUNT_FROM_"].values[0], {
  resolved: 1
});

Access the tokens of the slot value

  1. Access the slots object
  2. Access the _ACCOUNT_FROM_ object
  3. Access the values array
  4. Access the first element in the values array
  5. Access the tokens object

Here are some examples using JavaScript:

// using 'dot' notation
body.slots["_ACCOUNT_FROM_"].values[0].tokens;

To Before we get into connecting your application and the Clinc Platform, One strategy you can take to separate the HTTP interface from the data transformation is to build a function that takes a JSON payload as input and returns a modified version of that input. This will enable you to unit test your business logic without depending on the Clinc Platform.

Transformation 1: Account Balance

Implement the following steps in your server language:

  1. Check to see if state equals account_balance
  2. If it does, check to see if there is a slot with the name _SOURCE_ACCOUNT_.
  3. If there is, check to see if the source type found in in the tokens key of _SOURCE_ACCOUNT_ is a valid account type.
  4. If it is, fetch the account balance for the corresponding source value. Add a new balance property to the _SOURCE_ACCOUNT_ slot. Set the balance value to the balance found in step 3.
  • If it is not a valid account type, set an error property to invalid account type.
  1. Set the resolved property on the _SOURCE_ACCOUNT_ slot object to 1. 5.
  2. Return the body with the transformed _SOURCE_ACCOUNT_ object.
// ./lib/finance.js

const totallyRealAccounts = {
  checking: 100,
  savings: 500
};

const conversationResolver = body => {
  const { state } = body;

  if (state === "get_balance") {
    const {
      slots: { ["_SOURCE_ACCOUNT_"]: source }
    } = body;

    if (source) {
      console.log("Retrieving balance...");
      Object.assign(body.slots["_SOURCE_ACCOUNT_"].values[0], {
        resolved: 1
      });
      const { [source.values[0].tokens]: balance } = totallyRealAccounts;
      if (balance) {
        Object.assign(body.slots["_SOURCE_ACCOUNT_"].values[0], {
          balance
        });
      } else {
        Object.assign(body.slots["_SOURCE_ACCOUNT_"].values[0], {
          error: "invalid account type"
        });
      }
      return body;
    }
  }
};

module.exports = conversationResolver;

Transformation 2: Transfer Money

Implement the following steps in your server language:

  1. Check to see if state equals transfer_confirm
  2. If it does, add a _TRANSFER_ slot key to the body.slots with the following shape:
'_TRANSFER_': {
  resolved: 1,
  source: body.slots["_SOURCE_ACCOUNT_"].tokens,
  destination: body.slots["_DESTINATION_ACCOUNT_"].tokens,
  transferAmount: body.slots["_TRANSFER_AMOUNT_"].tokens,
}
  1. Validate the account types for _SOURCE_ACCOUNT_ and _DESTINATION_ACCOUNT_.
  2. If they are valid, transfer the _TRANSFER_AMOUNT_ from the _SOURCE_ACCOUNT_ to the _DESTINATION_ACCOUNT_.
    • If invalid, set an error property on the _TRANSFER_ slot object with an appropriate error message value.
  3. Set success and error properties on the _TRANSFER_ slot based on the result of the transfer function. For successful cases, error should be undefined/null/etc. and for error cases, success should be false or undefined.

IF SUCCESSFUL DO BUSINESS LOGIC TRANSITION

  1. Return the body with the transformed slots property that now has a _TRANSFER_ key.

An example in JavaScript (ES6):

const totallyRealAccounts = {
  checking: 100,
  savings: 500
};

const transfer = ({ account, source, destination, amount }) => {
  if (account[source] > amount) {
    account[source] -= amount;
    account[destination] += amount;

    return { success: true };
  } else {
    return { success: false, error: "insufficient funds" };
  }
};

const conversationResolver = body => {
  const { state } = body;

  if (state === "get_balance") {
    // ...
  } else if (state === "account_transfer_confirmed") {
    console.log("Initiating transfer...");
    const transferSlots = [
      "_SOURCE_ACCOUNT_",
      "_DESTINATION_ACCOUNT_",
      "_AMOUNT_"
    ];
    const [source, destination, amount] = transferSlots.map(
      slot => body.slots[slot].values[0].tokens
    );
    const transferData = { source, destination, amount: Number(amount) };
    body.slots["_TRANSFER_"] = {};
    Object.assign(body.slots["_TRANSFER_"], {
      values: [{ ...transferData, resolved: 1 }]
    });

    const invalidTypes = [source, destination].filter(
      account => !totallyRealAccounts.hasOwnProperty(account)
    );
    if (invalidTypes.length > 0) {
      Object.assign(body.slots["_TRANSFER_"].values[0], {
        success: false,
        error: `${invalidTypes}: invalid account type(s)`
      });
    } else {
      const { success, error } = transfer({
        ...transferData,
        account: totallyRealAccounts
      });
      Object.assign(body.slots["_TRANSFER_"].values[0], { success, error });
    }
    return body;
  }
};

module.exports = conversationResolver;

Exposing an Endpoint

Now that we have slot resolver logic in place, we can set up an endpoint for the Clinc Platform to POST to. The POST request handler should past the request body directly to the slot resolving function. If the result of the resolver is undefined/null/nil/etc., have the response send back a 500 error to have the Clinc Platform ignore the webhook results.

If the result is not undefined, merge the transformed

// ./server.js

app.post("/api/v1/clinc", (req, res) => {
  const resolved = conversationResolver(req.body);
  console.log(JSON.stringify(req.body));

  if (resolved) {
    console.log("RESPONSE: ", JSON.stringify({ ...req.body, ...resolved }));
    res.send(JSON.stringify({ ...req.body, ...resolved }));
  } else {
    // Clinc ignores any 400-500 responses
    res.sendStatus(500);
  }
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment