TriggerLinks

From OpenCog
Jump to: navigation, search

(design proposal)

From two different directions, I have recently seen the need for fine-grained control of events in the Atomspace, of the form: When Atom A is updated, then some other Atom B has some specific thing done to it (e.g. B is fed to the pattern matcher, or B has its truth value updated, etc.).

XXX FIXME: Atoms are immutable, and can never be "updated". They can only be created or deleted. This proposal needs to be tightened up to clarify terminology. Perhaps you meant this: when a valuation on an atom is updated, for example, when a TruthValue or an AttentionValue is updated? In that case, we would need triggers that happen on specific key-atom pairs that specify the specific valuation that is to be monitored. For example, the key for TruthValues is the PredicateNode "*-TruthValue Key-*".

It seems to me that this kind of control can be achieved via use of a new sort of link called a TriggerLink. Here I will discuss two possible kinds of TriggerLinks, BindTriggerLinks and ImplicationTriggerLinks.

XXX FIXME: Although the below proposes three different kinds of triggers, a closer reading reveals that they can all be accommodated with just one mechanism. There's a lot of extra complexity below that does not seem to be needed.

Currently this is a speculative design proposal, being circulated for comments.

StrengthTriggerLink

As a simple example, suppose we have a BindLink such as


BindLink [123]
    $X
    AndLink
        EvaluationLink 
            PredicateNode “starving”
            ConceptNode “me”
        EvaluationLink 
	    PredicateNode “near” 
	    ConceptNode “me”
            ConceptNode $X
        InheritanceLink 
             $X
             ConceptNode “food”
     ExecutionOutputLink
         PredicateNode “eat”
         ConceptNode $X


and we would like this to be fed to the PatternMatcher whenever the ANDLink is true.

What process will cause the PM to actually be fed this BindLink?

In this case, the trigger should be the event of starvation, as tied to the “starving” PredicateNode. If it ever occurs that

        EvaluationLink [456]
            PredicateNode “starving”
            ConceptNode “me”

(i.e. if the strength of [456] increases past some threshold), then this is good enough reason for the PM to try to apply the BindLink.

To enable this we could create a link such as

StrengthTriggerLink
     [456]
     [123]
     NumberNode .8

The updateStrength() method of the Atom [456] would then have to check for StrengthTriggerLinks associated with the Atom (or, to save time, check a flag that indicates if the Atom has are any StrengthTriggerLinks or not). If it finds a StrengthTriggerLink, then it checks if the strength of the link is above the threshold indicated by the trigger link (.8 in the above example), and if so it carries out the appropriate action on the target of the trigger link. If the target of the trigger link is a BindLink, then the appropriate action is to spawn a new thread running a Pattern Matcher query on this BindLink.

Currently the method updateStrength() lives in a TruthValue object. So we would need some sort of TriggerHappyTruthValue object, that knew how to deal with trigger links upon approprate truth value updates.

XXX FIXME

There is no "updateStrength()" method on a truth value, because truth values are immutable. Truth values, once created, cannot be changed. The only thing that can be done is to create a new truth value, and assign it to an atom, replacing the old truth value. This is generically true for values in general: not just truth values. So, perhaps, instead of a StrengthTriggerLink, you want a ValueChangeLink, which specifies a key and an atom, and whenever a new value is assigned to the key-atom pair, the trigger is triggered. Or perhaps you want to trigger whenever ANY value associated with a key is added, replaced or deleted.

Counter-proposal

Based on the above comments, the following should be sufficient:

ValueChangeLink
   PredicateNode "key-to-trigger-on"
   SomeFunctionLink

That's it. Nothing more than this. The PredicateNode "key-to-trigger-on" specifies the key to a valuation. Whenever that valuation changes, the trigger is triggered.

The SomeFunctionLink can be any link that can accept a key and an atom as arguments. Thus, it can be any appropriate FunctionLink, or generically an appropriately constructed LambdaLink. Special cases include the ExecutionOutputLinks, as well as EvaluationLinks.

