Download

{ This is a sample IVR application written in Delphi. Delphi IVR applications are
  implemented as DLLs exporting a function to handle a call, typically named Answer().
  This one speaks the current weather condition to a caller who has identified the
  locality whose weather he wants by entering its ZIP code on his telephone's keypad.

  Please note that while the sample may not be considered short, that has to do with what the
  sample does and not the form of its user interface. To put that another way, the entirety of
  the voice user interface code is held in the short (3 dozen executable lines) subroutine
  named Answer() which is below.

  Mostly because we don't want to be tied to a specific version of MSXML and to a lesser
  extent because we are lazy, we use Automation (IDispatch) rather than early binding to
  access the control.
}

unit DelphiWeatherApp;

interface

 uses ComObj,
      SysUtils,
      TClientCallImpl;

 function DoRequest(call : TClientCall; zip : string) : string;
 function TranslateXMLtoForecast(zip : string; xmlDoc : Variant) : string;
 function GetCity(xmlDoc : Variant) : string;
 function GetState(xmlDoc : Variant) : string;
 function GetWind(xmlDoc : Variant) : string;
 function GetOutlook(xmlDoc : Variant) : string;
 function GetTemperature(xmlDoc : Variant) : string;
 function ExpandState(abbrev : string) : string;
 function Direction(degrees : string) : string;

