Skip to content

Instantly share code, notes, and snippets.

@peternann
Created April 5, 2020 11:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peternann/41b68c561a955f858ec08285cd54ef25 to your computer and use it in GitHub Desktop.
Save peternann/41b68c561a955f858ec08285cd54ef25 to your computer and use it in GitHub Desktop.
Cognigy integration with Amazon Connect/Lex via Cognigy.AI Transformers
The code herein relates to the Cognigy blog post here:
https://
Be sure to read the blog post to understand the context of this code.
// USE THIS CODE, COMPLETE, IN A COGNIGY.AI 'NLU CONNECTOR' OF TYPE 'COGNIGY', FOR THE 'TRANSFORMER FUNCTIONS':
// BE SURE TO 'ENABLE POST NLU TRANSFORMER' IN THE NLU CONNECTOR CONFIG, ABOVE THE SOURCE CODE:
createNluTransformer({
/**
* This transformer is executed when receiving the output from the
* NLU engine, before executing the Flow.
*
* @param text The text from the message.
* @param data The data object from the message.
* @param nluResult Intent, type, slots and intentscore,
* after executing the NLU engine.
* @param connectorOutput The raw output from the NLU engine.
*
* @returns nluResult and/or data, as changed in the transformer.
*/
postNlu: async ({ text, data, nluResult, connectorOutput }) => {
// Since we are behind Lex, we received Lex NLU result data in the endpoint Transformer.
// All we have to do is borrow the intent details from Lex, and pretend it is the Cognigy result:
if ( data.lex && data.lex.currentIntent && data.lex.currentIntent.name) {
nluResult.intent = data.lex.currentIntent.name;
// Merge Lex slots in with any found by Cognigy:
Object.assign(nluResult.slots, data.lex.currentIntent.slots);
}
console.log( "######## nluResult transformed to: " + JSON.stringify(nluResult) );
return { data, nluResult };
},
});
// USE THIS CODE, COMPLETE, IN A COGNIGY.AI 'REST ENDPOINT', FOR THE 'TRANSFORMER FUNCTIONS':
// BE SURE TO 'ENABLE INPUT TRANSFORMER' AND 'ENABLE EXECUTION FINISHED TRANSFORMER' IN THE
// ENDPOINT CONFIG, ABOVE THE SOURCE CODE
// Lex request/response reference: https://docs.aws.amazon.com/lex/latest/dg/lambda-input-response-format.html
// Note that Cognigy.AI Transformers are actually Typescript code.
// Define Typescript types for Lex request/response formats:
/** There is more to this format, but this is what we use for now: */
interface RequestFromLex {
inputTranscript: string,
currentIntent: {
name: string,
slots:any
},
sessionAttributes: { [key: string]: string }
};
/** There is more to this format, but this is what we use for now: */
interface ResponseToLex {
dialogAction: {
type: string,
fulfillmentState?: string,
message?: {
contentType: string,
content: string
},
},
sessionAttributes?: { [key: string]: string }
}
createRestTransformer({
/**
* This transformer is executed when receiving a message
* from the user, before executing the Flow.
*
* @param endpoint The configuration object for the used Endpoint.
* @param request The Express request object with a JSON parsed body.
* @param response The Express response object.
*
* @returns A valid userId, sessionId, as well as text and/or data,
* which has been extracted from the request body.
*/
handleInput: async ({ endpoint, request, response }) => {
console.log( "######## Lex event: " + JSON.stringify(request.body));
const lex = <RequestFromLex>request.body;
const userId = lex.sessionAttributes.CustomerNumber || `ANON_${uuid.v4()}`;
const sessionId = lex.sessionAttributes.ContactId || `CGY_${uuid.v4()}`;
// The two items above are circulated back to Lex as SessionAttributes in handleExecutionFinished() below.
// In this way they become 'sticky', as Lex circulates all SessionAttributes each call.
const text = lex.inputTranscript;
const data = { lex: lex };
return { userId, sessionId, text, data };
},
/**
* This transformer is executed on every output from the Flow.
*
* @param output The raw output from the Flow. It is possible to manipulate
* and return every distinct output before they get formatted in the 'handleExecutionFinished'
* transformer.
*
* @param endpoint The configuration object for the used Endpoint.
* @param userId The unique ID of the user.
* @param sessionId The unique ID for this session. Can be used together with the userId
* to retrieve the sessionStorage object.
*
* @returns The output that will be formatted into the final response in the 'handleExecutionFinished' transformer.
*/
handleOutput: async ({ output, endpoint, userId, sessionId }) => {
return output;
},
/**
* This transformer is executed when the Flow execution has finished.
* For REST based transformers, the final output will be sent to
* the user.
*
* @param processedOutput This is the final object that will be sent to the user.
* It is therefore structured according to the Endpoint channel of the transformer.
*
* @param outputs This is an array of all of the outputs that were output by the Flow.
* These will be merged to create the 'processedOutput' object.
*
* @param userId The unique ID of the user.
* @param sessionId The unique ID for this session. Can be used together with the userId
* to retrieve the sessionStorage object.
* @param endpoint The configuration object for the used Endpoint.
* @param response The express response object that can be used to send a custom response back to the user.
*
* @returns An object that will be sent to the user, unchanged. It therefore has to have the
* correct format according to the documentation of the specific Endpoint channel.
*/
handleExecutionFinished: async ({ processedOutput, outputs, userId, sessionId, endpoint, response }) => {
const lex: ResponseToLex = {
// Default action, until proven otherwise:
dialogAction: {
type: "Close",
fulfillmentState: "Failed", // Or 'Fulfilled'
}
};
if (processedOutput.text === '') {
console.log( `Lex endpoint transformer - Null text returned from Flows - Treat as FAILED!`);
} else {
lex.dialogAction = {
type: "ElicitIntent",
message: {
// "contentType": "PlainText or SSML or CustomPayload",
contentType: "PlainText",
content: processedOutput.text,
},
};
}
// These MAY have been made, on the fly, at Cognigy session start, if they were not supplied from Lex/Connect:
// By returning them to Lex as Session Attributes, they should circulate back in the next request:
lex.sessionAttributes = {
CustomerNumber: userId,
ContactId: sessionId,
}
console.log( "######## Response to Lex: " + JSON.stringify(lex));
return lex;
}
});
// USE THIS CODE, COMPLETE, IN AN AWS LAMBDA FUNCTION OF YOUR OWN CREATION.
// ENSURE THAT YOUR Amazon Connect INSTANCE WILL BE ABLE TO GET PERMISSION TO
// ACCESS THE LAMBDA FUNCTION. SEE YOUR Connect/AWS ADMINISTRATOR.
// !!!! ADJUST THE URL SUPPLIED TO doPOST(), AS DESCRIBED BELOW !!!!
// As mentioned in the blog post, the full function includes more code for logging,
// error handling and https POST boilerplate:
exports.handler = async(event) => {
console.info( '######## Lex Lambda event:\n' + JSON.stringify(event,null,2) );
try {
return await doPOST(
// ADJUST THIS URL TO BE THE 'Endpoint URL' IN THE COGNIGY.AU REST ENDPOINT:
'https://endpoint-x.cognigy.ai/a73b381c09 ... 3e402d5294',
event);
}
catch (e) {
console.error( 'POST into Cognigy REST endpoint FAILED: ' + JSON.stringigy(e) );
return e;
}
}
const https = require('https');
// Helper function to do https JSON POST request-response using Node.js primitives:
// Of course, if you go beyond the inbuilt Lambda editor, you could use your
// preferred npm module to carry out the request more succinctly.
async function doPOST(url, postObject) {
return new Promise((resolve, reject) => {
// Use RegEx matcher to extract host and path from full URL:
const m = url.match(/https:\/\/([^\/]+)(\/.+)/);
const postData = JSON.stringify(postObject);
const options = {
hostname: m[1],
port: 443,
path: m[2],
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': postData.length,
}
};
const request = https.request(options, handleResponse);
request.on('error', (error) => {
console.error( `Error doing http POST to ${url} : ${error}`);
reject(error);
});
request.write(postData);
request.end();
function handleResponse(response) {
let data = '';
response.on('data', (d) => {
data += d.toString();
});
response.on('end', () => {
// Resolve with javascript object of JSON response:
resolve(JSON.parse(data));
});
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment