AutoGRAMS interpreter
This section goes into a greater depth on how autograms are actually interpreted
Detailed overview of autogram.reply()
autogram.reply() is the main point for entering a program that should be executed as a chatbot
The outer loop of autogram.reply() has 6 main steps
-
get variable output of previous node (skip this step if no previous node)
This calls the node.get_variable_output() method, which returns the variable output of the node. The specific behavior of this will depend on the node type, but this is often the text generated by the chatbot.
-
assign variables assigned in previous node to memory (skip this step if no previous node)
If any variable outputs were in the previous node's instruction, the memory object assigns the nodes variable output to a variable with the name defined in that instruction. This goes in the top level of the memory objects stack
-
apply transition function from previous node to get new_node_id (skip this step if no previous node)
This calls the node.apply_transition() method to get an unprocessed new_node_id. If there is only 1 possible transition in the
transitions
list of the node, then the result will usually this transition.If there are multiple transitions, the transition will depend on what the classifier predicts.There also may be interjection transitions if the node is a chat type. If the node is calling a function, the new_node_id will be the root node being called. -
post-process the new_node_id (skip this step if no previous node) Usually, the
new_node_id
output byapply_transition()
corresponds to another node in the graph, and the next step will be to simply move to that node. However, some special types of transitions do not, and require extra post processing. -
get the new node and apply its instruction This calls the node.apply_instruction() method to execute an instruction
-
return a result if the new node is a chat node, continue the loop otherwise
Here is a diagram of the outerloop of the autogram.reply() method:
Detailed overview of autogram.apply_fn()
The outer loop of autogram.apply_fn() is very similar to autogram.reply(), other than the last step to check the exit criteria
-
get variable output of previous node (skip this step if no previous node)
-
assign variables assigned in previous node to memory (skip this step if no previous node)
-
apply transition function from previous node to get new_node_id (skip this step if no previous node)
-
post-process the new_node_id (skip this step if no previous node)
-
get the new node and apply its instruction
-
check if we are exiting the function
Post processing the transtion
The process_node_id()
function in autograms/autogram_utils.py is used to post process new_node_id
variable. If the new_node_id is the name of the node, then no post processing is needed. However certain special transitions require post processing, including:
'.n' transitions -- a transition name with the suffix .n (for instance 'state1.n') is assumed to have a different version of state for the nth visit to that state.
'.' transitions -- a transition name with the suffix . (for instance 'state1.*') is assumed to have several possible transitions, and is often used for if/else like logic
'return' -- pop the stack in the memory object, and return to the last function node and function scope.
dynamic transitions -- transitions that use $ syntax to set the transition dynamically depending on a string variable
interpreting python statements
Python statements (as well as the boolean_condition
field of nodes with .a/.b/.c suffixes, and arguments to AutoGRAMS functions) are interpreted by the StatementInterpreter class /autograms/statement_interpreter.py
. At initialization, the StatementInterpreter uses the AutogramConfig to load all python imports that will be allowed within the scope of the program. It also overrides any python builtins that are not explicitly listed in the AutogramConfig to prevent them from being called.
When the StatementInterpreter called from a PythonNode (action="python_function"), it first parses out any variable assignments that are in the code. It then executes the code using the python eval
command, which unlike the exec
command, returns the resulting value of the code expression it evaluates. The python eval
command takes in 3 argument
- the code string-- this is the instruction in the PythonNode with any variable assignments parsed out
- global variables -- this is set to an empty dictionary
- local variables -- The statement interpreter includes all variables that are in scope, as well as all python modules/functions that it loaded during initialization. This includes functions that override any built ins that have been disabled, which will throw an exception if called.
The statement interpreter returns the name of the variable being assigned if any, as well as the result of applying eval()
to the code
calling an AutoGRAMS function
FunctionNodes in AutoGRAMS have special behavior during ApplyTransition() and ApplyInstruction() to facilitate the calling of functions. When a FunctionNode is encountered, it must first make sure it is a call and not a return from a previous call, which it can check using the memory_object.is_return() method that checks if the previous transition was a return. If it is a call, it checks whether certain requirements have been met--it is possible to gate function calls based on whether certain nodes have been visited. If no requirements are set or the requirements have been met, then the function must parse the name of the new node and the arguments from it's instruction. It also collects the arguments that the calling nodes accepts. It uses the memory object to find the variables in the current scope that match the calling argument names. Then it creates a dictionary that maps these variables to the new variable names set by the node being called. Finally, it calls the MemoryObject.manage_call() method to initialize a function call with the appropriate variables. manage_call() appends a new element to the memory stack, which will include the new variables that are initialized as arguments to the function. Finally, when we reach the apply_transition() method at the next iteration, the new_node_id
will be set to the node being called at the root of the function's graph.
returning from an AutoGRAMS function
The start of the return process first happens when the process_node_id()
function in autograms/autogram_utils.py
encounters a return transition. The new_node_id
will be set to the node name of the calling FunctionNode
. The memory_object.manage_return()
function will pop the stack to facilitate the reduction in scope depth. Depending on the type of the function (global, local, regular function) different information will be saved to the previous layer of the stack from the layer being popped. the return of the function, if any, will be saved to a temporary variable in the previous layer of the stack. The return is finalized when we revisit the FunctionNode that called the function, and hit its get_variable_output() method, which will return the temporary return variable and delete it from the memory object. It will be saved in a new variable if the function calling node used a variable assignment in its instruction.
An diagram showing the memory object's stack during the calling, execution, and returning of a function is given below.