It might be nice to invent a new kind of execution link, one that can take a valuation (or a value) directly as an argument. Equivalently, we could allow VariableNodes to range over values and not just atoms: i.e. to allow a LambdaLink to use VariableNodes that take on values. Some work would be needed to make clear how this might be accomplished.

Fancy trigger patterns

The fancy version of this would be to generalize the ValueChangeLink so it could trigger on any kind of pattern, and not just on a single key (or a single atom-key pair). That would allow one to write expressions such as "if A is true and the value X on atom B changed and the value Y on atom C changes and Inherits B C then trigger".

These kinds of fancy, complex triggers ARE ALREADY BEING USED in the Hanson Robotics behavior-tree code. The actual mechanism there is to write out explicit GetLinks, wrapping them with SequentialAndLink and SequentialOrLink links, so that when the various conditions are satisfied, then some processing is triggered. Typically, the processing is done by a PutLink that plugs in the values that caused the trigger to fire.

The red-light, green-light example provides a simple example of how to do this. Here: /examples/pattern-matcher/sequence.scm

Trigger-action (stimulus-response) systems

When designing systems that take actions based on triggers, there are two popular styles: behavior trees and finite state machines. The Hanson Robotics code initially used a behavior tree to trigger based on input stimulus.

Triggering on input stimulus can also be accomplished with finite state machines. The examples directory provides examples of varying complexity: /examples/pattern-matcher/fsm-basic.scm /examples/pattern-matcher/fsm-mealy.scm/examples/pattern-matcher/fsm-full.scm

Although these two are the popular styles, they are not the only styles. Hanson Robotics has already decided to replace the behavior-tree based stimulus response system with OpenPsi, below.

OpenPsi

Since the implementation of the original trigger sequences for the Hanson Robotics, the core mechanism was replaced by OpenPsi. The idea here is that instead of having explicit triggers that always run no mater what, one instead has a collection of different classes of rules that get triggered and run in different types of situations. Unfortunately, the current implementation of OpenPsi is a bit of a Frankenstein: although it starts out being a generic rule-selection and triggering infrastructure, it has elements of a human emotional model mixed into it. Thus, the code is written so that certain triggers fire only when the robot enters a "happy" state, and so on. Cleanup is required before the triggering can be generic, instead of being pop-psychology-based.

That is, to achieve certain strategic goals, one may wish to undertake certain tactical steps to approach that goal. The tactical steps are groupings of rules that trigger in response to certain conditions and input stimuli, and are executed by the rule engine. This partitioning of rule-bases into tactical groups to accomplish strategic goals has nothing to do with "happiness" or "sadness". It needs to be a generic mechanism.

Counter-proposal summary

The above makes clear that there are still some difficult design areas to be pondered. It would be nice to have some sort of explicit stimulus-response mechanism that can notice when a value changes. However, for it to be useful, it needs to accomplish several things:

  • It needs to resemble the current style of programming used in the behavior trees, such as the stop-go example.
  • It needs to be able to trigger on complex stimulus structures, and so has to resemble the GetLink, PutLink pairing (or a BindLink, if you wish).
  • The best way to accomplish the above two might be to create a link type that evaluates to true or false, depending on a value. That link could then be embedded into a pattern, thus altering the result of pattern matching. This would not be a trigger, per-se: one would have to run the pattern, to see if it is being satisfied. Time-dependent (state-dependent) pattern matching is already accomplished with StateLinks.
  • There needs to be a way of passing values or valuations to FunctionLinks, possibly by allowing VariableNodes to range over values. (The type of a value can already be specified with a TypeNode, and TypeNodes are already used to specify variable types).
  • There needs to be a generic way of treating value triggers as a part of the rule engine, so that values can appear in rules. That is, so that rules can run based on value changes.
  • The OpenPsi code base needs to be refactored to cleanly seprate the rule-selection and rule-triggering mechanisms (based on satisfying certain strategic goals, by means of tactical actions), from the rather simplistic and silly model of human emotions that got tangled into it.

RelationshipTriggerLink

