Posts Tagged ‘guest post’

Building Tropo apps with PHP and Limonade

Tuesday, June 29th, 2010

This is a guest post from Mark Headd, introducing a PHP library for the Tropo WebAPI.

This post is a continuation of the series on building cloud communication applications with Tropo and the PHP WebAPI Library.

In this post, we’ll be looking at Tropo’s support for multi-channel applications and using the incredibly flexible and powerful Limonade library for PHP (think Sinatra for PHP).

Working with the Session Object

As I explained very briefly in the previous post on this subject, the Tropo WebAPI is an HTTP/JSON API for building multi-channel communication apps.

What this means essentially is that the Tropo platform does all of the hard stuff involved with executing a communication app – DTMF/speech recognition, rendering Text-To-Speech (TTS), maintaining and managing all of the connections to the different communication networks (PSTN, SMS, IM networks, Twitter). You tell Tropo how to govern the interaction between a caller and your application on a specific channel by sending it a set of instructions in JSON format.

In this series of posts, we’re using the PHP WebAPI Library for Tropo to generate the JSON that gets sent to, and consumed by Tropo. But this exchange of JSON isn’t one-way – Tropo also sends JSON packages to your application with important information about (among other things) the network a user selects to interact with your application on and any input they have provided in response to prompts.

At the beginning of a user session (when a user first connects to your application), Tropo will deliver a JSON Session object to your application. This object contains all sorts of useful information that your app can use when rendering out JSON instructions to send back to Tropo. Let’s examine what a real life Session object looks like.

The easiest way to do this is to simply go over to PostBin.org and make a new PostBin. PostBin is a service that lets you see HTTP posts that get sent to the special URL that is generated when you create a new PostBin.

After you have created a new PostBin, log into your Tropo account and create a new WebAPI application. Use the PostBin URL as the URL that powers your new Tropo WebAPI app. After your app is created, you will have a newly provisioned Skype number that you can use to call it.

When you call your application using the Skype number provisioned by Tropo, you won’t hear anything – remember, we haven’t yet generated any JSON to tell the Tropo platform what to say or do when a user connects. After you make your call (it will be over quickly), go back to your PostBin URL (you may need to refresh) and you will see an object in JSON format, like this:

{
  "session": {
    "accountId": "9178", 
    "callId": "33e6f280ec9e740ad57ec3464175e502", 
    "from": {
      "channel": "VOICE", 
      "id": "mheadd", 
      "name": "unknown", 
      "network": "PSTN"
    }, 
    "headers": {
      "CSeq": "2 INVITE", 
      "Call-ID": "0-13c4-4c162a69-77e521a6-72fc-1d004d38", 
      "Contact": "<sip:mheadd@10.90.63.103:5060>", 
      "Content-Length": "249", 
      "Content-Type": "application\/sdp", 
      "From": "<sip:mheadd@10.90.63.103:5060>;tag=0-13c4-4c162a69-77e521a6-5ba", 
      "To": "<sip:9991443038@10.90.61.101:5060>", 
      "Via": "SIP\/2.0\/UDP 67.28.127.103:5060;received=10.90.63.103", 
      "x-sbc-allow": "BYE", 
      "x-sbc-call-id": "MTJjZTIzNzBkMDI1MzJhMmJlNjQ1YWQ0NmNmODZkMmM.", 
      "x-sbc-contact": "<sip:0000123456@192.168.14.61:16000>", 
      "x-sbc-cseq": "1 INVITE", 
      "x-sbc-from": "\"mheadd\"<sip:0000123456@192.168.14.61>;tag=93658f52", 
      "x-sbc-max-forwards": "70", 
      "x-sbc-record-route": "<sip:80.252.85.73:5061;r2=on;lr;ftag=93658f52>", 
      "x-sbc-request-uri": "sip:990009369991443038@67.28.127.103:5060", 
      "x-sbc-to": "<sip:990009369991443038@67.28.127.103:5060>", 
      "x-sbc-user-agent": "sipgw-1.0", 
      "x-sid": "cc6499d5ec479c60633f7ad2b6a4df15", 
      "x-voxeo-sbc": "true", 
      "x-voxeo-sbc-name": "10.90.63.103", 
      "x-voxeo-sbc-session-id": "cc6499d5ec479c60633f7ad2b6a4df15", 
      "x-voxeo-to": "<sip:990009369991443038@67.28.127.103:5060>"
    }, 
    "id": "6e15c4ecadf33b72c9e0ba52f707b18c", 
    "initialText": null, 
    "timestamp": "2010-06-14T13:11:04.977Z", 
    "to": {
      "channel": "VOICE", 
      "id": "9991443038", 
      "name": "unknown", 
      "network": "PSTN"
    }, 
    "userType": "HUMAN"
  }
}

