shellbot.machines package¶
Submodules¶
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'¶
-
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_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 asg
orç
9
- A digit such as0
or2
+
- When following#
or9
, 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
-
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 asg
orç
9
- A digit, such as0
or2
+
- When following#
or9
, 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:
A machine instance is created and configured:
a_bot = ShellBot(...) machine = Machine(bot=a_bot) machine.set(states=states, transitions=transitions, ...
The machine is switched on and ticked at regular intervals:
machine.start()
Machine can process more events than ticks:
machine.execute('hello world')
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')
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'))
-
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")
.
-
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.
-
-
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
-
-
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
-