As another example, suppose we want to keep a running count of, for instance, how many word-instances have been observed to occur as the subject of any given verb … e.g. the "positiveEvidence" of the truth value of

SatisfyingSetLink [666]
   $X
   EvaluationLink
      PredicateNode “subject” [333]
      PredicateNode “tickle” [222]
      VariableNode $X

We would like this count to be updated whenever a new instance of the relationship is seen, e.g. when

EvaluationLink
    PredicateNode “subject”
    PredicateNode “tickle”
    ConceptNode “Ben”

is observed…

Now, suppose the Atomspace contains the abstract inference rule

ForAllLink [777]
 $W,$X
 ImplicationLink
   EvaluationLink
       PredicateNode $W [888]
       PredicateNode $X
       AnyNode
   SatisfyingSetLink
       $Z
       EvaluationLink
         PredicateNode $W
         PredicateNode $X
         ConceptNode $Z

So for instance, when

EvaluationLink
    PredicateNode “subject”
    PredicateNode “tickle”
    ConceptNode “Ben”

is observed, the above rule will create

SatisfyingSetLink
    $Z
    EvaluationLink
       PredicateNode “subject”
       PredicateNode “tickle”
       ConceptNode $Z

What is needed is to trigger this rule whenever an EvaluationLink involving “subject” is formed….

One way to achieve this would be, writing

PredicateNode “subject” [444]   ,

to have a link such as

RelationshipTriggerLink
   [444]
   [777]
   [888]

where the latter should be interpreted that: Whenever a new link connected to [444] is created, it is checked whether [444] has any RelationshipTriggerLinks. If it does, then the targets of these are followed and appropriately processed.

If the second target is a BindLink, it gets processed by the PM in a new thread.

In this case the second target is an ImplicationLink [777], and the appropriate way to process it is to evaluate it as a BindLink, with [444] bound to position [888]

E.g. if the new link

EvaluationLink
    PredicateNode “subject”
    PredicateNode “tickle”
    ConceptNode “Ben”

is formed, then the BindLink causes the link

SatisfyingSetLink
    $Y
    EvaluationLink
       PredicateNode “subject”
       PredicateNode “tickle”
       ConceptNode $Y

to be formed with positiveEvidence of 1. But if this link already exists, then the formation of this link will trigger the revision rule, which will cause the existing positiveEvidence of the link to be incremented by 1. So we get automated update via the activity of the pattern matcher, carried out not via polling but triggered via addition of a new link to the incoming set of “subject”…

We have said above that the TriggerLink is followed “whenever a new link connected to [444] is created”…. How in practice does this happen? What we would need, it seems, is that when a link from A to B is created, some sort of “link_added” method associated with both A and B is invoked. If A doesn’t have any triggers associated with it, then A.link_added(Link L) won’t do anything and won’t take much time. If A has triggers associated with it, then these triggers will be activated when link_added is called.

XXX FIXME

Except that this proposal is no different than the one up above. There is no way to "observe" an atom. One can, however, update a count on it. That count will typically be kept in some value, perhaps in a CountTruthValue, or perhaps elsewhere. As long as you know the key-atom pair (or even just the key), you could trigger whenever a value associated to that key changes. So this proposal is exactly the same as the TriggerLink proposal above.

Generalizing a Bit

In general, either StrengthTriggerLink or RelationshipTriggerLink should be able to point to either a BindLink or an ImplicationLink, and should be able to occur with either 2 or 3 arguments, the optional 3rd argument indicating which term in the source of the Bind or Implication Link should be bound to the 1st argument (where an error would be thrown if the 1st and 3rd arguments don’t unify).

Triggering schema execution

Next, suppose we want a TriggerLink to trigger execution of some Scheme, Python or Haskell code...

We can do this by wrapping this code in a GroundedSchemaNode, and then pointing a TriggerLink at this GSN, e.g.

RelationshipTriggerLink
      ConceptNode "cheese"
      ExecutionOutputLink
           GroundedSchemaNode "newCheeseProcessor.py"

PingTriggerLinks

