Skip to content

Commit

Permalink
Implemented an observer pattern to handle commands
Browse files Browse the repository at this point in the history
  • Loading branch information
nicbou committed Dec 22, 2013
1 parent 653326c commit e6141c2
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 213 deletions.
2 changes: 1 addition & 1 deletion setup.py
@@ -1,7 +1,7 @@
from setuptools import setup from setuptools import setup


setup(name='winston', setup(name='winston',
version='0.1', version='0.2',
description='A voice-controlled virtual butler', description='A voice-controlled virtual butler',
url='http://github.com/nicbou/winston', url='http://github.com/nicbou/winston',
author='Nicolas Bouliane', author='Nicolas Bouliane',
Expand Down
11 changes: 8 additions & 3 deletions winston/__main__.py
Expand Up @@ -4,18 +4,23 @@


def main(): def main():
""" """
Allows Winston to be installed as a package and to be run from the command line Allows Winston to be installed as a package and to be run from the command line.
This simply inits Winston using the config file.
""" """


# Define and start a scheduler. These store tasks that are run at given times # Define and start a scheduler. These store tasks that are run at given times
scheduler = config.SCHEDULER scheduler = config.SCHEDULER
scheduler.start() scheduler.start()


# Load the commands in the interpreter. These dispatch commands. See the Interpreter's doc for details. # Load the commands in the interpreter. These dispatch commands. See the Interpreter's doc for details.
interpreter = Interpreter(commands=config.COMMANDS, scheduler=config.SCHEDULER) interpreter = Interpreter(scheduler=config.SCHEDULER)
for command in config.COMMANDS:
interpreter.register(command)


# Create a listener for pocketsphinx. It forwards recognized strings to Interpreters. See Listener's doc for details. # Create a listener for pocketsphinx. It forwards recognized strings to Interpreters. See Listener's doc for details.
listener = Listener(interpreters=[interpreter], fsg_path=config.GRAMMAR_FILE, dict_path=config.DICT_FILE) listener = Listener(fsg_path=config.GRAMMAR_FILE, dict_path=config.DICT_FILE)
listener.register(interpreter)


# And wait... # And wait...
raw_input() raw_input()
Expand Down
112 changes: 5 additions & 107 deletions winston/commands/__init__.py
Expand Up @@ -2,114 +2,12 @@


class Command(object): class Command(object):
""" """
Stores a command: Stores a command that is executed by external events such as a voice command,
- name: an identifier for the regex and JSGF file generation. Should be unique. a change of state or a notification.
- actions: a list of action variations: (say, tell us, tell me, tell)
- subjects: a list of action targets passed to the callback: (the time, the weather)
- callback: a function that executes the command with a subject string as the argument
You can create a new command by extending it or by instanciating it. Extending the Command
object gives you the ability to keep application state and redefine how commands are
dispatched.
""" """
_interpreter = None
_callback = None

def __init__(self, actions=[], subjects=[], callback=None, name='command', interpreter=None, always_active=False):
self.name = name # Used as a named group identifier in the regex
self.actions = actions
self.subjects = subjects
self.callback = callback
self.interpreter = interpreter # Reference to the interpreter that will run this command
self.always_active = always_active # This command will not run when the interpreter is inactive

def dispatch(self, command, subject):
"""
Dispatches the command to the callback with the specified subject
as a callback. Easily overridden.
command: The full matched command ('turn on the lights')
subject: The variable part of the command ('the lights')
"""
# Don't perform actions if the interpreter isn't active
if self.interpreter.active or (not self.interpreter) or self.always_active:
# Validate the existence of a subject, if any are specified
if not self.subjects:
self.callback(command)
elif isinstance(self.subjects, (tuple, list)) and subject in self.subjects:
# Match a subject list
self.callback(command, subject)
elif (not isinstance(self.subjects, (tuple, list))) and re.match(self.subjects, subject):
# Match a regex subject
self.callback(command, subject)
else:
print("Subject {0} does not exist for command {1}".format(subject, self.name))


@property def on_event(self, event, sender):
def regex(self, group_name=None):
""" """
Returns a regex string matching the command. Handles events from the interpreter and other sources
""" """
# Build the command # Do something here.
# e.g. (open|turn on) (the lights|the television)
command = ""
if self.subjects:
regex_actions = self.actions
regex_subjects = self.subjects

# If the actions is a list (and not a regex), join it into a single regex:
if isinstance(self.actions, (tuple, list)):
regex_actions = "|".join(regex_actions)

# If the subjects is a list (and not a regex), join it into a single regex:
if isinstance(self.subjects, (tuple, list)):
regex_subjects = "|".join(regex_subjects)

command = "({actions}) (?P<{name}Subject>({subject}))".format(
name = self.name,
actions = regex_actions,
subject = regex_subjects,
)
else:
regex_actions = self.actions

# If the actions is a list (and not a regex), join it into a single regex:
if isinstance(self.actions, (tuple, list)):
command = "({actions})".format(
actions = "|".join(self.actions),
)
else:
command = regex_actions

# Put the regex in a named group
named_group = "(?P<{name}>({command}))".format(
name = self.name,
command = command,
)

# Return a regex pattern string
return named_group

@property
def interpreter(self):
return self._interpreter

@interpreter.setter
def interpreter(self, value):
"""
Since this function is called when the interpreter is set,
this is the perfect place to attach initiation logic that
concerns the command's Interpreter.
For example, this is the place to add events to the interpreter's
scheduler.
"""
self._interpreter = value

@property
def callback(self):
return self._callback

@callback.setter
def callback(self, value):
self._callback = value
14 changes: 2 additions & 12 deletions winston/config.py
Expand Up @@ -15,18 +15,8 @@
BALANCE_PATH = "/var/www/scripts/winston_balance.txt" BALANCE_PATH = "/var/www/scripts/winston_balance.txt"


# Define your own commands and add them here # Define your own commands and add them here
from commands import say, activate, deactivate, account_balance, open_door, set_alarm, next_bus, dinner, Command COMMANDS = [
COMMANDS = [ # The list of commands passed to the interpreter
say.SayCommand(), # Simple command to get started
activate.ActivateCommand(), # Can activate winston
deactivate.DeactivateCommand(), # Can deactivate winston
account_balance.AccountBalanceCommand(), # Lots of variations, uses regex actions, and act proactively
open_door.OpenDoorCommand(),
set_alarm.AbsoluteAlarmCommand(),
set_alarm.RelativeAlarmCommand(),
next_bus.NextBusCommand(),
dinner.DinnerCommand(),
Command(name='whatTime', actions=('what time is it',), callback=say.sayTime), # A simple command
] ]


# Define a scheduler to store scheduled events # Define a scheduler to store scheduled events
Expand Down
104 changes: 30 additions & 74 deletions winston/interpreter.py
Expand Up @@ -2,92 +2,48 @@


class Interpreter(object): class Interpreter(object):
""" """
The interpreter turns matches commands with functions. It strongly relies The interpreter turns matches command strings with the right Commands. It's
on the Command object, bundling them together into a bigger regex. purpose is to notify all Commands of a new command string.
Strings come from the Listener, which is just a wrapper around pocketsphinx. The command strings come from the Listener, which is just a wrapper around pocketsphinx.
It only returns lowercase strings without punctuation.
""" """


# The name with which all commands begin. Can be a word or a regex. def __init__(self, scheduler=None):
# Example: jenkins, alfred, robot. "Jenkins! Turn on the lights!"
signal = "winston"

# Command prefixes and suffixes. Can be a tuple of words or a regex
prefixes = "( can you| could you)?( please)?"
suffixes = "( please)?"

# The actual commands. Expects a list of Command objects
commands = ()

def __init__(self, commands, scheduler=None):
""" """
Prepares the interpreter, compile the regex strings Prepares the interpreter
""" """

# Keep a reference to the scheduler # Keep a reference to the scheduler
self.scheduler = scheduler self.scheduler = scheduler

# Keep a reference to the interpreter, give command a unique name
for index, command in enumerate(commands):
command.interpreter = self
command.name = 'cmd' + str(index) # Every command needs a unique name. Any valid regex group name will do.


self.commands = commands # The commands
self.regex = self.regex() # Build the command matcher self.commands = []


# Commands can access self.interpreter.active and decide whether or not def register(self, command):
# to perform an action. Commands can also "shut down" winston by setting """
# active to false. Registers new commands to the interpreter's events

"""
# We still let the commands go through so a command can reactivate an if not command in self.commands:
# interpreter. self.commands.append(command)
self.active = True


def regex(self): def unregister(self, command):
""" """
Build a regex to match all possible commands Unregisters a command from the interpreter's events
""" """
# Basic command structure if command in self.commands:
basic_command = "{signal}{prefix} {command}{suffix}" self.commands.remove(command)


# Build the command regex by joining individual regexes def notify(self, event):
# e.g. (command_1|command_2|command_3) """
command_regexes = [] Notifies all commands of a change in the state of
the interpreter.
"""
for command in self.commands: for command in self.commands:
command_regexes.append(command.regex) command.on_event(event, self)
command_regex = "({0})".format("|".join(command_regexes))

# Wrap the command with the prefix and suffix regexes
final_regex = basic_command.format(
signal = self.signal,
command = command_regex,
prefix = self.prefixes,
suffix = self.suffixes,
)

# Return the compiled regex, ready to be used
return re.compile(final_regex)

def match(self, command):
# Try matching the command to an action
result = self.regex.match(command)
if result:
groups = result.groupdict() # Get all the group matches from the regex


print("Got '%s'" % command) def on_event(self, event, sender):

"""
if not self.active: Handles events from the listener and other classes
print("(interpreter is inactive)") it is registered to.

"""
for command in self.commands: if self.active:
# Check if the command name matches a regex group self.notify(event)
if command.name in groups and groups[command.name] is not None:
print('matched ' + command.__class__.__name__)
command_string = groups[command.name] # The command without "Winston, please ... thank you"
subject = None
if command.subjects:
subject = groups[command.name + 'Subject'] # The group of the subject ('the lights' in 'turn on the lights')
command.dispatch(command_string, subject)
else:
print("Could not match '{0}' to a command using regex".format(command))

0 comments on commit e6141c2

Please sign in to comment.