Drink Recipes I
Last Updated: 05-19-2009

This version of Drinks Recipies asks the caller for a drink name, and in response, plays back the drink's ingredients list and mixing instructions. This example demonstrates the use of large grammars as well as how to create data-driven content from a datasource.

By enabling access to existing data sources, phone applications instantly become more useful and valuable to callers. For simplicity, the data source in this example is a pair of flat files in semi-colon delimited format. Applications can just as easily interact with commercial databases, since the data source resides on the Web server. In an industrial-strength application, the server-side script would query a database using a database interface supported by the language and the database management system (DBMS), such as ADO/ODBC, typically for Microsoft Active Server Pages, or PERL DBI.

Try It!
  1. Set your Application URL to http://studio.247-inc.net/library2/code/ex-108/index.vxml
  2. Call the number shown in VXML Tools page
  3. Sign in, and play!

The grammar for this example, consists of over one-thousand drink names and maps each name to a four-digit identifier that uniquely identifies the drink. Here are a few lines from the grammar:

<?xml version= "1.0"?>

<grammar mode="voice"
         root="root_rule"
         tag-format="semantics/1.0"
         version="1.0"
         xml:lang="en-US"
         xmlns="http://www.w3.org/2001/06/grammar">
  <rule id="root_rule" scope="public">
    <item>
      <one-of>
        <item>
          classic cocktail
        </item>
      </one-of>
      <tag>out = "0095";</tag>
    </item>
    <item>
      <one-of>
        <item>
          coffee grasshopper
        </item>
      </one-of>
      <tag>out = "0096";</tag>
    </item>
    <item>
      <one-of>
        <item>
          cognac highball
        </item>
      </one-of>
      <tag>out = "0097";</tag>
    </item>
    <item>
      <one-of>
        <item>
          cold deck cocktail
        </item>
      </one-of>
      <tag>out = "0098";</tag>
    </item>
    <item>
      <one-of>
        <item>
          creme de cafe
        </item>
      </one-of>
      <tag>out = "0099";</tag>
    </item>
  </rule>

</grammar>

This identifier corresponds directly to a key in the "database" tables containing the data required to concoct the drink. The database in this example consists of two tables - names and ingredients. Because each drink typically requires multiple ingredients, the ingredients are stored in a separate table so that information isn't unnecessarily repeated. The schema for the names table is as follows:

id The unique identifier of the drink. This field serves as a primary key.
name The name of the drink.
type The type of drink.
method The procedure for making the drink.

The schema for the ingredients table is as follows:

id The unique identifier of the drink. This field serves as a foreign key.
order The order in which the ingredient is added to the cocktail.
ingredient The amount and name of the ingredient.

When the user utters the name of a drink in the grammar, the recognizer returns the identifier to the VoiceXML application. The application passes this identifier as a parameter to a CGI script, written in PERL, that performs a lookup of the recipe. If the recipe is found, the CGI script returns the recipe to the application in a well-defined XML schema. For example, if the user says "Allegheny", the recognizer returns "0001". The CGI script recieves the value "0001" as a parameter, performs the lookup, and returns the following:

<recipe id="0001">
  <name>Allegheny</name>
  <method>Shake with ice and strain into cocktail glass. Add a twist of lemon peel on top.</method>
  <ingr>1 oz Bourbon</ingr>
  <ingr>1 oz Dry Vermouth</ingr>
  <ingr>1 1/2 teaspoons Blackberry-flavored Brandy</ingr>
  <ingr>1 1/2 teaspoon Lemon Juice</ingr>
</recipe>

To play the recipe to the user, the application uses the data element to parse the XML data returned by the CGI script in an XML Document Object Model (DOM). A helper class, CRecipe, written in JavaScript, encapsulates access to the data via the DOM and exposes the following methods to the application code:

Init Initializes the recipe object with an XML DOM
GetID Returns the unique identifier of the specified recipe.
GetName Returns the name of the recipe.
GetMethod Returns the procedure for making the recipe.
GetIngredients Returns an array of recipe ingredients.

The VoiceXML code for this example follows:

<?xml version="1.0"?>
<!--
Tellme Studio Code Example 108
Copyright (C) 2003-2004 Tellme Networks, Inc. All Rights Reserved.

THIS CODE IS MADE AVAILABLE SOLELY ON AN "AS IS" BASIS, WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION,
WARRANTIES THAT THE CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A
PARTICULAR PURPOSE OR NON-INFRINGING.
-->
<vxml version="2.0">

