Introduction

The UAS runs applications that are written by you in Python. The applications control the call logic for one primary call which can be either inbound or outbound. The applications can also have the ability to use up to three extra channels which can be used to place outbound calls. Extra channels cannot be used to wait for inbound calls.

The application call logic is written using the API provided.

Applications are saved in a file with the extension .py; the files must be uploaded to the UAS using the controls provided in the Management Console. Once uploaded the applications will reside in a directory called Applications which is directly below the UAS’s operating directory.

An inbound call to a UAS will target a particular inbound application, the UAS will activate an instance of the application. Multiple instances of an application can run simultaneously. The UAS can be triggered to run an outbound application via the web services API. Sample applications are provided that illustrate how this API is used. Multiple instances of an outbound application can run simultaneously.

For an application to be targeted by an inbound call, or by the web services API it must first be registered against a service on cloud.aculab.com.

The lifetime of an application instance is not the same as the lifetime of the call. An application instance can continue to run after the primary call has hung up. In fact, an outbound application instance does not need to have a call associated with it at all. The UAS will never terminate an application instance, the instance will be allowed to end naturally.

Because the UAS will never terminate an application instance, it is extremely important that the application writer ensures that the application will terminate. An accidental infinite loop will result in an application instance that runs forever, and there is a cost associated with that.

Applications are not restricted to the API provided, the application is free to do whatever other tasks you wish it to do, you may, for instance, want to access an external database.

Call duration limiter

The UAS will ensure that calls don’t last forever. The default maximum duration for a call is four hours. The channel object, through which the application writer accesses the call API, exposes the property MinutesMaxElapsedTime. This property can be used to alter the maximum call duration on that channel for a given application instance. The maximum duration allowed is 24 hours, the minimum is five minutes.

Usage example:

channel.MinutesMaxElapsedTime = 60 # set the maximum call time to one hour

It is possible to turn off the call duration limiter, and thus allow infinite calls. The UAS configuration file can take an extra parameter max_call_time which can be used to set a new default call limit in minutes. Setting this to 0 will remove the call limit. The UAS will need to be restarted for this to take effect.

Usage example:

max_call_time:0     # the call limiter has been turned off

Creating loops within an application

A recurring problem among application writers is inadvertently creating infinite loops. The API provides a module that can be used to check whether the application needs to quit. In many cases an application loop will be calling other API functions, and these will raise a Hangup exception if the call associated with the channel goes idle, thus exiting the loop. However, if no API functions are being called, the ICanRun module can be used, as shown in the following example of a common wait function:

__uas_identify__ = "common"
__uas_version__ = "1.0b1"

import time
# import the module
from prosody.uas import ICanRun

def my_common_wait(channel, sleep_seconds):
    # create an instance of the class
    i_can_run = ICanRun(channel)
    end_time = time.time() + sleep_seconds
    # sleep for required time or until the check fails.
    while time.time() < end_time and i_can_run:
        time.sleep(1)

Applications and Services

For an application on a UAS to be invoked by an incoming call, or by the web services API, it must first be linked to an inbound or outbound service on cloud.aculab.com. To register inbound or outbound services on cloud.aculab.com please log into your account and visit the Inbound Services or Outbound Services pages under the Sevices tab.

Placing an outbound call

To place an outbound PSTN call you also must have a validated Caller ID. You can register a Caller ID on the Caller ID page under the Settings tab.

Please see the documentation for placing outbound calls.

If this all seems a bit daunting, please log in to cloud.aculab.com and follow the Quickstart guide which will help you to get your first application running quickly.

Importing applications

When the UAS starts up, it will import all the .py files that are present in the Applications directory. It needs to import the applications in order to run them when required. As the UAS imports them, it will perform some simple checks on the application files, for example, it will check that the function main is defined and that it has the required number of arguments; it will also check that the global variables __uas_version__ and __uas_identify__ have been defined. Applications that do not pass the checks will not be available to be run by the UAS and will not be listed by the management console. Application templates and further information is given below ... keep reading.

Please also note that application file names must not begin with a digit or contain a - (minus). This is a python restriction.

Importing other files

