shellbot.machines package

Module contents

class shellbot.machines.Input(bot=None, states=None, transitions=None, initial=None, during=None, on_enter=None, on_exit=None, **kwargs)[source]

Bases: shellbot.machines.base.Machine

Asks for some input

This implements a state machine that can get one piece of input from chat participants. It can ask a question, wait for some input, check provided data and provide guidance when needed.

Example:

machine = Input(bot=bot, question="PO Number?", key="order.id")
machine.start()
...

In normal operation mode, the machine asks a question in the chat space, then listen for an answer, captures it, and stops.

When no adequate answer is provided, the machine will provide guidance in the chat space after some delay, and ask for a retry. Multiple retries can take place, until correct input is provided, or the machine is timed out.

The machine can also time out after a (possibly) long duration, and send a message in the chat space when giving up.

If correct input is mandatory, no time out will take place and the machine will really need a correct answer to stop.

Data that has been captured can be read from the machine itself. For example:

value = machine.get('answer')

If the machine is given a key, this is used for feeding the bot store. For example:

machine.build(key='my_field', ...)
...

value = bot.recall('input')['my_field']

The most straightforward way to process captured data in real-time is to subclass Input, like in the following example:

class MyInput(Input):

    def on_input(self, value):
        mail.send_message(value)

machine = MyInput(...)
machine.start()
ANSWER_MESSAGE = u'Ok, this has been noted'
CANCEL_DELAY = 40.0
CANCEL_MESSAGE = u'Ok, forget about it'
RETRY_DELAY = 20.0
RETRY_MESSAGE = u'Invalid input, please retry'
ask()[source]

Asks the question in the chat space

cancel()[source]

Cancels the question

Used by the state machine on time out

elapsed

Measures time since the question has been asked

Used in the state machine for repeating the question and on time out.

execute(arguments=None, **kwargs)[source]

Receives data from the chat

Parameters:arguments (str) – data captured from the chat space

This function checks data that is provided, and provides guidance if needed. It can extract information from the provided mask or regular expression, and save it for later use.

filter(text)[source]

Filters data from user input

Parameters:text (str) – Text coming from the chat space
Returns:Data to be captured, or None

If a mask is provided, or a regular expression, they are used to extract useful information from provided data.

Example to read a PO mumber:

machine.build(mask='9999A', ...)
...

po = machine.filter('PO Number is 2413v')
assert po == '2413v'
listen()[source]

Listens for data received from the chat space

This function starts a separate process to scan the bot.fan queue until time out.

on_inbound(**kwargs)[source]

Updates the chat on inbound message

on_init(question=None, question_content=None, mask=None, regex=None, on_answer=None, on_answer_content=None, on_answer_file=None, on_retry=None, on_retry_content=None, on_retry_file=None, retry_delay=None, on_cancel=None, on_cancel_content=None, on_cancel_file=None, cancel_delay=None, is_mandatory=False, key=None, **kwargs)[source]

Asks for some input

Parameters:
  • question (str) – Message to ask for some input
  • question_content (str) – Rich message to ask for some input
  • mask (str) – A mask to filter the input
  • regex (str) – A regular expression to filter the input
  • on_answer (str) – Message on successful data capture
  • on_answer_content (str in Markdown or HTML format) – Rich message on successful data capture
  • on_answer_file (str) – File to be uploaded on successful data capture
  • on_retry (str) – Message to provide guidance and ask for retry
  • on_retry_content (str in Markdown or HTML format) – Rich message on retry
  • on_retry_file (str) – File to be uploaded on retry
  • retry_delay (int) – Repeat the on_retry message after this delay in seconds
  • on_cancel (str) – Message on time out
  • on_cancel_content (str in Markdown or HTML format) – Rich message on time out
  • on_cancel_file (str) – File to be uploaded on time out
  • is_mandatory (boolean) – If the bot will insist and never give up
  • cancel_delay (int) – Give up on this input after this delay in seconds
  • key (str) – The label associated with data captured in bot store

If a mask is provided, it is used to filter provided input. Use following conventions to build the mask:

  • A - Any kind of unicode symbol such as g or ç
  • 9 - A digit such as 0 or 2
  • + - When following # or 9, indicates optional extensions
    of the same type
  • Any other symbol, including punctuation or white space, has to match
    exactly.

For example:

  • 9999A will match 4 digits and 1 additional character
  • #9-A+ will match #3-June 2017

