Reporting Failures
So far, we’ve only dealt with successful examples: examples where the phrase has a solution. Let’s see what happens when we say a phrase that has no solution:
# "a file is large" in a world with no large files
def Example10():
# Note neither file is "large" now
state = State([Folder(name="Desktop"),
Folder(name="Documents"),
File(name="file1.txt", size=100),
File(name="file2.txt", size=100)])
# Start with an empty dictionary
mrs = {}
# Set its "index" key to the value "e2"
mrs["Index"] = "e2"
# Set its "Variables" key to *another* dictionary with
# keys that represent the variables. Each of those has a "value" of
# yet another dictionary that holds the properties of the variables
# For now we'll just fill in the SF property
mrs["Variables"] = {"x3": {},
"i1": {},
"e2": {"SF": "prop"}}
mrs["RELS"] = TreePredication(0, "_a_q", ["x3",
TreePredication(1, "_file_n_of", ["x3", "i1"]),
TreePredication(2, "_large_a_1", ["e2", "x3"])])
respond_to_mrs(state, mrs)
# Outputs:
Nothing gets printed out because we haven’t implemented a way to report errors from the system. Now it is time to dig into failures.
Failure Codes and Data
To centralize error reporting and make the code easy to maintain, we will separate the notion of the error code from the actual text that gets shown to the user. That way, we can report the same error in multiple places but change the wording shown to the user in one place.
The format will be a simple list. The first list item is a string representing the error code, and the rest of the list is whatever information is needed to generate the string later. For example:
["notAThing", "dog", "car"]
… could later be used to generate the string:
A "dog" is not a "car"
Recording Errors
Recall from the conceptual section on reporting errors that the best error is usually the deepest error, the one which was generated at the deepest part of the predication tree when traversing it in a depth-first manner. To track current depth, and allow for reporting errors, we’ll build a new class called ExecutionContext
and create a single global instance of it. Our MRS solver code will be modified to use it to record the current depth as the tree is traversed, and the predication implementations will record their errors in it. ExecutionContext
will use the current depth to only remember the “deepest” error.
Here’s the class, along with its global instance and a helper to retrieve it. It doesn’t yet include the changes to call()
needed to record the current predication index, we’ll do that next:
class ExecutionContext(object):
def __init__(self):
self._error = None
self._error_predication_index = -1
self._predication_index = -1
def deepest_error(self):
return self._error
def report_error(self, error):
if self._error_predication_index < self._predication_index:
self._error = error
self._error_predication_index = self._predication_index
# Create a global execution context
execution_context = ExecutionContext()
# Helper to access the global context so code is isolated from
# how we manage it
def context():
return execution_context
Now we can modify our main entry point, respond_to_mrs
to start using this. It will retrieve the error string if there were no solutions and use it for each sentence type in the failure case. Here we can see the changes for the ‘prop’ sentence type:
...
error = generate_message(state, context().deepest_error()) if len(solutions) == 0 else None
...
if force == "prop":
...
if len(solutions) > 0:
print("Yes, that is true.")
else:
print(f"No, that isn't correct:{error}")
etc.
… and here is the full code:
def respond_to_mrs(state, mrs):
# Collect all the solutions to the MRS against the
# current world state
solutions = []
for item in call(vocabulary, state, mrs["RELS"]):
solutions.append(item)
error = generate_message(state, context().deepest_error()) if len(solutions) == 0 else None
force = sentence_force(mrs)
if force == "prop":
# This was a proposition, so the user only expects
# a confirmation or denial of what they said.
# The phrase was "true" if there was at least one answer
if len(solutions) > 0:
print("Yes, that is true.")
else:
print(f"No, that isn't correct:{error}")
elif force == "ques":
# See if this is a "WH" type question
wh_predication = find_predication(mrs["RELS"], "_which_q")
if wh_predication is None:
# This was a simple question, so the user only expects
# a yes or no.
# The phrase was "true" if there was at least one answer
if len(solutions) > 0:
print("Yes.")
else:
print(f"No, {error}")
else:
# This was a "WH" question
# return the values of the variable asked about
# from the solution
# The phrase was "true" if there was at least one answer
if len(solutions) > 0:
wh_variable = wh_predication.args[0]
for solutions in solutions:
print(solutions.get_binding(wh_variable).value)
else:
print(f"{error}")
elif force == "comm":
# This was a command so, if it works, just say so
# We'll get better errors and messages in upcoming sections
if len(solutions) > 0:
# Collect all the operations that were done
all_operations = []
for solution in solutions:
all_operations += solution.get_operations()
# Now apply all the operations to the original state object and
# print it to prove it happened
final_state = state.apply_operations(all_operations)
print("Done!")
print(final_state.objects)
else:
print(f"Couldn't do that: {error}")
Next, we’ll need to update our functions that traverse the tree to record the current predication index. We’ll do this by moving the call()
and call_predication()
functions to be members of the ExecutionContext
class and update call()
to set the predication index. Here is the only change to call()
:
class ExecutionContext(object):
...
def call(self, vocabulary, state, term):
# See if the first thing in the list is actually a list
# If so, we have a conjunction
if isinstance(term, list):
...
else:
# The first thing in the list was not a list
# so we assume it is just a TreePredication term.
# Evaluate it using call_predication
last_predication_index = self._predication_index
self._predication_index += 1
yield from self.call_predication(vocabulary, state, term)
# Restore it since we are recursing
self._predication_index = last_predication_index
...
… and here is the full code of ExecutionContext
now:
class ExecutionContext(object):
def __init__(self):
self._error = None
self._error_predication_index = -1
self._predication_index = -1
def deepest_error(self):
return self._error
def report_error(self, error):
if self._error_predication_index < self._predication_index:
self._error = error
self._error_predication_index = self._predication_index
def call(self, vocabulary, state, term):
# See if the first thing in the list is actually a list
# If so, we have a conjunction
if isinstance(term, list):
# If "term" is an empty list, we have solved all
# predications in the conjunction, return the final answer.
# "len()" is a built-in Python function that returns the
# length of a list
if len(term) == 0:
yield state
else:
# This is a list of predications, so they should
# be treated as a conjunction.
# call each one and pass the state it returns
# to the next one, recursively
for nextState in self.call(vocabulary, state, term[0]):
# Note the [1:] syntax which means "return a list
# of everything but the first item"
yield from self.call(vocabulary, nextState, term[1:])
else:
# The first thing in the list was not a list
# so we assume it is just a TreePredication term.
# Evaluate it using call_predication
last_predication_index = self._predication_index
self._predication_index += 1
yield from self.call_predication(vocabulary, state, term)
# Restore it since we are recursing
self._predication_index = last_predication_index
# Takes a TreePredication object, maps it to a Python function and calls it
def call_predication(self, vocabulary, state, predication):
# Look up the actual Python module and
# function name given a string like "folder_n_of".
# "vocabulary.Predication" returns a two-item list,
# where item[0] is the module and item[1] is the function
module_function = vocabulary.predication(predication)
if module_function is None:
raise NotImplementedError(f"Implementation for Predication {predication} not found")
# sys.modules[] is a built-in Python list that allows you
# to access actual Python Modules given a string name
module = sys.modules[module_function[0]]
# Functions are modeled as properties of modules in Python
# and getattr() allows you to retrieve a property.
# So: this is how we get the "function pointer" to the
# predication function we wrote in Python
function = getattr(module, module_function[1])
# [list] + [list] will return a new, combined list
# in Python. This is how we add the state object
# onto the front of the argument list
predication_args = predication.args
function_args = [state] + predication_args
# You call a function "pointer" and pass it arguments
# that are a list by using "function(*function_args)"
# So: this is actually calling our function (which
# returns an iterator and thus we can iterate over it)
for next_state in function(*function_args):
yield next_state
The system will now remember which is the right (“deepest”) error to report. The next section will describe what the error should say. This is not as obvious as it might seem.
Comprehensive source for the completed tutorial is available here
Last update: 2024-10-24 by Eric Zinda [edit]