Sometimes it will be desirable to have a .py source file in the Applications directory which is not an application. For instance, two or more applications may want to import a common file that defines shared methods. Naturally, such a common file might not define a main function. The UAS distinguishes between application files and other .py files by looking at the variable __uas_identify__. For applications this variable will be "application", for other files it will be "common". If a file has identified itself as being common, the UAS knows not to check it for things that are required for application files. If the __uas_identify__ variable is missing, the file will fail the checks and not be imported.

Common files follow a different import mechanism to application files. This is to allow common files to be updated without affecting applications that are already running.

When a common file is uploaded, the contents are copied to a new file. The file that was uploaded becomes a wrapper for the new file. For example, if my_app_common.py is uploaded, the contents will be copied to __bcf1bcfmy_app_common.py and the contents of my_app_common will be a wrapper for __bcf1bcfmy_app_common.py. The next time my_app_common.py is uploaded, __bcf2bcfmy_app_common.py will be created (the 1 is incremented to 2) and my_app_common will become a wrapper for the new file. Any applications that are running at the time of the upload will continue to use __bcf1bcfmy_app_common.py, and new application instances will use __bcf2bcfmy_app_common.py.

Modifying applications

It is possible to modify an application while the UAS is running, and have it pick up the new file without having to be restarted. This is accomplished via the Management Console. After modifying an application file, it can be uploaded to the UAS and re-imported via the Management Console.

When an application file is being re-imported, there is a brief period (a matter of milliseconds) during which the application is not available to the UAS. During this brief period any telephone calls that require the application will fail, but existing telephone calls will be unaffected.

Modifying common files

As with applications, other python source files (files that identify themselves as common) can be modified and then uploaded to a running UAS via the UAS management console. Uploading a common file will cause the UAS to re-import the applications, so that any that require the new or modified behaviour defined in the common file will pick that up.

Application return codes

All applications should return an integer to indicate success or failure. The return value will be logged in the application data record (ADR), please see the ADR section for more information on return codes.

The most recent ADRs for failed application runs are also shown on the Management Console’s ADRs page.

The Error exception

There are several situations in which the python API will raise an Error exception. For instance, if a call channel’s play function is called whilst the call channel is connected to another call, or if connect is called on a call channel that is already connected. If this happens, it is an indication of an error in the application which needs to be corrected. The application writer might also want to raise an Error exception if something bad happens.

note:Applications should catch all exceptions and handle them appropriately, see the examples for more on this.

Example:

from prosody.uas import Error

# raising and logging an error string
try:
    raise Error('something bad has happened')
except Error as exc:
    my_log.error(str(exc))

The Hangup exception

This exception does not indicate an error in the application, it simply means that the call that is being worked with has gone to IDLE. Because a call can be hung-up at any time, most functions can raise this exception; the application must be ready to catch it and deal with the hang-up appropriately. The application writer might also want to raise a Hangup exception if, for instance, the call is in an unwanted state and the application wants to force a hang up.

note: Due to the way in which the PSTN works, if the called party of an outbound PSTN call hangs up, cloud.aculab.com will not necessarily be informed and, therefore, might not be able to tell your application. So, for this reason and for general usability, we strongly recommend that you write your applications to interact with the called party and, in the event of repeated no response, hang up the call.

note: Applications should catch all exceptions and handle them appropriately, see the examples for more on this.

Example:

from prosody.uas import Hangup

# raising and logging a hang-up string
try:
    raise Hangup('call has hung up')
except Hangup as exc:
    my_log.info(str(exc))

The ToneManager class

The tone manager is required for call transfer, including transfer to a conference. The application will create a tone manager and pass it to the channel to create a tone player.

Example:

from prosody.uas import ToneManager

# create a tone manager
tone_manager = ToneManager()
# create a tone player on the channel
channel.create_tone_player(tone_manager)

The application main function

Each application must have a main function. The UAS will attempt to execute main with a number of arguments. The arguments to the application are provided by cloud.aculab.com.

For outbound call applications main takes six arguments:

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):

For inbound call applications main takes five arguments:

