AFAS::APIs
From Agent Factory
This lesson is part of the AF-AgentSpeak Language Guide.
Contents |
Introduction
In addition to the Core API, which comes as a standard part of the AF-AgentSpeak language, it is also possible to use other pre-written APIs that provide additional capabilities to the agent. Typically, agent programming language APIs provide an interface to some external system or component. APIs are written in Java as a special type of agent component, known as a Module. Within the implementation, the developer creates various actions and sensors, which provide the acting and sensing capabilities that the agent will need to use the API.
Examples of APIs include:
- Platform Service APIs: Here, the API "agentifies" the platform service, identifying what information it can garner from the service and what it can do to the service.
- GUI APIs: Here, the API provides an interface through which the agent can monitor user activity, push content to the user and, where appropriate, adapt the interface to changes in its environment.
- Remote application APIs: Here, the API provides an interface to some remote application, such as a physical robot or a wireless sensor network, or a virtual environment.
- Data Processing APIs: Logic is not always the most appropriate was to represent and manipulate information. Sometimes it is more appropriate to use Java. In these cases, modules can be used to implement more appropriate data structures for storing the data, and algorithms can be provided to perform data analysis. The API exposes a view of the data stored in the data structures and provides actions that allow the agent to apply the associated data analysis algorithms.
This lesson illustrates first how to use an existing API, and then explores how to build your own APIs. Later lessons will introduce other pre-written APIs for specific components / applications.
For a detailed list of API's available for AF-AgentSpeak, see the Library section.
Using pre-written APIs
The Agent Management Service API
The API that the lesson will focus on is the Agent Management Service API. This API provides run-time support for the creation, suspension, resumption, and termination of agents resident on an agent platform. The actual functionality is provided as part of the Agent Management Service (AMS), a core platform service (the is mandatory in the FIPA Specifications) that is deployed on all agent platforms.
To use the AMS API, all you need to do is add a single line to the agent program:
module ams -> com.agentfactory.clf.library.AMSLibrary;
This sets up an association between the agent and the module, and allows the agent to access the actions and sensors associated with that module / API.
The API comes with one sensor, called "agents", which generates beliefs about what agents are running on their platform and their current state. The specific beliefs generated by the sensor are:
- localAgent(?name): One instance of this belief is generated per agent on the platform - each belief holds the name of one of the agents currently residing on the agent platform.
- agentType(?name, ?type): One instance of this belief is generated per agent on the platform. Here, ?type refers to the type of agent (e.g. AF-AgentSpeak, AF-APL, AF-TR, AF-RMA, ...).
- agentState(?name, ?state): One instance of this belief is generated per agent on the platform. Here, ?state refers to the current state of the agent, and may be one of: {active, initiated, suspended, terminated, transit, waiting}. These states have been chosen in compliance with the FIPA standards.
- agentID(?name, ?addr): One instance of this belief is generated per agent on the platform. This is the unique identifier of the agent.
In addition to these perceptions, the AMS API also provides a number of actions:
- create(?name, ?design): Creates an agent whose name is ?name that is based on the specified design, given by ?design.
- resume(?name): Causes the execution of the agent identified by ?name to be resumed. If the agent is currently active, then this action has no effect.
- suspend(?name): Causes the execution of the agent identified by ?name to be suspended. If the agent is currently suspended, then this action has no effect.
- terminate(?name): Causes the execution of the agent identified by ?name to be terminated. This results in the agent being suspended and then removed from the agent container.
To access the API actions, you must use an extended action syntax:
<module-name> . <action>
For example, to resume an existing agent called "rem", you would specify the following action:
ams.resume(rem)
The prefix to the action changes depending on the identifier you have associated with the module when you declared the module (in the examples here, we use the identifier "ams" in compliance with our declaration of the module above).
The remaining sections of this lesson will explore how to use this API in more detail through a number of simple examples. In later lessons, we will explore some other API's that have been built for AF-AgentSpeak.
Creating Agents Dynamically
Perhaps the most powerful feature of the AMS API is the ability to create agents dynamically (i.e. at run-time). The program below outlines how to do this based on a simple scenario in which 10 agents are created. Once started, each agent then creates the next agent in the sequence.
#agent creator
module ams -> com.agentfactory.clf.library.AMSLibrary;
+initialized : true <-
ams.setup,
?next = 0,
?count = 1,
while (?count < 11) {
if (~localAgent(agent+?count) & ~name(agent+?count)) {
?next = ?count,
?count = 11
} else {
?count = ?count + 1
}
},
if (?next > 0) {
ams.create(agent+?next, launcher.aspeak),
.println("Agent: agent" + ?count + " has been created."),
ams.resume(agent+?next)
};
To run the platform, we create a single agent, called "initial" that is an instance of the above program (lets call it launcher.aspeak). When run, the overall system goal is to create 10 agents labelled agent1-agent10 respectively. Each agent that is created at runtime is responsible for creating the next agent in the sequence. The plan that is used to achieve this can be broken into 3 parts:
- line 1: this first part of the plan sets up the AMS API by performing the ams.setup action. This links the API to the underlying platform service.
- lines 2-11: this second part of the plan uses the localAgent(...)' belief of the AMS API to work out which agent should be created next. Basically, this is a while loop in which the ?count variable is incremented repeatedly until there is a value for ?count such that there is no local agent with the name "agent+?count" and also, this is not the name of the agent itself (as there will not be a belief that the agent is itself a localAgent). Once such a name is identified, the ?next variable is set to the corresponding value of ?count. If no name is found (i.e. agent10 has been created), then ?next remains bound to 0 (which indicates that no agent should be created).
- lines 12-15: the final part of the plan creates the next agent (if required). It uses the ams.create(...) action to create the next agent, and the ams.resume(...) action to start the agent once it has been created.
Suspending Agents
To illustrate how to suspend agents at runtime, we can extend the example used in the previous section to include a fourth step to the plan in which the agent suspends itself once it has created and resumed the next agent.
#agent suspender
module ams -> com.agentfactory.clf.library.AMSLibrary;
+initialized : true <-
ams.setup,
?next = 0,
?count = 1,
while (?count < 11) {
if (~localAgent(agent+?count) & ~name(agent+?count)) {
?next = ?count,
?count = 11
} else {
?count = ?count + 1
}
},
if (?next > 0) {
ams.create(agent+?next, launcher.aspeak),
.println("Agent: agent" + ?count + " has been created."),
ams.resume(agent+?next)
},
foreach(name(?name)) {
ams.suspend(?name)
};
Here, the name(...) belief is used with a foreach(...) plan operator to bind the variable ?name to the agents name. This is then used in the ams.suspend(...) action.
Terminating Agents
For this final example, we show a simple agent program that is designed initially to be used by an agent with name A. Agent A its name, creates an agent called agent B, and terminates itself. Agent B, once started, checks its name, creates an agent with name A, and terminates itself. This continues on until the agent platform is stopped. The program is called "switcher.aspeak"
#agent terminator
module ams -> com.agentfactory.clf.library.AMSLibrary;
+initialized : name(?name) <-
ams.setup,
durative(.sleep(1000)),
if (?name == A) {
ams.create(B, switcher.aspeak),
.println("Creating B"),
ams.resume(B)
} else {
ams.create(A, switcher.aspeak),
.println("Creating A"),
ams.resume(A)
},
ams.terminate(?name);
The Queue API
The Queue API is an example of a Data Structure API that has been implemented for the Common Language Framework. This API provides support for the creation and manipulation of Queues.
The API consists of 4 actions:
- create(?id): This action creates a new queue with the given identifier.
- destroy(?id): This action destroys an existing queue with the given identifier.
- enqueue(?id, ?value): This action adds the given value to the queue specified by the given identifier. If no such queue exists, then the action fails.
- dequeue(?id): This action removes the head of the queue specified by the given identifier. If no such queue exists, then the action fails. If the queue is empty, then the action fails.
Additionally, the API provides 1 sensor, called state that generates beliefs about the states of any queues that have been created using the API. Specifically, the sensor generates the following beliefs:
- empty(?id): the queue specified by the identifier is empty
- front(?id, ?val): ?val is the value at the that is currently at the head of the queue specified by the identifier.
- size(?id, ?x): the number of items currently held in the queue specified by the identifier.
To use the API, add the following line to your agent program:
module queues -> com.agentfactory.clf.interpreter.QueueAPI;
Below, we illustrate how to use this API with a simple example:
#agent queuer
module queues -> com.agentfactory.clf.interpreter.QueueAPI;
+initialized : true <-
queues.create(test);
+empty(test) : true <-
queues.enqueue(test, token);
+front(test, token) : true <-
queues.dequeue(test);
With this program, the agent will continuously add and remove a token value from the test queue.
Building custom APIs
Above, we have described how to use an existing API. Such APIs can be built for a variety of reasons, including: providing support for a platform service, interfacing agents with GUIs, accessing a remote platform, and performing data processing tasks. For many applications, there will not be suitable pre-existing APIs, instead, the developer will need to create one. We now explain how to do this.
Introduction to API Programming
Up to now, we have discussed APIs only in the context of AF-AgentSpeak. In reality, these APIs are common components that can be used with any Agent Factory language that has been built using the Common Language Framework. Specifically, API developers extend three specific classes:
- The com.agentfactory.clf.interpreter.Module class: This is the top level class for implementing an API. Modules can contain instances of the other classes listed below.
- The com.agentfactory.clf.interpreter.Sensor class: This class implements support for generating beliefs from raw data. While Sensors can be implemented as stand-alone components, for APIs, Sensors are typically implemented as inline classes.
- The com.agentfactory.clf.interpreter.Action class: This class implements support for implementing primitive actions. As with Sensors, Actions can be implemented as stand-alone components, however, for APIs, Actions are also implemented as inline classes.
To create an API, all that you need to do is to extend the module class and implement sensors and actions, as is illustrated in the outline code below:
public class MyAPI extends Module {
public MyAPI() {
addAction("myAction", new Action() {
@Overrides
public boolean execute(Predicate activity) {
System.out.println("This is an action being performed!");
return true;
}
});
addSensor("mySensor", new Sensor() {
@Overrides
public void perceive() {
addBelief("sensor(mydata)");
}
});
}
}
In this example API implementation, we have created one sensor and one action. The action is specified as an inline class and associated with the API (module) through the addAction(...) method. This method takes two arguments:
- a string representation of the action identifier: this has the same form as a predicate in the logic.
- an instance of the action: here, this is provided by instantiating an inline class that extends the Action class.
The core of the action implementation is the execute(...) method. This is the method that is invoked when the agent attempts to perform the corresponding action. The method returns a boolean value indicating the success or failure of the action.
The sensor is also specified as an inline class and is associated with the API (module) through the addSensor(...) method. Again, this method takes two arguments:
- a string representing the sensor identifier: this has the same form as a predicate in the logic.
- an instance of the sensor: similarly with actions, this is provided by instantiating an inline class that extends the Sensor class.
The core of the sensor implementation is the perceive() method. This is the method that is invoked when the agent is updating its beliefs. As is shown in the example code, beliefs are added using the addBelief(...) method, which takes a string representation of the belief to be added.
The above API can be linked to any AF-AgentSpeak program. Below, a simple program is provided to illustrate the use of the API:
#agent foo
module myModule -> MyModule;
+sensor(mydata) : true <-
myModule.myAction;
The rule in this program states that, when the agent receives the added belief event that the belief sensor(myData) has been added, it should perform action myAction from the myModule API. This rule is triggered once, on the second iteration of the agent interpreter. On the first iteration, the special initialized event is handled (resulting in nothing happening), and the sensor(myData) belief is generated by the sensor (and the corresponding belief event is added to the event queue). On the second iteration, the initialised event has now been processed, so the agent now processes the sensor(myData) event. Because the agent re-adopts this belief on every iteration (as part of the perception process), no additional belief event is created. The current event is processed by matching the event to the single rule in the program, and the corresponding plan is executed.
Platform Service APIs
Platform Services are shared platform-level resources that may be used by one or more agents that are resident on the agent platform. Examples of Platform Service include: Message Transport Services (communication infrastructure), the Agent Management Service (AMS), and the Environment Interface Standard (EIS) Service (described later).
In this section, we explain how to build (based on current best practices) an API that can be linked to a platform service. To do this, we will use an updated version of the example platform service that was developed in the Platform Service Development Guide. The service in question is a very simplistic Whiteboard Service (a shared communication space) that allows agents to share one piece of information with each other.
The WhiteboardService class
package whiteboard;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.agentfactory.platform.core.IAgent;
import com.agentfactory.platform.impl.AbstractPlatformService;
public class WhiteboardService extends AbstractPlatformService {
public static final String NAME = "af.std.wb";
private Map<String, String> information;
public WhiteboardService() {
information = new HashMap<String, String>();
}
public synchronized void putInformation(IAgent agent, String info) {
information.put(agent.getName(), info);
}
public synchronized String removeInformation(IAgent agent) {
return (String) information.remove(agent.getName());
}
public synchronized List<String> getInformation() {
List<String> list = new ArrayList<String>();
for (String key: information.keySet()) {
list.add("information(" + key + "," + information.get(key) + ")");
}
return list;
}
}
As can be seen, this service implements 3 methods: two update methods, and one accessor method. It is basically a low-level component of the agent platform, and as such, cannot directly be used by an AF-AgentSpeak agent.
The Whiteboard API
To allow AF-AgentSpeak agents to use the service, we need to implement an API, the source code for which is outlined below:
package whiteboard;
import com.agentfactory.clf.interpreter.Action;
import com.agentfactory.clf.interpreter.CoreUtilities;
import com.agentfactory.clf.interpreter.Module;
import com.agentfactory.clf.interpreter.Sensor;
import com.agentfactory.clf.lang.Predicate;
public class WhiteboardAPI extends Module {
private WhiteboardService service;
public WhiteboardAPI() {
addAction("setup", new Action() {
@Override
public boolean execute(Predicate activity) {
agent.bindToPlatformService(WhiteboardService.NAME);
service = (WhiteboardService) agent.getPlatformService(WhiteboardService.NAME);
return true;
}
});
addAction("put(?info)", new Action() {
@Override
public boolean execute(Predicate activity) {
String info = CoreUtilities.presenter.toString(activity.termAt(0));
service.putInformation(agent, info);
return true;
}
});
addAction("clear", new Action() {
@Override
public boolean execute(Predicate activity) {
service.removeInformation(agent);
return true;
}
});
addSensor("view", new Sensor() {
@Override
public void perceive() {
if (service == null) return;
for (String bel : service.getInformation()) {
addBelief(bel);
}
}
});
}
}
As can be seen, the above code specifies 3 actions:
- setup: This action is used to configure the API and link it to the underlying platform service. The action should only be called once by the agent (it can be called multiple times, but will have no effect after the first invocation).
- put(?info): This action is used to enable the agent to put a piece of information onto the whiteboard. This is achieved by invoking the putInformation(...) method on the platform service. Any previous information shared by the agent is lost when this action is performed.
- clear: This action removes th current information that the agent has on the whiteboard.
Additionally, there is a single view sensor associated with the API. This sensor is used to read the current state of the whiteboard. This sensor generates beliefs of the form: information(?agt, ?info).
Example AF-AgentSpeak Program
To finish this section, we demonstrate the system with a simple example agent program that implements a system in which the agents use the whiteboard to pass a token:
#agent token
module whiteboard -> whiteboard.WhiteboardAPI;
+initialized : true <-
whiteboard.setup;
+is(starter) : true <-
whiteboard.put(token),
whiteboard.clear;
-information(?agt, token) : linkedTo(?agt) <-
whiteboard.put(token),
durative(.sleep(500)),
whiteboard.clear;
This program is broken into 3 rules:
- The first rule performs the configuration of the WhiteboardAPI.
- The second rule is used to initiate the token passing. To do this, one of the token agents is given the initial belief is(starter). This agent posts the first occurrence of the token on the whiteboard.
- The third rule is used to pass the token. Whenever an agent removes the token post from the whiteboard, each agent checks whether it was done by the the agent that it is "linked to". If it is, then the agent posts the token, leaves it there for 500ms and then clears it from the whiteboard.
Deploying the Example
To illustrate the program, we create three agents: A, B, and C. These agents have been linked so that A watches C, B watches A, and C watches B. A is the initiator of the behaviour. The Debugger Run Configuration for this example is shown below:
public class WhiteboardDebugConfiguration extends DebuggerRunConfiguration {
public String getName() {
return "whiteboard";
}
public String getDomain() {
return "debugging.ucd.ie";
}
@Override
public void configure() {
addLanguageSupport(new AgentSpeakInspectorFactory(), new AgentSpeakStateManagerFactory());
super.configure();
addArchitectureFactory(new AgentSpeakArchitectureFactory());
addPlatformService(WhiteboardService.class, WhiteboardService.NAME);
addAgent("A", "whiteboard/token.aspeak");
initAgent("A", "is(starter)");
initAgent("A", "linkedTo(C)");
addAgent("B", "whiteboard/token.aspeak");
initAgent("B", "linkedTo(A)");
addAgent("C", "whiteboard/token.aspeak");
initAgent("C", "linkedTo(B)");
}
public static void main(String[] args) {
new WhiteboardDebugConfiguration().configure();
}
}
In addition to the creation of the agents for the problem, this class also requires the configuration of the Whiteboard platform service. This is done through the following line:
addPlatformService(WhiteboardService.class, WhiteboardService.NAME);
Here, NAME is a String constant that has been defined in the WhiteboardService class that defines the standard service identifier for the service.
When you run the program, you will be able to watch the behaviour of the system by viewing the beliefs (the agent will have one information(...) belief at any time) and the Action Log of one of the agents.
Debugging the Service
Up to now, we have focused on using the Agent Factory Debugger to debug agents. However, the debugger also includes support for debugging platform services. Double clicking on a platform service will open the Service Inspector on that service. By default, this inspector contains one view that shows you which agents are currently bound to the service, however, the Inspector can easily be extended to support additional views.
The code below is an example view for a the ServiceInspector. It is designed to show the information that is stored on the whiteboard:
public class WhiteboardPanel extends JPanel implements IServiceInspectorPanel {
private static final long serialVersionUID = 1L;
private JList list;
private WhiteboardService service;
public WhiteboardPanel(IPlatformService service) {
this.service = (WhiteboardService) service;
this.setLayout(new BorderLayout());
list = new JList();
JToolBar toolbar = new JToolBar();
toolbar.add(new JLabel("Whiteboard State:"));
add(BorderLayout.NORTH, toolbar);
JScrollPane scrollPane = new JScrollPane(list);
add(BorderLayout.CENTER, scrollPane);
update();
}
public void update() {
DefaultListModel model = new DefaultListModel();
for (String info:service.getInformation()) {
model.addElement(info);
}
list.setModel(model);
}
}
To link this view to the debugger, you need to write a IServiceInspectorFactory class. These classes tend to be quite simple and are specific to a given application. As a result, they are typically implemented inline in the Debugger Run Configuration:
...
@Override
public void configure() {
addLanguageSupport(new AgentSpeakInspectorFactory(), new AgentSpeakStateManagerFactory());
addServiceInspectorFactory(new IServiceInspectorFactory(){
@Override
public boolean handles(Class<? extends IPlatformService> class1) {
return class1.getName().equals("whiteboard.WhiteboardService");
}
@Override
public ServiceInspector create(IPlatformService service) {
ServiceInspector inspector = new ServiceInspector(service);
inspector.addTab("Whiteboard Info", new WhiteboardPanel(service));
return inspector;
}
});
super.configure();
...
}
...
In order to generate updates to the additional view, you also need to make modifications to the service codebase. Specifically, updates are managed using the built-in Java Observer classes. Any platform service that is implemented by extending the AbstractPlatformService class is automatically declared observable, and all that you need to do is generate a notification whenever the service is updated by an agent (i.e. in the putInformation() and removeInformation() methods:
...
public synchronized void putInformation(IAgent agent, String info) {
information.put(agent.getName(), info);
setChanged();
notifyObservers();
}
public synchronized String removeInformation(IAgent agent) {
String temp = (String) information.remove(agent.getName());
setChanged();
notifyObservers();
return temp;
}
...
Any notification from the underlying platform service is received by the ServiceInspector and all the views are notified that a change has taken place via the invocation of the update() method.
If you make the above modifications to the whiteboard service and then run the updated Debugger Run Configuration, you can load the modified service inspector by double clicking on the whiteboard service. You should get something like this:
GUI APIs
GUI APIs are really an interface between the agent and a GUI that is receiving user input. From the GUI side, the API is used to monitor the GUI for key user events and to report these events to the agent. From the agent side, the API is used to enable the agent to affect changes in the GUI. These changes may involve something more complex, such as some form of dynamic adaption of the GUI, or may simply inform the user that some activity has been completed.
The typical solution for handling the receipt of user events is a private event queue that is implemented as part of the API. When some key event occurs, the GUI posts a user event on this event queue. A sensor is then used to generate beliefs about what user events have occurred since the last time the agent checked the GUI event queue.
From the agent side, there are two possible solutions to managing interaction with the GUI:
- For simple GUIs where the agents control of the GUI is limited to a small number of updates, then the most common approach is to implement one agent action per GUI update.
- For more complex GUIs, the most common approach is to implement a agent-GUI event model whereby the agent posts update events that are passed directly to and handled by the GUI.
While the choice of best model depends entirely on the given GUI, a rough figure for deciding when to move from the simple to complex model is when at lease 10 GUI updates are supported. The motivation for the change of model is simply complexity. The simple model does not scale well, and becomes difficult to maintain as the complexity of the GUI increases. As a result, the event-handler model is then preferred as it typically uses a fixed set of agent actions and manages the diversity of GUI updates through the parameters that are passed to those actions.
For this lesson, we will focus initially on the simple GUI model and investigate a simple GUI that supports the run-time creation and termination of agents.
Given the example is only intended to illustrate how to build a GUI API, this example will provide support for only two requirements:
- Present a list of agents currently residing on the agent platform
- Provide support for the run-time creation of agents
Agent Manager GUI and API
The GUI for this example is a standard Java Swing implementation. We give the entire code below, and then focus on the more important parts (from a GUI API development perspective):
package agentManager;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTextField;
public class AgentManagerGUI extends JFrame {
AgentManagerAPI api;
JList agentList;
DefaultListModel agentListModel;
JTextField agentName, design;
public AgentManagerGUI(AgentManagerAPI api) {
this.api = api;
setTitle("Agent Manager");
agentListModel = new DefaultListModel();
agentList = new JList(agentListModel);
setLayout(new BorderLayout());
add(agentList, BorderLayout.CENTER);
JPanel bottom = new JPanel();
bottom.setLayout(new FlowLayout());
agentName = new JTextField(10);
design = new JTextField(10);
bottom.add(new JLabel("Enter Agent Name: "));
bottom.add(agentName);
bottom.add(new JLabel("Enter Agent Design: "));
bottom.add(design);
JButton create = new JButton("Create");
create.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
AgentManagerGUI.this.api.postGuiEvent("create(" + agentName.getText() + ",\"" + design.getText() + "\")");
}
});
bottom.add(create);
add(bottom, BorderLayout.SOUTH);
pack();
setSize(640, 480);
}
public void addAgent(String name) {
agentListModel.addElement(name);
}
public void removeAgent(String name) {
agentListModel.removeElement(name);
}
}
The first point of note is the dependency between the GUI and the API. A reference to the AgentManagerAPI is passed to the AgentManagerGUI via the constructor and is stored in a field of the GUI. It is through this reference that the GUI will post agent events. An example of this is in the implementation of the ActionListener for the create button. Here, the GUI responds to the user clicking the create button by posting a GUI event to the API. In this example, the event posted takes the form: create(?name, ?design), which mirrors the arguments used by the AMS API (see previous lesson).
NOTE: For this example, the event is a string representation of an AFL predicate. Where appropriate, a more complex model can be implemented that utilises a GuiEvent class, which is itself converted into one or more beliefs.
The second point of note is the agent-GUI link. This is realised by the addAgent(...) and removeAgent(...) methods, which are invoked from within the API implementation (see below):
package agentManager;
import java.util.LinkedList;
import java.util.List;
import com.agentfactory.clf.interpreter.Action;
import com.agentfactory.clf.interpreter.CoreUtilities;
import com.agentfactory.clf.interpreter.Module;
import com.agentfactory.clf.interpreter.Sensor;
import com.agentfactory.clf.lang.Predicate;
public class AgentManagerAPI extends Module {
AgentManagerGUI gui;
List<String> eventQueue;
public AgentManagerAPI() {
eventQueue = new LinkedList<String>();
addAction("setup", new Action() {
@Override
public boolean execute(Predicate activity) {
gui = new AgentManagerGUI(AgentManagerAPI.this);
gui.setVisible(true);
return true;
}
});
addAction("showAgent(?name)", new Action() {
@Override
public boolean execute(Predicate activity) {
String name = CoreUtilities.presenter.toString(activity.termAt(0));
gui.addAgent(name);
return true;
}
});
addAction("hideAgent(?name)", new Action() {
@Override
public boolean execute(Predicate activity) {
String name = CoreUtilities.presenter.toString(activity.termAt(0));
gui.removeAgent(name);
return true;
}
});
addSensor("events", new Sensor() {
@Override
public void perceive() {
while (!eventQueue.isEmpty()) {
addBelief(eventQueue.remove(0));
}
}
});
}
public void postGuiEvent(String event) {
eventQueue.add(event);
}
}
As can be seen, the API implementation follows a similar pattern to the Platform Service API described in the previous section. Again, a setup action is implemented, which here, is responsible for creating and showing the GUI. The API also maintains an internal reference to the GUI, which it uses to invoke updates on the GUI. Examples of GUI updates include the showAgent(...) and hideAgent(...) actions which add / remove an agent to / from the GUI as appropriate.
Sensing of the GUI event queue is realised through the events sensor. This sensor empties the current event queue, generating beliefs about each event that it removes. By default, the agent is left to decide the actual order in which the events are handled.
Example AF-AgentSpeak Agent
We conclude this section by demonstrating how to use this API via a simple example AF-AgentSpeak program:
#agent amsGui
module agentManager -> agentManager.AgentManagerAPI;
module ams -> com.agentfactory.clf.library.AMSLibrary;
+initialized : true <-
agentManager.setup,
ams.setup;
+localAgent(?name) : true <-
agentManager.showAgent(?name);
-localAgent(?name) : true <-
agentManager.hideAgent(?name);
+create(?name, ?design) : ~localAgent(?name) <-
ams.create(?name, ?design);
This program makes use of the AMS API described in the previous lesson. The initial plan sets up both the GUI and the AMS APIs. Once set up, the second rule defines how the agent handles localAgent(...) belief addition events by adding the name of the agent just detected to the GUI (this is an example of a GUI update). The third rule represents the agent handling a GUI event. In this case, the agent creates the specified agent, but does so only so long as it believes that there is not agent with the name specified.
Data Structure APIs
In the previous lesson, the Queue API was introduced as a second example of how to use APIs with AF-AgentSpeak. This section introduces a second, Data Structure API, for Stacks. Although the Stack API is provided as part of the Common Language Framework, this lesson will examine its implementation to illustrate how to build Data Structure APIs.
The source code for the Stack API is given below:
package com.agentfactory.clf.interpreter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import com.agentfactory.clf.lang.Predicate;
public class StackAPI extends Module {
Map<String, LinkedList<String>> stackMap;
public StackAPI() {
stackMap = new HashMap<String, LinkedList<String>>();
addAction("push(?id,?item)", new Action() {
@Override
public boolean execute(Predicate activity) {
String id = CoreUtilities.presenter.toString(activity.termAt(0));
String item = CoreUtilities.presenter.toString(activity.termAt(1));
LinkedList<String> stack = stackMap.get(id);
if (stack == null) {
addBelief("noSuchStack(" + id +")");
return false;
}
stack.addFirst(item);
return true;
}
});
addAction("create(?id)", new Action() {
@Override
public boolean execute(Predicate activity) {
String id = CoreUtilities.presenter.toString(activity.termAt(0));
stackMap.put(id, new LinkedList<String>());
return true;
}
});
addAction("destroy(?id)", new Action() {
@Override
public boolean execute(Predicate activity) {
String id = CoreUtilities.presenter.toString(activity.termAt(0));
stackMap.remove(id);
return true;
}
});
addAction("pop(?id)", new Action() {
@Override
public boolean execute(Predicate activity) {
String id = CoreUtilities.presenter.toString(activity.termAt(0));
LinkedList<String> stack = stackMap.get(id);
if (stack == null) {
addBelief("noSuchStack(" + id +")");
return false;
}
if (stack.isEmpty()) {
addBelief("stackEmpty(" + id + ")");
return false;
}
stack.removeFirst();
return true;
}
});
addSensor("state", new Sensor() {
@Override
public void perceive() {
for (String name : stackMap.keySet()) {
LinkedList<String> stack = stackMap.get(name);
if (stack.isEmpty()) {
addBelief("empty(" + name + ")");
} else {
addBelief("top(" + name + ", " + stack.getFirst() + ")");
}
addBelief("size(" + name + ", " + stack.size() + ")");
}
}
});
}
}
As can be seen, the implementation consists of a Map of Linked Lists.
