Oh my god. It's full of code!

Posts tagged “Angel.com

I Hate The Angel.com IVR API

This is a venting post. Very little useful information will be contained within. I guess perhaps it can act as a warning, but honestly, I’m just mad. It’s mostly meant to be cathartic, and perhaps a bit entertaining.

Here is the deal, Angel.com is an IVR provider. They let you make phone calls and set up call sites, etc. They offer an outbound API, which allows you of course, to make calls programmatically. Sounds good in theory. The problem is, I don’t think they really know how to build an API. Here is a quick outline of how I arrived at this conclusion.

1) Limited amount of interactions per API Token
For some reason Angel.com has decided it is wise to only allow a certain amount of API interactions per token. You use your credentials to get a token that is supposed to be valid for a given time period, however it is also only available for like 10 transactions before you need to get a new one. This adds completely unnecessary complexity to working with them. Pretty much every other vendor I have worked with gives you a token that is valid for a time period so you know when you need to get a new one. In this case I have to keep track of how many things I’ve done lest I end up over my limit and have my next call fail. This is lame.

2) Two types of call identifiers
This may be more personal preference than a legitimate gripe, but it does make things more sucky. Of course in a phone system calls can be inbound or outbound. Angel.com has two unique Id’s depending which kind of call it was. Of course this gets complicated because say you use the API to place a call, they give you back an outbound GUID (globally unique identifier), fine. Then someone answers that call and they enter your voice site, now it has an inbound GUID as well. Which one do you use for further transactions? Well apparently the outbound but that was just found out via trial and error as it isn’t documented anywhere.

3) Absolutely terrible getstatus call.
Once a call is placed I need to go back and figure out what happened with it, if it was answered, how long it took etc, so the call time can be billed back to the client. Of course I’ve had to build this complex logging system because Angel doesn’t really have any mechanisms to track which calls pertain to which client. So I have to use the getStatus API call and an outbound GUID (or a list of them), to get the data I need to build my log. This function, is completely, hands down, god awful, for a few reasons.

– Archaic return type: Instead of just having the useful information as text, it’s all returned in these crazy function wrappers. You have to spend half an hour inspecting the data that is returned and playing guess and check to see how to get the data you want. Why, why do I have to do this? What is wrong with returning simple text?

– Very fragile: If even one GUID in the list you send them is invalid for some reason, the whole list aborts. It’s completely all or nothing, meaning you have to get it perfect every time, which wouldn’t be so bad except….

– Random reporting of invalid GUID: For some reason, beyond any comprehension often times I get this error. Angel claims that one or more of the GUIDs I give them do not belong to me. Then where the fuck did I get them smart guy? You think I just conjured some fucking GUID’s out of thin air? Pulled them out of my ass and thought it would be funny for you to try and parse them? No you slack jawed backwards ass API, you GAVE THEM TO ME. I placed the call using the API, recorded the GUID in my database and now I’m trying to figure out what happened. Don’t you tell me it’s invalid, you gave it to me. Fine though, if you say it’s bad I’ll remove it from my list and pretend it doesn’t exist, well I’d love to except…

– Horrible error data reporting: If you get the above error, good luck trying to figure out what GUID is causing the problem. They give you the list of them, sure… but it’s just in a string.. after some error text, part of a stack trace and wrapped in some HTML formatting. That’s right… HTML formatting, in a web service call error message return. Not only do they not return an array with each GUID and it’s status, no, you get one text blob with the GUID’s that are presumable the problem just kind of… hanging out in there. Just chilling among the human readable error, the stack track and some mother fucking HTML.