<var name="earcons_url" expr="'http://naturalsound.svc.tellme.com/common-audio/'"/>
<var name="drinkchoice"/>
<var name="drinkname"/>
<var name="drinkmethod"/>
<var name="drinkingr"/>

<!-- establish a default timeout for all dialogs; can be overridden -->
<property name="timeout" value="1ms"/>

<link event="help">

<grammar mode="dtmf"
         root="root_rule"
         tag-format="semantics/1.0"
         type="application/srgs+xml"
         version="1.0">
<rule id="root_rule" scope="public">
<item>
0
</item>
</rule>
</grammar>

<grammar mode="voice"
         root="root_rule"
         tag-format="semantics/1.0"
         type="application/srgs+xml"
         version="1.0"
         xml:lang="en-US">
<rule id="root_rule" scope="public">
<item weight="0.1">
help
</item>
</rule>
</grammar>

</link>

<form id="Init">
  <block>
    <prompt>Drink Recipes</prompt>
    <break time="500ms"/>
    <prompt>Hi there.</prompt>
    <goto next="#Main"/>
  </block>
</form>

<form id="Main">
  <field name="choice">

  <!-- this grammar maps a drink name to its assigned id -->
  <grammar type="application/srgs+xml" src="ex-108a-voice.grxml" mode="voice"/>

  <prompt>
    <prompt>Which drink do you wanna make? Say the name.</prompt>
    <audio expr="earcons_url + 'timeout.wav'"/>
  </prompt>

  <help>
    <audio expr="earcons_url + 'help.wav'"/>
    <prompt>
      You're in Drink Recipes. You say the name of a drink. And I tell you
      the ingredients and how to make it. To get outta here, say TELLMEE
      MENU, or press STAR STAR.
    </prompt>
    <audio expr="earcons_url + 'timeout.wav'"/>
    <reprompt/>
  </help>

  <nomatch>
    <prompt>sorry, i didn't catch that.</prompt>
    <reprompt/>
  </nomatch>

  <noinput>
    <prompt>sorry, i didn't hear you.</prompt>
    <reprompt/>
  </noinput>

  <filled>
    <!-- share state between dialogs by saving variables to document scope -->
    <assign name="drinkchoice" expr="choice"/>
    <log>* * * DRINK ID: <value expr="choice"/></log>
    <goto next="#GetDrink"/>
  </filled>

  </field>
</form>

<!-- Retrieve the drink's name, ingredients, and procedure -->
<form id="GetDrink">
  <!-- the properties of the named object (oResult) match the names of the 
       variables returned from the subdialog.
  -->
  <subdialog name="oResult" src="#GetDrinkInfo">
    <param name="drink_id" expr="drinkchoice"/>    
    <filled>
      <if cond="oResult.status == 'success'">
         <!-- populate document scoped variables with recipe data -->
         <assign name="drinkname" expr="oResult.name"/>
         <assign name="drinkmethod" expr="oResult.method"/>
         <assign name="drinkingr" expr="oResult.ingredients"/>
         <goto next="#PlayDrink"/>
      <elseif cond="oResult.status == 'failure'"/>
         <goto next="#FailDrink"/>
      <else/>
         <goto next="#FailDrink"/>
      </if>
    </filled>

  </subdialog>
</form>

<!-- 
   This subdialog shields the implementation details of fetching drink info from the server.
   The subdialog doesn't have access to the context of the call 
   even though it's contained in the same file. Use the param element to pass data in;
   use the return element to pass data out.
-->
<form id="GetDrinkInfo">
  <!-- in param -->
  <var name="drink_id"/>

  <!-- out -->
  <var name="name"/> 
  <var name="method"/>
  <var name="ingredients"/>
  <var name="status" expr="'success'"/>

  <!-- if the CGI fails; don't crash -->
  <catch event="">
    <assign name="status" expr="failure"/>
    <return namelist="status"/>
  </catch>

  <script src="recipe.js" />
  <data name="oDrinkData" expr="'get_recipe.cgi?id=' + drink_id"/>
  <script>
  var oRecipe = new CRecipe();
  vxmllog("root node = " + oDrinkData.documentElement.nodeName);
  if (oRecipe.Init(oDrinkData))
  {
     name = oRecipe.GetName();
     method = oRecipe.GetMethod();
     ingredients = oRecipe.GetIngredients();
  }
  else
  {
     status = "failure";
  }
  </script>
  <!-- The returned variables become properties of 
       the object named by the subdialog element -->
  <block>
    <return namelist="status name method ingredients" />
  </block>
