Part 4: Defining Custom Terms and Formulae

This guide is the fourth part in a series of guides on how to link an agent to an environment. As was described in part 3, this has traditionally been implemented through sensing (perceiving the state of the environment) and acting (effecting the environment). In ASTRA, we have introduced the idea of custom events as an alternative to the generation of belief events through sensing. Changes in an environment are typically represented as low-level events that are used to drive changes to an agents beliefs, indirectly generating belief events which drive agent behaviour. Custom events allow those low-level events to be directly transformed into agent-level events that can be used to drive behaviour. However, beliefs also provide a model of the environment that provide essential context for the decision making process. Skipping the belief update process means that this important context is lost with respect to environmental state. To overcome this, we provide mechanisms that allow agents to query internal state as if it were modelled as beliefs. This is achieved through a custom formula model. For convenience, we also provide a custom term model that enables agents to directly access the values stored in that internal state.

What you’ll build

We will adapt the simple light switch program that was developed in part 1. The completed source code for this prior version of the code can be downloaded from the ASTRA Gitlab Repository.

The code implements a module that models a light switch and provides the ability for the agent to turn the light switch on or off as required. The actual behaviour shown here will be the agent flashing the light 3 times (turning it on / off repeatedly 3 times).

What you’ll need

  • About 15 minutes
  • A favorite text editor or IDE
  • JDK 1.8 or later
  • Maven 3.3+

Creating a project from scratch

In a project directory of your choosing, create the following directory structure (for example, on *nix systems, type mkdir -p src/main/astra):

|-- src
|    |-- main
|          |-- astra
|-- pom.xml

Now create the maven build file (pom.xml):

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>examples</groupId>
    <artifactId>module-terms</artifactId>
    <version>1.4.1</version>

    <parent>
        <groupId>com.astralanguage</groupId>
        <artifactId>astra-base</artifactId>
        <version>1.4.1</version>
    </parent>

    <build>
        <defaultGoal>clean compile astra:deploy</defaultGoal>
        <plugins>
            <plugin>
                <groupId>com.astralanguage</groupId>
                <artifactId>astra-maven-plugin</artifactId>
                <version>1.4.1</version>
            </plugin>
        </plugins>
    </build>
</project>

You should change the groupId and artifactId to reflect your own project.

Creating a project from an archetype

To use the maven archtetype mechanism to create an ASTRA project, go to a folder of your choice, and type in the following command:

mvn archetype:generate \
    -DarchetypeGroupId=com.astralanguage \
    -DarchetypeArtifactId=astra-archetype \
    -DarchetypeVersion=1.2.0 \
    -DgroupId=examples \
    -DartifactId=module-terms \
    -Dversion=0.1.0

You should change the groupId and artifactId to reflect your own project.

Source Code

The full source code for this guide can be downloaded from the ASTRA Gitlab repository.

Starting Point

The starting point for this guide is the code created in the previous guide. The implementation of the module is repeated below.

public class Switch extends Module {
  static {
    Unifier.eventFactory.put(SwitchEvent.class, new SwitchEventUnifier());
  }

  private boolean on = false;

  @ACTION
  public boolean flip() {
    on = !on;
    agent.addEvent(new SwitchEvent(
      Primitive.newPrimitive(on ? "on":"off")
    ));
    return true;
  }

  @EVENT( symbols={}, types = {"string"}, signature = "$swe")
  public Event event(Term state) {
      return new SwitchEvent(state);
  }
}

As can be seen, this code provides an @ACTION that allows the agent to flip the switch and an @EVENT that is generated whenever the switch is flipped. When compared against the previous approach described in the sensor guide, the code is much neater as there is no need to manage the beliefs of the agent (as shown in the updateBeliefs() method). The downside of this approach is that the agent had to maintain a belief about the state so that it could query the current state of the switch. This is exemplified by the example code below:

agent Main {
  module Switch switch;
  module Console console;

  types switch {
    formula switch(string);
  }

  initial switch("off");

  rule +!main(list args) {
    !flash(3);
    console.println("FINISHED");
  }

  rule +!flash(int N) : N > 0 {
    !switch("on");
    !switch("off");
    !flash(N-1);
  }

  rule +!flash(int N) {}

  rule +!switch(string state) : ~switch(state) {
    switch.flip();
    wait(switch(state));
  }

  rule +!switch(string state) {
    console.println("should not happen");
  }

  rule $switch.state(string state) {
    -+switch(state);
    console.println("switch=" + state);
  }
}

It is worth pointing out that the modules includes a property on that maintains the state of the switch. Rather than requiring the agent manage its beliefs, a better approach would be to allow the agent to query the internal state directly. Support for this is provided via the @TERM and @FORMULA annotations.

Creating a Custom Term

Lets start by creating a custom term. This is done by creating a method in the module that returns a value and annotating that method with @TERM. Once annotated, the agent can retrieve the internal state directly.

  @TERM
  public boolean on() {
    return on;
  }

This method returns the current value of the on property. The code below provides an example of how it can be used:

agent Main {
  module Switch switch;
  module Console console;

  rule +!main(list args) {
    !flash(3);
    console.println("FINISHED");
  }

  rule +!flash(int N) : N > 0 {
    !switch("on");
    !switch("off");
    !flash(N-1);
  }

  rule +!flash(int N) {}

  rule +!switch(string state) : state ~= switch.on() {
    switch.flip();
    wait(switch.on() == state);
  }

  rule +!switch(string state) {
    console.println("should not happen");
  }

  rule $switch.state(string state) {
    console.println("switch=" + state);
  }
}

The code is very similar to the event version of the agent program given earlier in this example. The difference is that there is no longer a need to specify and maintain a switch(...) belief. That said, the comparison is not a particularly nice solution as it is a little long winded. A better approach is to use a custom formula as is shown next. If you want to run the code, simply type in the following:

$ mvn

The output should again look something like this:

[main]switch=on
[main]switch=off
[main]switch=on
[main]switch=off
[main]switch=on
[main]switch=off
[main]FINISHED

Creating a Custom Formula

The second part of this guide explains how to create a custom formula. As we saw in the previous section, custom terms can be used to extract information about the current state of the system, but really, it is designed to allow you to write code that uses the information in an expression. In cases where you want to query part of the state held within the module, a better solution is to us a custom formula.

Custom formulae are created by implementing a method in the module that returns a formula that can be compared

and annotating that method with @TERM. Once annotated, the agent can retrieve the internal state directly.

  @TERM
  public boolean on() {
    return on;
  }

This method returns the current value of the on property. The code below provides an example of how it can be used:

agent Main {
  module Switch switch;
  module Console console;

  rule +!main(list args) {
    !flash(3);
    console.println("FINISHED");
  }

  rule +!flash(int N) : N > 0 {
    !switch("on");
    !switch("off");
    !flash(N-1);
  }

  rule +!flash(int N) {}

  rule +!switch(string state) : state ~= switch.on() {
    switch.flip();
    wait(switch.on() == state);
  }

  rule +!switch(string state) {
    console.println("should not happen");
  }

  rule $switch.state(string state) {
    console.println("switch=" + state);
  }
}

The code is very similar to the event version of the agent program given earlier in this example. The difference is that there is no longer a need to specify and maintain a switch(...) belief. That said, the comparison is not a particularly nice solution as it is a little long winded. A better approach is to use a custom formula as is shown below.

Summary

This guide has introduced the concecustom events. It has built on the first part of the series which focused on how to create custom actions. This use of the belief about the state of the switch in this program is a small fudge that will be unnecesary when we combine the use of a custom event with the use of custom terms and formulae in the next part of the guide.