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.
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 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 < <
Greater than > >
Ampersand & &
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:
Each application must define a global variable
__uas_version__
to hold a version string.Each application must define a global variable
__uas_identify__
to hold the string"application"
.Each application must define the function
main
with the appropriate arguments.To use the
Hangup
orError
exceptions, an application must import them fromprosody.uas
.To use the tone manager (required for call transfer), an application must import
ToneManager
fromprosody.uas
.Applications should return a positive integer (>= 0) to indicate success.
Applications should return a negative integer (<= -100) to indicate failure (-1 to -99 are reserved for the UAS).
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:
Each common file must define a global variable
__uas_identify__
to hold the string"common"
.To use the
Hangup
orError
exceptions, a common file must import them fromprosody.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.