Jim Rutt has suggested what he calls "PingTriggerLinks", as a "no side-effects" way to trigger procedures. Atoms could have a method called "Ping" that would trigger all PingTriggerLinks associated with that Atom. An Atom.ping API call would process the PingTriggerLinks associated with the Atom without requiring any changes to the Atom.

For instance,

ConceptNode "Red Button"

PingTriggerLink
   ConceptNode "Red Buttont"
   ExecutionOutputLink
       GroundedSchemaNode  "destroyworld.py"

This form provides for a clean way for "Red Button" to serve as the equivalent of a function variable when "programming in atomspace" - "Red Button" could be used semantically to mean "if the shit really hits the fan, do something". In geopolitics during the Cold War it's linked functionality would be "load and launch the missiles", TODAY it could be: "convene the G7 and wring handkerchiefs". This allows us to easily and cleanly build up indirection and thereby modularity.

As a tangible example, we could have an artificial deer having a "Red Button" throughout it's life, but with rather different meanings when it is a new born fawn, a yearling, and a big mean old buck. Other logic could evolve to "press the Red Button" (ie Ping it) when in extreme distress, without worrying about "what was behind the Red Button" thus achieving modularity in a way that is transparent in the Atomspace.

There might be others ways to achieve that, but a "no-side-effects' TriggerLink would be one useful and clear way to do it.

More complex examples would also be possible, e.g.

PingTriggerLink
   ConceptNode "Red Button"
   IfElseLink
         ExecutionOutputLink
               GroundedPredicateNode "Presidential_Authority_Received.py"
         SequentialAndLink                                                                 <----- TRUE branch
                   ExecutionOutputLink
                           GroundedSchemaNode "activate_ICBMs.py"
                   ExecutionOutputLink
                           GroundedSchemaNode "activate_warheads.py"
                   ExecutionOutputLink
                           GroundedSchemaNode "launch_ICBMs.py"
         SequentialAndLink                                                               <----- FALSE branch
                   ExecutionOutputLink
                           GroundedSchemaNode "notify_president_of_unauthorized_use.py"
                   ExecutionOutputLink
                           GroundedSchemaNode "press_release_generator.py"
  

On the other hand, it would be possible to achieve the same effect as a PingTriggerLink via different methods, e.g. by saying

  EquivalenceLink <1>
     SchemaNode "Red Button"
     GroundedSchemaNode  "destroyworld.py"


, along with a convention that invoking ExecutionOutputLink on an ungrounded SchemaNode should invoke a search for Equivalent GroundedSchemaNodes and execution of all of these with strength/confidence above a certain threshold, would do the trick...

But it's not totally clear this would be better than a PingTriggerLink... It seems a judgment call.

XXX FIXME

But this proposal is identical to the plain-old TriggerLink. All you'd need to do is to touch the value associated to the PredicateNode "*-Ping Key-*" You can have as many different kind of ping keys as you wish, no need to be limited to only just one. So this also seems like a redundant proposal.

Scheduled Execution Links

A related idea is to provide an Atomspace mechanism for tasks that are to be run periodically or when some condition is true. Perhaps support for these tasks could be compressed to a queued job mechanism. Analogous to cron in unix. Some tasks could be "house keeping" tasks that run every hour or every day. Others could be tasks waiting for a condition to true to fully execute, that perhaps check the condition once every few seconds. We could have cron like functionality to specify run once, or repeating.

For instance, we could have something like

ScheduledExecutionLink 
    NumberNode    <--- run every NumberNode millisecs
    ConceptNode "Repeating"     <-- alternatively "RunOnce" 
    some stuff to execute
        might have a conditional check first

and Deleting the ScheduledExecutionLink would delete any pending jobs associated with it.

XXX FIXME

The above is not needed. It can already be done with the existing atom types. Its already used in the Hanson Robotics behavior-tree code. Here's how it works: execution is tail-recursive, so you can write an infinite loop just by "doing it again". If you want to wait a while, just call SleepLink. If you want to run in a new thread, call ParallelLink and if you want to join that thread, then call JoinLink. See the behavior-tree code for a working example.