</form>

<!-- Plays the name of the drink and list of ingredients to the user -->
<form id="PlayDrink">
  <block>
    There are <value expr="drinkingr.length"/> ingredients
    in a <value expr="drinkname"/>
    <break time="300ms"/>
    <foreach item="ingr" array="drinkingr">
      <prompt><value expr="ingr"/></prompt>
      <break time="300ms"/>
    </foreach>
    <goto next="#PlayMethodQuestion"/>
  </block>
</form>

<!-- Asks the user to hear the method -->
<form id="PlayMethodQuestion">
  <property name="timeout" value="0s"/>

  <field name="more" type="boolean">
    <prompt>
      Do you wanna hear details on making this drink?
      Say yes or no.
      <audio expr="earcons_url + 'timeout.wav'"/>
    </prompt>

    <help>
      <audio expr="earcons_url + 'help.wav'"/>
      To learn how to make this drink, say yes.
      Otherwise, say no
      <audio expr="earcons_url + 'timeout.wav'"/>
      <reprompt/>
    </help>

    <noinput>
       Sorry. I didn't hear you.
      Say yes or no.
    </noinput>

    <noinput count="3">
      <goto next="#StartOver"/>
    </noinput>

    <nomatch>
      sorry, i didn't catch that.
      Say yes or no.
    </nomatch>

    <filled>
      <if cond="more">
        <goto next="#PlayMethod"/>
      <else/>
        <goto next="#StartOver"/>
      </if>
    </filled>
  </field>
</form>

<!-- play back the procedure for making the drink to the user -->
<form id="PlayMethod">
  <block>
    <prompt>
      <value expr="drinkmethod"/>
    </prompt>
    <break time="1s"/>
    <goto next="#StartOver"/>
  </block>
</form>

<!-- inform the user that the drink recipe couldn't be found -->
<form id="FailDrink">
  <block>
    <prompt>
      Oops. I forgot how to make that one.
    </prompt>

    <goto next="#StartOver"/>
  </block>
</form>

<!-- ask the user if they want the recipe for another drink -->
<form id="StartOver">
  <field name="yesno" type="boolean">

    <prompt>
      <prompt>Want the recipe for another drink? Say YES or NO.</prompt>
      <audio expr="earcons_url + 'timeout.wav'"/>
    </prompt>

    <help>
      <audio expr="earcons_url + 'help.wav'"/>
      <prompt>
        You're in Drink Recipes. Want the recipe for another drink?
        Say YES or NO.
      </prompt>
      <audio expr="earcons_url + 'timeout.wav'"/>
    </help>

    <catch event="nomatch noinput">
      Please say yes to request another recipe,
      or say no to quit.
      <audio expr="earcons_url + 'timeout.wav'"/>
    </catch>

    <filled>
      <if cond="yesno">
        <goto next="#Main"/>
      <else/>
        <goto next="#Exit"/>
      </if>
    </filled>
  </field>
</form>

<form id="Exit">
  <block>
    Bye for now.
    <exit/>
  </block>
</form>

</vxml>

The CGI script for this example follows:

#!/usr/local/bin/perl -w

# use the CGI to get the recipe id
# open the recipe 'database', 
# find the appropriate recipe, 
# and return it using the designated XML schema

use strict;
use CGI;

