- language:ruby - sdk:compatibility - product:voice This application demonstrates how easy it is to place a call, accepting both DTMF and text input, and using SignalWires advanced TTS capabilities to speak dates and times in the correct way. If the user changes their appointment to one of the slots we offer, we will also send them a reminder SMS. What do I need to run this application? Find the full code on [Github](https://github.com/signalwire/guides/tree/main/Voice/Make%20Appointment%20Reminder%20Calls%20with%20Ruby) You will also need some additional packages: [SignalWire Ruby SDK](/compatibility-api/sdks) [Sinatra](https://github.com/sinatra/sinatra) for quickly creating web applications in Ruby [Dotenv](https://github.com/bkeepers/dotenv) for managing our environment variables As well as a SignalWire account which you can create [here](https://m.signalwire.com/signups/new?s=1), and your SignalWire Space credentials which can be found in the API tab of your SignalWire Space. For more information on navigating your Space check out this [guide](/guides/navigating-your-space). Running the Application -- ### Configure your .env file Start by copying the env.example file to a file named .env, and fill in the necessary information. The application needs a SignalWire API token. You can sign up [here](https://signalwire.com/signup), then put the Project ID and Token in the .env file as SIGNALWIRE_PROJECT_KEY and SIGNALWIRE_PROJECT_KEY, together with your full SignalWire Space URL as SIGNALWIRE_SPACE. You will also need to set the APP_DOMAIN to your server URL or SSH tunnel youll use to access the code publicly. :::info You may need to use a SSH tunnel for testing this code if running on your local machine. – we recommend [ngrok](https://ngrok.com/). You can learn more about how to use ngrok [here](/guides/how-to-test-webhooks-with-ngrok). After starting the tunnel, you can use the URL you receive from ngrok in the .env APP_DOMAIN variable. ::: ### Configure your json file Then, copy the config.json.example file to config.json and change **at least** the phone number in the reminder block. You can also edit other values if you would like to test with multiple customers to remind or different names. ### Run the application natively If you are running the application with Ruby on your computer, run bundle install followed by bundle exec ruby app.rb after configuring the .env and config.json files. ### Build and Run on Docker To use the bundled Docker configuration, set up your .env and config.json, build the container using docker build . -t rubyreminders then run the application with docker run -it --rm -p 4567:4567 --name mfaruby --env-file .env rubyreminders. :::info After starting the process with either of the two methods, head to http://localhost:4567 and click on Confirm. ::: Step by Step Code Walkthrough -- In the [Github Repo](https://github.com/signalwire/signalwire-guides/tree/master/code/ruby_laml_reminders) you will find several files, but we are primarily interested in the app.rb, config.json.example, and env.example files. Additionally there are the index.erb and layout.erb in the views folder which will be used by Sinatra. ### The config.json file The JSON file below contains two arrays of objects, reminders and available. The reminders array contains information about the appointment and the to number of the customer to call and remind. The available array contains other possible date/time pairs that could be used for customers to reschedule their appointments. Although this JSON file is quite simple, it contains most of the data needed for our app.rb file. json { reminders: [{ name: Mr. Smith, to: +1214xxxxxxx, date: 05/10/2021, time: 10:00AM }], available : [{ date: 05/10/2021, time: 11:15AM }, { date: 06/10/2021, time: 2:30PM }] } ### app.rb #### Create Call and Message Functions To begin with, we must create two functions using the [Create Call API](/rest/compatibility-api/endpoints/create-a-call) to place a call to our customer and the [Create Message API](/rest/compatibility-api/endpoints/create-message) to send a message with the reminder in it. For our place_call() function, we can get the from number and the url from the ENV file, and we will append /reminder to the url so that we can direct it to the proper route that we will define further down. The to number will be passed through as a parameter and we will get it from the **config.json** file. ruby def place_call(to_number) client = Signalwire::REST::Client.new ENV[SIGNALWIRE_PROJECT_KEY], ENV[SIGNALWIRE_TOKEN], signalwire_space_url: ENV[SIGNALWIRE_SPACE] call = client.calls.create( url: ENV[APP_DOMAIN] + /reminder, to: to_number, from: ENV[FROM_NUMBER] ) end For our send_reminder() function, we will get the from number from the ENV file. The to number will be passed through as a parameter along with the body of the message. ruby def send_reminder(text, to_number) client = Signalwire::REST::Client.new ENV[SIGNALWIRE_PROJECT_KEY], ENV[SIGNALWIRE_TOKEN], signalwire_space_url: ENV[SIGNALWIRE_SPACE] msg = client.messages.create( to: to_number, from: ENV[FROM_NUMBER], body: text ) end #### Date-Time function The last function we need to set up before moving forward is needed to handle reading dates/times that are in a computer-friendly format in a more human-friendly way. In this function, we will take the object, date, and time as parameters. We will use the object to compose a sentence where the date is read out loud instead of in a timestamp format. ruby def say_date_and_time(say_object, date, time) say_object.say_as(date, interpretAs: date, format: mdy) say_object.add_text( at ) say_object.say_as(time, interpretAs: time, format: hms12) end ### Sinatra Routes Great! Now that we have defined all of our necessary functions, we can move on to the three Sinatra routes we will be using. #### Default Our first route is the default, / , and it will store all of the reminders in the config.json file into a variable. Using the index.erb file in the GitHub repo, we will populate the data into a simple front-end display. ruby get / do @reminders = config[reminders] erb :index end #### /Call When you click **Confirm** on the page above, the code in **index.erb** redirects to our next route, /call. In this route, we will grab the first object in the reminder array and use the to number to pass through as a parameter in our place_call() function. ruby get /call do reminder = config[reminders].first place_call(reminder[to]) redirect / end #### /Reminder Next, we have our /reminder route. As with any route involving gathering input from the customer, the first thing we need to do is check to see if any digits were pressed or speech results were gathered. Based on the input of the caller, we will either confirm the appointment and hang up or redirect to our route /choose which will take care of changing the appointment. If no speech result/digits pressed are passed through as parameters, we will assume this is the first time that we are speaking to the customer and play a greeting first. We will then use our say_date_and_time() function to read out the appointment time for the customer. We will ask the callee to press 1 to confirm or 2 to reschedule and wait for a speech result or digit to be pressed. ruby post /reminder do puts params # set reminder to first element in reminders array from config file reminder = config[reminders].first # instantiate voice response response = Signalwire::Sdk::VoiceResponse.new # check for digits or speech result passed through as http request parameters # if speech/digit is 1, confirm appointment and hang up. # if speech/digit is 2, redirect to choose route # otherwise retry to read out appt and gather confirmation again if params[Digits] || params[SpeechResult] if params[Digits] == 1 || params[SpeechResult].match?(/yes/) response.say(message: Thank you for confirming your appointment. Goodbye!) response.hangup elsif params[Digits] == 2 || params[SpeechResult].match?(/no/) response.redirect(/choose) else # we didnt understand response.say(message: Sorry, I could not understand you.) response.redirect(/reminder?retry=1) end else # read out hello message to the appointment holder, unless it is a retry in which case well repeat the main message only response.say(message: Hello, #{reminder[name]}) unless params[retry] # read out appointment time by calling our say date and time functiona bove with the date and time from the reminder array response.say(message: We are calling you from the dental clinic to remind you about your appointment on ) do |say| say_date_and_time(say, reminder[date], reminder[time]) end # gather input, 1 to confirm, 2 to deny response.gather(input: speech dtmf, timeout: 5, num_digits: 1) do |gather| message = Do you confirm that date and time? message += Press 1 or say yes to confirm, or press 2 or say no to choose another option. if params[retry] == 1 gather.say(message: message) end end # convert response to string as response must return proper XML response.to_s end #### /Choose In our final route, we will handle changing an appointment if a customer would like to reschedule. If we have detected digits or speech, we will set picked to the correct date based on what the customer input via speech or dtmf. Once the caller has chosen their new date, we will play a short message to read out the new date and then send a reminder text for good measure. If no speech or digits are detected, we will assume its the first time that weve accessed this route and play the caller a list of all of their possible appointment date/time options. We will then use to get their input and handle it accordingly. ruby post /choose do # set reminder to first element in reminders array from config file reminder = config[reminders].first # instantiate voice response response = Signalwire::Sdk::VoiceResponse.new # set up an array with first word string and second word string as parameters words = %w{first second} # handles sending reminder text for new appointment if appointment is changed if params[Digits] || params[SpeechResult] picked = nil # based on parameters passed, assign one of the available dates to the variable picked if params[Digits] == 1 || params[SpeechResult].match?(/first/) picked = config[available][0] elsif params[Digits] == 2 || params[SpeechResult].match?(/second/) picked = config[available][1] end # if picked was assigned to something, play thank you message and call our say date and time function again to make the format more understandable for the caller if picked response.say(message: Thank you! Your new appointment is on ) do |say| say_date_and_time(say, picked[date], picked[time]) end # call our send reminder function to send a message with the date and time passed through in the body parameter send_reminder(Your appointment at the Dental Clinic is on #{picked[date]} at #{picked[time]}., reminder[to]) else # if we didnt understand response.say(message: Sorry, lets try again.) response.redirect(/choose) end # if no input has been detected, assume caller has not heard prompt and offer to make an appointment else response.say(message: Lets pick another appointment for you. ) # set up to gather input via dtmf or speech response.gather(input: speech dtmf, timeout: 5, num_digits: 1) do |gather| # grab available dates from config.jason file and loop through them offering possible dates/times gather.say do |say| config[available].each_with_index do |slot, i| say.add_text(press #{i + 1} or say #{words[i]} for) say_date_and_time(say, slot[date], slot[time]) end end end end # convert response to string as response must return proper XML response.to_s end Wrap Up - This guide shows how easy it can be to create a fully functioning call reminder system in Ruby with the SignalWire Ruby SDK and Sinatra. By the end of this guide you will hopefully understand how SignalWires API can be used to send outbound calls and sms, as well as use text-to-speech to prompt the user for dtmf and speech input. ### Resources [Github](https://github.com/signalwire/guides/tree/main/Voice/Make%20Appointment%20Reminder%20Calls%20with%20Ruby) [SignalWire Ruby SDK](/compatibility-api/sdks) [Sinatra](https://github.com/sinatra/sinatra) for quickly creating web applications in Ruby [Dotenv](https://github.com/bkeepers/dotenv) for managing our environment variables [Ngrok](https://ngrok.com/) Sign Up Here If you would like to test this example out, [create a SignalWire account and Space](https://m.signalwire.com/signups/new?s=1). Please feel free to reach out to us on our [Community Slack](http://signalwire.community/) or create a Support ticket if you need guidance!