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.
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 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 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:
- Access the
slots
object - Access the
_ACCOUNT_FROM_
object - Access the
values
array - Access the first element in the values array
- Set the
resolved
to1
.
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
slots
object - Access the
_ACCOUNT_FROM_
object - Access the
values
array - Access the first element in the values array
- 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.
Implement the following steps in your server language:
- Check to see if state equals
account_balance
- If it does, check to see if there is a slot with the name
_SOURCE_ACCOUNT_
. - If there is, check to see if the source type found in in the
tokens
key of_SOURCE_ACCOUNT_
is a valid account type. - If it is, fetch the account balance for the corresponding source value. Add
a new
balance
property to the_SOURCE_ACCOUNT_
slot. Set thebalance
value to the balance found in step 3.
- If it is not a valid account type, set an
error
property toinvalid account type
.
- Set the
resolved
property on the_SOURCE_ACCOUNT_
slot object to1
. 5. - 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;
Implement the following steps in your server language:
- Check to see if state equals
transfer_confirm
- If it does, add a
_TRANSFER_
slot key to thebody.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,
}
- Validate the account types for
_SOURCE_ACCOUNT_
and_DESTINATION_ACCOUNT_
. - 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.
- If invalid, set an
- Set
success
anderror
properties on the_TRANSFER_
slot based on the result of the transfer function. For successful cases,error
should beundefined
/null
/etc. and for error cases,success
should befalse
orundefined
.
IF SUCCESSFUL DO BUSINESS LOGIC TRANSITION
- 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;
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);
}
});