This is the Session object for the call you just made. It’s what is sent to your application (via HTTP POST) each time a new session is started on Tropo. Working with this object using the PHP WebAPI Library is easy. You just create a new instance of the Session object in PHP and you can start accessing the properties of this object:

$session = new Session();
$from_info = $session->getFrom();
echo $from_info['channel'];
// Using the example Session object JSON from above would render VOICE.

Being able to access the channel and network a user is accessing your application from can be useful when you want to tailor prompts or actions to a specific channel – e.g., a phone call vs. an IM session.

Also make note of the initialText property – this will be important when building SMS and IM applications, where a user will begin an interaction with your application by sending information to it. This property will allow you to process the initial input for those channels without having to ask the user for it again (something users generally dislike).

Next, let’s take a look a the Result object that is sent from Tropo to your application when a user provides input in response to a prompt or direction. In order to do this, we need to take a sip of Limonade.

Mmmm… Limonade!

Limonade is a lightweight PHP framework that is very much like the Sinatra framework for Ruby. I won’t go into too much detail on it, as there is ample documentation available on the Limonade site , but here is quick introduction that will let us build enough of a structure to see the Tropo result object.

When you use Limonade, you set up routes for HTTP requests. A route is comprised of an HTTP method, a URL matching pattern and a PHP method. When an HTTP request is made to a URL that matches the pattern, and uses the method specified in the route, the designated PHP function gets invoked. For example:

dispatch_post('/', 'test');
  function test() {
    echo 'This is a test.';
}

The ‘dispatch_post()’ directive specifies that the HTTP method for this route with be POST (which is what is used by Tropo to send JSON to your application). The two parameters to this directive specify the URL pattern to match (in this case, the root directory on the domain were this script is located) and the PHP method to invoke, which is defined below this directive. In a nutshell, whenever an HTTP POST is made to the root domain where this script is located, the text This is a test will be rendered.

Let’s build out a simple shell that we’ll use to construct our Tropo application for the next few posts in this series:

// Include Tropo classes.
require('TropoClasses.php');

// Include Limonade framework (http://www.limonade-php.net/).
require('path/to/limonade/lib/limonade.php');

dispatch_post('/start', 'zip_start');
function zip_start() {
	// Tell the user to enter their zip code.
}

dispatch_post('/end', 'zip_end');
function zip_end() {
	// Do something with the entered zip code.
}

dispatch_post('/error', 'zip_error');
function zip_error() {
	// Tell the user an error has occurred.
}

// Run this sucker!
run();

Our Tropo application will collect a user’s zip code and then look up some information based on the input they provide. As you can see, we’ve included the PHP WebAPI Library and the Limonade Framework. We’ve also set up three Limonade routes start, end and error (all using the HTTP POST method) and stubbed out the PHP function that will render JSON for Tropo to consume.

To get a look at the Tropo Result object, lets add some logic to the zip_start() function:

dispatch_post('/start', 'zip_start');
function zip_start() {

	// Step 1. Create a new instance of the Session object, and get the channel information.
	$session = new Session();
	$from_info = $session->getFrom();
	$network = $from_info['channel'];	

       // Step 2. Create a new instance of the Tropo object.
	$tropo = new Tropo();

	// Step 3. Welcome prompt.
	$tropo->say("Welcome to the Tropo PHP zip code example for $network");

	// Step 4. Set up options for zip code input.
	$options = array("attempts" => 3, "bargein" => true, "choices" => "[5 DIGITS]", "name" => "zip", "timeout" => 5);

	// Step 5. Ask the caller for input, pass in options.
	$tropo->ask("Please enter your 5 digit zip code.", $options);

	// Step 6. Tell Tropo what to do when the user has entered input. Enter your PostBin URL in the "next" array element.
	$tropo->on(array("event" => "continue", "next" => "http://www.PostBin.org/xxxxxxx", "say" => "Please hold."));

	// Step 7. Render the JSON for the Tropo WebAPI to consume.
	return $tropo->RenderJson();

}

