Download

{ This is a sample IVR application written in Delphi.Net

  All .Net IVR applications are implemented as classes derived by the base class NetClientcall
  in the namespace IVRForBeginners. The class implements an application's voice user interface
  in its Answer() method. 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. }

unit DelphiNetWeatherDTMFApp;

interface

uses

  { from the Big House }

  System.IO,
  System.Net,
  System.Web,
  System.Xml,
  System.Text,
  System.Threading,
  System.Reflection,
  System.Collections,

  { from our house }

  IVRForBeginners;

var

 { The data common to all instances of the application }

 dirs          : Hashtable;
 states        : Hashtable;
 prompt        : string;
 welcome       : string;
 regrets       : string;
 patience      : string;
 baseQuery     : string;
 nameSpaceUri  : string;
 nameSpaceName : string;

type

  TDelphiWeatherDTMF = class(IVRForBeginners.NetClientCall)

  private

   zip     : array[0..5] of char;
   iar     : IAsyncResult;
   req     : WebRequest;
   resp    : HttpWebResponse;
   weather : string;

   class procedure BuildStatesTable();

   procedure RequestWeather();
   procedure CompleteRequest();
   procedure CancelRequest();
   function  GetCity(doc : XmlDocument; mgr : XmlNamespaceManager) : string;
   function  GetWind(doc : XmlDocument; mgr : XmlNamespaceManager) : string;
   function  GetOutlook(doc : XmlDocument; mgr : XmlNamespaceManager) : string;
   function  GetTemperature(doc : XmlDocument; mgr : XmlNamespaceManager) : string;
   function  Direction(num : integer) : string;

  public

   procedure Answer();
   class function Initialize() : System.Boolean; static;

  end;

implementation

 { This function is called once when the application is loaded. It opens a
   configuration file containing the user name and password for the web
   service. It would be somewhat shorter if we wanted to hardcode the
   credentials. It also builds a couple of hastables we use to translate
   abbreviations to their pronounceable equivalents. }

