Architecture / Interpreter Development Guide
From Agent Factory
The Architecture / Interpreter Development Guide outlines the support provided within Agent Factory for the creation and deployment of new agent architectures and interpreters. Additionally, this guide explains how to create and execute custom agent implementations that are implemented directly in Java. To this end, the Guide is broken up into three basic parts. The first part explains the basic API that is provided to facilitate the implementation of new types of agent. The second part then explains how to create a simple agent that has been directly implemented in Java. The final part of this guide then goes on to describe how to create more general solutions that can be customised via custom configuration files (e.g. programming language files).
Contents |
Necessary Packages
This guide is only intended for version 1.1.0 or greater of the AF-RTE.
Editorial History
??/??/2007: Version 1 - Initial description of the Agent class and how to implement a basic scheduler
??/??/2008: Version 2 - Updated to add example of Reactive Messaging Agent
19/03/2008: Version 3 - Updated to include revised version of controller that incorporates the use of the signalEndOfTimeSlice() method.
The Basic Architecture API
When creating an agent in Agent Factory, be it a custom Java agent, or a generic agent architecture/interpreter, you must extend the abstract com.agentfactory.plaform.core.Agent class. This class implements the basic functionality required by all types of agent, and includes:
- integration with a global scheduler, which schedules the execution of ALL deployed agents,
- a message queue component that is used to store all messages that are sent to a given agent,
- an address book component, which can be used to store known agent identifiers (and which is used to resolve agent names to agent addresses during the transmission of a message).
- a platform services manager component, which provides an interface through which agents can bind to available platform services, and
- a initialization process, through which agents are automatically bound to all available Message Transport Services and a corresponding agent identifier is created.
This section outlines both the basic methods that are provided to support the creation of agents, and the abstract methods that must be made concrete in your implementation.
Abstract Methods
Most of the abstract methods that are defined in the generic Agent base class, with the exception of the first two methods, are intended to manipulate the underlying control algorithm of the agent.
The initialise() method
This method is used to initialise the agent. The corresponding parameter is passed directly from the APScriptHandler and contains the contents of the INITIALISE statement of the APS file. This statement combines an agent name and some initialization information, for example, an AFAPL agent called "rem" may have part of its mental state initialised by a line of the form: INITIALISE rem BELIEF(likes(beer)).
The getType() method
This method is provided to allow your implementation to return a String that denotes the type of the agent. It is included to allow Run-time Tools to understand what type of agent each agent is. For example, it is currently used primarily by the Agent Factory Debugger to determine which inspector should be used to inspect the state of the agent.
Specifically, the AFAPL and AFAPL2 interpreters return the strings "AFAPL" and "AFAPL2" respectively. These cause the Agent Factory Debugger to launch either the AFAPLInspector or the AFAPL2Inspector whenever the developer attempts to inspect one of these types of agent.
The execute() method
This method is invoked by the global scheduler when the agent is scheduled to be executed. Specifically, it is called from within the Scheduler thread. Implementations of this method should NOT implement the core agent control algorithm, but should instead trigger a separate control thread, in which the underlying control algorithm should be executed. An illustration of best practice for the use of this method is described later in this section of the guide.
The endOfTimeSlice() method
This method is the counterpart of the above execute() method. It is invoked by the global scheduler to indicate that the agent should stop executing as its time slice has completed. In Agent Factory, we currently allow some flexibility in the scheduling process - while the endOfTimeSlice() method signals that the end of the time slice has been reached, the scheduler does not currently enforce this time limit - enforcement is left as the responsibility of the developer. The principle motivation for this is to allow the underlying control algorithm to complete gracefully rather than suspending the control algorithm in state that is less desirable. Best practice for the use of this method is describe later in this section of the guide.
The terminate() method
This method is invoked when an agent is to be terminated. The principle responsibility of the method is to ensure that the underlying control algorithm terminates gracefully as the agent is terminated. Use of the method is illustrated later in this guide.
The step() method
This method is invoked by the Agent Factory Debugger. It should implement a step of the underlying control algorithm, which may take the form of either the execution of a complete iteration of that algorithm or the execution of a section of that algorithm up to a specified breakpoint.
Support Methods
In addition to the abstract methods outlined above, the generic agent base class also provides a number of concrete methods. This section outlines some of the more useful ones.
The metaRegistration() method
This method is invoked externally by the Agent Management Service during the agent creation process. Specifically, it is invoked immediately after the actual agent object has been created. The purpose of this method is to bind the agent to all available Message Transport Services and to generate an initial agent identifier for the agent based on the set of MTS that it has successfully bound to.
The bindToService() method
This method binds the agent to a given service, identified by a string-based service identifier that is passed as a parameter to the method. The method itself invokes the bindToService() method on the platform manager, and if successful, adds the newly bound service to a set of internal data structures including a list of services (ordered by age & priority) and a serviceMap (which maps service identifiers to services). If the bound service is a message transport service, the method also updates the agents personal agent-identifier.
The getAvailableMTSServices() method
This method returns a Vector of the Message Transport Services that the agent is currently bound to.
The getService() method
Returns a reference to a service identified by a service identifier that is passed as a parameter (if the agent is is bound to that service).
Overview of Agent Creation
The creation of agents is the responsibility of the Agent Management Service (AMS). This is a prefabricated Platform Service that is a mandatory component of all FIPA Agent Platforms. The agent creation process is split broadly into two phases: the construction and initialization of the agent; and the registration of that agent with the set of available message transport services.
While the second phase is fixed, and implemented through the metaRegistration() method (see above), the first phase varies depending on the type of agent that is being created. As is discussed in this guide, Agent Factory supports two broad categories of agent:
- Custom Java Agents: This category of agent is implemented directly in Java and does not depend on an external file for initialization of its state. An example of this agent is developed in the next main section of this guide.
- Generic Agents: This category of agent is implemented over two tier. An underlying set of Java classes implement some form of architecture / interpreter, and a corresponding configuration/source code file contains necessary initial state configuration. This category includes all agents written in the AFAPL and AFAPL2 agent programming languages.
The construction and initialization of phase of the agent creation process varies slightly depending on the category of agent that is being instantiated. Two pieces of information are required as inputs to this process: a name for the agent, and a design, which takes the form of a Java resource path (e.g. /com/agentfactory/example/MyAgent.java, com/agentfactory/example/MyAgent.afapl). The design parameter incorporates an extension. This extension determines the type of agent the is being created, which, in turn, determines how the agent is created.
In Agent Factory, agents may be created in one of two ways:
- Through explicit declaration as part of the agent platform script
- Dynamically through invocation of the createAgent() method provided by the Agent Management Service platform service.
For completeness, the figure below presents an example agent platform script in which two agents are created: a Custom Java Agent called bob, and an AFAPL agent called fred.
CREATE_AGENT bob example/CustomAgent.java CREATE_AGENT fred example/GenericAgent.agt START_AGENT bob START_AGENT fred
The above script is read during the startup of the Agent Factory platform, and results in two agents being created: one agent, called bob, which is an instance of the class example.CustomAgent; and another agent, called fred, which is an instance of the example/Genericagent.agt agent program (this extension refers to a machine readable version of an AFAPL agent program), which is loaded into an instance of the com.agentfactory.afapl.AFAPLInterpreter class.
In order for the above example to work, the platform configuration file (the file with the .cfg extension), must include the following statement:
AGENT_INTERPRETER com.agentfactory.afapl.AFAPLInterpreter agt
Creating Custom Java Agents
For Custom Java Agents, designs are specified as resource paths that end with a .java extension. The AMS treats the remainder of the design parameter as a reference to the Java class that implements that design. reference to an agent that is implemented completely in Java (i.e. it refers to a Java class).
To create the agent, the AMS looks for a constructor that takes two parameters:
- a string representing the name of the agent
- a reference to the agent platform
If it finds such a constructor, then that constructor is invoked, and the agent is created. Otherwise, the agent creation process fails.
Creating Generic Agents
The term generic agent refers all agents that require some form of external file to initialize the agent. The design parameter is treated as a resource path that references this file, and the extension is used to associate that design with a particular agent architecture / interpreter (these are declared in the platform configuration file using the AGENT_INTERPRETER keyword).
To create the agent, the AMS looks for a constructor that takes four parameters:
- a string representing the name of the agent
- the design parameter
- a reference to the agent platform
- the set of interpreter configuration parameters that are provided as part of the interpreter declaration (again, this is taken from the AGENT_INTERPRETER statements provided in the platform configuration file).
If it fails to find an appropriate constructor, the agent process fails, otherwise the constructor is invoked and the agent is created.
Integration with the Global Scheduler
Agent Factory employs a global scheduler for managing the execution of agents on all agent platforms within a given Java Virtual Machine. As is indicated above, all agents are automatically registered with this scheduler during the creation process. Once registered, the scheduler adds the agent to an internal queue and employs round-robin scheduling. When an agent is scheduled for execution, the scheduler checks the current state of the agent. The agent is only executed when it is in an ACTIVE state. This is in compliance with the current FIPA standards for agent technology. If not in an active state, the scheduler immediately enqueues the agent and retrieves a reference to the next agent in the queue.
Once an agent in an ACTIVE state is scheduled for execution, the scheduler invokes the execute() method on the agent signaling that it should commence execution. The scheduler then goes to sleep for the corresponding time slice. If the agent completes executing before the allotted time slice has ended, it interrupts the scheduler, freeing up the remaining time in the time slice. Alternatively, if the agent does not complete execution before the end of the allotted time slice, the scheduler re-awakens, and invokes the endOfTimeSlice() method to let the agent know that it has run out of time. After invoking this second method, the scheduler again goes to sleep, and waits for the agent to signal that it is finished.
This approach results in the agent execution time slice limit being less strictly enforced. Our rationale for this is to allow for graceful suspension of the agents execution cycle. While it is expected that, in normal practice, the agents will simply suspend execution gracefully by completing their current action and relinquishing control to the scheduler, it is also possible to immediately suspend the agent interpreter upon invocation of the endOfTimeSlice() method. As a result, it is left to the developer of the underlying agent architecture to decide how strictly to enforce the time slice limits.
The design of the global scheduler requires that each custom agent implementation include an internal control thread that contains the core reasoning process of the agent. For example, the AFAPL Interpreter currently uses the following class to implement the core control thread:
private class ControllerThread extends Thread {
boolean completed;
boolean endOfSlice;
public ControllerThread(String name) {
super(name + " Control Thread");
completed = false;
endOfSlice = false;
}
private synchronized void waitForInterrupt() {
try {
wait();
} catch (InterruptedException) {}
}
public void run() {
while (!completed) {
waitForInterrupt();
setEndOfSlice(false);
// Start of Main Reasoning Algorithm
beliefManager.manage();
commitmentManager.manage();
ScheduleItem item = null;
while (!schedule.isEmpty() && !endOfSlice) {
item = (ScheduleItem) schedule.remove(0);
try {
if (item.action.execute(item.commitment.getActivityIdentifier())) {
item.commitment.update(Commitment.EVENT_SUCCEED);
} else {
item.commitment.update(Commitment.EVENT_FAIL);
}
} catch (RuntimeException re) {
item.commitment.update(Commitment.EVENT_FAIL);
}
}
// End of Main Reasoning Algorithm
// Send out end of Time Slice signal (implemented in Agent base class)
signalEndOfTimeSlice();
}
}
public synchronized void setCompleted(boolean completed) {
this.completed = completed;
}
public synchronized void setEndOfSlice(boolean endOfSlice) {
this.endOfSlice = endOfSlice;
}
public synchronized boolean getCompleted() {
return completed;
}
public synchronized boolean getEndOfSlice() {
return endOfSlice;
}
}
This is implemented as a private inner class within the com.agentfactory.afapl.AFAPLInterpreter class, which encapsulates the core agent interpreter.
Implementing a Reactive Agent Architecture
To illustrate how alternate agent architectures may be implemented in Agent Factory, this section explores the implementation of a simple reactive agent architecture. Specifically, we implement a type of agent that is able to react to incoming messages and send response messages. We then illustrate how this architecture can be used by implementing a simple Agent Management Service (AMS) agent.
Our approach to implementing this architecture is to implement a core MessageProcessor component that can be configured to handle different messages via the implementation of MessageHandlers.
Requirements for Running the Example
To run the example below, you should use the following packages:
- AF-RTE version >= 1.1.5
- AF-Common / Logic version >= 1.0.1
The MessageHandler class
A message handler is a Java class that implement behaviours that are enacted in response to the receipt of a given message. Message handlers are implemented by extending the core.agentfactory.rma.MessageHandler class:
package com.agentfactory.rma;
import com.agentfactory.platform.core.Agent;
import com.agentfactory.platform.mts.Message;
import com.agentfactory.platform.service.PlatformService;
public abstract class MessageHandler {
protected Agent agent;
private String performative;
public abstract boolean filter(Message message);
public abstract boolean act(Message message);
protected MessageHandler(Agent agent, String performative) {
this.agent = agent;
this.performative = performative;
}
public String getPerformative() {
return performative;
}
public boolean send(Message message) {
return agent.getMessageQueue().sendMessage(message);
}
public PlaformService getService(String id) {
return agent.getService(id);
}
}
As can be seen in the above code, the base class is an abstract class that requires concrete subclasses to implement two core methods: the filter(...) method and the act(...) method. The first of these methods is used to check whether or not the given message should be handled by that handler, while the latter method specifies the behaviour that should be enacted should the handler be selected for handling a given message.
Finally, you should note that the MessageHandler constructor takes two arguments a reference to the agent, and a string representation of the performative that corresponds to the type of message that is handled by the handler. This latter parameter is used to speed up retrieval of message handlers by the MessageProcessor.
The MessageProcessor class
This class maintains a set of references to the message handlers that are associated with a given agent implementation. When new messages are received by the agent, the message processor class is invoked. It is the responsibility of this class to select an appropriate message handler for that message.
package com.agentfactory.rma;
import com.agentfactory.rma.MessageHandler;
import com.agentfactory.platform.mts.Message;
import com.agentfactory.platform.util.Logger;
import java.util.*;
public class MessageProcessor {
private static final int CLASS_LOG_LEVEL = Logger.LEVEL_DETAIL;
private Map handlerMap;
public MessageProcessor() {
handlerMap = new HashMap();
}
public void addHandler(MessageHandler handler) {
String performative = handler.getPerformative().toUpperCase();
List list = (List) handlerMap.get(performative);
if (list == null) {
list = new LinkedList();
handlerMap.put(performative, list);
}
list.add(handler);
Logger.detail("MessageProcessor: Added Handler With Performative: " + performative, CLASS_LOG_LEVEL);
}
public boolean handleMessage(Message message) {
boolean handled = false;
List list = (List) handlerMap.get(message.getPerformative());
if (list != null) {
MessageHandler handler = null;
Iterator it = list.iterator();
while (!handled && it.hasNext()) {
handler = (MessageHandler) it.next();
if (handler.filter(message)) {
handled = handler.act(message);
}
}
}
return handled;
}
}
As can be seen, this class implements two basic methods: the addMessageHandler(...) method is used to associate message handlers with the underlying agent, and the handleMessage(...) method is used to process incoming messages. The underlying storage of message handlers is realised through a HashMap class, which maintains a mapping between message performatives and lists of message handlers. This is done to improve the efficiency of the underlying message handler selection algorithm,
Also, it is worth noting that the implementation of the handleMessage(...) method allows for multiple message handlers to be used to handle a given message. The message is only considered handled if a message handler is located whose associated behaviour is executed successfully (which is indicated by the message handlers act(...) method returning true).
The ReactiveMessageAgent architecture
The final part of our example agent architecture involves the creation of the underlying agent class. This is implemented by extending the com.agentfactory.platform.core.Agent class as follows:
package com.agentfactory.rma;
import com.agentfactory.platform.AgentPlatform;
import com.agentfactory.platform.core.Agent;
import com.agentfactory.platform.mts.Message;
import com.agentfactory.platform.util.Logger;
import java.util.ArrayList;
import java.util.Iterator;
public abstract class ReactiveMessageAgent extends Agent {
private static final int CLASS_LOG_LEVEL = Logger.LEVEL_WARNING;
private ControllerThread controller;
private MessageProcessor messageProcessor;
private class ControllerThread extends Thread {
boolean completed;
boolean endOfSlice;
public ControllerThread(String name) {
super(name + " Control Thread");
completed = false;
endOfSlice = false;
}
private synchronized void waitForInterrupt() {
try {
wait();
} catch (InterruptedException) {}
}
public void run() {
while (!completed) {
waitForInterrupt();
setEndOfSlice(false);
// Start of core architecture control algorithm
Message message = null;
Iterator it = getMessageQueue().getMessages().iterator();
while (it.hasNext()) {
message = (Message) it.next();
if (!messageProcessor.handleMessage(message)) {
Logger.warning("Message Not Understood: " + message, CLASS_LOG_LEVEL);
}
}
// End of core architecture control algorithm
// Signal End of time slice (method implemented in base Agent class)
signalEndOfTimeSlice();
}
}
public synchronized void setCompleted(boolean completed) {
this.completed = completed;
}
public synchronized void setEndOfSlice(boolean endOfSlice) {
this.endOfSlice = endOfSlice;
}
public synchronized boolean getCompleted() {
return completed;
}
public synchronized boolean getEndOfSlice() {
return endOfSlice;
}
}
public ReactiveMessageAgent(String name, AgentPlatform platform) {
super(name, null, platform, null);
messageProcessor = new MessageProcessor();
// BIND TO AVAILABLE MTS:
ArrayList mts = platform.getPlatformServiceManager().getAvailableMTS(name);
for (int i = 0; i < mts.size(); i++) {
bindToService((String) mts.get(i));
}
controller = new ControllerThread(name);
controller.start();
}
public void addMessageHandler(MessageHandler handler) {
messageProcessor.addHandler(handler);
}
public void initialise(String data) {
Logger.warning("[" + getName() + "] Does not support initialization: " + data, CLASS_LOG_LEVEL);
}
public void execute() {
controller.interrupt();
}
public void endOfTimeSlice() {
controller.setEndOfSlice(true);
controller.interrupt();
}
public void terminate() {
controller.setCompleted(true);
controller.interrupt();
}
public void step() {
controller.interrupt();
}
}
As can be seen, this class includes an inner class that implements the main control thread as was discussed in the previous section of this document. The main change between the example code from the previous section and the code used in this inner class is the core algorithm that is implemented. For the reactive agent, this algorithm retrieves any messages that were received since the last iteration, and invokes the MessageProcessor.handleMessage(...) method for each one.
In the main agent class, the core scheduler methods execute() and endOfTimeSlice interrupts the control thread. Similarly, the terminate() method sets the completed flag to be true and performs an interrupt, and the step() method, which is called by the debugger, interrupts the control thread.
Finally, as can be seen in the constructor, the agent is bound to the available message transport services by retrieving all valid MTS for that agent, and binding to each one in turn.
Using the ReactiveMessageAgent architecture
To use this architecture, you simply subclass the ReactiveMessageAgent class as is shown below:
import com.agentfactory.rma.ReactiveMessageAgent;
import com.agentfactory.platform.AgentPlatform;
import com.agentfactory.platform.mts.Message;
import com.agentfactory.service.ams.AgentManagementService;
import com.agentfactory.platform.util.Logger;
public class AMSAgent extends ReactiveMessageAgent {
private static final int CLASS_LOG_LEVEL = Logger.LEVEL_WARNING;
public AMSAgent(String name, AgentPlatform platform) {
super(name, platform);
bindToService(AgentManagementService.NAME);
addMessageHandler(new CreateAgentHandler(this, Message.REQUEST));
}
public String getType() {
return "AF-FIPA-AMS";
}
}
The only method that must be implemented is the getType() method, which returns a string that uniquely defines the type of the agent. The set of message handlers associated with the agent are then specified as part of the constructor. The agent is also bound to the AgentManagementService platform service in the constructor.
The code for the CreateAgentHandler is given below:
import com.agentfactory.rma.MessageHandler;
import com.agentfactory.logic.lang.FOS;
import com.agentfactory.platform.core.Agent;
import com.agentfactory.platform.mts.Message;
import com.agentfactory.platform.mts.StringMessage;
import com.agentfactory.platform.util.Logger;
import com.agentfactory.service.ams.AgentManagementService;
public class CreateAgentHandler extends MessageHandler {
private static final int CLASS_LOG_LEVEL = Logger.LEVEL_ERROR;
public CreateAgentHandler(Agent agent, String performative) {
super(agent, performative);
}
public boolean filter(Message message) {
if (!message.getLanguage().equals("AFAPL")) {
return false;
}
FOS fos = new FOS(message.getContent());
return fos.getFunctor().equals("createAgent");
}
public boolean act(Message message) {
FOS fos = new FOS(message.getContent());
String name = fos.argAt(0).toString();
String type = fos.argAt(1).toString();
AgentManagementService ams = (AgentManagementService) agent.getService(AgentManagementService.NAME);
if (ams == null) {
Logger.error("AMSAgent: Not Bound To Agent Management Service", CLASS_LOG_LEVEL);
return false;
}
Agent newAgent = ams.createAgent(name, type);
if (newAgent == null) {
Logger.error("AMSAgent: Failed to Create: " + name + " [" + type + "]", CLASS_LOG_LEVEL);
return false;
}
StringMessage response = StringMessage.newInstance();
response.setPerformative(Message.INFORM);
response.setLanguage("AFAPL");
response.setSender(agent.getAgentID());
response.getReceivers().add(message.getSender());
response.setContent("createdAgent(" + name + "," + type + ")");
response.setContentLength(response.getContent().length());
send(response);
return true;
}
}
As can be seen in this example handler, the handler filters messages based on their content being in the AFAPL language, an the actual content being a first-order structure whose functor is the string "createAgent".
Deploying the Example Agent
For the moment, we will explore how to deploy our AMS agent as an application agent. For information on how to deploy the agent as a system agent, see the Application Deployment Guide.
To deploy our AMS agent, you simply add the following line to the relevant Agent Platform Script (APS) file:
CREATE_AGENT ams AMSAgent.java
If you want the agent name to be platform specific, use the following line:
CREATE_AGENT ams AMSAgent.java platformSpecific
You do not need to modify the Platform Configuration (CFG) file as you are using a Java-based agent implementation.
