# 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
slotsobject - Access the
_ACCOUNT_FROM_object - Access the
valuesarray - Access the first element in the values array
- Set the
resolvedto1.
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
slotsobject - Access the
_ACCOUNT_FROM_object - Access the
valuesarray - Access the first element in the values array
- Access the
tokensobject
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
tokenskey of_SOURCE_ACCOUNT_is a valid account type. - If it is, fetch the account balance for the corresponding source value. Add a new
balanceproperty to the_SOURCE_ACCOUNT_slot. Set thebalancevalue to the balance found in step 3.
- If it is not a valid account type, set an
errorproperty toinvalid account type.
- Set the
resolvedproperty 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.slotswith 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
errorproperty on the_TRANSFER_slot object with an appropriate error message value.
- If invalid, set an
- Set
successanderrorproperties on the_TRANSFER_slot based on the result of the transfer function. For successful cases,errorshould beundefined/null/etc. and for error cases,successshould befalseorundefined. - If the transfer is successful, set
statetoget_balance. - Return the body with the transformed
slotsproperty that now has a_TRANSFER_key and the transformedstateproperty 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>.