As you can see, inside this function we create a new instance of the Session object and get the channel the user is accessing our application from. We also create a new instance of the Tropo object (this is what we’ll use to send JSON instructions back to the Tropo platform).

The next several steps are fairly self explanatory, but take special note of Step 6. Here we are telling the Tropo platform that when a ‘continue’ event is raised (when a user finishes entering input) tell them to ‘Please hold’ and then POST the results of their input to a PostBin URL. (Note – replace the value above with the PostBin URL you used at the beginning of this tutorial.)

Working with the Result Object

Save your script and change the URL for your WebAPI application in the Tropo Applications manager to point to it. You can now test your script using the the Skype number for your app as we did before . When you access your script, you’ll get the instructions to enter a zip code, after which Tropo will POST the results to the PostBin URL you inserted into the script in Step 6 above.

Now, when you look at your PostBin URL, you’ll see something like this:

{
  "result": {
    "actions": {
      "attempts": 1, 
      "confidence": 100, 
      "disposition": "SUCCESS", 
      "interpretation": "12345", 
      "name": "zip", 
      "utterance": "1 2 3 4 5", 
      "value": "12345", 
      "xml": "<?xml version=\"1.0\"?>\r\n<result grammar=\"0@9ea5756d.vxmlgrammar\">\r\n    <interpretation grammar=\"0@9ea5756d.vxmlgrammar\" confidence=\"100\">\r\n        \r\n      <input mode=\"dtmf\">dtmf-1 dtmf-2 dtmf-3 dtmf-4 dtmf-5<\/input>\r\n    <\/interpretation>\r\n<\/result>\r\n"
    }, 
    "callId": "6d620e133c2f7f265b4b34f94438fd95", 
    "complete": true, 
    "error": null, 
    "sequence": 1, 
    "sessionDuration": 10, 
    "sessionId": "bc73448b723c8c5dc4498d1a322851ec", 
    "state": "ANSWERED"
  }
}

As you can see, the Result object that gets sent from Tropo to your app has a wealth of information on what the user entered, how it was interpreted by Tropo and even the confidence level of the recognition (if speech recognition is used).

You can access the Result object using the PHP WebAPI Library just like you can the Session object:

$result = new Result();<br />

$zip = $result->getValue();<br />
echo $zip
// Using the example Result object JSON from above would render 12345<br />

You would use the Result object in the zip_end() function we stubbed out above. You use the value of the zip code entered to look up information relevant for that zip code (like a weather forecast) and present it to the caller. In the next post in this series, we’ll complete our simple zip code example by adding a weather forecast lookup and present it to the user. We’ll also tweak our script to optimize it for different channels that a user might employ to access it, to ensure the experience is optimized for phone, IM and SMS.

Stay tuned…

About the author: Mark J. Headd is an experienced voice, mobile and web application developer who has been certified as a VoiceXML Application Developer by the VoiceXML Forum. He currently works as a Senior Application developer for Tele-Works, Inc., which develops IVR solutions and software for local government across the country. This post originally appeared on Mark’s blog.

A PHP library for Tropo’s Web API

Friday, June 25th, 2010

This is a guest post from Mark Headd, introducing a PHP library for the Tropo WebAPI.

A few months back, I wrote a series of posts on building NoSQL telephony applications with Tropo and CouchDB. Today I’m going to start a continuation of that series, focusing on how to build cutting edge cloud communications apps with the Tropo WebAPI.

What is the Tropo WebAPI?

The Tropo WebAPI is, in a nutshell, an HTTP/JSON API for building multi-channel communication applications – applications that you interact with via phone, IM, SMS or Twitter. While my earlier series on Tropo focused on building applications in Tropo’s scripting environment (another fine option for developers), this series will focus on building JSON-based applications (generated using PHP) that can be hosted anywhere and executed in the Tropo cloud environment.

The Tropo service is truly multi-channel – using the Tropo WebAPI you can build applications that work on a range of different communication channels, not just phones (although you can build some pretty slamming phone apps as well).

Since I’m a phone app developer at heart, some of the features that Tropo provides for phone applications really get me excited. Tropo supports both DTMF entry and speech recognition. It also has broad multilingual support. In addition, Tropo gives phone application developers the ability to manipulate SIP headers, an important feature in building sophisticated cloud communication apps that I hope to demonstrate down the road a bit.

