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