Thursday, December 10, 2015

[Salesforce / Amazon Echo] Integrate Alexa with Salesforce

Among the #swag I got from my colleagues from their trip to DreamForce 15, there was a pearl: Amazon Echo, well known as Alexa.

The first (funny) thing I wanted to do was to make it (her) speak italian, and believe me it was as fun as learning how to use the Alexa Skill Kit: the only language you can use is English of course but you can still make her say strange words that "sound like" italian (after practicing building the library, even you would call it "her").

As usual, if you are a TL;DR person (btw I'm one of you guys), this is the GitHub repository with all the Apex Code.

If you know how to setup a Connected App and expose e public webservice, you are ready to go (some changes on the code are still necessary, refer to the README.md file).

When developing an Alexa Skill you have two choices:


In this post I'm not going to show you how Alexa's Request/Response paradigm works, but instead I'll show you how to use the simple Apex library I've developed to help you implement your own skills and make the provided example service run on your Org.

Why Salesforce?


First of all because the Force.com platform is awesome and you can easily create REST services in few steps.

Secondly because you can integrate Alexa with Salesforce services (read this awesome post by Jeff Douglas, he has already developed some cool examples in NodeJS playing with Alexa) using your CRM with just voice commands.

So instead of setting up a middleware between Alexa and Salesforce I thought that it would have been amazing to have the service built directly inside Salesforce!

The recipe


This is what we'll be using:

  • A new Alexa Skill (defined in the developer portal)
  • A Salesforce Alexa Skill Kit implementation (the library)
  • A REST webservice (public, to whom Alexa will be speaking)
  • A Connected App (to enable OAuth, your service will be using an auth. token)
  • A Salesorce Community (custom domain + Branding + Profiles)
  • An Amazon Echo (don't you say?!?)

Create a new Alexa Skill


The first thing is to define a new skill.

With your amazon account go to the Amazon Developer site.



Choose the Alexa Skill Kit and create a new skill:



The Invocation name is the name that you have to say when calling this skill (e.g. Alexa, ask alexaforce for my user).

The Endpoint is the URL of the service (it will be your custom Salesforce REST service inside the Community): for now add a fake endpoint, you'll be configuring it later (after configuring the Community and creating the REST service).

Write down your new Application ID.

By clicking the Next button you are setting the skill's schema:



We'll be having 3 simple intents:

  • AlexaForceHelp: this helps the user by saying what you can do with this skill
  • AlexaForceStop: this is used when you don't want to use the skill anymore (e.g. the AlexaFOrceHelp intent ask for a continuation, by saying "stop" the skill ends)
  • SFUserInfo: Alexa reads the full name of the current user (if he is logged)

The next configuration step sets up the SSL certificate option (this is all handled by Salesforce platform):



Click "Next" again and the Test step allow to do some tests with your service (no service is defined for now, so leave this step for now):



The last step is about branding:



And account linking:



This configuration can only be completed when the Connected app has been created so you can link you Salesforce account to the skill: Alexa will be sending to Salesforce REST service the authorization token got when activating the skill on the Alexa site.

For now get the Redirect URL and write it down somewhere but select Account Linking or Creation to No (we'll be activating this further).

The skill is half configured: to complete the configuration we need some Salesforce configuration.

The skill is now connected to your developer account, so it is shown in the Skill section of your https://alexa.amazon.com site.




Implement your skill in Apex


First thing to create is a new Skill class by creating a new AlexaSkill object:

AlexaSkill skill = new AlexaSkill();
skill.setApplicationId('amzn1.echo-sdk-ams.app.APP_ID');
skill.addIntent(new AlexaUserInfoIntent());
skill.addIntent(new AlexaForceHelpIntent());
skill.addIntent(new AlexaForceStopIntent());
skill.addOnLaunchIntent(new AlexaForceHelpIntent());
return skill;

The setApplicationId sets the application you got from the first page of configuration of the skill on the Amazon site.

The next lines create the new intents.

Each intent is an extension of the AlexaIntent class:

public class AlexaForceHelpIntent extends AlexaIntent{
    public AlexaForceHelpIntent(){
        super('AlexaForceHelp');
        List<AlexaIntent.Slot> slots = new List<AlexaIntent.Slot>();
        List<String> utterances = new List<String>();
        utterances.add('Help'); 
        utterances.add('What to do');
        utterances.add('What can i do');
        utterances.add('What can you do');
        utterances.add('Help me'); 
        this.setSlots(slots);
        this.setUtterances(utterances);
    }
    
    public override ASkillResponse execute(ASkillRequest request){
        String responseText = 'You can use ask for Salesforce info. For example try asking Alexaforce for "my user".';
        return this.say(responseText, null, null, null, false);
    }
}

The intent have to provide:

  • Its describe informations, such as the slots you will use and the utterances (both are not mandatory but suggested to help the configuration phase of the skill)
  • The implementation of the execute() method that handles the code behind the intent

The AlexaSkill class, among various getters and setters (to get/get utterances, slots and the name) provides an helper method to automatically create a response, the say() method.

You can pass the following parameters say(String text, String cardTitle, String cardContent, String reprompt, Boolean shouldEndSession):
  • text: text to be "said" by Alexa
  • cardTitle: title of the card sent to your alexa app (void for no card)
  • cardContent: content of the card
  • reprompt: reprompt sentence
  • shouldEndSession: this response will or will not close the current session

The AlexaSkill class provides the following methods:

  • getIntentSchema(): provides a JSON rapresentation of the intent's schema. This will be exposed by the REST service we are going to implement
  • getUtterances(): provides a JSON rapresentation of the intent's utterances. Exposes by the REST service
  • addOnLauchIntent(): (and remove method) allow to set an intent called when you call your skill without saying any of the utterances (e.g. Alexa ask alexaforce. This may be used to guide the user
  • addIntent(): (and remove method) adds an intent to the skill
  • addDefaultIntent(): (and remove method) this is an intent that is called when Alexa asks for an intent but this has not (yet) been implemented (not mandatory)
  • execute(): this method has 2 implementation, we'll be using the one returning a String and getting a String as the only parameters: this method simply accepts a request JSON string and output the JSON response (this is the core method of the skill)

This classes use the ASkillRequest and ASkillResponse classes that rapresent the JSON request and response object.

Implement the Apex REST service


Once we have implemented our skills, we can create our new Skill REST service:

@RestResource(urlMapping='/AlexaRestTest/*')
global class AlexaRestTest {
    @HttpGet
    global static void get(){
        AlexaSkillRESTApp.handleGet(createAlexaSkill());
    }
    @HttpPost
    global static void post(){
        AlexaSkillRESTApp.handlePost(createAlexaSkill());
    }
    
    //Creates a new Skill
    private static AlexaSkill createAlexaSkill(){
        AlexaSkill skill = new AlexaSkill();
        skill.setApplicationId('amzn1.echo-sdk-ams.app.APP_ID');
        skill.addIntent(new AlexaUserInfoIntent());
        skill.addIntent(new AlexaForceHelpIntent());
        skill.addIntent(new AlexaForceStopIntent());
        skill.addOnLaunchIntent(new AlexaForceHelpIntent());
        return skill;
    }

}

This REST service uses the AlexaSkillRESTApp helper class that handles the GET and POST methods: this class implements a simple Debug process by registering every request/response couple in the RestLog__c Sobject:



You can disable this behavior changing the following member on the AlexaSkillRESTApp class to return false:

public class AlexaSkillRESTApp {
    
    //Enable debug
    public static BOOLEAN ENABLE_DEBUG{
        get{
            //to be replace with a CS / Custom Metadata
            return true;
        }
    }
    //. . .
}

The GET method supports the parameter schema to which you can pass the values utterances to get the JSON rappresentation to be copied in the skill configufation on the Amazon Developer site or leave it blank to get all the intant definitions.

E.g. https://alexaforce-00-developer-edition.eu5.force.com/alexaforce/services/apexrest/AlexaRestTest?schema=utterances
Outputs:
SFUserInfo get my user info
SFUserInfo get my user
SFUserInfo my user
SFUserInfo user info
AlexaForceHelp Help
AlexaForceHelp What to do
AlexaForceHelp What can i do
AlexaForceHelp What can you do
AlexaForceHelp Help me
AlexForceStop Stop
AlexForceStop Bye
AlexForceStop Bye bye
AlexForceStop Shut up
AlexForceStop Shut the fuck up
AlexForceStop Go to sleep
AlexForceStop sleep

E.g. https://alexaforce-00-developer-edition.eu5.force.com/alexaforce/services/apexrest/AlexaRestTest?schema
Outputs:
{
  "intents" : [ {
    "slots" : [ ],
    "intent" : "SFUserInfo"
  }, {
    "slots" : [ ],
    "intent" : "AlexaForceHelp"
  }, {
    "slots" : [ ],
    "intent" : "AlexForceStop"
  } ]
}

The next step is to configure a public endpoint (es. the one provided in the example) to make this REST service available.

Configure the Community


The Salesforce community will be used to give public access to the REST service we'll be implementing, to get a custom domain and to allow different profiles to get logged in into the community.

Go to Setup > Communities > Settings and enable Communities and create a custom domain (e.g. alexaforce-00-developer-edition.eu5.force.com).

Now create a new community and activate it:



Add the profiles you want to log into the community:



The community allow branding so you can change default styling (color and logo) with few clicks.

This is not the only valid solution (you could have used a simple Site with custom Visualforce pages).

Make the REST service public


Go to Setup > Developer > Sites and search for the Community site just created:



Click on its name:



And then on Public Access Settings. Search for the Enable Apex Class Access Settings and add the AlexaRestTest class:



This way your service will be accessible outside with the endpoint https://[community_base_url]/[community_endpoint_folder]/services/apexrest/AlexaRestTest: note that the [community_endpoint_folder] is not mandatory when configuring the Community access url.

You have to set this endpoint on the skill's configuration:



Last step is to configure the account linking.

Implement a connected intent


In the code provided you have an intent that does a simple request that requires a valid session ID:

public class AlexaUserInfoIntent extends AlexaIntent{
    
    public AlexaUserInfoIntent(){
        super('SFUserInfo');
        List<String> utterances = new List<String>();
        utterances.add('get my user info');
        utterances.add('get my user');
        utterances.add('my user');
        utterances.add('user info');
        this.setUtterances(utterances);
    }
    
    public override ASkillResponse execute(ASkillRequest request){
        Http h = new Http();
        HttpRequest req = new HttpRequest();
        //change this with your current community folder name (or leave blank string)
        String communityFolder = '/alexaforce';
        String endpoint = URL.getSalesforceBaseUrl().toExternalForm()+communityFolder+'/services/Soap/c/34.0';
        req.setMethod('POST');
        req.setEndpoint(endpoint);
        req.setHeader('Content-Type', 'text/xml; charset=utf-8');
        req.setHeader('SOAPAction','getUserInfo');
        
        String authToken = request.session.user.accessToken;
        
        String soapBody = '<?xml version="1.0" encoding="utf-8"?><soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:enterprise.soap.sforce.com">  <soapenv:Header><urn:SessionHeader><urn:sessionId>'
                    +authToken+'</urn:sessionId></urn:SessionHeader></soapenv:Header><soapenv:Body><urn:getUserInfo /></soapenv:Body></soapenv:Envelope>';
        
        req.setBody(soapBody);
        HttpResponse resp = h.send(req);
        Dom.Document doc = resp.getBodyDocument();
        //Retrieve the root element for this document.
        Dom.XMLNode soapenvEnvelope  = doc.getRootElement();
        String SOAPENV_NS = 'http://schemas.xmlsoap.org/soap/envelope/';
        String NS = 'urn:enterprise.soap.sforce.com';
        Dom.XMLNode bodyNode = soapenvEnvelope.getChildElement('Body', SOAPENV_NS);
        Dom.XMLNode userinfoRespNode = bodyNode.getChildElement('getUserInfoResponse', NS);
        Dom.XMLNode resultNode = userinfoRespNode.getChildElement('result', NS);
        Dom.XMLNode fullnameNode = resultNode.getChildElement('userFullName',NS);
        String fullName = fullnameNode.getText();
        String responseText = 'Your user\'s full name is '+fullName;
        return this.say(responseText, 'Salesforce User Info', responseText, null, true);
    }  
}

The intent's execute() method makes a call getting the (SOAP) UserInfo info about the current users associated to the authorization token provided with the request received (request.session.user.accessToken value): the response will be Alexa telling "You user's full name is ...".

Remember to add a Remote Site Setting configuration to your community domain, otherwise the callouts won't work.

The code is not optimized at all (you have to change the communityFolder variable to fit to your Community configuration).


Create a Connected App


Let's enable OAuth within the Salesforce Org:



The Callback URL is set with the Redirect URL of the Skill configuration in the Amazon developer site (the one got at the beginning of the article).

The Consumer Key will be filled the Client Id field on the Skill configuration in the Amazon developer site (see the first part of this article)

The last piece of configuration of the Account Linking is the Authorization URL that should be the the Community's login page like https://alexaforce-00-developer-edition.eu5.force.com/alexaforce/login: unfortunately when linking accounts Amazon's Alexa Skills page doesn't sends to the login page the redirect_uri parameter that is mandatory for successfull Salesforce OAuth process completion.

That's why we need a custom login page that will be a proxy to the real login page: this is the AlexaOAuthStarter Visualforce page.

Open its controller class named AlexaOAuthStarterController:

public class AlexaOAuthStarterController {

    //Load action executed by the visualforce page called by Alexa's linked account attempt
    //ATTENTION: change the "communityName" and "alexaOauthCallbackURL" variables with the 
    //correct values
    public PageReference onLoad(){
        //change this with your current community folder name (or leave blank string)
        String communityFolder = '/alexaforce';
        //redirect uri configured in the Alexa Skill configuration
        String alexaOauthCallbackURL = 'https://pitangui.amazon.com/spa/skill/account-linking-status.html?vendorId=XXXXXXXXX';
        String communityURL = URL.getSalesforceBaseUrl().toExternalForm()+communityFolder;
        PageReference page = new PageReference(communityURL+'/services/oauth2/authorize');
        for(String k : ApexPages.currentPage().getParameters().keyset()){
            page.getParameters().put(k, ApexPages.currentPage().getParameters().get(k));
        }
        //appends the "redirect_uri" that amazon's configuration does not send
        //this class should be configured to change depending on the context (1 visualorce page per skill?)
        page.getParameters().put('redirect_uri',alexaOauthCallbackURL);
        return page;
    }
}

Replace the following variables:

  • communityFolder: replace with your community endpoint folder, if any
  • alexaOauthCallbackURL: the redirect URL got from Alexa's skill configuration

This page will be your entry point for the authorization process.

Remember to enable this page to all the Community's profiles.

Let's finalize the configuration


Go again on the Alexa's skill configuration page and let's add the remainig configurations:



Where:
  1. https://[community_base_url]/[community_endpoint_folder]/AlexaOAuthStarter
  2. Consumer Key of the Connected App
  3. The domain of the community ([community_base_url])
  4. The redirect URL you have set up in the Connected App
  5. Add a fake URL for the privacy policy

Connect your skill


Your skill is ready to be used.

Go to the Skill section of your https://alexa.amazon.com site:



And click Enable (Chrome blocks the popup, so pay attention to the warning massages):



After you login allow the Connected App:



And you'll be redirected to the following page (that you can close):



Now Alexa has your access token to make requests to your Community's API interface (whether it is via SOAP or REST).

Remember: Salesforce sessions don't last forever and the Amazon Skill documentation states that you are forced to disable and re-enable the skill if you have to refresh the access token.

Now try to say Alexa ask alexaforce for my user and she'll magically cry out your name!

You have no excuse to develop some awesome app integrating Alexa with Salesforce.

Enjoy!