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