In this monthly column, an industry expert will
answer common questions about VoiceXML and related technologies.
Readers are encouraged to submit questions about VoiceXML,
including development, voice-user interface design,
and speech technology in general, or how VoiceXML is
being used commercially in the marketplace. If you have
a question about VoiceXML, e-mail it to
speak.and.listen@voicexmlreview.org
and be sure to read future issues of VoiceXML Review
for the answer.
By Matt Oshry
Q: I'm writing a banking application in VoiceXML.
Different users subscribe to different sets of banking services.
Some of these services include savings, checking, certificates of deposit,
and mortgages. I'd like to present each user with the ability to
get from one service to another from most places within the application.
If a user doesn't subscribe to a service I don't want to present it to her,
nor do I want the flow of the application to be disrupted if the user
happens to say the name of the service to which they do not subscribe.
What's the most efficient way to do this?
We'll tackle the input and output issues separately. Let's start with the output.
First, let's assume that the user's profile,
including the list of services to which that user is subscribed
is accessible via HTTPS. We'll call this the 'getprofile' Web API.
The API takes a user id and pin as parameters, performs some validation,
and returns the user's profile in XML format.
Assuming we've already collected an id and a pin from the user,
here's a code snippet that calls the 'getprofile' API:
<data name="domProfile" src="https://www.acme-bank.com/getprofile.cgi" namelist="usr_id usr_pin"/> |
The variable named "domProfile" stores a reference to a Document Object Model (DOM) representation of the XML document returned by getprofile.cgi to which you've passed the id and pin of the caller.
Here's a simplified version of the serialized representation that we get in return:
<?access-control allow="*.acme-bank.com"?> <accountinfo id="12345"> <fname>Bob</fname> <lname>Smith</lname> <services>11</services> </accountinfo> |
The user identified as "12345" is "Bob Smith". Reading through the specification
for the hypothetical 'getprofile' API, we learn that the
services tag is a bitmask identifying the services to which the user has subscribed.
Here's what each bit in the mask represents:
1 - checking
2 - savings
4 - cd
8 - mortgage
Thus, according to Bob's profile, he has one or more checking accounts, one or more savings accounts,
and one or more mortgages.
Armed with this information, we can write a couple of helper functions in ECMAScript.
The first, getServicesMask, takes a reference to a DOM representing a user profile as a parameter and
extracts the services bitmask. The second, getServicesPrompts, generates an ECMAScript array
of friendly names corresponding to the services bitmask. You'll see shortly how getServicesMask
comes in handy for controlling both the output and the input.
// retrieve the inner text of the services tag // the value is a bitmask identifying the services // to which the user is subscribed
function getServicesMask(aProfile) { var mask = 0;
try { var r = aProfile.documentElement; for (var i = 0; i < r.childNodes.length; i++) { var n = r.childNodes.item(i); if (n.nodeName == 'services') { mask = Number(n.childNodes.item(0).data); break;
} } } catch(e) { // exception }
return mask; }
// given the services bitmask, // return an array of service names // to which the user is subscribed function getServicesPrompts(aMask) { var a = [];
if (aMask & 1)
{ a.push('checking'); } if (aMask & 2) { a.push('savings'); } if (aMask & 4) { a.push('certificates of deposit'); } if (aMask & 8) { a.push('mortgages'); }
return a; } |
Once you have the service names in an ECMAScript array,
you can use the
tag to add them to the prompt queue:
<var name="mask" expr="getServicesMask(domProfile)"/> <var name="services" expr="getServicesPrompts(mask)"/> <foreach item="service" array="services"> <value expr="service"/> <break time="300ms"/> </foreach> |
Now that we've seen how to customize output based on the user's profile, the next step is to control the input. For this, we'll use another hypothetical Web API I'll call "getsvcgram" that, given the services bitmask, returns an SRGS grammar that includes the services to which the user has subscribed. We can retrieve the grammar using the <grammar> tag and its srcexpr attribute.
<grammar type="application/srgs+xml" srcexpr="'getsvcgram.cgi?mask=' + getServicesPrompts(getServicesMask(domProfile))"/> |
The application server returns a grammar that accepts as input only those services
relevant to the user. Here's a simplified version of getsvcgram.cgi written in Perl:
#!/usr/local/bin/perl -w use strict; use CGI qw(param);
my $mask = param('mask');
print qq{Content-Type: text/xml
<?xml version="1.0"?> <grammar xmlns="http://www.w3.org/2001/06/grammar" root="top" version="1.0" xml:lang="en-US"> <tag>var glob = new Object();</tag> <rule id="top" scope="public"> <one-of> };
# keep count, and print VOID rule if count is 0 my $items = 0;
if ($mask & 1)
{ $items++; printItem('checking', 'chk'); } if ($mask & 2)
{ $items++; printItem('savings', 'sav'); } if ($mask & 4) { $items++; printItem('certificates of deposit', 'cd'); } if ($mask & 8) { $items++; printItem('mortgages', 'mtg'); }
if (0 == $items)
{ print qq{<item><ruleref special="VOID"/></item>}; }
print qq{ </one-of> <tag>\$ = glob;</tag> </rule>
</grammar>};
sub printItem {
my($lhs, $rhs) = @_;
print qq{<item>$lhs <tag>glob.option = "$rhs";</tag> </item>}; } |
Regarding your desire to allow the user to access these services throughout the application,
you can use a <link> at application scope and specify the dynamic grammar as a child of the <link>.
<link event="onservice"> <grammar type="application/srgs+xml" srcexpr="'getsvcgram.cgi?mask=' + getServicesMask(userProfile)"/> </link> |
The srcexpr attribute will be evaluated when the grammar needs to be activated.
When you want to disable the grammar, you can do one of the following:
1) set the modal attribute to true on the appropriate form items, or
2) ensure the srcexpr evaluates to a URI that will return a valid empty grammar
When the link grammar is active, you can determine which service the user selected
by throwing an event, and, in the catch handler for that event, check
lastresult$.interpretation.
<catch event="keyword"> <log>keyword=<value expr="lastresult$.toSource()"/></log> <if cond="lastresult$.interpretation == 'chk'"> <goto next="#checking"/> <elseif cond="lastresult$.interpretation == 'sav'"/> <goto next="#savings"/> <elseif cond="lastresult$.interpretation == 'cd'"/> <goto next="#cd"/> <elseif cond="lastresult$.interpretation == 'mtg'"/> <goto next="#mtg"/> <else/> <log>unexpected keyword '<value expr="lastresult$.interpretation"/>'</log> <reprompt/> </if> </catch> |
|