annot perform web service invocation getStatus.The fault returned when invoking the web service operation is:<br> <pre>AxisFault faultCode: {http://schemas.xmlsoap.org/soap/envelope/}Server faultSubcode: faultString: Some of the CallGUIDs that you have submitted do not belong to you. SubscriberID: 40225 token: 0a14022d-0b-132634f962b-685b305c-ccb@40225@403e4852@1315926152748@1@1cef1c329a519114c4922915bbca946b GUID: [list of GUIDs here]

What the fuck is this shit?

GUID’s don’t want to be in that mosh pit, they belong as their own data structure that I can iterate over easily so I can mark that I shouldn’t try to get them again. Even after parse the text blob, cleaning the HTML, extracting the GUIDs and attempting to update my database so that I don’t attempt to get those statuses again, then I get this error…

– stackTrace:The email entered does not match our records. – WHAT. THE. FUCK. What the hell is this error? Email does not match your records? My credentials are hard coded in. These credentials just worked moments ago. Why now, all of a sudden as if being randomly smote by Thor does this error crop up? No real explanation, no reasoning, just error. Your credentials are bad, but yet good enough to get this far? My credentials half work? They exist in some kind of quantum super position of working and not working at the same time. Suck my ass.

So that’s about it. The main reasons I hate Angel. Apparently they have some new REST API which I’m sure they’ll tout as being better, but that doesn’t do me a ton of good seeing as I’ll have to rewrite my entire integration library to use it, and I have no guarantee it doesn’t suck just as hard.

Whew, I feel a little better now.


Coldfusion, Angel.com, Google Maps Directions, and You!

Okay, so even the title is a mouthful, this post is probably going to be insane you are thinking. Well… maybe, but it’s cool stuff. So picture this. You are using Angel.com as an IVR provider. So people call in and talk to a phone machine for data. Now say you want to give directions over the phone. Say you want those directions to be dynamic, and given to the user step by step. So for example you are hosting an event. People pre-register for this event, and have provided their address, which you have stored in a database. Bob Johnson calls in (he has registered and provided his address before) and wants directions from his house to your event center. You might think you’d need a live person to do this. Blasphemy! Have Bob authenticate so we can find his address in the database. Feed that address into an Angel.com variable (if there were a better way to enter addresses over the phone, you wouldn’t even need to pre-register, but because entering data in phones sucks we kind of need their address to already exist somewhere we can get it). Once that variable is in Angel.com, pass it, along with the destination to this tool via URL arguments. This tool will then give step by step directions that the IVR can read aloud back to Bob. He even has the ability to replay each direction, and navigate backward and forward through the steps.

Just copy and paste this and run host it on a ColdFusion server somewhere. It’s ready to be called with all configs just being passed in the URL at runtime.

<cfparam name="url.start" type="string" default="1405+Olive+Ln+N,+Plymouth,+Hennepin,+Minnesota+55447">
<cfparam name="url.end" type="string" default="1111+Cambridge+St.+Hopkins,+MN+55343+(White+Castle)">
<cfparam name="url.stepID" type="integer" default="1">

<!---- The id of the page that calls this webservice in angel.com ---->
<cfparam name="url.thisPage" type="string" default="1">

<!---- the id of the page to go to if this thing errors for some reason --->
<cfparam name="url.failPage" type="string" default="2">

<!---- the id of the page to go when we are all done giving directions ---->
<cfparam name="url.nextPage" type="string" default="3">

<!---- the id of the page to go to if we just can't find directions or a route ---->
<cfparam name="url.reEnterInfoPage" type="string" default="4">

<!---- the id of the page to go to if the person decides they want to talk to a person ---->
<cfparam name="url.transferToCC" type="string" default="5">

<!---- the id of the page to go to if the person wants to hang up---->
<cfparam name="url.disconnectPage" type="string" default="6">

<cfparam name="XMLData" type="string" default="">
<cfparam name="text" type="string" default="">

<!--- Format the directions for sending to google --->
<cfset url.start = replacenocase(url.start, " ", "+")>
<cfset url.end = replacenocase(url.end, " ", "+")>

<cfhttp url="http://maps.google.com/" result="KMLData">
    <cfhttpparam name="saddr" value="#url.start#" type="url">
    <cfhttpparam name="daddr" value="#url.end#" type="url">
    <cfhttpparam name="output" value="kml" type="url">
</cfhttp> 


<cfoutput>
    <cftry>
        <cfset XMLData = xmlParse(KMLData.FileContent)>
        <cfset totalNumberOfSteps = arraylen(XMLData.kml.Document.XmlChildren)-4>
        <cfset text = XMLData.kml.Document.XmlChildren[stepID+3].XmlChildren[1].XmlText>
        <cfset text = text&" . .">
        <!--- Some extra text formatting for reading over the IVR. You can easily add more abbreviations here if there
              are some I forgot --->
        <cfset text = replacenocase(text, " LN ", " Lane ")>
        <cfset text = replacenocase(text, " BLVD ", " Boulevard ")>
        <cfset text = replacenocase(text, " RD ", " Road ")>
        <cfset text = replacenocase(text, " ST ", " Street ")>
        <cfset text = replacenocase(text, " Ave ", " Avenue ")>
        <cfset text = replacenocase(text, " NW ", " North West ")>
        <cfset text = replacenocase(text, " NE ", " North East ")>
        <cfset text = replacenocase(text, " SE ", " South East ")>
        <cfset text = replacenocase(text, " SW ",  "South West")>            
        <cfset text = replacenocase(text, " N ", " North ")>
        <cfset text = replacenocase(text, " E ", " East ")>
        <cfset text = replacenocase(text, " W ", " West ")>
        <cfset text = replacenocase(text, " S ",  "South ")>

        
        <cfsavecontent variable="PromptMessage">
            #text#
            <cfif stepID LT totalNumberOfSteps>
                Press 1 for the next direction. Press 2 to repeat this direction. Press 3 to hear the previous direction.
                <cfelse>
                    You have reached your destination.
                    Press 1 to disconnect. Press 2 to repeat this direction. Press 3 to hear the previous direction.
            </cfif>    
        </cfsavecontent>
    
        <cfif stepID LT totalNumberOfSteps>
            <cfsavecontent variable="XMLLinks">
                <LINK dtmf="1" returnValue="#stepID+1#" destination="#url.thisPage#" />
                <LINK dtmf="2" returnValue="#stepID#" destination="#url.thisPage#" />
                <LINK dtmf="3" returnValue="#stepID-1#" destination="#url.thisPage#" />
            </cfsavecontent>
            
            <cfelse>
                <cfsavecontent variable="XMLLinks">
                    <LINK dtmf="1" returnValue="#url.nextPage#" destination="#url.thisPage#" />
                    <LINK dtmf="2" returnValue="#stepID#" destination="#url.thisPage#" />
                    <LINK dtmf="3" returnValue="#stepID-1#" destination="#url.thisPage#" />
                </cfsavecontent>            
        </cfif>
        
        
        <cfset counter = 1>
        <cfset VariablesObject[counter] = structnew()>
        <cfset VariablesObject[counter]["Name"] = "totalDirections">
        <cfset VariablesObject[counter]["Value"] = totalNumberOfSteps>

         

        <cfcatch type="any">
            <cfset promptMessage = "Sorry we couldn't find a route with the information supplied. Press 1 to try a different direction method. Press 2 to disconnect, or press 3 to be transferred to customer care.">
            <cfsavecontent variable="XMLLinks">
                <LINK dtmf="1" returnValue="#reEnterInfoPage#" destination="#failPage#" />
                <LINK dtmf="2" returnValue="#disconnectPage#" destination="#url.thisPage#" />
                <LINK dtmf="3" returnValue="#transferToCC#" destination="#url.thisPage#" />
            </cfsavecontent>        
            

            <cfset VariablesObject[1] = structnew()>
            <cfset VariablesObject[1]["Name"] = "ErrorType">
            <cfset VariablesObject[1]["Value"] = cfcatch.Type>        

            <cfset VariablesObject[2] = structnew()>
            <cfset VariablesObject[2]["Name"] = "ErrorMessage">
            <cfset VariablesObject[2]["Value"] = cfcatch.Message>

            <cfset VariablesObject[3] = structnew()>
            <cfset VariablesObject[3]["Name"] = "ErrorDetails">
            <cfset VariablesObject[3]["Value"] = cfcatch.Detail>                                    
        </cfcatch>
    </cftry>    
    
    <cfset ReturnObject = printQuestionReturnVariables('stepID',PromptMessage,XMLLinks,url.failPage,VariablesObject)>
#trim(ReturnObject)#
</cfoutput>


<cffunction name="printQuestionReturnVariables" access="remote" hint="Print Question Data For Angel IVR with returnable variables">
    <cfargument name="varName" default="none" type="string">
    <cfargument name="promptMessage" default="none" type="string">
    <cfargument name="linkMessage" default="none" type="string">
    <cfargument name="failPage" default="failPagePlaceholder" type="string">
    
    <!--- This is an array of structures, with keys "name" and "value" --->
    <!--- EX variables[1].Name = Gender --->
    <!--- EX variables[1].Value = Male --->
    <cfargument name="variablesToInclude" default="" type="any" required="no">
    
    <!--- create, scope and set the loop counter variable used below --->
    <cfset var i = 0>
    
    <cfoutput>
        <cfsavecontent variable="ReturnMessage">
            <ANGELXML>
                <QUESTION var="#ucase(varname)#">
                    <PLAY>
                        <PROMPT type="text">
                            #promptMessage#
                        </PROMPT>
                    </PLAY>
                    <RESPONSE>
                        <KEYWORD>
                            #linkMessage#
                        </KEYWORD>
                    </RESPONSE>
    
                    <ERROR_STRATEGY type="nomatch" reprompt="true">
                        <PROMPT type="text"> Sorry I did not get that. </PROMPT>
                        <PROMPT type="text"> I still did not get that. </PROMPT>
                        <PROMPT type="text"> Since I am having so much trouble; please hold while I transfer you to a customer representative who can better serve you. </PROMPT>
                        <GOTO destination="/25" />
                    </ERROR_STRATEGY>
                    <ERROR_STRATEGY type="noinput" reprompt="true">
                        <PROMPT type="text"> Sorry I did not get that. </PROMPT>
                        <PROMPT type="text"> I still did not get that. </PROMPT>
                        <PROMPT type="text"> Since I am having so much trouble; please hold while I transfer you to a customer representative who can better serve you. </PROMPT>
                        <GOTO destination="/25" />
                    </ERROR_STRATEGY>
                </QUESTION>        
                <cfif IsArray(arguments.variablesToInclude)>
                    <VARIABLES>
                        <cfloop from="1" to="#arraylen(arguments.variablesToInclude)#" index="i">
                            <cftry>
                                <cfif structkeyexists(arguments.variablesToInclude[i],'Name') and  structkeyexists(arguments.variablesToInclude[i],'value')>
                                    <VAR name="#ucase(arguments.variablesToInclude[i]['Name'])#" value="#arguments.variablesToInclude[i]['Value']#" />
                                </cfif>
                                <cfcatch type="any">
                                    <cfset ErrorData = structnew()>
                                    <cfset ErrorData.Error = cfcatch>
                                    <cfset ErrorData.Arguments = arguments>
                                    <cfset ErrorData.form = form>
                                    
                                    <!--- You might wanna email this data to yourself or something --->
                                 
                                </cfcatch>    
                            </cftry>                            
                        </cfloop>

                    </VARIABLES>
                </cfif>    
                
            </ANGELXML>
        </cfsavecontent>
    </cfoutput>
    
    <cfreturn ReturnMessage>
</cffunction>

So really what’s happening here is that the page gets called with some URL variables. Those variables are used to construct a google maps http request. That request actually prints out XML. We take the XML and clean it up a little bit and format it. Then print it off in a nice Angel XML package so it can be read by the system. The page just provides one step at a time, and uses a recursive style setup to just continually give directions until there are no more.

If you just want to use the step by step direction giving, you can of course easily remove all the Angel XML junk and just access the #return# variable and do whatever you like with it. This could easily be adapted to an online direction giver, or for Twilio, SMS, whatever. For now it is ColdFusion based, but I may try and convert it to an Apex class in the not too distant future. Depending on how the user is interacting with this, you could even remove the need to have the address pre stored. You could for example have a dedicated number where users just text their current address and you text them back step by step directions to your office or whatever. Really the sky is the limit here.

Here is a sample to see how it works. The demo has some extra stuff to make it more “person usable” instead of stripped down to be consumed by a computer.
Example of ColdFusion/Google Maps/Angel.com dynamic directions

Anyway, hope you guys think this is cool. It was a lot of fun to write!