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
LightGrammar
class that extends a standardparsetron.Grammar
. Defining grammars in classes helps modularization.on lines 4-5, we used a
parsetron.Set
class 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.Regex
builds 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.Or
relation to specify alternative ways of representingtimes
:times = Set(['once', 'twice', 'three times']) | Regex(r'\d+ times')
So
times
can either match “three times”, or “3 times”.on line 9, we defined a
one_parse
of 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.And
relation, matching a sequence of tokens. For unknown words parsetron simply ignores them. Theparsetron.Optional
class is a kind of syntactic sugar indicating that we can match 0 or 1 time oftimes
here. Thus this singleone_parse
parses both of the following sentences:- blink my top light in red
- blink my top light twice in red
Note that
one_parse
doesn’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_parse
two 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
GOAL
contains aone_parse
one 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
GOAL
can 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
GOAL
so the parser knows where to start.GOAL
here is equivalent to what conventionally is called the START symbolS
in 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 tree
is aparsetron.TreeNode
class, mainly for the purpose of eye-checking results.parse result
is aparsetron.ParseResult
class. It is converted fromparse tree
and 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.times
is a string (e.g., “three times”), we’d like to see instead an integer value (e.g., 3);one_parse.color
is 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.