Solution Group Handlers and Non-logical Meaning

So far, all the examples we’ve built have a very “logical” behavior. They are literally asking about things in the world, possibly in relation to other things. We ask about their existence, where they are, etc.

? what file is large?
? a file is large
? where am i
? go to a folder
? students are lifting a table
? which students are lifting the table

The system “solves” the MRS by finding variable assignments that make it true and then gives built-in answers like “that is true” or lists what was requested.

Next, we are going to walk through how to implement phrases that aren’t logical in that same way. For example, imagine we want to implement a rudimentary help system for our file system example and we’d like users to be able to ask for help using phrases too. To find out what they can do, a user might ask, “Do you have commands?”.

We’ll start by adding the notion of a “command” object into the system, creating one for each of the commands we have so far (“copy” and “go”) and implementing the new predications from the MRS, which are _have_v_1 and _command_n_1:

[ "Do you have commands?"
  TOP: h0
  INDEX: e2 [ e SF: ques TENSE: pres MOOD: indicative PROG: - PERF: - ]
  RELS: < [ pron<3:6> LBL: h4 ARG0: x3 [ x PERS: 2 IND: + PT: std ] ]
          [ pronoun_q<3:6> LBL: h5 ARG0: x3 RSTR: h6 BODY: h7 ]
          [ _have_v_1<7:11> LBL: h1 ARG0: e2 ARG1: x3 ARG2: x8 [ x PERS: 3 NUM: pl IND: + ] ]
          [ udef_q<12:21> LBL: h9 ARG0: x8 RSTR: h10 BODY: h11 ]
          [ _command_n_1<12:20> LBL: h12 ARG0: x8 ] >
  HCONS: < h0 qeq h1 h6 qeq h4 h10 qeq h12 > ]

               ┌────── pron(x3)
pronoun_q(x3,RSTR,BODY)            ┌────── _command_n_1(x8)
                    └─ udef_q(x8,RSTR,BODY)
                                        └─ _have_v_1(e2,x3,x8)

Once we do that, the interaction will look like this:

? Do you have commands?
Yes.

? Which commands do you have?
copy
go

You can almost hear the user say “Ugh! Dumb computer” after the first phrase. A human would interpret that as “Tell me what commands you have, if you have them”. This is known in linguistics as “Pragmatics”. The area of Pragmatics concerns the meaning of phrases that can’t be “logically” or “mechanically” interpreted since they require taking into account the context the phrase is uttered in and potentially taking some implied leaps to understand what is actually meant.

Making “Do you have commands?” actually say something custom and not purely logical requires customizing how Perplexity responds when it finds a Solution Group. That is where we’ll finish.

Logical Interpretation

First let’s create a FileCommand class which represents commands in the system, using the same pattern we used for the File and Folder classes:

class FileCommand(UniqueObject):
    def __init__(self, name):
        super().__init__()
        self.name = name
        self._hash = hash(self.name)

    def __hash__(self):
        return self._hash

    def __repr__(self):
        return f"Command(name={self.name})"

    def __eq__(self, obj):
        return isinstance(obj, FileCommand) and str(self.name) == str(obj.name)

Then, we need to add all the commands in the system to our state object, so they are returned when the system iterates through “all objects in the system” using the State.all_individuals() method:


class FileSystemState(State):
    def __init__(self, file_system, current_user=None, actors=None):
        super().__init__([])
        self.file_system = file_system
        self.current_user = file_system_example.objects.Actor(name="User", person=1, file_system=file_system) if current_user is None else current_user
        self.actors = [self.current_user,
                       file_system_example.objects.Actor(name="Computer", person=2, file_system=file_system)] if actors is None else actors
        self.commands = [file_system_example.objects.FileCommand("copy"), file_system_example.objects.FileCommand("go")]
       
    def all_individuals(self):
        yield from self.file_system.all_individuals()
        yield from self.actors
        yield from self.commands 
        
    ...

With that in place, we implement the predication that is generated when the user utters “command” so the system will be able to fill variables with these objects:

@Predication(vocabulary, names=["_command_n_1"])
def _command_n_1(context, state, x_binding):
    def bound_variable(value):
        if isinstance(value, file_system_example.objects.FileCommand):
            return True
        else:
            context.report_error(["valueIsNotX", value, x_binding.variable.name])
            return False

    def unbound_variable():
        for item in state.all_individuals():
            if bound_variable(item):
                yield item

    yield from combinatorial_predication_1(context,
                                           state,
                                           x_binding,
                                           bound_variable,
                                           unbound_variable)

And, finally, we can implement the _have_v_1 predication, so that it is true only for 2nd person phrases like “do you have a command”, and so that the only thing anything “has” are commands:

