# Business Logic
# 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 aJSON 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 (opens new window).
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 account_transfer
and get_balance
to be enabled for business logic. The platform will only POST
to the webhook endpoint for competencies that are enabled. Make sure toClick "SAVE SETTINGS".
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,
"session_info": {},
"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
, slots
, and session_info
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
:
- 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 tokens
of the slot value
- 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;
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:
- 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
. - 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:
- 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_"].values[0].tokens,
destination: body.slots["_DESTINATION_ACCOUNT_"].values[0].tokens,
transferAmount: body.slots["_TRANSFER_AMOUNT_"].values[0].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 the transfer is successful, set
state
toget_balance
. - Return the body with the transformed
slots
property that now has a_TRANSFER_
key and the transformedstate
property set toget_balance
.
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
});
if (success) {
Object.assign(body.slots['_TRANSFER_'].values[0], { success });
body.state = 'get_balance';
} else {
Object.assign(body.slots['_TRANSFER_'].values[0], { error });
}
}
return body;
}
};
module.exports = conversationResolver;
# Process POST
Requests
Now that we have slot resolver logic in place, we can set up the endpoint to process requests from Clinc. The POST
request handler should past the request body directly to the slot resolver 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 body object with the original request object. The request should respond with the newly merged object that has any slot and/or state transformations.
// ./server.js
app.post('/api/v1/clinc', (req, res) => {
console.log(
'XXXXXXXXXXXXXXXXX REQUEST DATA XXXXXXXXXXXXXXXXXXXX',
JSON.stringify(req.body)
);
const resolved = conversationResolver(req.body);
if (resolved) {
console.log(
'XXXXXXXXXXXXXXXXX RESPONSE DATA XXXXXXXXXXXXXXXXXXXX',
JSON.stringify({ ...req.body, ...resolved })
);
res.send(JSON.stringify({ ...req.body, ...resolved }));
} else {
// Clinc ignores any 400-500 responses
res.sendStatus(500);
}
});
You should now be able to run the following conversation in the query sidebar:
How much money do I have in checking?
<balance response>
Can I transfer $50 from there to savings?
<transfer confirmation>
Yes.
<show new checking account balance>.