implementation

  { The data common to all instances of the application }

 const

  wait      : string = 'Please wait ...';
  prompt    : string = 'Please enter a five digit ZIP code or press the pound key to quit.';
  welcome   : string = 'Welcome to the weather demonstration application.';
  regrets   : string = 'We are sorry but we had trouble determining the weather for that ZIP code. ' +
                       'If the ZIP code is valid you can try again later.';
  baseQuery : string = 'http://weather.yahooapis.com/forecastrss?p=';
  nameSpace : string = 'xmlns:yweather=''http://xml.weather.yahoo.com/ns/rss/1.0''';

  { 50 states, DC and PR }

  states : array [0..51, 0..1] of string =
  (
   ('AL', 'Alabama'),
   ('AK', 'Alaska'),
   ('AZ', 'Arizona'),
   ('AR', 'Arkansas'),
   ('CA', 'California'),
   ('CO', 'Colorado'),
   ('CT', 'Connecticut'),
   ('DE', 'Delaware'),
   ('DC', 'D C'),
   ('FL', 'Florida'),
   ('GA', 'Georgia'),
   ('HI', 'Hawaii'),
   ('ID', 'Idaho'),
   ('IL', 'Illinois'),
   ('IN', 'Indiana'),
   ('IA', 'Iowa'),
   ('KS', 'Kansas'),
   ('KY', 'Kentucky'),
   ('LA', 'Louisiana'),
   ('ME', 'Maine'),
   ('MD', 'Maryland'),
   ('MA', 'Massachusetts'),
   ('MI', 'Michigan'),
   ('MN', 'Minnesota'),
   ('MS', 'Mississippi'),
   ('MO', 'Missouri'),
   ('MT', 'Montana'),
   ('NE', 'Nebraska'),
   ('NV', 'Nevada'),
   ('NH', 'New Hampshire'),
   ('NJ', 'New Jersey'),
   ('NM', 'New Mexico'),
   ('NY', 'New York'),
   ('NC', 'North Carolina'),
   ('ND', 'North Dakota'),
   ('OH', 'Ohio'),
   ('OK', 'Oklahoma'),
   ('OR', 'Oregon'),
   ('PA', 'Pennsylvania'),
   ('PR', 'Puerto Rico'),
   ('RI', 'Rhode Island'),
   ('SC', 'South Carolina'),
   ('SD', 'South Dakota'),
   ('TN', 'Tennessee'),
   ('TX', 'Texas'),
   ('UT', 'Utah'),
   ('VT', 'Vermont'),
   ('VA', 'Virginia'),
   ('WA', 'Washington'),
   ('WV', 'West Virginia'),
   ('WI', 'Wisconsin'),
   ('WY', 'Wyoming')
  );

 { This function is called once when the application is loaded. Any
   application-wide initialization steps can be taken here. If no
   initialization is required it may be removed. We show it here
   just for the sake of completeness. }

 function Initialize(shutdown : integer) : Boolean; stdcall;
 begin;

  result := true;
 end;

 { The required Answer() method always comprises the entire Voice User
  Interface for an IVR application. }

 procedure Answer(call : TClientCall); stdcall;
 var
  i        : Integer;
  c        : char;
  ok       : Boolean;
  zip      : array[0..5] of char;
  syn      : TSynthesizer;
  done     : Boolean;
  forecast : string;

 begin

  try

   { We want an exception if the user disconnects }

   call.hangupThrows(true);

   { Get an instance of the synthesizer and greet the caller
     We should check for a nil object which indicates failure }

   syn := call.getSynthesizer();
   syn.speak(welcome);
   syn.wait();

   done := false;

   while ( not call.isDisconnected() and not done ) do
   begin

    { Ask the user for a ZIP code }

    syn.speak(prompt);
    syn.wait();

    { Wait for 5 strokes or a terminator but not more than 20 seconds }

    ok := call.inputWait(5, '#', 20000);

    { If the time out elapses or the caller hangs up we are done }

    if not ok then
     done := true;

    { We have no patience if he gave us less than 5 digits }

    if call.inputAvailable() < 5 then
     done := true;

    { Build a string from the key strokes
      But if there is a '#' or '*' key in there we are done }

    for i := 0 To 4 do
    begin
     c := call.getChar();
     if (c <> '#') and (c <> '*') then
      zip[i] := c
     else
      zip[i] := #0;
    end;
    zip[5] := #0;

    { We bail out if we don't have a complete ZIP code }

    if StrLen(zip) <> 5 then
     done := true;

    if not done then
    begin

     { Tell the caller to be patient }

     syn.speak(wait);

     { Amuse him with "music on hold" }

     call.loopMessage('strumming');

     { Get his forecast while he listens to music }

     forecast := DoRequest(call, zip);

     { Stop playing music and put a delay between the music and the speech }

     call.stopPlaying();
     call.wait(500);

     { Speak the forecast }

     syn.speak(forecast);

     { Log the input and output to the call detail records table }

     call.cdrStatusMessage(0, zip);
     call.cdrStatusMessage(1, forecast);

     { Let him hear the forecast before we ask if he wants to go again }

     syn.wait();

    end;

   end;

  { Be polite and say goodbye before hanging up }

  syn.speak('Good bye.');
  syn.wait();

  except

   { Nothing serious here; caller hung up }

   on t : TClientCallTermination do
    Writeln(t.Message);

   { This might just be serious }

   on e : Exception do
    Writeln(e.Message);

  end;

 end;

 { Access a web service to retrieve the weather for the ZIP code }

 function DoRequest(call : TClientCall; zip : string) : string;
 var
   query  : string;
   xmlReq : Variant;
   xmlDoc : Variant;

 begin

  { Complete the query string and use XML HTTP to send it }

  query  := baseQuery + zip;
  xmlReq := CreateOLEObject('Msxml2.XMLHTTP');
  xmlReq.open('GET', query, false);
  xmlReq.send();

  {  Apologize if the web service did not return the weather }

  if xmlReq.Status <> 200 then
  begin
   result := regrets;

   { Purely for diagnostic purposes we display any failure code }

   Write('The weather service fails us: ');
   Write(xmlReq.statusText);
   Write(' ( ');
   Write(xmlReq.Status);
   WriteLn(' )');
  end

  { On success we need to parse the XML }

  else
   xmlDoc       := CreateOLEObject('Msxml2.DomDocument.4.0');
   xmlDoc.async := false;
   xmlDoc.loadXML( xmlReq.responseText );
   xmlDoc.setProperty('SelectionLanguage', 'XPath');
   xmlDoc.setProperty('SelectionNamespaces', nameSpace);

   result := TranslateXMLtoForecast(zip, xmlDoc);

 end;

 { Crack open the XML document and build a two sentence forecast }

 function TranslateXMLtoForecast(zip : string; xmlDoc : Variant) : string;
 var

  city        : string;
  wind        : string;
  state       : string;
  outlook     : string;
  temperature : string;

 begin

  { For now we are interested in the city and state,
    wind speed and direction, current condition and temperature }

  city        := GetCity(xmlDoc);
  state       := GetState(xmlDoc);
  wind        := GetWind(xmlDoc);
  outlook     := GetOutlook(xmlDoc);
  temperature := GetTemperature(xmlDoc);

  { Build two sentences from the four components above }

  result := 'In ' + city + ' ' + state +
            ', the weather is ' + outlook +
            ' with a temperature of ' + temperature +
            '. The winds are ' + wind + '.';
 end;

 { Pull the city from the document }

 function GetCity(xmlDoc : Variant) : string;
 var
  node : Variant;

 begin

  node   := xmlDoc.selectSingleNode('//channel/yweather:location');
  result := node.attributes.getNamedItem('city').text;

 end;

 { Pull the state from the document }

 function GetState(xmlDoc : Variant) : string;
 var
  node : Variant;
  temp : string;

 begin

  node := xmlDoc.selectSingleNode('//channel/yweather:location');
  temp := node.attributes.getNamedItem('region').text;

  result := ExpandState(temp);

 end;

 { Pull the wind speed and direction from the document }

 function GetWind(xmlDoc : Variant ) : string;
 var
  speed   : string;
  degrees : string;
  node    : Variant;

 begin

  node    := xmlDoc.selectSingleNode('//channel/yweather:wind');
  speed   := node.attributes.getNamedItem('speed').text;
  degrees := node.attributes.getNamedItem('direction').text;
  result  := 'from the ' + Direction(degrees) + ' at ' + speed + ' miles per hour';

 end;

{ Pull the current condition from the document }

 function GetOutlook(xmlDoc : Variant) : string;
 var
  node : Variant;

 begin

  node   := xmlDoc.selectSingleNode('//channel/item/yweather:condition');
  result := node.attributes.getNamedItem('text').text;

 end;

{ Pull the current temperature from the document }

 function GetTemperature(xmlDoc : Variant) : string;
 var
  node : Variant;
  temp : string;

 begin

  node   := xmlDoc.selectSingleNode('//channel/item/yweather:condition');
  temp   := node.attributes.getNamedItem('temp').text;
  result := temp + ' degrees Fahrenheit';

 end;

 { Expands a two-character abbreviation for states D.C. and PR to a full name }

 function ExpandState(abbrev : string) : string;
 var
  i : Integer;

 label found;

 begin

  result := abbrev;

  for i := 0 to 51 do
  begin

    if  abbrev = states[i, 0] then
    begin

     result := states[i, 1];
     goto found;
    end;
  end;

  found:

 end;

 { Translates a numeric wind heading to compass direction }

 function Direction(degrees : string) : string;
 var
  num : Integer;

 begin

  num := StrToInt(degrees);

  if num <= 11 then
   result := 'north'

  else if num <= 33 then
   result := 'north northeast'

  else if num <= 56 then
   result := 'northeast'

  else if num <= 78 then
   result := 'east northeast'

  else if num <= 101 then
   result := 'east'

  else if num <= 123 then
   result := 'east southeast'

  else if num <= 146 then
   result := 'southeast'

  else if num <= 168 then
   result := 'south southeast'

  else if num <= 191 then
   result := 'south'

  else if num <= 213 then
   result := 'south southwest'

  else if num <= 236 then
   result := 'southwest'

  else if num <= 258 then
   result := 'west southwest'

  else if num <= 281 then
   result := 'west'

  else if num <= 303 then
   result := 'west northwest'

  else if num <= 326 then
   result := 'northwest'

  else if num <= 348 then
   result := 'north northwest'

  else
   result := 'north';

 end;

exports

  { Export Initialize() and Answer() so that the
    server and simulator can find them }

  Initialize,
  Answer;

end.