Part 1: Creating your own Actions

Most applications require the ability to link an agent to a specific environment. Typically, this is implemented through sensing (perceiving the state of the environment) and acting (effecting the environment). Support for creating sensors and actions in ASTRA is provided through the notion of a module. You should already have seen an example of this in the Building and Deploying ASTRA Programs Guide where we used the Console module to print to the console using the println(…) action.

This guide will explain how to create your own actions for ASTRA.

What you’ll build

You will write a simple light switch example that models a light switch as a module 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-actions</artifactId>
    <version>1.3.2</version>

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

    <build>
        <defaultGoal>clean compile astra:deploy</defaultGoal>
        <plugins>
            <plugin>
                <groupId>com.astralanguage</groupId>
                <artifactId>astra-maven-plugin</artifactId>
                <version>1.3.2</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.0.2 \
    -DgroupId=examples \
    -DartifactId=module-actions \
    -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.

Declaring a Custom module

Modules are annotated Java classes that can be referenced in ASTRA code and whose methods may be invoked in a number of contexts. In this guide, we will explore one such context – namely, @ACTION methods.

Modules are defined by extending the pre-existing astra.lang.Module class:

package example;

public class Switch extends Module {
}

Once you have done this, you can then link the module to an ASTRA program by using the module statement:

agent Main {
  module Switch switch;
}

To get the agent to do something with the module, you must next implement some methods…

Modelling a Light Switch

For the purposes of this guide, we will model a light switch as a boolean value that is true when the switch is on and false when the swtich is off. Initially, the switch will be turned off.

package example;

public class Switch extends Module {
  private boolean on = false;
}

Creating an Action

Actions are simply Java methods that are annotated with @ACTION and which must return a boolean value to indicate the success or failure of the action. If a java.lang.RuntimeException is thrown, this is also interpreted as the failure of the action.

The action of turning on a light switch can be modelled in multiple ways. For example, we could model turning the light on and off as two separate actions: turnOn() and turnOff().

package example;

public class Switch extends Module {
  private boolean on = false;

  @ACTION
  public boolean turnOn() {
    on = true;
    return true;
  }

  @ACTION
  public boolean turnOff() {
    on = false;
    return true;
  }
}

However, a simpler approach is to model both actions as being relative to the current state of the light switch. In this way, we can use the flip() action to turn the switch on or off relative to its current state. An example of this action is given below and we will use it rather than the explicit actions above for the remainder of this guide.

package example;

public class Switch extends Module {
  private boolean on = false;

  @ACTION
  public boolean flip() {
    on = !on;
    return true;
  }
}

The problem with both of the above approaches is that the agent does not know the current (or initial) state of the light switch. To fix this, we need to give the agent an initial belief. This can be done by overriding the setAgent(...) method.

package example;

public class Switch extends Module {
  private boolean on = false;

  public void setAgent(Agent agent) {
    super.setAgent(agent);

    Predicate belief = new Predicate("switch", new Term[] {
      Primitive.newPrimitive(on ? "on":"off")
    });
    agent.beliefs().addBelief(belief);
  }

  @ACTION
  public boolean flip() {
    on = !on;
    return true;
  }
}

The above piece of code creates a Java object to represent a belief. This is basically an instance of the astra.formula.Predicate class and generates a belief of the form switch("on") or switch("off") depending on the value of the on field.

The next problem is how to change the belief when the state of the light switch changes (i.e. when we flip the switch). We can use the same code as above to create a new belief about the state of the light, but the problem is that the old belief does not get deleted (this means that the agent will belief both that the light is on and that it is off simultaneously, which should not happen). The solution is to store the current belief about the state of the light in the module, and to remove it when a new belief is required.

package example;

public class Switch extends Module {
  private boolean on = false;
  private Predicate belief;

  public void setAgent(Agent agent) {
    super.setAgent(agent);
    updateBeliefs();
  }

  @ACTION
  public boolean flip() {
    on = !on;
    updateBeliefs();
    return true;
  }

  private void updateBeliefs() {
    if (belief != null) {
      agent.beliefs().dropBelief(belief);
    }
    belief = new Predicate("switch", new Term[] {
      Primitive.newPrimitive(on ? "on":"off")
    });
    agent.beliefs().addBelief(belief);
  }
}

Note that the updateBeliefs() method is called from both the setAgent(...) method and the flip() method - basically any place in the code where the state of the on field can change. The updateBeliefs() method simply updates the agents beliefs to reflect the state of the on field.

Using the Module

To illustrate how to use the module, lets explore an ASTRA agent program that flashes a light (hypothetically connected to the switch) 3 times.

agent Main {
  module Switch switch;
  module Console console;

  types switch {
    formula switch(string);
  }

  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(string state) {
    console.println("switch=" + state);
  }
}

The main behaviour in this program is the one defined by the second and third rules, which handle the !flash(...) goal. These rules basically implement a loop, and the latter rule is included to terminate the loop (it is used for the case where N is 0). The former rule makes a single “flash” occur – this involves turning the light on and then off again.

From an agent programming perspective, it is interesting to note that the progam adopts the goal !switch(...) to drive the behaviour. The idea here is that the agent defines the state they would like the world to be in and then tries to find a plan (another rule) to achieve that state. This is handled by the fourth and fifth rules. Again, describing the rules in reverse order, the fifth rule states that is the state of the switch is the same as the goal state, do nothing. Conversely, the fourth rule states that, if the state of the switch is not the same as the goal (target) state, flip the switch and wait for the state to match the target state. The expectation here is that that, whichever rule is selected, the resulting state after the corresponding plan is executed will be the goal state.

The key step in the fourth rule is the invocation of the custom action we have developed. This is done using a dot notation that is similar to that used to invoke methods in Java. The identifier before the dot is used to reference the module and the action signature after the block is used to indicate which method is to be invoked.

<module_name>.<action_signature>

The ASTRA compiler checks that there is a Java method with the equivalent signature in the module; that the method has been annotated as an @ACTION method and that it returns a boolean value. If any of these three requirements are not met, the compiler will generate a compiler error.

Finally, the sixth rule is used to print out the state of the switch each time it changes, and the first rule triggers the agents behaviour, which is to flash 3 times.

To compile and run the resulting program, simply type:

$ mvn

The output should look something like this:

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

Note, seven switch states are printed out. The first state is the initial state, and the other six states reflect the flipping of the switch.

Summary

This guide has introduced the concept of a module and has explained how to create actions that can be associated with agent types and used to implement agent behaviours. This is one of a number of ways in which modules support the development of agents in ASTRA. For example, a more traditional approach to managing and updating the beliefs of an agent is through the use of sensors. How to do this is described in Part 2: Adding a Sensor in ASTRA.