Alternatively, you can provide a regular expression (regex) to extract useful information from the input.

You can use almost every regular expression that is supported by python. If parenthesis are used, the function returns the first matching group.

For example, you can capture an identifier with a given prefix:

machine.build(question="What is the identifier?",
              regex=r'ID-\d\w\d+', ...)
...

id = machine.filter('The id is ID-1W27 I believe')
assert id == 'ID-1W27'

As a grouping example, you can capture a domain name by asking for some e-mail address like this:

machine.build(question="please enter your e-mail address",
              regex=r'@([\w.]+)', ...)
...

domain_name = machine.filter('my address is foo.bar@acme.com')
assert domain_name == 'acme.com'
on_input(value, **kwargs)[source]

Processes input data

Parameters:value (str) – data that has been captured

This function is called as soon as some input has been captured. It can be overlaid in subclass, as in the following example:

class MyInput(Input):

    def on_input(self, value):
        mail.send_message(value)

machine = MyInput(...)
machine.start()

The extra parameters wil be used in case of attachment with the value.

receive()[source]

Receives data from the chat space

This function implements a loop until some data has been actually captured, or until the state machine stops for some reason.

The loop is also stopped when the parameter general.switch is changed in the context. For example:

engine.set('general.switch', 'off')
say_answer(input)[source]

Responds on correct capture

Parameters:input (str) – the text that has been noted
say_cancel()[source]

Says that input has been timed out

say_retry()[source]

Provides guidance on retry

search_expression(regex, text)[source]

Searches for a regular expression in text

Parameters:
  • regex (str) – A regular expression to be matched
  • text (str) – The string from the chat space
Returns:

either the matching expression, or None

You can use almost every regular expression that is supported by python. If parenthesis are used, the function returns the first matching group.

For example, you can capture an identifier with a given prefix:

machine.build(question="What is the identifier?",
              regex=r'ID-\d\w\d+', ...)
...

id = machine.filter('The id is ID-1W27 I believe')
assert id == 'ID-1W27'

As a grouping example, you can capture a domain name by asking for some e-mail address like this:

machine.build(question="please enter your e-mail address",
              regex=r'@([\w.]+)', ...)
...

domain_name = machine.filter('my address is foo.bar@acme.com')
assert domain_name == 'acme.com'
search_mask(mask, text)[source]

Searches for structured data in text

Parameters:
  • mask (str) – A simple expression to be searched
  • text (str) – The string from the chat space
Returns:

either the matching expression, or None

Use following conventions to build the mask:

  • A - Any kind of unicode symbol, such as g or ç
  • 9 - A digit, such as 0 or 2
  • + - When following # or 9, indicates optional extensions
    of the same type
  • Any other symbol, including punctuation or white space, has to match
    exactly.

Some mask examples:

  • 9999A will match 4 digits and 1 additional character
  • #9-A+ will match #3-June 2017

Example to read a PO mumber:

machine.build(question="What is the PO number?",
              mask='9999A', ...)
...

po = machine.filter('PO Number is 2413v')
assert po == '2413v'
class shellbot.machines.Machine(bot=None, states=None, transitions=None, initial=None, during=None, on_enter=None, on_exit=None, **kwargs)[source]

Bases: object

Implements a state machine

