Fly with Buzz – “To infinity and beyond”¶
In this example we deal with commands that take significant time to execute. How to run long-lasting transactions in the background, so that the bot stays responsive?
View the source code of this example
Buzz is flying from Earth to some planets and come back. Obviously, this is the kind of activity that can take ages, yet here each mission lasts about 30 seconds.
Ok. So, when I type explore Uranus
in the chat box, do I have to
wait for 30 seconds before the next command is considered? Hopefully not!
How to execute commands asynchronously?¶
The two commands explore
and blast
are non-interactive. This means
that they are pushed to a pipeline for background execution.
With this concept, you can get a dialog similar to the following:
> buzz explore Mercury
Ok, I am working on it
#1 - Departing to Mercury
> buzz blast Neptune
Ok, will work on it as soon as possible
#1 - Approaching Mercury
#1 - Landed on Mercury
> buzz planets
Available destinations:
- Venus
- Moon
...
In other terms, the bot is always responsive, whatever is executing in the background. Also, non-interactive commands are executed in the exact sequence of their submission.
These concepts are implemented with instances of Rocket
that are attached
to bots (Rocket source code).
Every rocket has a queue that receives commands submitted
in the chat box. And of course, every rocket is running a separate process
to pick up new missions and to execute them.
How to attach a rocket and make it fly, for every bot?¶
Since the objective is that each bot has its own rocket attached, we provide with a custom driver that does exactly this:
class FlyingBot(ShellBot): # add a rocket to each bot
def on_init(self):
self.rocket = Rocket(self)
self.rocket.start()
Then the engine is instructed to use this custom driver instead of the regular one:
engine = Engine(driver=FlyingBot, ...)
With this way of working, each time the bot is invited to a channel (direct or group), a new rocket is instantiated and ready to go.
How can a command interact with the rocket?¶
The command delegates a new mission with a simple function call, like for
example in the command explore
:
class Explore(Command):
keyword = u'explore'
information_message = u'Explore a planet and come back'
usage_message = u'explore <destination>'
def execute(self, bot, arguments=None, **kwargs):
"""
Explores a planet and comes back
"""
if not arguments:
bot.say(u"usage: {}".format(self.usage_message))
return
bot.rocket.go('explore', arguments)
On rocket side, the mission is pushed to a queue for later processing:
def go(self, action, planet):
"""Engages a new mission"""
self.inbox.put((action, planet))
Within the rocket instance, a process is continuously monitoring the
inbox
queue to pick up new missions and to execute them, one at a time.
How to store data separately for each bot?¶
With shellbot, each bot is coming with its own data store, that is distinct
from data stores of other bots.
Content of the bot store can be statically initialized by the engine itself, if
settings starting with the label bot.store
are provided. This mechanism is
used in this example for listing available planets:
engine.set(
'bot.store.planets',
['Mercury', 'Venus', 'Moon', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune'])
The function bot.recall()
can then be used to retrieve the list of
planets. This is exactly what is done for the command planets
:
class Planets(Command):
keyword = u'planets'
information_message = u'List reachable planets'
list_header = u"Available destinations:"
def execute(self, bot, arguments=None, **kwargs):
"""
Displays the list of available planets
"""
items = bot.recall('planets', [])
if len(items):
bot.say(self.list_header
+ '\n- ' + '\n- '.join(items))
else:
bot.say(u"Nowhere to go right now.")
When a planet has been blasted, it is removed from the data store with code similar to this:
items = self.bot.recall('planets', [])
items.remove(planet)
self.bot.remember('planets', items)
Keep in mind that the list of available planets evolve over time,
since some of them can be nuked by end users. So, if Mercury is blasted in one
channel, and Neptune in another channel, there is a need for independent
management of planets across bots. This is exactly what bot.remember()
and
bot.recall()
provide, hopefully.
Commands: planets, explore, blast¶
- planets
- provides a list of available destinations
- explore <planet>
- you then track in real-time the progress of the mission
- blast <planet>
- similar to exploration, except that the planet is nuked
How to run this example?¶
To run this script you have to provide a custom configuration, or set environment variables instead:
CHANNEL_DEFAULT_PARTICIPANTS
- Mention at least your e-mail addressCISCO_SPARK_BOT_TOKEN
- Received from Cisco Spark on bot registrationSERVER_URL
- Public link used by Cisco Spark to reach your server
The token is specific to your run-time, please visit Cisco Spark for Developers to get more details:
For example, if you run this script under Linux or macOs with support from ngrok for exposing services to the Internet:
export CHANNEL_DEFAULT_PARTICIPANTS="alice@acme.com"
export CISCO_SPARK_BOT_TOKEN="<token id from Cisco Spark for Developers>"
export SERVER_URL="http://1a107f21.ngrok.io"
python buzz.py