Developing plugins

Plugins are the main functional piece of pysiriproxy. Plugins provide the implementation of custom responses to the user’s requests.

Plugins are defined by creating a subclass of the pysiriproxy.plugins.plugin.BasePlugin class. Plugins can be defined in any Python module within the pysiriproxy configuration plugins directory (see the section on Configuring pysiriproxy).

Note

pysiriproxy expects all plugin classes to be named ‘Plugin’. Only classes with that name will be loaded as plugins.

Plugins are standard Python classes, but can contain special functions which can be called in the even that certain objects are received by pysiriproxy. The functions are called object filters and can be called in the event that an object originated from a specific location (i.e., the iPhone or Apple’s web server), or the object filters can be called in the event that an object of a specific type is received. These are called directional and class filters respectively.

Object filters

The iPhone communicates the user’s requests by sending an object containing the request data to Apple’s web server. The web server processes the request and send an object containing the response data to the iPhone. All requests and responses are transmitted through different types of objects which indicate to the web server what request is being made, and indicate to the iPhone how to display the response to the user.

Object filters are specific functions of a plugin that is called in the event that a specific object is received from either the iPhone, or from Apple’s web server. The object filter is then able to modify the object, e.g., change Siri’s speech in the response, create an entirely new object, or keep the object from reaching its destination.

How to define an object filter function

An object filter is defined by using a special function decorator. This decorator tags the function to indicate what object type it is filtering.

Object filter functions can filter objects based on the object type (also called the object’s class), or the origin of the object, i.e., the iPhone or Apple’s web server. These object filters are called class filters, and origin filters respectively.

An origin filter function can be created by applying the following function decorators to a plugin function:

  • directions.From_iPhone
  • directions.From_Server

A class filter function can be created by applying the any of the following function decorators to a plugin function:

Multiple object filter decorators can be applied at the same time to allow a function to receive various types of classes or directions.

Here are a few examples of creating object filters:

from pysiriproxy.plugins import BasePlugin, From_iPhone, From_Server, \
    SpeechPacket, StartRequest, CancelRequest, CancelSpeech, ClearContext


class Plugin(BasePlugin):
    name = "Example-Plugin"

    @From_Server
    def filterServer(self, obj, direction):
        '''This filter is called with objects received from Apple's
        web server.

        * obj -- The received object
        * direction -- The direction of the received data

        '''
        return obj

    @SpeechPacket
    def filterSpeech(self, obj, direction):
        '''This filter is called with objects received that have the
        SpeechPacket class.

        * obj -- The received object
        * direction -- The direction of the received data

        '''
        return obj

    @From_iPhone
    @StartRequest
    def filterSpeech(self, obj, direction):
        '''This filter is called with objects received from the iPhone
        that have the StartRequest class.

        * obj -- The received object
        * direction -- The direction of the received data

        '''
        return obj

    @From_iPhone
    @ClearContext
    @CancelSpeech
    @CancelRequest
    def filterSpeech(self, obj, direction):
        '''This filter is called with objects received from the iPhone
        that have either the ClearContext, CancelSpeech, or CancelRequest
        classes.

        * obj -- The received object
        * direction -- The direction of the received data

        '''
        return obj

How to define a custom class filter decorator

In many instances a developer might find the need to use a class filter that is not built into pysiriproxy. In this case, the developer has the ability to create a custom class filter by using the objectClasses.createClassFilter function.

Here is an example of creating and using a custom class filter:

from pysiriproxy.plugins import BasePlugin, createClassFilter


# Create a class decorator which matches the "SpecialObject" class
customDecorator = createClassFilter("SpecialObject")

class Plugin(BasePlugin):
    name = "Example-Plugin"

    # Now this decorator can be used to create a filter function
    @customDecorator
    def specialFilter(self, obj, direction):
        '''This filter is called with objects received with the
        SpecialObject class.

        * obj -- The received object
        * direction -- The direction of the received data

        '''
        return obj

Speech rules

Plugins also have the ability to create speech rule functions. Speech rules allow plugins to execute a function in the event that the iPhone user’s speech was recognized and matched an expected format.

Speech rule functions can be created by decorating a plugin function with one of the following decorators:

Here is an example of creating a speech rule using the two different decorators:

from pysiriproxy.plugins import BasePlugin, matches, regex