The life cycle of a machine can be described as follows:

  1. A machine instance is created and configured:

    a_bot = ShellBot(...)
    machine = Machine(bot=a_bot)
    
    machine.set(states=states, transitions=transitions, ...
    
  2. The machine is switched on and ticked at regular intervals:

    machine.start()
    
  3. Machine can process more events than ticks:

    machine.execute('hello world')
    
  4. When a machine is expecting data from the chat space, it listens from the fan queue used by the shell:

    engine.fan.put('special command')
    
  5. When the machine is coming end of life, resources can be disposed:

    machine.stop()
    

credit: Alex Bertsch <abertsch@dropbox.com> securitybot/state_machine.py

DEFER_DURATION = 0.0
TICK_DURATION = 0.2
build(states, transitions, initial, during=None, on_enter=None, on_exit=None)[source]

Builds a complete state machine

Parameters:
  • states (list of str) – All states supported by this machine
  • transitions (list of dict) –

    Transitions between states. Each transition is a dictionary. Each dictionary must feature following keys:

    source (str): The source state of the transition target (str): The target state of the transition
    Each dictionary may contain following keys:
    condition (function): A condition that must be true for the
    transition to occur. If no condition is provided then the state machine will transition on a step.
    action (function): A function to be executed while the
    transition occurs.
  • initial (str) – The initial state
  • during (dict) – A mapping of states to functions to execute while in that state. Each key should map to a callable function.
  • on_enter (dict) – A mapping of states to functions to execute when entering that state. Each key should map to a callable function.
  • on_exit (dict) – A mapping of states to functions to execute when exiting that state. Each key should map to a callable function.
current_state

Provides current state

Returns:State

This function raises AttributeError if it is called before build().

execute(arguments=None, **kwargs)[source]

Processes data received from the chat

Parameters:arguments (str is recommended) – input to be injected into the state machine

This function can be used to feed the machine asynchronously

get(key, default=None)[source]

Retrieves the value of one key

Parameters:
  • key (str) – one attribute of this state machine instance
  • default (an type that can be serialized) – default value is the attribute has not been set yet

This function can be used across multiple processes, so that a consistent view of the state machine is provided.

is_running

Determines if this machine is runnning

Returns:True or False
on_init(**kwargs)[source]

Adds to machine initialisation

This function should be expanded in sub-class, where necessary.

Example:

def on_init(self, prefix='my.machine', **kwargs):
    ...
on_reset()[source]

Adds processing to machine reset

This function should be expanded in sub-class, where necessary.

Example:

def on_reset(self):
    self.sub_machine.reset()
on_start()[source]

Adds to machine start

This function is invoked when the machine is started or restarted. It can be expanded in sub-classes where required.

Example:

def on_start(self):  # clear bot store on machine start
    self.bot.forget()
on_stop()[source]

Adds to machine stop

This function is invoked when the machine is stopped. It can be expanded in sub-classes where required.

Example:

def on_stop(self):  # dump bot store on machine stop
    self.bot.publisher.put(
        self.bot.id,
        self.bot.recall('input'))
on_tick()[source]

Processes one tick

reset()[source]

Resets a state machine before it is restarted

Returns:True if the machine has been actually reset, else False

This function moves a state machine back to its initial state. A typical use case is when you have to recycle a state machine multiple times, like in the following example:

if new_cycle():
    machine.reset()
    machine.start()

If the machine is running, calling reset() will have no effect and you will get False in return. Therefore, if you have to force a reset, you may have to stop the machine first.

Example of forced reset:

machine.stop()
machine.reset()
restart(**kwargs)[source]

Restarts the machine

This function is very similar to reset(), except that it also starts the machine on successful reset. Parameters given to it are those that are expected by start().

Note: this function has no effect on a running machine.

run()[source]

Continuously ticks the machine

This function is looping in the background, and calls step(event='tick') at regular intervals.

The recommended way for stopping the process is to call the function stop(). For example:

machine.stop()

The loop is also stopped when the parameter general.switch is changed in the context. For example:

engine.set('general.switch', 'off')
set(key, value)[source]

Remembers the value of one key

Parameters:
  • key (str) – one attribute of this state machine instance
  • value (an type that can be serialized) – new value of the attribute

This function can be used across multiple processes, so that a consistent view of the state machine is provided.

start(tick=None, defer=None)[source]

Starts the machine

Parameters:
  • tick (positive number) – The duration set for each tick (optional)
  • defer (positive number) – wait some seconds before the actual work (optional)
Returns:

either the process that has been started, or None

This function starts a separate thread to tick the machine in the background.

state(name)[source]

Provides a state by name

Parameters:name (str) – The label of the target state
Returns:State

This function raises KeyError if an unknown name is provided.

step(**kwargs)[source]

Brings some life to the state machine

Thanks to **kwargs, it is easy to transmit parameters to underlying functions: - current_state.during(**kwargs) - transition.condition(**kwargs)

Since parameters can vary on complex state machines, you are advised to pay specific attention to the signatures of related functions. If you expect some parameter in a function, use ``kwargs.get()``to get its value safely.

For example, to inject the value of a gauge in the state machine on each tick:

def remember(**kwargs):
    gauge = kwargs.get('gauge')
    if gauge:
        db.save(gauge)

during = { 'measuring', remember }

...

machine.build(during=during, ... )

while machine.is_running:
    machine.step(gauge=get_measurement())

Or, if you have to transition on a specific threshold for a gauge, you could do:

def if_threshold(**kwargs):
    gauge = kwargs.get('gauge')
    if gauge > 20:
        return True
    return False

def raise_alarm():
    mail.post_message()

transitions = [

    {'source': 'normal',
     'target': 'alarm',
     'condition': if_threshold,
     'action': raise_alarm},

     ...

    ]

...

machine.build(transitions=transitions, ... )

while machine.is_running:
    machine.step(gauge=get_measurement())

Shellbot is using this mechanism for itself, and the function can be called at various occasions: - machine tick - This is done at regular intervals in time - input from the chat - Typically, in response to a question - inbound message - Received from subscription, over the network

Following parameters are used for machine ticks: - event=’tick’ - fixed value

Following parameters are used for chat input: - event=’input’ - fixed value - arguments - the text that is submitted from the chat

Following parameters are used for subscriptions: - event=’inbound’ - fixed value - message - the object that has been transmitted

This machine should report on progress by sending messages with one or multiple self.bot.say("Whatever message").

stop()[source]

Stops the machine

This function sends a poison pill to the queue that is read on each tick.

class shellbot.machines.Sequence(bot=None, machines=None, **kwargs)[source]

Bases: object

Implements a sequence of multiple machines

This implements one state machine that is actually a combination of multiple sub-machines, ran in sequence. When one sub-machine stops, the next one is activated.

Example:

input_1 = Input( ... )
input_2 = Input( ... )
sequence = Sequence([input_1, input_2])
sequence.start()

In this example, the first machine is started, then when it ends the second machine is triggered.

get(key, default=None)[source]

Retrieves the value of one key

Parameters:
  • key (str) – one attribute of this state machine instance
  • default (an type that can be serialized) – default value is the attribute has not been set yet

This function can be used across multiple processes, so that a consistent view of the state machine is provided.

is_running

Determines if this machine is runnning

Returns:True or False
on_init(**kwargs)[source]

Adds to machine initialisation

This function should be expanded in sub-class, where necessary.

Example:

def on_init(self, prefix='my.machine', **kwargs):
    ...
on_reset()[source]

Adds processing to machine reset

This function should be expanded in sub-class, where necessary.

reset()[source]

Resets a state machine before it is restarted

Returns:True if the machine has been actually reset, else False

This function moves a state machine back to its initial state. A typical use case is when you have to recycle a state machine multiple times, like in the following example:

if new_cycle():
    machine.reset()
    machine.start()

If the machine is running, calling reset() will have no effect and you will get False in return. Therefore, if you have to force a reset, you may have to stop the machine first.

Example of forced reset:

machine.stop()
machine.reset()
run()[source]

Continuously ticks the sequence

This function is looping in the background, and calls the function step() at regular intervals.

The loop is stopped when the parameter general.switch is changed in the context. For example:

bot.context.set('general.switch', 'off')
set(key, value)[source]

Remembers the value of one key

Parameters:
  • key (str) – one attribute of this state machine instance
  • value (an type that can be serialized) – new value of the attribute

This function can be used across multiple processes, so that a consistent view of the state machine is provided.

start()[source]

Starts the sequence

Returns:either the process that has been started, or None

This function starts a separate thread to run machines in the background.

stop()[source]

Stops the sequence

This function stops the underlying machine and breaks the sequence.

class shellbot.machines.Steps(bot=None, states=None, transitions=None, initial=None, during=None, on_enter=None, on_exit=None, **kwargs)[source]

Bases: shellbot.machines.base.Machine

Implements a linear process with multiple steps

This implements a state machine that appears as a phased process to chat participants. On each, it can add new participants, display some information, and run a child state machine..

For example, to run an escalation process:

po_input = Input( ... )
details_input = Input( ... )

decision_menu = Menu( ...)

steps = [

    {
        'label': u'Level 1',
        'message': u'Initial capture of information',
        'machine': Sequence([po_input, details_input]),
    },

    {
        'label': u'Level 2',
        'message': u'Escalation to technical experts',
    },

    {
        'label': u'Level 3',
        'message': u'Escalation to decision stakeholders',
        'participants': 'bob@acme.com',
        'machine': decision_menu,
    },

    {
        'label': u'Terminated',
        'message': u'Process is closed, yet conversation can continue',
    },

]
machine = Steps(bot=bot, steps=steps)
machine.start()
...
current_step

Gets current step

Returns:current step, or None
if_end(**kwargs)[source]

Checks if all steps have been used

Since this function is an integral part of the state machine, it should be triggered via a call of the step() member function.

For example:

machine.step(event='tick')
if_next(**kwargs)[source]

Checks if next step should be engaged

Parameters:event (str) – a label of event submitted to the machine

This function is used by the state machine for testing the transition to the next available step in the process.

Since this function is an integral part of the state machine, it should be triggered via a call of the step() member function.

For example:

machine.step(event='next')
if_ready(**kwargs)[source]

Checks if state machine can engage on first step

To be overlaid in sub-class where complex initialization activities are required.

Example:

class MyMachine(Machine):

    def if_ready(self, **kwargs):
        if kwargs.get('phase') == 'warming up':
            return False
        else:
            return True
next_step()[source]

Moves to next step

Returns:current step, or None

This function loads and runs the next step in the process, if any. If all steps have been consumed it returns None.

If a sub-machine is running at current step, it is stopped before moving to the next step.

on_init(steps=None, **kwargs)[source]

Handles extended initialisation parameters

Parameters:steps (list of Step or list of dict) – The steps for this process
on_reset()[source]

Restore initial state of this machine

If a sub-machine is running at current step, it is stopped first.

step_has_completed(**kwargs)[source]

Checks if the machine for this step has finished its job

Returns:False if the machine is still running, True otherwise
class shellbot.machines.Menu(bot=None, states=None, transitions=None, initial=None, during=None, on_enter=None, on_exit=None, **kwargs)[source]

Bases: shellbot.machines.input.Input

Selects among multiple options

This implements a state machine that can capture a choice from chat participants. It can ask a question, wait for some input, check provided data and provide guidance when needed.

Example:

machine = Menu(bot=bot,
               question="What would you prefer?",
               options=["Some starter and then main course",
                        "Main course and sweety dessert"])
machine.start()
...

if machine.get('answer') == 1:
    prepare_appetizer()
    prepare_main_course()

if machine.get('answer') == 2:
    prepare_main_course()
    prepare_some_cake()

In normal operation mode, the machine asks a question in the chat space, then listen for an answer, captures it, and stops.

When no adequate answer is provided, the machine will provide guidance in the chat space after some delay, and ask for a retry. Multiple retries can take place, until correct input is provided, or the machine is timed out.

The machine can also time out after a (possibly) long duration, and send a message in the chat space when giving up.

If correct input is mandatory, no time out will take place and the machine will really need a correct answer to stop.

Data that has been captured can be read from the machine itself. For example:

value = machine.get('answer')

If the machine is given a key, this is used for feeding the bot store. For example:

machine.build(key='my_field', ...)
...

value = bot.recall('input')['my_field']

The most straightforward way to process captured data in real-time is to subclass Menu, like in the following example:

class MyMenu(Menu):

    def on_input(self, value):
        do_something_with(value)

machine = MyMenu(...)
machine.start()
RETRY_MESSAGE = u'Invalid input, please enter your choice as a number'
ask()[source]

Asks which menu option to select

If a bare question is provided, then text is added to list all available options.

If a rich question is provided, then we assume that it also contains a representation of menu options and displays it ‘as-is’.

filter(text)[source]

Filters data from user input

Parameters:text (str) – Text coming from the chat space
Returns:Text of the selected option, or None
on_init(options=[], **kwargs)[source]

Selects among multiple options

Parameters:
  • question (str) – Message to ask for some input (mandatory)
  • options (list of str) – The options of the menu
  • on_answer (str) – Message on successful data capture
  • on_answer_content (str in Markdown or HTML format) – Rich message on successful data capture
  • on_answer_file (str) – File to be uploaded on successful data capture
  • on_retry (str) – Message to provide guidance and ask for retry
  • on_retry_content (str in Markdown or HTML format) – Rich message on retry
  • on_retry_file (str) – File to be uploaded on retry
  • retry_delay (int) – Repeat the on_retry message after this delay in seconds
  • on_cancel (str) – Message on time out
  • on_cancel_content (str in Markdown or HTML format) – Rich message on time out
  • on_cancel_file (str) – File to be uploaded on time out
  • is_mandatory (boolean) – If the bot will insist and never give up
  • cancel_delay (int) – Give up on this input after this delay in seconds
  • key (str) – The label associated with data captured in bot store