@Predication(vocabulary, names=["_have_v_1"])
def _have_v_1(context, state, e_introduced_binding, x_actor_binding, x_target_binding):
    def actor_have_target(item1, item2):
        if isinstance(item2, file_system_example.objects.FileCommand):
            return True
        else:
            context.report_error(["doNotHave", x_actor_binding.variable.name, x_target_binding.variable.name])
            return False

    def all_actors_having_target(item2):
        if False:
            yield None

    def all_targets_had_by_actor(item1):
        if False:
            yield None

    if x_actor_binding.value is not None and len(x_actor_binding.value) == 1 and x_actor_binding.value[0] == Actor(name="Computer", person=2):
        yield from in_style_predication_2(context,
                                          state,
                                          x_actor_binding,
                                          x_target_binding,
                                          actor_have_target,
                                          all_actors_having_target,
                                          all_targets_had_by_actor)

    else:
        context.report_error(["doNotHave", x_actor_binding.variable.name, x_target_binding.variable.name])
        return False
        

With that code, we can now make the first phrases work:

? do you have commands?
Yes.

? which commands do you have?
(Command(name=copy),)(Command(name=go),

Pragmatic Interpretation

As described in the Solution Group Algorithm topic, by default, when a solution group is found for a phrase, and the phrase is a yes/no question, Perplexity responds with “Yes.” To give a different response, we need to customize the way the system handles solution groups for this case.

To do this, we implement a solution group handler. It looks very much like the have_v_1 handler above, with a few differences:

@Predication(vocabulary,
             names=["solution_group__have_v_1"])
def _have_v_1_group(context, state_list, e_introduced_binding_list, x_actor_variable_group, x_target_variable_group):
    yield state_list

First, instead of using “_have_v_1” as the name, we prefix the name with “solution_group_” and use “solution_group__have_v_1” as the name. This tells the system we are implementing a solution group handler.

It has the same number of arguments as the “have_v_1” function, but, because this is handling a solution group, the arguments contain objects that return multiple items: one item for each solution in the group.

Implementing a solution group handler allows you to inspect each solution group that Perplexity finds and write custom logic to invalidate it or respond differently than the system would. Yielding a list of state objects from the solution group handler indicates that this list of state objects is a valid solution group. If nothing is yielded, the incoming list of state objects is invalidated and Perplexity continues looking for alternative solution groups.

As written in the code above, the Perplexity behavior won’t change since the code simply yields the solution group, as is. This indicates it is a valid solution group. Since the code doesn’t do anything to respond in a custom way, Perplexity continues responding with its default behavior.

If we want a different response, we could start by doing this:

@Predication(vocabulary,
             names=["solution_group__have_v_1"])
def _have_v_1_group(context, state_list, e_introduced_binding_list, x_actor_variable_group, x_target_variable_group):
    new_solution_group = [state_list[0].record_operations([RespondOperation("You can use the following commands: 'copy' and 'go'.")])]
    yield new_solution_group

Instead of yielding the original solution group, we now are only yielding the first solution, after modifying it to include a built-in operation called RespondOperation. This is just like the DeleteOperation we created in an earlier section, but this operation’s job is to print text for the user. Putting a RespondOperation in any of the state objects we yield tells the system that we want to override the default output.

So, now we get this:

? Do you have commands?
You can use the following commands: copy and go

? Which commands do you have?
You can use the following commands: copy and go

Because we are overriding the output, it really doesn’t matter that we are changing the list of state objects in the solution group. Since they were only used to generate the default output, Perplexity won’t pay attention to them anymore. Obviously we could instead run code to look up the commands in the system and dynamically build the string instead of hard coding it.

Concepts

While that works, it seems inefficient to have to create FileCommand objects, implement them in several places, have Perplexity go through the process of solving the MRS, only to throw it all way and print out a custom message! It turns out that work was not wasted. It would be required for other phrases like “Do you have a ‘copy’ command?” and many other scenarios. But: it really is unnecessary to build up a solution group with all the commands to only throw it away for this scenario.

Instead, we can use another Perplexity feature called a “Concept” to make it more efficient. The idea is that, sometimes, instead of Perplexity assigning actual objects to variables, we’d like the “concept” of them (called a referring expression in linguistics) to be assigned. I.e. instead of assigning x8 = FileCommand('copy'), we’d like to have x8 = {representation of whatever they said before it got resolved into an actual object} so that we can look at what they said “conceptually” instead of dealing with the actual instances of objects that it generated. This allows the solution group handler to just see if they are were talking about “you” having “the concept of commands” and, if so, generate the custom text. This would be much more efficient.

To do this, we start by adding an alternative _command_n_1 predication since that is where the instances of commands get generated. Perplexity allows adding more than one interpretation of a predication by creating more than one function and indicating that they use the same predication name. It treats them as alternatives and attempts to solve the MRS once using the first interpretation and then again using the next.

In the new interpretation, instead of yielding instances, we yield a Concept("command") object. The Concept object in Perplexity literally does nothing except tell the system it is an opaque “Concept” object. Here are both interpretations:

@Predication(vocabulary, names=["_command_n_1"])
def _command_n_1_concept(context, state, x_binding):
    def bound_variable(value):
        if isinstance(value, Concept) and value == Concept("command"):
            return True
        else:
            context.report_error(["valueIsNotX", value, x_binding.variable.name])
            return False

    def unbound_variable():
        yield Concept("command")

    yield from combinatorial_predication_1(context,
                                           state,
                                           x_binding,
                                           bound_variable,
                                           unbound_variable)
                                           
                                               
@Predication(vocabulary, names=["_command_n_1"])
def _command_n_1(context, state, x_binding):
    def bound_variable(value):
        if isinstance(value, file_system_example.objects.FileCommand):
            return True
        else:
            context.report_error(["valueIsNotX", value, x_binding.variable.name])
            return False

    def unbound_variable():
        for item in state.all_individuals():
            if bound_variable(item):
                yield item

    yield from combinatorial_predication_1(context,
                                           state,
                                           x_binding,
                                           bound_variable,
                                           unbound_variable)

With that in place, we’ll first get a set of solution groups that have the Concept("command") object assigned to variables. Perplexity will do nothing with the Concept object except recognize that it is one and disable its logic to do any collective/cumulative/distributive solution group testing. Instead, it will still create all potential solution groups and call your solution group handler. It is now up to you to decide if they are valid readings. For this case it will be simple since we are going to treat any phrase of the form “Do you have {x} commands?” as a request to see the help string. That includes any of these:

Do you have commands?
Do you have 2 commands?
Do you have several commands?
Do you have any commands?
etc.

Next, recall that the implementation of _have_v_1 only succeeds if the arguments are of type FileCommand:

**@Predication(vocabulary, names=["_have_v_1"])
def _have_v_1(context, state, e_introduced_binding, x_actor_binding, x_target_binding):
    def actor_have_target(item1, item2):
        if isinstance(item2, file_system_example.objects.FileCommand):
            return True
        else:
            context.report_error(["doNotHave", x_actor_binding.variable.name, x_target_binding.variable.name])
            return False

    ...
    
    if x_actor_binding.value is not None and len(x_actor_binding.value) == 1 and x_actor_binding.value[0] == Actor(name="Computer", person=2):
        yield from in_style_predication_2(context,
                                          state,
                                          x_actor_binding,
                                          x_target_binding,
                                          actor_have_target,
                                          all_actors_having_target,
                                          all_targets_had_by_actor)**

    else:
        context.report_error(["doNotHave", x_actor_binding.variable.name, x_target_binding.variable.name])
        return False                                       

This means it will fail when Perplexity attempts to solve it using the new Concept object. So, we need a new interpretation of _have_v_1 as well:

@Predication(vocabulary, names=["_have_v_1"])
def _have_v_1_concept(context, state, e_introduced_binding, x_actor_binding, x_target_binding):
    def actor_have_target(item1, item2):
        if isinstance(item2, Concept) and item2 == Concept("command"):
            return True
        else:
            context.report_error(["doNotHave", x_actor_binding.variable.name, x_target_binding.variable.name])
            return False

    def all_actors_having_target(item2):
        if False:
            yield None

    def all_targets_had_by_actor(item1):
        if False:
            yield None

    if x_actor_binding.value is not None and len(x_actor_binding.value) == 1 and x_actor_binding.value[0] == Actor(name="Computer", person=2):
        yield from in_style_predication_2(context,
                                          state,
                                          x_actor_binding,
                                          x_target_binding,
                                          actor_have_target,
                                          all_actors_having_target,
                                          all_targets_had_by_actor)

    else:
        context.report_error(["doNotHave", x_actor_binding.variable.name, x_target_binding.variable.name])
        return False

Finally, we want to change our solution group handler to only be used when the conceptual interpretation is used. We’ll let perplexity handle the original interpretation. To do this, we set the handles_interpretation argument of the Predication class to point to the interpretation we want to pair it with, like this:

@Predication(vocabulary,
             names=["solution_group__have_v_1"],
             handles_interpretation=_have_v_1_concept)
def _have_v_1_group(context, state_list, e_introduced_binding_list, x_actor_variable_group, x_target_variable_group):
    yield [state_list[0].record_operations([RespondOperation("You can use the following commands: copy and go")])]

Now, we can try all the phrases below:

? which commands do you have?
You can use the following commands: copy and go

? do you have a command?
You can use the following commands: copy and go

? do you have 2 commands?
You can use the following commands: copy and go

? do I have a command?
you do not have a command

? do I have a file?
you do not have a file

? do you have a file?
us do not have a file

Final Loose Ends

Three things to note here:

First, all those phrases end up using the conceptual interpretation. The instance-based interpretation is currently unused. The most likely use for it would be for phrases like “Do you have a copy command?” or “How do I use the copy command?”, i.e. in phrases where the user is talking about a particular instance, and not the general concept of commands.

Second, there also a whole set of phrases that happen to work that probably shouldn’t:

? did you have a command?
You can use the following commands: copy and go

? you are having a command?
You can use the following commands: copy and go

? Are you having a command?
You can use the following commands: copy and go

Finally, there are some phrases that point to another area we need to focus on, global criteria:

? Do you have 3 commands?
You can use the following commands: copy and go

? Do you have many commands?
You can use the following commands: copy and go

? Do you have several commands?
You can use the following commands: copy and go

All of these phrases are using words like “several” or “3” which quantify how many of something the speaker is interested in. When we implement a solution group handler we take over responsibility for doing the counting, and we’re not doing that. Let’s do that next.

Comprehensive source for the completed tutorial is available here

Last update: 2024-10-25 by Eric Zinda [edit]