Parsetron Tutorial¶
In the following we provide a simple example of controlling your smart lights with natural language instructions. Corresponding code of this tutorial is hosted on Github.
A Light Grammar¶
We start by writing a grammar for smart light bulbs. Our lights of interest are the Philips Hue. The starter pack contains three light bulbs and a bridge:
Your application talks to the bridge through shared WiFi and the bridge talks to the light bulbs through Zigbee. The bridge responds to RESTful API calls.
In Kitt.AI office, we have the 3-bulb starter pack set up like this:
The three bulbs are named top, middle, and bottom for easier reference. Most people would name them by living room, bedroom, etc. It’s your choice. However, no matter what you name those bulbs, you must register them with the Hue bridge either through their Restful APIs, or through the app:
Now imagine we want to parse the following simple sentences:
- set my top light to red
- set my top light to red and change middle light to yellow
- set my top light to red and change middle light to yellow and flash bottom light twice in blue
We can define a simple grammar in the following Python code:
1 2 3 4 5 6 7 8 9 10 | from parsetron import *
class LightGrammar(Grammar):
action = Set(['change', 'flash', 'set', 'blink'])
light = Set(['top', 'middle', 'bottom'])
color = Regex(r'(red|yellow|blue|orange|purple|...)')
times = Set(['once', 'twice', 'three times']) | Regex(r'\d+ times')
one_parse = action + light + Optional(times) + color
GOAL = one_parse | one_parse + one_parse | one_parse + one_parse + one_parse
|
The above code defined a minimal grammar that would parse our test sentences. Here’s a step-by-step explanation.
on line 3 we defined a
LightGrammarclass that extends a standardparsetron.Grammar. Defining grammars in classes helps modularization.on lines 4-5, we used a
parsetron.Setclass to match anything that’s in the set:action = Set(['change', 'flash', 'set', 'blink']) light = Set(['top', 'middle', 'bottom'])
on line 6, instead of using a set, we used a regular expression to encode color names:
color = Regex(r'(red|yellow|blue|orange|purple|...)')
Note that there could be hundreds of color names. A
parsetron.Regexbuilds a finite state machine to efficiently code them. But of course we can also use a Set.on line 7, we introduced the
|operator, which encodes aparsetron.Orrelation to specify alternative ways of representingtimes:times = Set(['once', 'twice', 'three times']) | Regex(r'\d+ times')
So
timescan either match “three times”, or “3 times”.on line 9, we defined a
one_parseof a sentence, which represents a single minimal set of information encoded in a parse:one_parse = action + light + Optional(times) + color
The
+operator here encodes aparsetron.Andrelation, matching a sequence of tokens. For unknown words parsetron simply ignores them. Theparsetron.Optionalclass is a kind of syntactic sugar indicating that we can match 0 or 1 time oftimeshere. Thus this singleone_parseparses both of the following sentences:- blink my top light in red
- blink my top light twice in red
Note that
one_parsedoesn’t parse sentences 2 and 3 above, which contain coordination:- coordination: set my top light to red and change middle light to yellow
- coordination: set my top light to red and change middle light to yellow and flash bottom light twice in blue
thus on line 10 we concatenated
one_parsetwo and three times to make parses:GOAL = one_parse | one_parse + one_parse | one_parse + one_parse + one_parse
line 10 is ugly however. Alternatively we can write:
GOAL = one_parse * [1, 3] # or: GOAL = one_parse * (1, 3)
meaning that a
GOALcontains aone_parseone to three times. But then it is not flexible: what if there’s a fourth coordination? So we simply change it to:GOAL = one_parse * (1, ) # one or more times, but better with: GOAL = OneOrMore(one_parse)
Now our
GOALcan parse however manyone_parse‘s usingparsetron.OneOrMore!Note
You can freely define all kinds of variables in your grammar, but then have to define a
GOALso the parser knows where to start.GOALhere is equivalent to what conventionally is called the START symbolSin CFGs.Warning
The
|operator has lower precedence than the+operator. Thus the following code:a = b | c + d
is equal to:
a = b | (c + d)
rather than:
a = (b | c) + d
Finally we have a very simple grammar defined for smart light:
1 2 3 4 5 6 7 8 9 10 | from parsetron import *
class LightGrammar(Grammar):
action = Set(['change', 'flash', 'set', 'blink'])
light = Set(['top', 'middle', 'bottom'])
color = Regex(r'(red|yellow|blue|orange|purple|...)')
times = Set(['once', 'twice', 'three times']) | Regex(r'\d+ times')
one_parse = action + light + Optional(times) + color
GOAL = OneOrMore(one_parse)
|
Let’s Parse It¶
To parse sentences, we first construct a parsetron.RobustParser, then
call its parsetron.RobustParser.parse() function:
parser = RobustParser(LightGrammar()
sents = ["set my top light to red",
"set my top light to red and change middle light to yellow",
"set my top light to red and change middle light to yellow and flash bottom light twice in blue"]
for sent in sents:
tree, result = parser.parse(sent)
print '"%s"' % sent
print "parse tree:"
print tree
print "parse result:"
print result
print
And here’s the output:
"set my top light to red"
parse tree:
(GOAL
(one_parse
(action "set")
(light "top")
(color "red")
)
)
parse result:
{
"one_parse": [
{
"action": "set",
"one_parse": [
"set",
"top",
"red"
],
"color": "red",
"light": "top"
}
],
"GOAL": [
[
"set",
"top",
"red"
]
]
}
"set my top light to red and change middle light to yellow"
parse tree:
(GOAL
(one_parse
(action "set")
(light "top")
(color "red")
)
(one_parse
(action "change")
(light "middle")
(color "yellow")
)
)
parse result:
{
"one_parse": [
{
"action": "set",
"one_parse": [
"set",
"top",
"red"
],
"color": "red",
"light": "top"
},
{
"action": "change",
"one_parse": [
"change",
"middle",
"yellow"
],
"color": "yellow",
"light": "middle"
}
],
"GOAL": [
[
"set",
"top",
"red"
],
[
"change",
"middle",
"yellow"
]
]
}
"set my top light to red and change middle light to yellow and flash bottom light twice in blue"
parse tree:
(GOAL
(one_parse
(action "set")
(light "top")
(color "red")
)
(one_parse
(action "change")
(light "middle")
(color "yellow")
)
(one_parse
(action "flash")
(light "bottom")
(Optional(times)
(times
(Set(three times|twice|once) "twice")
)
)
(color "blue")
)
)
parse result:
{
"one_parse": [
{
"action": "set",
"one_parse": [
"set",
"top",
"red"
],
"color": "red",
"light": "top"
},
{
"action": "change",
"one_parse": [
"change",
"middle",
"yellow"
],
"color": "yellow",
"light": "middle"
},
{
"one_parse": [
"flash",
"bottom",
"twice",
"blue"
],
"color": "blue",
"Set(three times|twice|once)": "twice",
"Optional(times)": "twice",
"times": "twice",
"light": "bottom",
"action": "flash"
}
],
"GOAL": [
[
"set",
"top",
"red"
],
[
"change",
"middle",
"yellow"
],
[
"flash",
"bottom",
"twice",
"blue"
]
]
}
The parsetron.RobustParser.parse() function returns a tuple of
(parse tree, parse result):
parse treeis aparsetron.TreeNodeclass, mainly for the purpose of eye-checking results.parse resultis aparsetron.ParseResultclass. It is converted fromparse treeand allows intuitive item or attribute setting and getting. For instance:In [7]: result['one_parse'] Out[7]: [{'action': 'set', 'one_parse': ['set', 'top', 'red'], 'color': 'red', 'light': 'top'}, {'action': 'change', 'one_parse': ['change', 'middle', 'yellow'], 'color': 'yellow', 'light': 'middle'}, {'one_parse': ['flash', 'bottom', 'twice', 'blue'], 'color': 'blue', 'light': 'bottom', 'Optional(times)': 'twice', 'times': 'twice', 'Set(Set(three times|twice|once))': 'twice', 'action': 'flash'}] In [8]: result.one_parse Out[8]: [{'action': 'set', 'one_parse': ['set', 'top', 'red'], 'color': 'red', 'light': 'top'}, {'action': 'change', 'one_parse': ['change', 'middle', 'yellow'], 'color': 'yellow', 'light': 'middle'}, {'one_parse': ['flash', 'bottom', 'twice', 'blue'], 'color': 'blue', 'light': 'bottom', 'Optional(times)': 'twice', 'times': 'twice', 'Set(Set(three times|twice|once))': 'twice', 'action': 'flash'}] In [9]: len(result.one_parse) Out[9]: 3 In [10]: result.one_parse[0].color Out[10]: 'red'
Note here how parsetron has extracted variable names from the LightGrammar
class to its parse tree and parse result, both explicitly and implicitly.
Take the last sentence:
{ 'GOAL': [ ['set', 'top', 'red'],
['change', 'middle', 'yellow'],
['flash', 'bottom', 'twice', 'blue']],
'one_parse': [ {'action': 'set', 'one_parse': ['set', 'top', 'red'], 'color': 'red', 'light': 'top'},
{'action': 'change', 'one_parse': ['change', 'middle', 'yellow'], 'color': 'yellow', 'light': 'middle'},
{'one_parse': ['flash', 'bottom', 'twice', 'blue'], 'color': 'blue', 'light': 'bottom', 'Optional(times)': 'twice', 'times': 'twice', 'Set(Set(three times|twice|once))': 'twice', 'action': 'flash'}]}
The implicitly constructed variable names, such as Optional(times), are
also present in the result.
The values in parsing results cover the parsed lexicon while respecting the
grammar structures. Thus GOAL above contains a list of three items, each
item is a list of lexical strings itself, corresponding to one one_parse.
parsetron also tries to flatten the result as much as possible when there is
no name conflict. Thus unlike in the parse tree, here one_parse is
in parallel with GOAL, instead of under GOAL. In this way we can
easily access deep items, such as:
In [11]: result.one_parse[2].times
Out[11]: 'twice'
Otherwise, we would have used something like the following, which is very inconvenient:
In [11]: result.GOAL.one_parse[2]['Optional(times)']['times']['Set(Set(three times|twice|once)']
Out[11]: 'twice'
Convert to API Calls¶
With the parse result in hand, we could easily extract one_parse‘s from
the result and call the Philips Hue APIs. We use the python interface
phue for interacting with
the hue:
# pip install phue
from phue import Bridge
b = Bridge('ip_of_your_bridge')
b.connect()
for one_parse in result.one_parse:
if one_parse.action != 'flash':
b.set_light(one_parse.light, 'xy', color2xy(one_parse.color))
else:
# turn on/off a few times according to one_parse.times
The above code calls an external function color2xy() to convert a string
color name to its XY values,
which we do not specify here. But more information can be found in
core concepts
of Hue.
Calling external APIs is beyond scope of this tutorial. But we have a simple working system called firefly for your reference.
Advanced Usage¶
So far we have introduced briefly how to parse natural language texts into actions with a minimal grammar for smart lights. But parsetron is capable of doing much more than that, for instance:
one_parse.timesis a string (e.g., “three times”), we’d like to see instead an integer value (e.g., 3);one_parse.coloris also a string (.e.g., “red”, maybe we can directly output its RGB (e.g., (255, 0, 0)) or XY value from the parser too?
In the next page we introduce the parsetron.GrammarElement.set_result_action()
function to post process parse results.
Corresponding code of this tutorial is hosted on Github.