class Plugin(BasePlugin):
    name = "Example-Plugin"

    @matches("This is the text to match")
    def matchesRule(self, text):
        '''This speech rule is called whenever the user says exactly: "This
        is the text to match"

        * text -- The text spoken by the user

        '''
        self.completeRequest()

    @regex(".*Siri Proxy.*")
    def regexRule(self, text):
        '''This speech rule is called whenever the user says anything
        including the phrase "Siri Proxy".

        * text -- The text spoken by the user

        '''
        self.completeRequest()

Note

All speech rules should always call the completeRequest() function. Otherwise, Siri will continue to spin.

How to write custom speech rule decorators

In many instances, a developer might find the need for a different method of matching recognized speech for a speech rule. In this event, the developer has the ability to define a custom speechRules.SpeechRule class and create a function decorator for that class.

First, the developer must create a subclass of the speechRules.SpeechRule class. This class should define the speechRules.SpeechRule.test() function, which a string containing the recognized speech and returns True if the recognized speech matches the speech rule, and False if it does not.

Here is an example of creating a custom SpeechRule:

from pysiriproxy.plugins import SpeechRule


class EndsWithSpeechRule(SpeechRule):
    def test(self, text):
        '''Test the text to see if it matches our expected text.

        * text -- The recogized speech to test

        '''
        return text.endswith(self.text)

The constructor for the SpeechRule class takes a string which is stored in the speechRules.SpeechRule.text attribute. This allows the speech rule to be updated dynamically for different functions to match different pieces of text.

Once the speechRules.SpeechRule subclass has been defined, a function decorator for this custom speech rule needs to be created. This can be accomplished by using the speechRules.createSpeechRule() function. This function takes a single parameter which is the speechRules.SpeechRule class that is used for this decorator.

Here is an example of creating a custom speech rule decorator:

from pysiriproxy.plugins import createSpeechRule


endswidth = createSpeechRule(EndsWithSpeechRule)

Now the endswith property can be used to decorate a function in order to create a speech rule that applies the EndsWithSpeechRule to the function.

Here is an example of putting it all together to create a plugin speech rule function:

from pysiriproxy.plugins import SpeechRule, createSpeechRule, PluginBase


class EndsWithSpeechRule(SpeechRule):
    def test(self, text):
        '''Test the text to see if it matches our expected text.

        * text -- The recogized speech to test

        '''
        return text.endswith(self.text)

# Create the speech rule decorator
endswidth = createSpeechRule(EndsWithSpeechRule)

class Plugin(BasePlugin):
    name = "EndsWithPlugin"

    @endswith("special")
    def specialRule(self, text):
        '''This function is called in the event that the user
        says anything ending with the word 'special'.

        * text -- The speech spoken by the user

        '''
        self.completeRequest()

How to control how Siri responds to the user

The BasePlugin class has several functions which are responsible creating different types of respones that Siri can present to the iPhone user. These functions are as follows:

These functions, including examples of using each one, are discussed below.

Completing a request

Each time the iPhone user asks Siri a question a request is started. The iPhone waits until it receives a notification that the request has been completed before it displays the response to the user. The iPhone’s Siri icon continues to spin endlessly if the notification is never received.

In order for a speech rule to work as expected, developers must be sure to call the completeRequest() function at the end of each speech rule. This will notify the iPhone that the request is complete and that it should now display the response to the user.

Example:

from pysiriproxy.plugins import BasePlugin, matches, regex


class Plugin(BasePlugin):
    name = "Example-Plugin"

    @matches("Test Example")
    def matchesRule(self, text):
        self.completeRequest()

Have Siri say something to the user

The say() function allows developers to have Siri say, and display, a specific piece of text. The function takes two parameters which are:

  • text – This is the text that Siri will display

  • spoken – This is the text that Siri will speak. If this is None,

    Siri will speak the display text by default. This is None by default.

Here is an example of how to get Siri to display and speak text:

from pysiriproxy.plugins import BasePlugin, matches, regex


class Plugin(BasePlugin):
    name = "Example-Plugin"

    @matches("Text speech")
    def matchesRule(self, text):
        # This text will be spoken and displayed
        self.say("Hi, my name is Siri!")
        self.completeRequest()

Here is an example of how to get Siri to display one piece of text, and speak a different piece of text:

from pysiriproxy.plugins import BasePlugin, matches, regex


