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