AFAS::Introduction
From Agent Factory
This is a lesson from the AF-AgentSpeak language guide.
Contents |
Introduction
AgentSpeak(L) is a widely documented agent programming language. To gain a basic understanding of the language, we recommend that you read Anund Rao's paper AgentSpeak(L): BDI Agents speak out in a logical computable language. This document assumes that you have read this document, and focuses mainly on explaining how to program the Agent Factory variant of the langauge.
AgentSpeak(L) programs combine an event model, a model of beliefs, a model of goals, and a planning language that allows programmers to define a collection of plan rules that define how the agent should respond to an event for a given context.
Beliefs: In AF-AgentSpeak, beliefs take the form of grounded predicate formulae. For example, the belief that Socrates is mortal would take the form: is(Socrates, mortal).
Events: Events in AF-AgentSpeak refer primarily to changes in the beliefs and goals of the agent. Whenever an agent generates a new belief or goal (thiys is defined as a belief or goal that was not present in the previous iteration of the agent interpreter), an addition event is generated. For example, when a soccer playing agent sees the football, it may generate the belief sees(ball). This results in the creation of a belief addition event +sees(ball). Similarly, when the ball moves out of the agents line of sight, the belief is dropped and a corresponding belief removal event is generated, which takes the form -sees(ball). All agents are created with a "+initialised" event on the event queue to provide an initial trigger for agent programs.
Goals: Goals encode the decisions of the agent as to what it should do, 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). In AgentSpeak, goals are not represented explicitly, but are represented implicitly through a combination of goal events (which indicate the adoption or removal of a goal) and an intention stack that models the progress of the plan that was adopted to achieve the goal. The syntax of addition and removal goal events is the same as for beliefs - the goal is prefixed by a + or - respectively.
Plans: In AF-AgentSpeak, plans are represented as rules that identify some sequence of actions that should be performed whenever a given event occurs and in a given context. Typically, plan rules take the form:
te : ctxt <-
Ac1,
Ac2,
Ac3;
Where te is a triggering event; ctxt is the plans context; and Ac1, Ac2, and Ac3 represent the sequence of actions that should be performed whenever the specified triggering event occurs and the associated context is satisfied.
To get you started, in the language, we will present a number of simple examples that illustrate both how to write an AF-AS program, and how to use the Core API (basic actions and sensors) that is available by default for all AF-AS agents (in fact, this Core API is available to all agent programming languages implemented using the Common Language Framework). In later lessons, we will explain both how to use other APIs and how to develop your own APIs.
Printing to the Console
AF-AS provides two basic actions for outputting to the console: .print(...) and .println(...). These actions are basically wrappers for the Java System.out.print(...) and the System.out.println(...) methods. The simplest program demonstrates how to use these actions is the "hello world" program that prints out hello world to the console, which is given below:
#agent helloworld
+initialized : true <-
.println("Hello World");
This program prints the text "Hello World" to the console once. An important point to note is the #agent line. This line basically declares that the file contains an agent program and that the name of the agent program is "helloworld". AF-AgentSpeak uses this line to check that the program is written in a file named "helloworld.aspeak". If it is not, then an error is reported. While this may seem a pointless addition a later lesson will introduce a reuse model that underpins AF-AgentSpeak that is based on the template mechanism. This reuse model includes support for inheritance, abstract plans (and agents), and overriding of plans / plan rules.
Writing and Running an AF-AS Agent Program
To write and run an AF-AS agent program, you need to install the Agent Factory Eclipse Plugin. Once you have the plugin installed, you should perform the following steps:
- Create a new Agent Factory Project: The first step is to create an Agent Factory project. To do this select "File->New->Project..." and select "New Agent Factory Project" from the "Agent Factory" category. Click on the "Next" button and enter a name for the project (here enter "HelloWorld"). Click "Finish" and you are done!
- Create an Agent Speak Program File: Now, you need to create the source code file in which you will write your AF-AS program. To do this, select "File->New->Other" and select "New Agent Speak file" from the "Agent Factory" category. Click on the "Next" button and enter a name for the file (here, enter "helloworld.aspeak" as the filename). Now, click "Finish" and you are done.
- Write the Agent Speak Program: Next, you need to write your agent program. For this step, lets use the example agent program from the previous section. Save the file, and Eclipses "autobuild" process will take care of the rest (it will do a syntax check on the file, and report any syntax errors).
- Create a Run Configuration class to deploy the program: The final step requires that you write a Run Configuration class to deploy the program. Two examples of this kind of class is given in the following sections. Java-based configurations are used over configuration files to allow maximum flexibility when deploying agent platforms (i.e. it can be used stand alone, integrated into an EJB container, deployed as an OSGi service, ...).
The next subsections outline two ways of deploying an agent program. The approach you choose depends upon whether or not you wish to debug the agent program. First we will look at debugging, and afterwards, we will look at deploying.
Writing a Debugger Run Configuration
The Agent Factory Debugger is a generic component of Agent Factory that can be customised for different agent languages and architectures. Basic support for using the debugger is provided via the DebuggerRunConfiguration class. To debug an application, you simply extend this class, override the configure() method and then write a very simple main method. Current best practice involves creating an inner class for the configuration and an outer class for the main method. We illustrate this with the example code below:
import com.agentfactory.agentspeak.interpreter.AgentSpeakArchitectureFactory;
import com.agentfactory.agentspeak.debugger.AgentSpeakStateManagerFactory;
import com.agentfactory.agentspeak.debugger.inspector.AgentSpeakInspectorFactory;
import com.agentfactory.visualiser.DebuggerRunConfiguration;
public class Main {
public static class MyConfig extends DebuggerRunConfiguration {
public String getName() {
return "helloworld";
}
public String getDomain() {
return "ucd.ie";
}
public void configure() {
addLanguageSupport(new AgentSpeakInspectorFactory(), new AgentSpeakStateManagerFactory());
super.configure();
addArchitectureFactory(new AgentSpeakArchitectureFactory());
addAgent("Bob", "helloworld.aspeak");
}
}
public static void main(String[] args) {
new MyConfig().configure();
}
}
The getName() and getDomain() methods are used by the superclasses as part of the platform configuration, removing them will result in a default platform name and domain being applied. The core of the platform configuration is done in the configure() method. The first line of this method configures AF-AgentSpeak support for the Agent Factory Debugger. The second line causes the agent platform to be created. The third line installs architectural support for the AF-AgentSpeak language (basically it links the AF-AgentSpeak interpreter to the Run-Time Environment). Finally, the fourth line creates a helloworld.aspeak agent with name "Bob".
The main method simply creates an instance of the class and calls the configure() method.
When you run this class (this is just a standard Java class with a main method), the debugger should be loaded, which looks something like this:
To open the agent inspector shown above, you double-click on the agent name in the tree view on the left-hand side. You are then able to start, stop, and step an agent by clicking on the buttons at the top of the corresponding inspector. The various views expose different aspects of the agents internal state.
Writing a Deployment Run Configuration
Deployment Run Configurations are very similar to Debugger Run Configurations. In fact, the only changes you need to make are:
- Extend DefaultRunConfiguration instead of DebuggerRunConfiguration.
- Remove any debugger specific code from the configuration (this is anything that appears before the super.configure() line.
The Deployment Run Configuration that is equivalent to the Debugger Run Configuration code given above is:
import com.agentfactory.agentspeak.interpreter.AgentSpeakArchitectureFactory;
import com.agentfactory.platform.impl.DefaultRunConfiguration;
public class Main {
public static class MyConfig extends DefaultRunConfiguration {
public String getName() {
return "helloworld";
}
public String getDomain() {
return "ucd.ie";
}
public void configure() {
super.configure();
addArchitectureFactory(new AgentSpeakArchitectureFactory());
addAgent("Bob", "helloworld.aspeak");
}
}
public static void main(String[] args) {
new MyConfig().configure();
}
}
Self Sensing
AF-AS provides a default sensor, called ".self" that provides basic beliefs about the agent. In its current form, the sensor provides 2 beliefs:
- name(?x): is a belief about the name of the agent (the variable ?x is replaced by the name)
- agentID(?x, ?addr): is a belief about the FIPA Agent Identifier of the agent. These identifiers are used for inter-agent communication.
To illustrate how to use these beliefs, we extend the hello world program to include the name of the agent in the string that is printed to the console:
#agent helloworld
+initialized : name(?name) <-
.println("Hello World from " + ?name);
The output of this program depends on the name of the agent you create. For example, the previous section, described how to create and deploy a "helloworld" agent called "Bob". If you modified the .aspeak file to match the above program, then running the program would cause the text "Hello World from Bob" to be printed to the console once.
Communicating
The syntactic structure of the messages that are sent between agents, and the underlying transport mechanisms have been developed in compliance with the existing FIPA Standards. Rather than give a detailed overview of these standards, we will start here with simple examples and add further information in later lessons.
Support for communication in AF-AS takes the form of a special sensor that generates events corresponding to messages that are received and the .send(...) action that enables agents to send messages. The format of the message events is:
+message(?performative, ?senderAgentID, ?content)
where ?performative is a message type that is compliant with the communicative acts specified in FIPA ACL; ?senderAgentID is a representation of the senders agent identifier, and is the same as the agent identifier belief outlined in the previous section; and ?content is the content of the message. Similarly, the .send(...) action action takes 2 parameters: the communicative act, and the content of the message.
The two most basic types of messages that can be sent between agents are inform and request messages. These messages capture the sharing of beliefs / information, and asking for help / actions to be performed respectively. To illustrate how to use the communication infrastructure, we will create a simple "monitoring" agent that repeatedly checks to see if an agent that it is monitoring is "alive" (active). To implement this, we need to write the following programs:
#agent monitor
// Monitor agent code:
+monitoring(?name, ?addr) : true <-
.send(request, agentID(?name, ?addr), status);
+message(inform, agentID(?name, ?addr), status(alive)) : monitoring(?name, ?addr2) <-
.send(request, agentID(?name, ?addr), status);
// Monitored agent code:
+message(request, ?sender, status) : true <-
.send(inform, ?sender, status(alive));
For simplicity, we have implemented this behaviour as one program. In real world systems, the code may be separated over two different agent programs.
The first rule in this program states that, if you have an event that you have started to monitor a given agent, then send a message to that agent requesting its status. The receipt of this message by the monitored agent is encoded in the third rule, which states that, if the agent has received a message requesting its status, it should respond by informing the requester that its status is "alive". The receipt of the response is encoded in the second rule, where the agent responds to the receipt of the message by again requesting the status of the monitored agent.
The use of the context condition on the second rule encodes the scenario that the agent only continue monitoring the second agent so long as it believes that it should be monitoring that agent. Once it no longer has the belief, then it will ignore the status update and the monitoring will stop.
To run this program, create a new AgentSpeak source file called "monitor.aspeak" and copy the code into it. Next, you simply replace the following line of code in the Run Configuration class:
addAgent("Bob", "helloworld.aspeak");
with:
addAgent("A", "monitor.aspeak");
addAgent("B", "monitor.aspeak");
initAgent(A, "monitoring(B, addresses(local:ping.ucd.ie))");
The first two lines create 2 agents A and B, and the third line gives A an initial belief that it should be monitoring B (this causes A to start monitoring B via the first rule in the agent program).
Failing
The actions of an agent can succeed or fail depending on whether the action code executed correctly. The failure action, .fail always fails. An example of this action is use is the following rule:
#agent failer
+initialized : name(?name) <-
.println("Hello World from " + ?name),
.fail,
.println("Goodbye World from " + ?name);
This plan rule defines a plan that involves a sequence of three steps:
- the previous print statement;
- the .fail action; and
- a second print statement that prints out "Goodbye World from ..."
In this example, the agent will execute the first step of the plan, printing out the first string. The second step of the plan will also be performed, but this step will fail (because it is the fail action) resulting in the plan being dropped. As a result, the second string will never be printed out.
Remembering and Forgetting
In standard AgentSpeak(L), you are able to add and remove beliefs using the + and - operators. For example, the following plan adds a belief about the agents name:
#agent rememberer
+initialized : name(?name) <-
+iam(?name);
Whenever a belief is added that is not currently a belief of the agent, the agent generates a belief addition event (in the case where this rule was applied to an agent called Bob, then Bob would generate the belief iam(Bob) and this would result in the creation of the belief event +iam(Bob).
Similarly, to remove beliefs, you use the - operator, for example the following program prints "Hello World from XXX" followed by "Goodbye World from XXX". The first string is printed on the addition of the iam(...) belief and the second string is printed on its removal:
#agent rememberer
+initialized : name(?name) <-
-iam(?name),
+iam(?name);
+iam(?name) : true <-
.println("Hello World from " + ?name);
-iam(?name) : true <-
.println("Goodbye World from " + ?name);
However, the + and - operators only work on ground beliefs (i.e. beliefs that have no variables). Sometimes we want to create plans that remove multiple beliefs matching some ungrounded formula. To do this, we can apply the .abolishAll(...) action.
#agent abolisher
+initialized : true <-
+know(agent1),
+know(agent2),
.abolishAll(know(?x));
+know(?name) : true <-
.println("I know " + ?name);
-know(?name) : true <-
.println("I forget " + ?name);
The output generated when you run this program is:
I know agent1 I know agent2 I forget agent1 I forget agent2
Working with Goals
A key feature of AgentSpeak(L) is the provision for achievement goals, which are written as predicate statements prefixed by an exclamation mark. Whenever a goal is declared (in a plan), a corresponding goal addition event is created. It is this event that is used to match the goal to a corresponding plan.
To illustrate how goals work, the program below adapts our hello world program to use the goal !say("Hello World"):
#agent goaler
+initialized : true <-
!say("Hello World");
+!say(?string) : true <-
.println(?string);
AgentSpeak(L) goals are a lot like method or function calls in Object-Oriented or Procedural languages. The agent handles the adoption of the goal by generating a goal event that is matched to the triggering event part of the plan rules. Once a plan has been selected, the actions of that plan are added to the existing intention stack in which the goal was raised. This is the same as occurs with the method/program call stack more traditional programming languages. Once the goal has been completed, the agent continues execution of the plan in which the initial goal was raised.
#agent hello
+initialized : true <-
!say("Hello World"),
.println("You are a wonderful place."),
!say("Goodbye World"),
.println("I am sorry to leave");
+!say(?string) : true <-
.println(?string);
The example program above generates the following output:
Hello World You are a wonderful place. Goodbye World I am sorry to leave
As this shows, the agent completes the first !say(...) goal before moving on to the second line of the first plan.
One of the main differences between goals and method / function calls is the lack of a return value. In AgentSpeak, return values are realised indirectly through the addition of beliefs that represent the outcome of a given plan.
Sleeping and Durative Actions
The final primitive action that is provided as part of the Core API of AgentSpeak is the .sleep(?timeInMS) action. This action basically exposes the Thread.sleep method, where the parameter provided to the action is treated as a time in milliseconds. This action can be used in two ways:
- If the action is invoked directly by the agent, it will cause the interpreter to stop for the specified time (no perception or deliberation will take place).
- If the action is wrapped within the durative(...) plan operator, then the plan is suspended for the allotted time period, but the agent interpreter cycle continues to execute (allowing other intentions to be achieved).
In general, it is recommended that you use the durative plan operator with the sleep action (unless you explicitly want the interpreter to stop). To illustrate how you should use it, the following program prints out the string "Hello World" after 10 seconds have passed:
#agent sleepy
+initialized : true <-
durative(.sleep(10000)),
.println("Hello World");