class Plugin(BasePlugin):
    name = "Example-Plugin"

    @matches("Text speech")
    def matchesRule(self, text):
        # Siri will display: "How are you today"
        # Siri will say: "Hi, my name is Siri"
        self.say("How are you today?", spoken="Hi, my name is Siri!")
        self.completeRequest()

Have Siri ask the user a question

Plugins can also have Siri ask the user a question and wait for the user’s answer. The ask() function commands Siri to ask the user a question, and allows the Plugin to be notified of the user’s response and continue execution.

Here is an example of commanding Siri to ask the user a question:

from pysiriproxy.plugins import BasePlugin, matches, regex


class Plugin(BasePlugin):
    name = "Example-Plugin"

    @matches("Ask me a question")
    def testAsk(self, text):
        # Ask the question and wait for the response
        self.ask("What is your favorite color?")

        # Wait for the user's response. If the user responds, the
        # response variable will be set to a string containing
        # the user's speech
        response = yield

        self.say("My favorite color is %s too!." % response)
        self.completeRequest()

The above example commands Siri to ask the user what their favorite color is. The yield command stops the execution at this line until it is notified of the user’s response, at which time the response variable will contain the user’s speech. Once the user replies, the plugin uses the response to have Siri say that her favorite color is the same color they said.

In the event that the user does not respond, or cancels the request before responding, the function execution does not restart.

Developers also have the ability to ask the user a question and wait for the user to answer in a specific way. This is achieved through use of the ResponseList class. The ResponseList class takes a list of responses to wait for the user to say, a question to ask the user, and a response for Siri to say in the event that the user says something other than what is expected.

Here is an example of creating and using a ResponseList:

from pysiriproxy.plugins import BasePlugin, matches, regex, \
    ResponseList


class Plugin(BasePlugin):
    name = "Example-Plugin"

    @regex(".*Confirmation.*")
    def confirmTest(self, text):
        # The list of valid responses for the user to say
        responses = ["yes", "no"]

        # The question to ask the user
        question = "Please say 'yes' or 'no'..."

        # What Siri will say when the user says something that is
        # not in the list of responses
        unknown = "Excuse me?"

        # Wait for a valid response to be spoken
        response = yield ResponseList(responses, question, unknown)

        self.say("You said %s" % response)
        self.completeRequest()

Creating a view

Developers may also desire to display different types of graphical objects to the user, e.g., a button, and the ObjectFactory class was created. The ObjectFactory class provides methods for creating various types of objects that can be displayed to the iPhone user.

Here is an example of creating a view that contains three buttons which execute custom commands to which the plugin will respond:

from pysiriproxy.plugins import BasePlugin, matches
from pysiriproxy.objects import ObjectFactory, Buttons


class Plugin(BasePlugin):
    name = "ButtonTest-Plugin"

    # Define a dictionary mapping custom command names, to the plugin
    # function names that are called when the command is executed
    customCommandMap = {
        "Command 1": "callbackOne",
        "Command 2": "callbackTwo",
        "Command 3": "callbackThree"
        }

    @matches("Test buttons")
    def testButtons(self, text):
        # Create a list of tuples pairing the button text to the
        # commands that are executed when the button is pressed
        buttonList = [
            ("Button 1", "Command 1"),
            ("Button 2", "Command 2"),
            ("Button 3", "Command 3")
            ]

        # Create the buttons
        buttons = self.__createButtons(buttonList)

        # Create an utterance to go along with the buttons
        utterance = ObjectFactory.utterance("Please press a button")

        # Now create a view which displays the utterance and the buttons
        self.makeView([utterance] + buttons)

        self.completeRequest()

    def callbackOne(self, _obj):
        '''Called when the 'Command 1' command is executed.'''
        self.say("You pressed the first button!")
        self.completeRequest()

    def callbackTwo(self, _obj):
        '''Called when the 'Command 2' command is executed.'''
        self.say("You pressed the second button!")
        self.completeRequest()

    def callbackThree(self, _obj):
        '''Called when the 'Command 3' command is executed.'''
        self.say("You pressed the third button!")
        self.completeRequest()

    def __createButtons(self, buttonList):
        buttons = []

        # Create buttons to execute custom commands for each of the
        # buttons in the list of buttons
        for buttonText, command in buttonList:
            button = ObjectFactory.button(Buttons.Custom, buttonText,
                                          command)
            buttons.append(button)

        return buttons

Please view the documentation for the ObjectFactory class for more details on what objects can be created.