use vars qw(%XML_ENTITY_MAP);
%XML_ENTITY_MAP = qw(& amp < lt > gt " quot ' apos);

my $query = new CGI();
my $id_in = $query->param('id');

Run($id_in);

sub Run
{
   my($id_in) = @_;

   my $bSuccess = 0; # indicates whether or not we found recipe; default to false

   print "Content-type: text/xml\n\n";

   if (!defined($id_in))
   {
      PrintFailure("0", "0", "id param missing");
      return 0;
   }
   
   my ($id, $name, $type, $method, $ingr, $order);

   my %ingrlist = (); # this hash stores the ordered list of ingredients
   
   my $DBROOT = "./";
   my $DBFILE_NAME = "ex-108NAME.db";
   my $DBFILE_INGR = "ex-108INGR.db";
   
   # open the drink name 'table', and search for the drink with the specified id
   $! = ();
   open HNAME, "$DBROOT/$DBFILE_NAME";
   if ($! == 0)
   {
      while (<HNAME>)
      {
        if ($_ =~ /^$id_in\;/)
        {
          ($id, $name, $type, $method) = split /\;/, $_;
          chomp $method;
          last; 
        }
      }
      close HNAME;
   }
   else
   {
      PrintFailure("0", "1", $!);
      return 0;
   }

   $! = (); # clear i/o errors

   # open the ingredients 'table', and search for ingredients with the specified id
   open HINGR, "$DBROOT/$DBFILE_INGR";
   if ($! == 0)
   {
      while (<HINGR>)
      {
        if ($_ =~ /^$id_in\;/)
        {
          ($id, $order, $ingr) = split /\;/, $_;
          chomp $ingr;
          $ingrlist{$order} = $ingr;
        }
      }
      close HINGR;
   }
   else
   {
     PrintFailure("0", "2", $!);
     return 0;
   }

   # Clean up $id_in before printing as XML
   $id_in = XML_Escape($id_in);
   if ($name && $type && $method && defined $ingrlist{'1'})
   {
      $bSuccess = 1;
      PrintSuccess($id_in, $name, \$method, \%ingrlist);
   }
   else
   {
      PrintFailure($id_in, "3", "Recipe not found.");
     return 0;
   }

   $bSuccess;
}

sub PrintSuccess
{
   my($id, $name, $rMethod, $rhIngrList) = @_;

   print <<EOF1;
   <?access-control allow="*"?>
   <recipe id="$id">
      <name>$name</name>
      <method>$$rMethod</method>    
EOF1

   foreach $_ (sort keys %$rhIngrList)
   {
      print "<ingr>" . $rhIngrList->{$_} . "</ingr>\n";
   }
   print "</recipe>\n";
}

sub PrintFailure
{
   my($id, $statuscode, $statusmsg) = @_;

print <<EOF;
<recipe id="$id" statuscode="$statuscode" statusmsg="$statusmsg" />
EOF
}

sub XML_Escape
{
    local $_ = shift;
    s/[&<>"']/\&$XML_ENTITY_MAP{$&};/g;
    return $_;
}

The JavaScript code for the CRecipe class follows:

// R E C I P E . J S

/* 
The CRecipe class encapsulates a recipe
The class manipulates an XML DOM to return a recipe's name, description, and list of ingredients
The data should be formatted like this:

<recipe id="0001">
  <name>Allegheny</name>
  <method>Shake with ice and strain into cocktail glass. Add a twist of lemon peel on top.</method>
  <ingr>1 oz Bourbon</ingr>
  <ingr>1 oz Dry Vermouth</ingr>
  <ingr>1 1/2 teaspoons Blackberry-flavored Brandy</ingr>
  <ingr>1 1/2 teaspoon Lemon Juice</ingr>
</recipe>

*/
function CRecipe() {}

// Initialize the recipe object by passing in an XML DOM
CRecipe.prototype.Init = function(oData)
{
   if (oData)
   {
      this._data = oData;
      return true;
   }
   else
   {
      return false;
   }
}

// Returns the text data contained within the specified element
CRecipe.prototype.GetDataOf = function(sName)
{
   var sData = "";
   try {
     var oNodes = this._data.getElementsByTagName(sName);
     return oNodes.item(0).firstChild.data;
   }
   catch(e)
   {
     vxmllog("Exception: Unable to retrieve data for '" + sName + "' element");
   }
   
   return sData;
}

// Returns the recipe id
CRecipe.prototype.GetID = function()
{
   try {
      return this._data.documentElement.getAttribute("id");
   }
   catch(e)
   {
      vxmllog("Exception: Unable to retrieve recipe id.");      
   }
}

// Returns the recipe name
CRecipe.prototype.GetName = function()
{
   return this.GetDataOf("name");
}

// Returns the procedure for making the recipe
CRecipe.prototype.GetMethod = function()
{
   return this.GetDataOf("method");
}

// Returns the recipe ingredients as an array
CRecipe.prototype.GetIngredients = function()
{
   var aIngr = []; // array

   try {
     var oNodes = this._data.getElementsByTagName("ingr");
     for (var i = 0; i < oNodes.length; i++)
     {
       var oNode = oNodes.item(i);
       aIngr.push(oNode.firstChild.data);
     }     
   }
   catch(e)
   {
     vxmllog("Exception: Unable to retrieve ingredients.");
   }

   return aIngr;
}

[24]7 Inc.| Terms of Service| Privacy Policy| General Disclaimers