def main(channel, application_instance_id, file_man, my_log, application_parameters):
The arguments
  • channel
    is the call channel that is handling the call.
  • application_instance_id
    is a unique id for this instance of the application.
  • file_man
    is a reference to a file manager that provides some basic file based operations.
  • my_log
    is a reference to a logger.
  • application_parameters
    is a string of application arguments.
  • outbound_parameters
    is a string of application arguments relevant to outbound calls only. Typically, this will be the destination address of the outbound call.

application_parameters

The application_parameters argument is passed into inbound and outbound applications. It is a string which the user sets in the Inbound and Outbound Service Registration pages (the Application Parameters field) on cloud.aculab.com and is passed unchanged to the application.

outbound_parameters

The outbound_parameters argument is passed into outbound call applications only. It is a string which the user sets when using the Web Services API to invoke an outbound service. Typically this string will be used to provide the destination address of the outbound call. See the web services API and the web services samples for information on how to invoke an outbound application.

The call channel class

A reference to the channel class is passed into the main function as the first parameter, channel in the examples above. This class provides all the call control functions that the application will require.

Note that an inbound media application will only be passed a call channel object if the corresponding inbound service requests it. If a call channel is not requested, channel will be None. Normally, an inbound media application will not need to place a call and the channel object, along with the API it provides, will not be required.

In addition to the call control functions, the channel can have a reference to extra channels that can be used to place outbound calls. Access to the extra channels is through a public property called ExtraChannel, which is a list. To get a reference to an extra channel do the following:

out_channel = channel.ExtraChannel[0]

The number of extra channels you want to allocate to your application is set on the Inbound and Outbound Service Registration pages on cloud.aculab.com.

Please see the tutorial for more information on using the extra channels.

For information on using the channel class API please go here.

The file manager class

A reference to the file manager class is passed into the main function as the third parameter, file_man in the example above. This class provides some basic file management functions.

The logger class

A reference to the logger is passed into the main function as the fourth parameter, my_log in the example above. This class provides logging functionality.

Use the my_log parameter to write your trace to the applications log file. For example my_log.debug('Hello world.') will write your text to the log file at debug level. An example for each log level is given below:

my_log.debug('Hello world.')
my_log.trace('Hello world.')
my_log.info('Hello world.')
my_log.report('Hello world.')
my_log.warning('Hello world.')
my_log.error('Hello world.')
my_log.exception('Hello world.')

Exception level logging will always be logged and, in addition to your text, will log some stack trace as well.

The unique application ID

The unique application ID is constructed from two other IDs; an ID that represents the media server and an application ID. The two IDs are joined with a dot to form a unique ID for the application instance.

Text to Speech

Text To Speech (TTS) is supported on cloud.aculab.com, allowing quick and easy application prototyping and, more generally, the ability for your applications to read text to their users. Several TTS voices are supported, please see the TTS documentation on cloud.aculab.com.

The TTS engine supports the Speech Synthesis Mark-up Language (SSML). This is a very flexible way of expressing how you’d like it to speak and is documented fully at http://www.w3.org/TR/speech-synthesis . In brief, SSML allows you to do things like:

channel.FilePlayer.say("<prosody rate='x-slow'>This is me speaking slowly.</prosody>")

channel.FilePlayer.say("<prosody pitch='+50%'>This is me speaking with a very high pitch.</prosody>")

SSML can also be used to choose between TTS engines and voices:

channel.FilePlayer.say("<acu-engine name='Polly'><voice name='Brian'>Hello, this is Brian from the Polly TTS engine.</voice></acu-engine>")

TTS has one forbidden character, the | (pipe) which is not allowed to be part of the string.

There are also three reserved characters, these are the XML characters < (less than), > (greater than) and & (ampersand). If they are to be included in a TTS string they must be escaped as follows:

Less than    <  &lt;
Greater than >  &gt;
Ampersand    &  &amp;

Some languages, such as Spanish, require unicode characters. To ensure that unicode characters are preserved, the application or common file must be saved as a UTF-8 encoded file. The first line of the file must be:

# -*- coding: utf-8 -*-

Any text that contains unicode characters must be identified as unicode for example:

u"¿Cae la lluvia en España principalmente en la llanura?."

Automatic Speech Recognition

Automatic recognition of spoken words is supported on cloud.aculab.com. Please see the online ASR documentation for more details.

ASR is exposed via the SpeechDetector property on the channel object.

Application templates

The applications need to adhere to a few rules:

  1. Each application must define a global variable __uas_version__ to hold a version string.
  2. Each application must define a global variable __uas_identify__ to hold the string "application".
  3. Each application must define the function main with the appropriate arguments.
  4. To use the Hangup or Error exceptions, an application must import them from prosody.uas.
  5. To use the tone manager (required for call transfer), an application must import ToneManager from prosody.uas.
  6. Applications should return a positive integer (>= 0) to indicate success.
  7. Applications should return a negative integer (<= -100) to indicate failure (-1 to -99 are reserved for the UAS).
  8. Applications should catch all possible exceptions and handle them appropriately.

An inbound call application template:

from prosody.uas import Hangup
from prosody.uas import Error
from prosody.uas import ToneManager

__uas_version__  = "0.0.1"
__uas_identify__ = "application"

def main(channel, application_instance_id, file_man, my_log, application_parameters):
    return_code = 0
    try:
        # call handling code here
        pass

    except Hangup as exc:
        # a call has hung up, this is not an error
        my_log.info("Hangup exception reports: {0}".format(exc))

    except Error as exc:
        # an error has occurred, return a negative value
        my_log.error("Error exception reports: {0}".format(exc))
        return_code = -102

    except Exception as exc:
        # an unexpected exception, return a negative value.
        # for these exceptions it can be useful to log some traceback.
        # and this is achieved by logging at exception level.
        my_log.exception("Got unexpected exception {0}".format(exc))
        return_code = -101

    finally:
        # the finally clause will always happen, return a value here
        if channel.state() != channel.State.IDLE:
            channel.hang_up()
        return return_code

An outbound application will be the same, except that main will take one additional parameter:

def main(channel, application_instance_id, file_man, my_log, application_parameters, outbound_parameters):

Common file template

Common files need to adhere to a few rules:

  1. Each common file must define a global variable __uas_identify__ to hold the string "common".
  2. To use the Hangup or Error exceptions, a common file must import them from prosody.uas.

Common file template:

from prosody.uas import Hangup
from prosody.uas import Error

__uas_version__  = "0.0.1"
__uas_identify__ = "common"

Unlike application files, common files do not have to define a version string, but it is not prohibited.

Global variables

Please note that a global variable in an application instance will be global across all instances of that application. It is highly recommended that you do NOT use global variables as this will probably result in unwanted interactions between your application instances. However, if you do want to use a global variable (possibly to interact with other instances of the same application) please be extremely careful.

Working with media files

Media files are accessed on cloud.aculab.com via a highly reliable distributed storage system. The way it works will generally be transparent to you as an application writer, but there are a few important things to bear in mind. For more information on this please read up on Eventual Consistency.

Recording, replacing and deleting

When recording a new file within a telephone call, it will be available for use immediately in that and other calls, and within about half a minute from cloud.aculab.com and its Web Services API.

When writing a new file via cloud.aculab.com or the Web Services API, it will be available for access within about half a minute from the completion of the write.

When deleting or overwriting an existing file via cloud.aculab.com or the Web Services API, the change will take up to 15 minutes to become available to your applications. Until this has been accomplished, telephone calls will continue to access the old version.

While these levels of delay may be fine when you’ve just published a new version of your ‘Hello caller, what would you like to do?’ WAV file via cloud.aculab.com or the Web Services API, it may not be so helpful when you’ve just uploaded an updated emergency announcement and the intended recipient dials into the system soon after to receive it. So, for cases where you need to access new media files quickly, please make them new files by using new filenames.

Directories

It’s generally a good idea to organise your media files into directories. The directory delimiter is /, as in intro/hello.wav. Directories are created and destroyed on demand by the storage system so, assuming that the intro directory doesn’t exist, channel.FileRecorder.record("intro/hello.wav") would create that directory and file_man.delete_file("intro/hello.wav") would remove it along with the file.