Determining the Right Scope-Resolved MRS
In the previous section, we wrote the code to generate all the MRS parses for a phrase, and all the that result from the MRS. Next we have to decide which one the user intended and write the code to run it. As discussed in the conceptual topic on Choosing a Scope-Resolved MRS, returning the response from the first MRS parse and scope-resolved MRS that succeeded (or failed) is good heuristic to use in general.
To implement the code for choosing the right scope-resolved MRS, we’re going to create a new class that will be the main entry point into the whole system. It is called UserInterface
and its main method is interact_once()
. Each call to that method does a single “command/response” interaction with the system mostly using code we’ve already written. Here’s a summary of its logic:
- Use the code we wrote in the previous topic to convert the phrase to MRS and then generate the scope-resolved MRSs for the MRS, go through them in order.
- Solve the scope-resolved MRSs using a modification to the
call()
function from Conjunctions topic which is calledsolve
. Go through these in order. - If an error occurs when solving a scope-resolved MRS, get a string for it by calling
generate_message_with_index
(which gets passed as an argument tointeract_once
). We built this in the English Domain Names section - When we are done, actually respond using the (slightly refactored)
respond_to_solutions()
function we built in the Propositions Section
The new code is at the end of the function, where we apply all the operations to a single state object and then store it away as the new state. Otherwise, changes would just get discarded and the next interaction wouldn’t see them.
Our new code iterates through every MRS, then every scope-resolved MRS it has. If the function succeeds on a particular scope-resolved MRS, it stops processing. Otherwise, it continues until it finds a succcess or runs out of trees. If nothing succeeds, it will report the first tree failure.
Here is the full code for it:
class UserInterface(object):
def __init__(self, state, vocabulary):
self.max_holes = 13
self.state = state
self.execution_context = ExecutionContext()
self.vocabulary = vocabulary
# response_function gets passed three arguments:
# response_function(mrs, solutions, error)
# It must use them to return a string to say to the user
def interact_once(self, response_function):
# input() pauses the program and waits for the user to
# type input and hit enter, and then returns it
user_input = str(input("? "))
best_failure = None
# Loop through each MRS and each tree that can be
# generated from it...
for mrs_raw in self.mrss_from_phrase(user_input):
for tree in self.trees_from_mrs(mrs_raw):
print(tree)
# Collect all the solutions for this tree against the
# current world state
mrs = {"Index": mrs_raw.index,
"Variables": mrs_raw.variables,
"RELS": tree}
call_state = self.state.set_x("mrs", (mrs, ))
solutions = []
try:
for item in context().solve(vocabulary, call_state, mrs["RELS"]):
solutions.append(item)
except NotImplementedError as e:
print(str(e))
continue
# Determine the response to it
error_text = response_function(call_state,
context().deepest_error_predication_index(),
context().deepest_error()) if len(solutions) == 0 else None
message = respond_to_solutions(call_state, mrs, solutions, error_text)
if len(solutions) > 0:
# This worked, give a response
print(message)
# apply the results to the current world state if it was a command
if sentence_force(mrs) == "comm":
self.state = self.apply_solutions_to_state(self.state, solutions)
print(f"New state: {str(self.state.objects)}")
return
else:
# This failed, remember it if it is the "best" failure
# which we currently define as the first one
if best_failure is None:
best_failure = message
# If we got here, nothing worked: print out the best failure
print(best_failure)
...
Here is the modified ExecutionContext
that has the new solve()
method that is only there to reset the predication index since it can now be called multiple times as we process new phrases:
class ExecutionContext(object):
def __init__(self):
self.reset()
def reset(self):
self._error = None
self._error_predication_index = -1
self._predication_index = -1
def solve(self, vocabulary, state, term):
self.reset()
yield from call(vocabulary, state, term)
def call(self, vocabulary, state, term):
...
...
Below is the modified respond_to_solutions()
function. It no longer generates solutions or error text, instead it requires the caller to pass them in. That allows us to centralize the code that does the solving into interact_once()
. It also returns the response instead of printing it now. The rest of the code is the same:
def respond_to_solutions(state, mrs, solutions, error_text):
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:
return "Yes, that is true."
else:
return f"No, that isn't correct: {error_text}"
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:
return "Yes."
else:
return f"No, {error_text}"
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:
return f"{str(solutions.get_binding(wh_variable).value)}"
else:
return f"{error_text}"
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:
return f"Done!"
else:
return f"Couldn't do that: {error_text}"
We also moved the functions that parse and build trees (mrss_from_phrase
, tree_from_assignments
and trees_from_mrs
) to be part of the UserInterface
class just to clean up the code.
With this, we now have a (simple) fully-functioning interactive natural language system! Here’s a simple example that runs it in a loop, and an interactive session using some phrases we’ve used throughout the tutorial to test it:
def Example12():
state = State([Actor(name="Computer", person=2),
Folder(name="Desktop"),
Folder(name="Documents"),
File(name="file1.txt", size=2000000),
File(name="file2.txt", size=100)])
user_interface = UserInterface(state, vocabulary)
while True:
user_interface.interact_once(generate_message_with_index)
print()
Running that sample leaves you at the command prompt waiting for you to interact. Here’s a session that runs all the phrases we’ve done in previous sections:
? a file is large
[_a_q(x3,[_file_n_of(x3,i8)],[_large_a_1(e2,x3)])]
Yes, that is true.
? which file is large?
[_which_q(x3,[_file_n_of(x3,i8)],[_large_a_1(e2,x3)])]
(File(file1.txt, 2000000),)
? delete a large file
[_a_q(x4,[_large_a_1(e9,x4), _file_n_of(x4,i10)],[_delete_v_1(e2,i3,x4)])]
Implementation for Predication _delete_v_1(e2,i3,x4) not found
[pronoun_q(x3,[pron(x3)],[_a_q(x8,[_large_a_1(e13,x8), _file_n_of(x8,i14)],[_delete_v_1(e2,x3,x8)])])]
Done!
New state: [Actor(name=Computer, person=2), Folder(Desktop), Folder(Documents), File(file2.txt, 100)]
? which file is large?
[_which_q(x3,[_file_n_of(x3,i8)],[_large_a_1(e2,x3)])]
[udef_q(x3,[_which_q(x10,[generic_entity(x10)],[nominalization(x3,[_file_v_1(e14,x10,i15)])])],[_large_a_1(e2,x3)])]
Implementation for Predication udef_q(x3,[_which_q(x10,[generic_entity(x10)],[nominalization(x3,[_file_v_1(e14,x10,i15)])])],[_large_a_1(e2,x3)]) not found
[_which_q(x10,[generic_entity(x10)],[udef_q(x3,[nominalization(x3,[_file_v_1(e14,x10,i15)])],[_large_a_1(e2,x3)])])]
Implementation for Predication generic_entity(x10) not found
which file is not large
? which folder is large?
[_which_q(x3,[_folder_n_of(x3,i8)],[_large_a_1(e2,x3)])]
which folder is not large
? a folder is large
[_a_q(x3,[_folder_n_of(x3,i8)],[_large_a_1(e2,x3)])]
No, that isn't correct: a folder is not large
interact_once()
prints out the trees that get found so it is easier to see which is being used and which have missing predications. It also helps point out when a tree failed and the solver is trying more trees to find a success. You can see that in action by looking at the difference between the first “which file is large?” which only generates one tree since it succeeds, and the second one which fails since there are no large files. The second one keeps going trying to find a tree that works.
Comprehensive source for the completed tutorial is available here
Last update: 2024-10-28 by Eric Zinda [edit]