ASTRA is based upon AgentSpeak(L) in that it provides all of the same basic functionality as AgentSpeak(L) but then augments this basic functionality with a range of additional features that we believe result in a more pratical Agent Programming Language. Many of the features of the language will be introduced over the coming lessons, here we attempt to provide a direct mapping from AgentSpeak(L) to the equivalent ASTRA functionality. We start by introducing AgentSpeak(L).
Introduction
AgentSpeak(L) is an agent programming language, that is based on Belief-Desire-Intention (BDI) theory. This theory models rational decision making as a process of state manipulation. The idea is that the current state of the world can be represented as a set of beliefs (basically facts about the state of the world) and the ideal state of the world can be represented as a set of desires. The theory then maps out how a rational decision maker achieves its desires – that is, how it changes the world so that its desires become its beliefs. For instance, a decision maker may believe that it is in Ireland, but it may also have a desire to be in China. BDI theory attempts to explain how the decision maker selects some course of action so that it eventually believes that it is in China, thus satisfying its desire to be in China.
The way in which BDI theory achieves this is by adding a third state – intentions – defined as a subset of desires that the decision maker is committed to achieving . Why a subset? Basically, in BDI theory, it is considered acceptable for desires to be mutually inconsistent. That is, an agent can have two desires that cannot be realised at the same time. For example, in addition to desiring to be in China, our example decision maker may also desire to be in France. The problem is that there is a physical constraint on the achievement of the desires – the decision-maker cannot be in two places at the same time – so it can only satisfy one of its desires at a time. This issue can be generalised to the idea that decision-makers are resource bounded entities and may not be able to achieve all their desires due to a lack of sufficient resources. As a result, they must select a subset of those desires that they “believe” they are capable of realising given their resource constraints – these are their intentions. Once selected, the decision maker attempts to make its intentions into beliefs by identifying and following an appropriate course of action. The identification of this course of action, known as a plan, can be based on selection of the plan from a library of pre-written plans or through the use of a planner that constructs the plan on the fly (beliefs are the start state and intentions are the end/goal state). AgentSpeak(L) adopts the former of these approaches (a plan library).
There are two further refinements to BDI theory. First, the concept of a goal is often introduced as a replacement for desires. Goals are defined as a mutually consistent set of desires (so the decision maker could desire to be in both China and France, but could only have a goal to be in one of those places). AgentSpeak(L) adopts goals as the representation of future state. The second refinement is the idea of how to represent intentions. In the pure model, intentions are a subset of desires, but intentions are associated with commitment. This implies that a decision maker has some “plan of action” for achieving its intentions. As such, it is possible to represent intentions as either state (intention-to-achieve) or as the plan that will bring about that state (intention-to-do). AgentSpeak(L) adopts the latter model of intention (intention-to-do).
AgentSpeak(L) defines a set of programming constructs, encoded using a specific syntax and supported by a corresponding interpreter algorithm that is based on a Belief-Goal-Intention(to-do) model of rational decision making. The core constructs provided are:
- Beliefs: predicate formulae representing facts about the state of the agents environment. Together, the set of beliefs held by an agent are equivalent to the state of an object.
- Goals: predicate formulae (prefixed with a bang operator (!)) identify what the agent wants to do. Goals are not stored explicitly in the agent state, instead, they are declared as required and mapped contextually to a behaviour that will realise the goal. Goals are equivalent to method calls in object-oriented programming. The mapping is achieved through the use of events and associated event handler, known as plan rules.
- Events: Events drive the behaviour of an agent. Internally, the agent contains an event queue. Each iteration of the interpreter, one event is selected from the event queue and processed through contextual mapping of the event to an event handler (plan rule). AgentSpeak(L) includes events for: the adoption of new beliefs, the retraction of existing beliefs, and the adoption of goals. There is no analogy between events and object-oriented programming – perhaps the closest concept they map on to is the message that is received by an object (which is matched against one of the methods supported by the object).
- Plan Rules: Plan rules are the heart of an agent program; they define the core behaviours of the agent, contextually mapping those behaviours to the events that trigger them. Behaviours are specified as a sequence of plan operators; which support the following functionality: belief adoption/retraction; sub goal adoption; belief querying and private actions. Plan rules are equivalent to methods in object-oriented programming, where the triggering event is equivalent to the method signature and the behaviour is equivalent to the method implementation.
The core interpreter cycle for AgentSpeak(L) can be reduced to the following steps:
1. select an event, e, from the agents event queue 2. match the event to a plan rule, p whose triggering event matches e, and whose context is satisfied. 3. if the event is a belief adoption / retraction event, then create a new intention to process the behaviour specified in p else update the intention that generated e to also process the behaviour specified in e. 4. select one intention, i and execute its next step. 5. return to 1
AgentSpeak(L) Example: Hello World
As a first example of AgentSpeak(L) we present the basic hello world program. This program consists of two statements: an initial goal (line 01) and a plan rule (lines 03-04). As can be seen, statements are terminated by a period (,). The first statement declares a goal, !init(). This goal results in a goal adoption event being added to the agents event queue. This is only done once before the first iteration of the interpreter. The second statement is a plan rule. This rule is designed to handle the goal adoption event generated by the first statement. To specify a goal adoption event, the goal is simply prefixed by a + operator. The arrow (<-) operator is used to separate the triggering event from the behaviour implementation (which is on line 04). The behaviour contains a single plan operator – a private action that prints out the argument to the console.
01 !init(). 02 03 +!init() <- 04 println(hello world).
In terms of execution: on the first iteration, the interpreter selects the +!init() event; matches this event to the rule; and creates an intention to execute the behaviour associated with the rule. Next, the interpreter selects this newly created intention and executes the next step, which in this case involves “hello world” being printed out (this is an example of a private action). Upon completion of the step, the intention is marked as completed, and dropped. The agent continues to execute, but it never generates another event. This means that it never adopts another intention, which in turn means that the program does nothing more.
AgentSpeak(L) Example: Subgoals
The second example program illustrates the use of goals (and in particular, subgoals) in AgentSpeak(L) programs. The program itself is a slightly modified version of the Hello World program that moves the code to print out “Hello World” into a subgoal.
01 !init(). 02 03 +!init() <- 04 !printHello(). 05 06 +!printHello() <- 07 println(hello world).
This program is a slight modification of the previous program where the print action is moved to a separate rule that is used to handle the adoption of the !printHello() goal (lines 06-07). This goal is invoked as a subgoal on line 04 of the program (in the previous program, this line contained the actual print action),
In terms of execution, the following happens: on iteration 1, the agent removes the +!init() goal adoption event from the event queue and matches it to the first rule (lines 03-04), causing an intention to be created. This intention is then selected by the agent and the first step is executed. This step is a subgoal plan operator, which has the effect of creating a +!printHello() goal adoption event. Because it is a subgoal, the intention is also suspended (this means that the intention cannot be selected for execution). The goal adoption event also includes a reference to this intention, indicating that the event corresponds to a subgoal. On iteration 2, the agent removes the +!printHello() goal adoption event from the event queue and matches it to the second rule (lines 06-07). Because the event was generate by a subgoal plan operator, the agent appends the plan part of the rule to the intention from which the subgoal was invoked, and resumes that intention. The result of this is that the agent has a single intention that combines the first and the second rules. This is achieved by making an intention a stack. Each element of the stack contains a plan body and a program counter to indicate what step of that plan body is next. In this example, after the second event is handled, the intention contains 2 elements: an entry that represents the body of the !init() rule (lines 03-04) with a program counter indicating that the first step has been completed; a second entry then represents the body of the !printHello() rule (lines 06-07) indicating that no steps have been completed. The second entry is at the top of the stack. This intention is then selected by the agent, and the next step is executed.In this case, the agent peeks at the top of the stack and executes the first rule of the second entry (which calls the print action). On the 3rd iteration, the agent has no new events to process, so it simply selects the intention and executes the next step. When it peeks at the top entry in the intention, it notes that the entry is completed, so it removes that entry and then peeks at the new top entry. Again, the agent notices that this entry is also complete, so it removes the second entry, leaving the stack empty. This indicates to the agent that the intention has been completed, so it is dropped.
AgentSpeak(L) Example: Belief Queries
The third example program specifies an AgentSpeak(L) agent that uses the belief query plan operator. It also includes an initial belief (as well as an initial goal):
01 !init(). 02 is(rem, happy). 03 04 +!init() <- 05 println(starting); 06 ?is(rem, happy); 07 println(first hurdle passed); 08 ?is(rem, sad); 09 println(ending).
This program includes an initial goal !init() (line 01); an initial belief is(rem, happy) (line 02); and a plan rule with a more complex bahaviour (lines 04-09). As in the first example, the initial goal is used to generate a goal adoption event. The addition of the initial belief results in the adoption of that belief and the generation of a belief adoption event, which is also added to the agents event queue. The plan rule involves a behaviour consisting of a sequence of 5 plan operators. Each operator is separated by a semi-colon (;) and the plan rule is terminated by a period (.). The behaviour consists of two types of plan operator: the println private action and the belief query operator. This second operator works by checking the agents beliefs. If the provided belief can be matched to a belief in the agents belief set, then the operator succeeds, otherwise it fails (and the corresponding intention fails).
In terms of execution, the following happens: on iteration 1, the agent removes the +!init() goal adoption event from the event queue and then matches it to the plan rule, causing an intention to be created. This intention is then selected by the interpreter and the first step is executed, causing “starting” to be printed to the console. On iteration 2, the agent removes the +is(rem, happy) belief adoption event, but fails to match the event to a rule – this means that the event is ignored. Next, the agent again selects the intention and performs the second step which is a belief query for the belief is(rem, happy). This belief is in the agents belief set (it was added as an initial belief) so the query succeeds. On the third iteration, the event queue is empty, so no event is processed. Again the intention is selected and the next step is executed resulting in “first hurdle passed” being printed to the console. On the fourth iteration, there is still no event and the same intention is selected (there is only one intention). The fourth step is executed, which is another belief query, this time for is(rem, sad). This belief is not in the agents belief set, so the query fails, and the intention fails. This results in the intention being dropped (the fifth step of the intention is not executed).
AgentSpeak(L) Example: Belief Update
This final example illustrates how AgentSpeak(L) permits the modification of the agents internal state through the belief update plan operators. One operator is provided to support the addition of new beliefs and a second operator is provided to support the removal of existing beliefs. No operator is provided for the modification or an existing belief (this is achieved through the retraction of the existing belief and the subsequent adoption of the new belief).
01 light(on). 02 03 +light(on) <- 04 println(the light is on, turn it off!); 05 -light(on); 06 +light(off). 07 08 +light(off) <- 09 println(the light is off, turn it on!); 10 -light(off); 11 +light(on).
This program includes an initial belief, representing the fact the a light is on, and two rules. The triggering event of the first rule (lines 03-06) is the event that the agent adopts a belief that the light is on (like the initial belief). The body of the rule consists of a sequence of three actions: (1) It prints out a message to the console, (2) it retracts the belief that the light is on, and (3) it adopts a belief that the light is off. The second rule (lines 08-11) does the opposite of this – it retracts the belief that the light is off and adopts the belief that the light is on. In terms of behaviour, this program implements an infinite loop, where either the first rule or the second rule is executed on each iteration. In fact, the last operation of each rule generates the event that triggers the next rule.
In terms of execution, the following happens: on iteration 1, the agent adopts the belief that the light is on and adds the associated belief adoption event to the event queue. The agent then selects that event from the event queue and handles it by matching the event with the first rule (lines 03-06) and adopting a new intention that contains the associated plan. The agent then selects this intention for execution and executes the first step of the plan which prints out the “the light is on, turn it off”. On iteration 2, the agent does not select an event because the event queue is empty. It does, however select the intention again, this time executing the second step of the plan, which causes the belief light(on) to be dropped. This action has the side effect of generating a belief retraction event that is added to the agents event queue. On the 3rd iteration, the agent selects the belief retraction event from the event queue and attempts to match it against rule. No matching rule exists, so this event is ignored (in some implementations the event queue is filtered so that this type of event is never added as it can never affect the behaviour of the agent). The intention is selected for a third time, and the last step is executed, resulting in the belief light(off) being adopted. Again, this has a side effect – namely the generation of a belief adoption event, which is added to the event queue. At the end of this iteration, the intention is marked as completed and dropped. On the 4th iteration, the agent selects the belief adoption event and matches it to the second rule (lines 08-11). This results in the adoption of a new intention that contains the associated plan. The agent selects this intention, and executes the first step, which results in the following message being printed to the console: “the light is off, turn it on”. Over the next two iterations, the agent drops the belief light(off) and adopts the belief light(on), resulting in first rule being triggered and the behaviour described on iterations 1-3 is repeated. this behaviour finishes on iteration 9,resulting in the behaviour of iterations 4-6 being repeated and so on. In fact, the overall behaviour of the agent is an infinite loop where the agent prints out the statement that the “light is on…” followed by the statement that the “light is off…” repeatedly.
Beliefs, Goals and Events in ASTRA
In terms of beliefs, goals, and events, the main difference is that terms and variables are typed. The type system employed by ASTRA is based on the Java type system – simply because this is the underlying implementation language so adopting a similar type system makes translating between ASTRA and Java simpler and more transparent.
The full list of types is given in the table below.
Type | Description | Example Literals |
---|---|---|
int | integer value that maps onto the java.lang.Integer class and the int Java data type. | 5, -11, 127 |
long | integer value that maps onto the java.lang.Long class and the long Java data type. | 55l, -225l, 12954l |
float | real value that maps onto the java.lang.Float class and the float Java data type. | 12.3f, -4.567f |
double | real value that maps onto the java.lang.Double class and the double Java data type. | 9.876, -1234.5678 |
char | character value that maps onto the java.lang.Character class and the char Java data type. | 'a', '3', '@' |
boolean | boolean value that maps onto the java.lang.Boolean class and the boolean Java data type. | true or false |
string | string of characters that maps onto the java.lang.String class. | "animal", "feel", "12\t34" |
list | list of values that maps onto the astra.term.ListTerm class which itself extends java.util.List. | [1, 2, 3, 4], ["the", 4, 'a', true] |
object | a generic type that can be mapped onto any Java class (e.g. object | cannot be directly represented |
This means that representing the belief that the light is on would be done by a predicate with a string term: light(“on”); or the belief that the agent is alive, could be represented by the predicate: state(“alive”) – note that in both cases, the argument is a string. Similarly, goals in ASTRA are the same as in AgentSpeak, with the exception that all terms must be typed. For example, if you are a soccer playing agent and your team is attacking, then you may have a goal to score. This could be represented by a goal such as: !score(“goal”).
Given all terms must be literals of a valid type, clearly all variables must also be typed. Further, variables can only be associated with values that matches the type of the variable.
Events in ASTRA are again the same as in AgentSpeak(L) with the exception that all terms must be typed. For example, the event that corresponds to the adoption of the belief light(“on”) is +light(“on”) and the event that corresponds to the retraction of that a belief is -light(“on”). Similarly, the event that corresponds to the adoption of the goal !printState(“alive”) is +!printState(“alive”). Further, the triggering events that may be matched against the above events (where variables as used for all terms) would be: +light(string state), -light(string state), and +!printState(string state) respectively. Notice that in these examples, state is a variable.
Finally, ASTRA does also include other event types beyond the ones defined here. These additional event types correspond to message events (receipt of messages), EIS and CArtAgO events (environment events), and ACRE events (conversation management events). This use of an expanded set of event types is one of the distinguishing features of ASTRA when compared with AgentSpeak(L) or Jason.
Plan Rules in ASTRA
In ASTRA, plan rules are defined using a different syntax to that used in AgentSpeak(L). Specifically, the syntax adopted in ASTRA attempts to be closer to the syntax used in languages like Java and C – the plan body part of a rule is implemented as a code block which itself is made up of a sequence of primitive statements, control flow statements, or sub-blocks. Primitive statements are terminated by a semi-colon (;). An illustration of this can be seen below:
rule te : ctxt { Ac1; Ac2; { Ac3; Ac4; } }
Notice that in the above example, the rule keyword is used to declare that we have a rule. this is followed by a triggering event (te); and the plans context (ctxt), which is optional and may be omitted when it is true. The plan body is defined as a code block that consists of a sequence of three statements: 2 primitive statements Ac1 and Ac2, and a code sub-block that contains two more (sequentially executed) primitive statements Ac3 and Ac4. This syntax is the standard syntax for any C-derived programming language, including Java.
Writing ASTRA Programs
Before we launch into some examples of ASTRA programs, the last thing we need to do is to introduce the overall structure of an ASTRA program. ASTRA is different form AgentSpeak(L) in the following ways:
- agent programs are organised into agent classes that can be extended based on a multiple inheritance model
- the primitive actions used must be explicitly declared in the agent program
- ASTRA programs can reference java classes, so support needs to be provided for providing fully qualified class names
- in addition to plan rules, ASTRA programs include partial plans (named plan bodies) in order to improve code/class resuability.
- ASTRA aims to be familiar to developers who know mainstream programming languages, and in particular Java.
The result is that ASTRA programs are a little more structured than AgentSpeak(L) programs. An illustration of the basic structure is given below:
package path.to.folder; import java.lang.Object; agent MyAgent { // code goes here }
As can be seen here, ASTRA has adopted the package nomenclature that is used in Java. The package is the path from the root (src) folder to the folder containing the ASTRA class. As with Java, if the program is declared in the root folder, no package statement is required.
Import statements can be used to resolve Java class names to their fully qualified canonical class names.
The agent keyword is used to denote the start of the agent program and curly braces identify the body of that program (known here as an agent class). Currently only one class program is permitted per file and each file must have the same name as the agent class and the .astra file extension must be used. For example, the above program must be in a file called MyAgent.astra.
The body of an agent program is made up of 4 components are identified by unique keyword as is shown in the table below:
Keyword | Description | Example |
---|---|---|
module | Links an agent class to a Java module. | module Console console; |
types | Declares valid formulae | types eg { formula isa(string,string); } |
initial | Declares initial state of the agent | initial state("alive"); initial is("rem", "happy"), !init(); |
rule | Declares a plan rule | rule +!init() { console.println("hello"); } |
plan | Declares a partial plan | plan myplan() { console.println("plan"); } |
function | Declares a teleo-reactive function | function myfunction() { true -> console.println("function") } |
The module keyword is used to enable Java code to be directly executed from ASTRA code. Specifically, Java methods can be annotated as one of the following: an action, a term, a formula or a sensor. How the method is invoked depends on the annotation. For example, code that is annotated as an action can be executed as primitive actions. Methods that are annotated as a term can be used wherever a term can be used. Methods that are annotated as a formula can be used in any logical formula. Finally, methods that are annotated as sensors are invoked once at the start of each cycle of the agent interpreter.
ASTRA Example: Hello World
At their core, ASTRA programs are executed in exactly the same way as AgentSpeak(L) programs. The main differences come in the syntax, and the anciliary supports, such as modules, multiple inheritance, and typing. To help you move from the AgentSpeak(L) syntax, to the ASTRA syntax, this section (and the next three sections) present the ASTRA equivalents of the AgentSpeak(L) example programs given earlier.
The first program was a basic hello world program for AgentSpeak(L). The ASTRA equivalent is:
01 agent Hello { 02 module Console console; 03 04 initial !init(); 05 06 rule +!init() { 07 console.println("hello world"); 08 } 09 }
This program is semantically identical to the AgentSpeak(L) program, but is longer. The reason for this additional length is that ASTRA is more verbose and also, ASTRA does not hide as much (specifically, you must explicitly declare any modules that provide primitive actions used by the program).
Because the program prints out to the console, we have had to declare that we are using the console module (this provides the functionality to print to and read from the console).
Secondly, we are assuming that the program is to be saved in the root folder (the default package) so we do not need a package statement at the start.
ASTRA Example: Subgoals
The second example illustrates the use of goals in an AgentSpeak(L) program. The ASTRA equivalent is:
01 agent Subby { 02 module Console C; 03 04 initial !init(); 05 06 rule +!init() { 07 !printHello(); 08 } 09 10 rule +!printHello() { 11 C.println("hello world"); 12 } 13 }
Notice that in this example, we use identifier C for the Console module. This is to illustrate that the identifier does not need to match the name of the module.
ASTRA Example: Belief Queries
The third example below show an ASTRA agent that uses the belief query plan operator. It also includes an initial belief (as well as an initial goal).
01 agent Query { 02 module Console C; 03 04 types eg { 05 formula is(string, string); 06 } 07 08 initial !init(); 09 initial is("rem", "happy"); 10 11 rule +!init() { 12 C.println("starting"); 13 query(is("rem", "happy")); 14 C.println("first hurdle passed"); 15 query(is("rem", "sad")); 16 C.println("ending"); 17 } 18 }
Notice that the query operator “?” is replaced by the “query” keyword in ASTRA. Additionally, for the first time, we see the introduction of the types … keyword. This keyword is used to specify the types of beliefs that are valid for a given program. They are used for type checking – if you use a belief that is not declared in this section of the program, the compiler will generate an error indicating that the belief has not been specified. The identifier “eg” is used to associate a unique label with the types. A program can have many type blocks.
ASTRA Example: Belief Update
This final example illustrates how AgentSpeak(L) permits the modification of the agents internal state through the belief update plan operators. The ASTRA equivalent is:
01 agent Uppy { 02 module Console C; 03 04 types uppy { 05 formula light(string); 06 } 07 08 initial light("on"); 09 10 rule +light("on") { 11 C.println("the light is on, turn it off!"); 12 -light("on"); 13 +light("off"); 14 } 15 16 rule +light("off") { 17 C.println("the light is off, turn it on!"); 18 -light("off"); 19 +light("on"); 20 } 21 }
Using the ASTRA IDE to write an Agent Program
Step 1: Creating an ASTRA Project
To write an ASTRA program, you need to install the ASTRA Eclipse Plugin. Once you have the plugin installed, you should select “File→New→ASTRA Project…” and enter a name for the project (here enter “HelloWorld”) and click “Finish” to create the project.
Step 2: Writing an ASTRA Program
To write a program, you need to first create an ASTRA file by selecting “File→New→ASTRA Class” and then entering a name for the class (here, enter “HelloWorld” as the agent class name) and click “Finish” to create the ASTRA file.
Now, copy the program below into the editor:
agent HelloWorld { module Console console; // Adopt an intial goal to trigger the rule initial !init(); // A rule that is triggered whenever an !init() goal is adopted rule +!init() { // This line invokes the println(...) action in the console API. console.println("Hello World"); } }
This program prints the text “Hello World” to the console once. Notice that the agent keyword declares that the file contains an agent program and that the name of the agent program is “helloworld”. ASTRA uses this line to check that the program is written in a file named “HelloWorld.astra”. If it is not, then an error is reported.
When you have copied the file in to the editor, save it. When you save the file, Eclipse will automatically compile the code and report any errors.
Step 3: Running an ASTRA Agent Program