Getting Started

Head on over to Tropo.com and set up a new account (if you don’t have one already). Take a little time to review the documentation for the Tropo WebAPI. For the example applications in this series of blog posts I’ll be using a PHP class library I developed specifically to interact with the Tropo WebAPI.

The crew behind Tropo have provided a Ruby Gem for interacting with the Tropo WebAPI. However, since I like to do my cloud telephony work with PHP I decided to write my own set of classes for doing this. Whether you’re a Ruby-head or a PHP enthusiast, using one of these tools to generate JSON for consumption by the Tropo WebAPI can make build an application significantly easier, particularly as you get into more sophisticated application development.

You can get my PHP Library, as well as some of the sample apps we’ll be looking at, from GitHub:

$ git clone git@github.com:mheadd/TropoPHP.git

Or download a Zip file with the library.

You’ll need to host these classes and the PHP scripts you write with them on a server that can be accessed from the Tropo environment. Any web server that supports PHP will do.

My First Tropo WebAPI Application

Let’s start with the standard Hello World app:

<?php

// Include Tropo classes.
require('path/to/TropoClasses.php');

// Create a new instance of the Tropo object.
$tropo = new Tropo();

// Add a prompt to the object using the Say() method.
$tropo->Say("Hello World!");

// Render the JSON for the Tropo WebAPI to consume.
$tropo->RenderJson();

?>

You can look at the rendered JSON in your browser, and you should see something like this:

{
    "tropo": [
        {
            "say": [
                {
                    "value": "Hello World!"
                }
            ]
        }
    ]
}

Go to the Applications section in your Tropo account and set up a new WebAPI application that points to the location of this script.

When you create your application, Tropo will automatically provision a Skype number, a SIP number and an iNum. You can additionally add a PSTN number in a range of different area codes at no charge.

You may also notice the section below the provisioned phone numbers entitled “Instant Messaging Networks” – this section allows you to set up any number of different IM accounts (and Twitter!) that your application can use. We’ll dive deeper into this in future posts.

For now, we’ll keep it simple and use the auto provisioned Skype number – when you call this number, you will hear it say “Hello World.”

The next post in this series will focus on a more sophisticated application that uses the TropoPHP classes and the utterly awesome Limonade PHP framework.

Stay tuned…

About the author: Mark J. Headd is an experienced voice, mobile and web application developer who has been certified as a VoiceXML Application Developer by the VoiceXML Forum. He currently works as a Senior Application developer for Tele-Works, Inc., which develops IVR solutions and software for local government across the country. This post originally appeared on Mark’s blog.

Twitter mashups with Tropo

Wednesday, January 27th, 2010

This is a guest post by Mark Silverberg (@skram on Twitter). Mark acted as a guinea pig for the Tropo Web API and Ruby library in the weeks leading up to its release last week. He wrote a sample app mashing up Twitter and voice and provided this overview and code walk through.