class function TDelphiWeatherDTMF.Initialize() : System.Boolean;
 begin;

  prompt        := 'Please enter a five digit ZIP code or press the pound key to quit.';
  welcome       := 'Welcome to the weather demonstration application.';
  regrets       := '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.';
  patience      := 'Please wait ...';
  baseQuery     := 'http://weather.yahooapis.com/forecastrss?p=';
  nameSpaceUri  := 'http://xml.weather.yahoo.com/ns/rss/1.0';
  nameSpaceName := 'yweather';

  BuildStatesTable();
  result := true;

 end;

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

 procedure TDelphiWeatherDTMF.Answer();
 var

  i        : Integer;
  c        : char;
  ok       : Boolean;
  syn      : ISynthesizer;
  done     : Boolean;
  millis   : Integer;

 begin

  try

   { We want an exception if the user disconnects }

   hangupThrows(true);

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

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

   done := false;

   { Keep going until the user gets bored }

   while not 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 := inputWait(5, '#', 20000);

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

    if not ok then
     done := true;

    { We have no patience if we got fewer than 5 digits }

    if  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 := getChar();
     if (c <> '#') and (c <> '*') then
      zip[i] := c
     else
      done := true
    end;
    zip[5] := #0;

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

    if not done then
    begin

     { Tell the caller to be patient }

     syn.speak(patience);

     { Initiate the request to the web service }

     RequestWeather();

     while not iar.IsCompleted do
     begin

      { Amuse the user with "music on hold" while we wait }

      millis := playMessage('strumming');

      { Wait for the request to complete, the music to stop,
        or for the user to hang up. When the music stops we
        just play it again but we could count the iterations
        of the loop to enforce a time out. }

      wait(iar.AsyncWaitHandle, millis);
     end;

   // When the request completes we fetch the data and build the forecast

    CompleteRequest();

    // That done we kill the music, add a bit of silence
    // and then deliver the forecast to the caller

    stopPlaying();
    wait(500);
    syn.speak(weather);

    // While speaking we add an entry to the call detail records

    cdrStatusMessage(0, zip);
    cdrStatusMessage(1, weather);

    // Then we wait for the forecast to be completely spoken

    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 : NetClientCallTermination do
   begin
    Writeln(t.Message);
    CancelRequest();
   end;

   { This might just be serious }

   on e : Exception do
   begin
    Writeln(e.Message);
    CancelRequest();
   end;

  end;

 end;

 { We use this hash table to translate state abbreviations we get from the web
   service to more easily pronounceable stuff. }

 class procedure TDelphiWeatherDTMF.BuildStatesTable();
 begin

  states := Hashtable.Create();

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

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

 procedure TDelphiWeatherDTMF.RequestWeather();
 var

  query : string;

 begin

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

  query := System.String.Create(zip);
  query := baseQuery + query;

  req := WebRequest.CreateDefault( Uri.Create(query)  );
  iar := req.BeginGetResponse(nil, nil);

 end;

 { When the asynchronous HTTP transation completes we fetch the results }

 procedure TDelphiWeatherDTMF.CompleteRequest();
 var

  doc     : XmlDocument;
  mgr     : XmlNamespaceManager;
  msg     : StringBuilder;
  city    : string;
  temp    : string;
  wind    : string;
  isOpen  : Boolean;
  outlook : string;

 begin

  doc     := nil;
  mgr     := nil;
  resp    := nil;
  isOpen  := false;
  weather := nil;

  try

   resp := HttpWebResponse ( req.EndGetResponse(iar) );

   try

    isOpen := true;

    // The response from the web service is XML so load it into a document

    doc := XmlDocument.Create();
    doc.Load( resp.GetResponseStream() );

    // Get a namespace manager and add the namespace

    mgr := XmlNamespaceManager.Create(doc.NameTable);
    mgr.AddNamespace(nameSpaceName, nameSpaceUri);

    // Then pull the city, temperature, current condition and wind speed from it

    city    := GetCity(doc, mgr);
    temp    := GetTemperature(doc, mgr);
    wind    := GetWind(doc, mgr);
    outlook := GetOutlook(doc, mgr);

    // We take those bits of information to build a two sentence forecast

    msg := StringBuilder.Create();
    msg.Append('In ');
    msg.Append(city);
    msg.Append(', the weather is ');
    msg.Append(outlook);
    msg.Append(' with a temperature of ');
    msg.Append(temp);
    msg.Append('. The winds are ');
    msg.Append(wind);
    msg.Append('.');

    weather := msg.ToString();

   except

    on ioe : InvalidOperationException do
     isOpen := false;

    on e : Exception do
     Console.WriteLine(e.Message);

   end;

  // Make sure we close the connection when we are done

  finally

   if isOpen then
    resp.Close()

  end;

  // If we failed to build a forecast we'll offer an apology instead

  if  weather = nil then
   weather := regrets;

 end;

 { We will cancel the outstanding web service request if the user hangs
   up of if an exception occurs }

 procedure TDelphiWeatherDTMF.CancelRequest();
 begin

 try

   if  req <> nil then
   begin

    req.Abort();
    CompleteRequest();

   end;

  except

   on e : Exception do
   Console.WriteLine( e.Message );

  end;

 end;

 { Pull the city from the document }

 function TDelphiWeatherDTMF.GetCity(doc : XmlDocument; mgr : XmlNamespaceManager) : string;
 var

  city  : string;
  temp1 : string;
  temp2 : string;
  node  : XmlNode;

 begin

  // The city and the state reside in the same node ...

  node := doc.SelectSingleNode('//channel/yweather:location', mgr);

  // ... as distinct attributes

  temp1 := node.Attributes.GetNamedItem('city').InnerText;
  temp2 := node.Attributes.GetNamedItem('region').InnerText;

  // Expand the state abbreviation if we know it ...

  if ( states.Contains(temp2) ) then
   city := temp1 + ' ' + states.Item[temp2].ToString()

  // ... or take it as is if we don't

  else
   city := temp1 + ' ' + temp2;

  result := city;

 end;

 { Pull the wind speed and direction from the document }

 function TDelphiWeatherDTMF.GetWind(doc : XmlDocument; mgr : XmlNamespaceManager) : string;
 var

  node    : XmlNode;
  temp1   : string;
  temp2   : string;
  degrees : Integer;

 begin

  node  := doc.SelectSingleNode('//channel/yweather:wind', mgr);
  temp1 := node.Attributes.GetNamedItem('direction').InnerText;
  temp2 := node.Attributes.GetNamedItem('speed').InnerText;

  degrees := System.Convert.ToInt32(temp1);
  result  := 'from the ' + Direction(degrees) + ' at ' + temp2 + ' miles per hour';

 end;

{ Pull the current condition from the document }

function TDelphiWeatherDTMF.GetOutlook(doc : XmlDocument; mgr : XmlNamespaceManager) : string;
var

 node : XmlNode;

begin

  node   := doc.SelectSingleNode('//channel/item/yweather:condition', mgr);
  result :=  node.Attributes.GetNamedItem('text').InnerText;

end;

{ Pull the current temperature from the document }

 function TDelphiWeatherDTMF.GetTemperature(doc : XmlDocument; mgr : XmlNamespaceManager) : string;
 var

  temp : string;
  node : XmlNode;

 begin

  node   := doc.SelectSingleNode('//channel/item/yweather:condition', mgr);
  temp   := node.Attributes.GetNamedItem('temp').InnerText;
  result := temp + ' degrees Fahrenheit';

 end;

 { Translates a numeric wind heading to compass direction }

 function TDelphiWeatherDTMF.Direction(num : integer) : string;
 begin

  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;

end.