Download

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

  All .Net IVR applications are implemented as classes dreived 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 speaking the digits of its ZIP code.

  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 DelphiNetWeatherSRApp;

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;
 sayWhat       : string;
 welcome       : string;
 regrets       : string;
 patience      : string;
 baseQuery     : string;
 nameSpaceUri  : string;
 nameSpaceName : string;

const

 { We use oddball numbers to make the point that grammar IDs are arbitrary }

 ZipGrammarId   = 22;
 QuitGrammarId  = 67;

 { In this application we use 65% as a confidence threshold for recognition.
   You can use whatever makes sense in your application. A more robust
   application might have two thresholds. Confidence scores below the lower
   number might cause the application to prompt again, confidence scores
   above the higher number might cause the application to accept the
   recognition, and scores between the two numbers might require confirmation }

 ReqdConfidence = 6500;

 { Each digit is a property of the whole 'phrase' to be recognized }

 properties : array[0..4] of string =
 (
  'Digit1',
  'Digit2',
  'Digit3',
  'Digit4',
  'Digit5'
 );

type

  TDelphiWeatherSR = class(IVRForBeginners.NetClientCall)

  private

   zip     : string;
   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;
   procedure NoRecognition(rec : IRecognizer; syn : ISynthesizer);

  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 equivakents. }

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

  prompt        := 'Please tell me the ZIP code for the town whose weather forecast you want to hear.';
  welcome       := 'Welcome to the weather demonstration application.';
  sayWhat       := 'I''m sorry, but I didn''t get that.';
  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 TDelphiWeatherSR.Answer();
 var
  i      : Integer;
  rec    : IRecognizer;
  syn    : ISynthesizer;
  conf   : Integer;
  done   : Boolean;
  millis : Integer;

 begin

  try

   { We want an exception if the user disconnects }

   hangupThrows(true);

   { Get an instance of the recognizer
     Load a grammar that handles the request for the ZIP code
     Load another that handles the request to quit the application }

   rec := getRecognizer();
   rec.loadGrammar('ZIP',  ZipGrammarId);
   rec.loadGrammar('Quit', QuitGrammarId);

   { 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;

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

    { Start listening first and then prompt the caller }

    rec.listen();
    syn.speak(prompt);

    { Wait for the recognizer to tell us how it did.
      We will wait at most 10 seconds for the user to begin to speak.
      In no case will we wait more than 30 seconds. }

    conf := rec.wait(10000, 30000);

    { Stop listening }

    rec.stopListening();

    { Dump the state of the recognition for debugging }

    rec.dump();

    { On an error or disconnect we are done }

    if conf < 0  then
     done := true;

    { We need at least minimum confidence to proceed }

    if ( conf >= ReqdConfidence ) then
    begin

     { If we recognized a phrase in Quit grammar then we are done }

     if rec.getGrammarId() = QuitGrammarId then
      done := true

     { Otherwise we need a five digit ZIP code }

     else begin
      zip := '';

      for i := 0 to 4 do
       zip := zip + rec.getPropertyText(  properties[i] );

      if Length(zip) <> 5 then
       conf := 0;
     end;

    end;

    { Apologize if we didn't understand }

    if  (not done) and (conf < ReqdConfidence) then
     NoRecognition(rec, syn)

    { If all is well, we proceed }

    else 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 apologize if we don't understand the user }

 procedure  TDelphiWeatherSR.NoRecognition(rec : IRecognizer; syn : ISynthesizer);
 begin
  rec.reset();

  syn.speak(sayWhat);
  syn.wait();
 end;

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

 class procedure TDelphiWeatherSR.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 TDelphiWeatherSR.RequestWeather();
 begin

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

  req := WebRequest.CreateDefault( Uri.Create(baseQuery + zip)  );
  iar := req.BeginGetResponse(nil, nil);

 end;

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

 procedure TDelphiWeatherSR.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 TDelphiWeatherSR.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 TDelphiWeatherSR.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 TDelphiWeatherSR.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 TDelphiWeatherSR.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 TDelphiWeatherSR.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 TDelphiWeatherSR.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.