With Tropo’s newly released web API, developers can create unified communications (telephone, cellular text message, instant message (Jabber, AIM, Yahoo, MSN) applications using traditional document-based web development flow. The code gets stored on the developer’s choice of host and is referenced each time a call comes in (or is initiated using the Session API for outgoing phone calls). This additional approach to creating voice and IM-enabled applications allows developers to integrate Tropo code into their existing and new applications seamlessly, using their own hosting and servers. (The existing, hosted Tropo is still available.)

Here’s a twitter-by-phone example that uses the standard Twitter RSS feed for content, Sinatra as a web daemon (you could use rails or something else too) and the Tropo web (JSON) API. Since Tropo code is stored and run on the server of your choice, you can use any gems, custom classes, etc. to connect your data and content with phones and chat clients globally and on your own terms.

Instead of being a flat-file script with loops to validate input and drive the call flow with blocks of scripting logic, the JSON API with this shiny ruby gem allows for a document-based approach – like traditional web development. You can think of http://example.com/index.json, the URL you set as your Tropo App URL, as your website’s landing page. It is the first page/document ‘hit’ when a user calls your application.

tweet flow chart

Once the instructions in that document are finished, one of two Tropo API events are triggered (more about the code for this below): continue or incomplete. The events are triggered after the requested input is received (or not received). Example: If we ask for someone’s zip code, as soon as they say it, we continue to the specified next document. It’s as if, by saying, entering, or texting 5 numbers, the user has pressed the “Submit” button and they continue through your app to hear the weather, traffic conditions, tweets in proximity, what-have-you.

Making it all work together

Note, you could even integrate this right into your existing Sinatra/Rails/etc. web app. Just add a responds_to or document view for index.json, and put your Tropo code right in there. In the context of this Twitter example, I could have index.html be a HTML form asking for how many tweets the user would like to see at once, process.html be an intermediary view, and then page_of_tweets.html be, like it is in our voice application, a page-view of the number of tweets they requested. Your HTML page would have a “next” button for page, unlike the code published here which automatically reads the next page.

Before diving into the code (the version described below is in full here or the latest copy is available on GitHub), you may want to try this out for yourself. Call the @voxeo tweets by phone line at (253) 217-4272 or with Skype at +99000936 9991429531 for a demo. You can also create a Tropo app and point it to wherever you have hosted this web service. We deployed on Heroku for our demo, but you can use any Ruby web hosting you like.

As you’ll see, this code sample presents some number of tweets per ‘page’, defined by the caller. It will continue on to infinity, or however many tweets are available. As you step through the code, it may also be helpful to take a look at the more basic examples included with the Ruby library.

The call flow for this web service is quite simple. A call comes in and index.json is rendered.

post '/index.json' do

Sessions and Variables (twitter.rb, lines 5-9)

v = Tropo::Generator.parse request.env["rack.input"].read
session[:id] = v[:session][:id]
session[:caller] = v[:session][:from]
session[:user] = "voxeo"
session[:page] = 1  # This shouldn't be tinkered with unless you don't want the most recent tweets to be heard.

At one fell swoop, this web route collects the information Tropo sent the web service (like session ID, caller information, and timestamp) and stores relevant information into a session object Sinatra provides us for variable storage which will last the duration of the session. For this application, we’re storing the call_id and caller information information for debugging purposes. We also store the user we want to hear the tweets for here. Sessions are a convenient way to store not only caller information, but data we will want to use later in the call flow such as the page number of tweets we are reading.

Events (lines 11-14)

t.on :event => 'continue', :next => '/process.json'
t.on :event => 'error', :next => '/error.json'     # For fatal programming errors. Log some details so we can fix it
t.on :event => 'hangup', :next => '/hangup.json'   # When a user hangs or call is done. We will want to log some details.
t.on :event => 'incomplete', :next => '/incomplete.json'

On line 10, we initiate our Tropo::Generator object to which will append what we want Tropo to do for us. Lines 11-14 define our events for our index.json route. These tell Tropo what to do next, if there’s an error, if the user hangs up, or if the user doesn’t do what we expect them to do. As you’ll see at the beginning of the other routes, we want the same routes of error.json and hangup.json to be triggered for their respective events, so we repeat those lines.

Say & Ask (lines 15-22)

t.say "You have reached @#{session[:user]}'s tweets-by-phone."

t.ask :name => 'count', :bargein => true, :timeout => 7, :required => true, :attempts => 4,
    :say => [{:event => "timeout", :value => "Sorry, I did not hear anything."},
             {:event => "nomatch:1 nomatch:2 nomatch:3", :value => "That wasn't a one digit number."},
             {:value => "How many tweets do you want to listen to at once? Enter or say a one digit number."},
             {:event => "nomatch:3", :value => "This is your last attempt. Watch it."}],
              :choices => { :value => "[1 DIGITS]"}

Line 22 is the first thing the user hears. We welcome to our app and dynamically (because we stored the twitter username into our session hash) let them know whose tweets they are about to hear. Lines 17-22 are all for one Tropo action: ask. This block of code, which will be latter referenced as the action “count” prompts the user for how many tweets they will want to hear per proverbial page. It may seem weird that the actual question is sandwiched by events within the :say => [] block, but it’s actually very intuitive and conversation-like. When an event is matched (for example timeout, nomatch, or nomatch:#), the corresponding value will be said to the user before or after the question being posed. In this example, we sternly warn the user after 3 unmatchable responses that it’s their last chance after the question is repeated.

Continue: If the user inputs (here, they can speak a one-digit number or enter it using their telephone keypad) a valid answer to our question, the continue event will be triggered and the call continues.

Incomplete: If the user fails all four attempts we allotted them, they are transferred to the “incomplete” event we defined which in this case notifies them of what happened and terminates the call. Because we defined the hangup event again inside of incomplete.json, hangup.json (lines 69-74) will be executed once the call is terminated and Sinatra will print the information on the screen. For actual applications, this is where you’d probably want to log information for historical reporting and/or billing fun.

process.json (lines 40-49)

post '/process.json' do
  v = Tropo::Generator.parse request.env["rack.input"].read
  t = Tropo::Generator.new
    t.on  :event => 'error', :next => '/error.json'     # For fatal programming errors. Log some details so we can fix it
    t.on  :event => 'hangup', :next => '/hangup.json'   # When a user hangs or call is done. We will want to log some details.
    t.on  :event => 'continue', :next => '/say_page_of_tweets.json'
    session[:count] = v[:result][:actions][:count][:value]
    # t.say "@#{session[:user]} tweets coming right up!"
  t.response <br />
end

For calls that give us a one-digit number for the question we asked on line 17, we store that number into our session hash and send them to the next document which will read the tweets they requested.

say_page_of_tweets.json (lines 51-67)

post '/say_page_of_tweets.json' do
  t = Tropo::Generator.new
    t.on  :event => 'error', :next => '/error.json'     # For fatal programming errors. Log some details so we can fix it
    t.on  :event => 'hangup', :next => '/hangup.json'   # When a user hangs or call is done. We will want to log some details.
    t.on  :event => 'continue', :next => '/say_page_of_tweets.json'
    t.say "I'm about to read you the"
    t.say say_as_ordinal(session[:page])
    t.say " #{session[:count]} tweets by #{session[:user]}."
    source = "http://twitter.com/statuses/user_timeline/#{session[:user]}.rss?count=#{session[:count]}&page=#{session[:page]}"
    rss = REXML::Document.new(open(source).read).root
    rss.root.elements.each("channel/item") { |element|
      t.say "Tweet from about #{Time.parse(element.get_text('pubDate').to_s).to_pretty}"
      t.say reformat_uris(element.get_text('title').to_s) + "," # comma for extra pause between tweets.
    }
    session[:page] += 1
  t.response
end

This is where our application loops and reads the requested tweets one-by-one. This is the moment the session hash has been waiting for. We use the information we collected in index.json and process.json to report the requested information to the user.

Lines 61 through 64 is where we render the Twitter RSS feed we requested in line 60, and go through each tweet received to read the age of the tweet, followed by its content. As you can see on lines 57, 62, and 63, we use helpers whose code can be found in the accompanying goodies.rb file to present the data in a more user-friendly way.

At the end of each execution of say_page_of_tweets.json, as seen on line 65, we increment the session variable for page, so the next X-number of tweets are read when the action is re-executed (because, you guessed it, line 55 tells Tropo to execute that same document again).

Goodies.rb

As you can see on the first line of the script, we require()’d the ruby file goodies.rb. In it, I’ve provided some helpful methods for this particular app. I simply put them in a separate file so our main file is cleaner.

reformat_uris() removes the http[s]:// from URLs found in twitter status updates. We need to do this because Tropo handily (though not in this case) tries to stream URLs as sound files. This is very helpful when you want to play a MP3 file over the phone, but if you provide it a regular HTML site, it’ll synthesize it into white-noise.

say_as_ordinal() returns a string of VoiceXML. Tropo is built upon Voxeo’s highly tested infrastructure which, until now, has been mostly used for power and good of VoiceXML. Thankfully to use, Tropo stays true to its roots and allows us to execute VoiceXML. It can do a lot of handy things like turn “1″ into “first”, saving us from coding that conversion ourself and making sure it scales and is in correct English grammar.

to_pretty() is similar to the DateTime helper time_ago_in_words() available to Rails users. I found this method addition for the Ruby Time class and like it. As you can see, it’s very easy to customize too.

Conclusion

As you can see, this is a fairly basic application of a voice technology but it could be vastly expanded to become exponentially more feature-rich. For example, we could ask the user for more input such as giving them a choice of which twitter username to query, much more input validation, and more.

Tropo is backed by Voxeo’s extreme support. Check out Tropo’s contact page for all the ways to get help with Tropo and making the web applications you already have, and the ones you are only just now dreaming up, come alive with voice and IM-technology.