Architecture / Interpreter Development Guide

From Agent Factory

Jump to: navigation, search

Contents

Introduction

Agent Factory is an open framework that allows you to develop your own agents / agent architectures without needing to be concerned with greater framework issues such as communication infrastructure or platform development. Building a new agent in Java requires only that you extend the com.agentfactory.platform.impl.AbstractAgent class and implement the requisite abstract methods. Part 1 of this guide outlines the Basic API that you are required to implement and explains how to deploy custom Java agents. Part 2 moves on to explain how to use the Basic API to implement more complex agent interpreters.

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.

01/02/2010: Version 4 - Revised to reflect AF2.0.

The Basic Architecture API

All agent types are required to conform to the com.agentfactory.platform.core.IAgent interface, which defines the minimal set of methods needed for an agent to be deployable on an Agent Factory agent platform. Because this interface includes a number of methods that require a quite detailed knowledge of the internal workings of Agent Factory, we provide a default agent implementation that you can use as a starting point for your own agent class. This implementation, com.agentfactory.platform.impl.AbstractAgent requires that you implement only four methods:

  • execute(): this is called whenever the agent is scheduled for execution and should finish by calling the endOfIteration() method which is defined in the com.agentfactory.platform.impl.AbstractAgent class.
  • getType(): returns a type string that is used to match the agent to an inspector / state manager / ...
  • initialise(): this method is used to pass initialisation strings to the agent when it is created
  • update(): standard update method for an Observable object (NOT USED HERE)

Additionally, you are required to implement a constructor that takes one argument (a string) and which simply calls the same constructor in the parent class.

Creating a Custom Java HelloWorldAgent

Lets start exploring how to use this API by creating a simple Hello World agent. What we want is an agent that prints out hello world when ever it is invoked. To do this, all that we need to do is to subclass the AbstractAgent class and implement the execute() method as is shown below:

public class HelloAgent extends AbstractAgent {
    public HelloAgent(String name) {
        super(name);
    }

    @Override
    public void execute() {
        System.out.println("Hello World!");
        endOfIteration();
    }

    @Override
    public String getType() {
        return null;
    }

    @Override
    public void initialise(String data) {
    }

    @Override
    public void update(Observable arg0, Object arg1) {
    }
}

As a slight improvement, we could modify the getType() method to return a type, say "HelloWorld", but really this is not necessary unless you want to integrate our HelloWorld agent with some of the more advanced development tools (e.g. the Agent Factory Debugger).

Using the Messaging Infrastructure

Lets delve a little further by exploring how to create an agent that sends and receives messages. Specifically, lets look at building a "ping" agent. This agent will have two modes:

  • passive: respond to incoming pings by sending a pong
  • active: send pings to specified agents

The idea here is that we will create a generic "ping" agent and allow the developer to specify, at deployment time, whether the agent should be passive or active (note: we will consider that all active agents are also passive).

Sending a Message

As a starting point, lets explore how to send the initial ping message. Support for FIPA messages is provided via a combination of the IMessage interface and the StringMessage class, which implements the interface supporting the FIPA String Representation. To create a message, you simply create an instance of the StringMessage class and set relevant properties as is highlighted in the code below, and then invoke the send(...) message, which is provided as part of the underlying agent implementation:

StringMessage response = StringMessage.newInstance();
response.setPerformative(IMessage.REQUEST);
response.setContent("ping");
response.getReceivers().add(recieverId);
response.setSender(this.getAgentID());
send(response);

As can be seen above, the minimal set of properties required to send a message are:

  • the agent identifier of the sender
  • the agent identifier of the receiver
  • the performative
  • some content

When sending messages, the sender identifier is the agents own identifier (and it is got by calling the getAgentID() method). What is less obvious is how to get the receiver identifier. Basically, any agent that sends a message needs to know the identifier of the agent that will receive the message before it sends the message (this is not strictly true because AF allows regular expressions to be used for the name part of the identifier). For this example, we will assume that the developer decides at deployment time which agents are to be pinged. This behaviour can be supported by making use of the initialise() to build a list of agents that are to be pinged.

The main issue in doing this is that the agent identifiers are instances of the AgentID class, while the initialize method takes only Strings. This means that we need some form of textual representation of an agent identifier which we can then convert to an AgentID object. The FIPA standards specify a representation format for agent identifiers using their own formal language, FIPA-SL0. Agent Factory provides a FIPAContent class that models FIPA-SL0 and also provides methods to convert from FIPAContent objects to AgentIDs. The modified initialise() method looks something like this:

public void initialise(String data) {
    targets.add(FIPAHelper.fromFIPASL(FIPAContent.newInstance(data)));
}

Here, targets is a field of the agent that is of type List<AgentID>. The method itself takes strings of the form:

(agent-identifier :name myName :addresses (sequence http://localhost:4444/acc))

Where myName is the name of the agent and http://localhost:4444/acc is the URI of the HTTP-Message Transport Service that has been deployed on myName's agent platform.

Finally, the implementation of an agent that is able to ping a set of deployment-time specified target agents looks something like this:

public class PingAgent extends AbstractAgent {
    private List<AgentID> targets;

    public PingAgent(String name) {
        super(name);
        targets = new LinkedList<AgentID>();
    }

    @Override
    public void execute() {
        // Send ping to all target agents
        for (AgentID target: targets) {
            StringMessage response = StringMessage.newInstance();
            response.setPerformative(IMessage.REQUEST);
            response.setContent("ping");
            response.getReceivers().add(target);
            response.setSender(this.getAgentID());
            send(response);
        }
	
        this.endOfIteration();
    }

    @Override
    public String getType() {
        return null;
    }

    @Override
    public void initialise(String data) {
        targets.add(FIPAHelper.fromFIPASL(FIPAContent.newInstance(data)));
    }

    @Override
    public void update(Observable arg0, Object arg1) {
    }
}

Receiving a Message

Receiving a message is almost as simple as sending a message. It involves two basic steps:

  • Loading of new messages into the agents inbox (from its incoming message queue); and
  • Processing any messages you have received.

The first step is required because it gathers any messages that the agent has received and stores them in the agents inbox. Only the messages stored in the inbox should be processed during that iteration of the agents execution cycle. Any messages received after this are then left to the next iteration of the agents execution cycle. The second step involves processing of any messages found in the inbox. The expected behaviour is that ALL messages found in the inbox are handled in that execution cycle.

An outline of the expected code is given below:

retrieveNewMessages();

for (IMessage message: getInbox()) {
    // handle the message
}

For the ping agent example, which we have been working through in this section, we want to handle a ping request message by sending back a pong message. To do this, we simple create a "pong" inform message and send it to the agent the send the ping message:

if (message.getPerformative().equals(IMessage.REQUEST)) {
    if (message.getContent().equals("ping")) {
        StringMessage response = StringMessage.newInstance();
        response.setPerformative(IMessage.INFORM);
        response.setContent("pong");
        response.getReceivers().add(message.getSender());
        response.setSender(this.getAgentID());
        send(response);
    }
}

Now, we have the bones of an agent design that can: (1) be used to send "ping" messages to a set of target agents and (2) handle incoming "ping" messages by sending a "pong" message back. The final bit of the puzzle is writing a bit of code to handle a "pong" message, such as:

if (message.getPerformative().equals(IMessage.INFORM)) {
    if (message.getContent().equals("pong")) {
        System.out.println("Received Pong from: " + FIPAMessage.toFIPAString(message.getSender()));
    }
}

Finally, the complete implementation looks something like this:

public class PingAgent extends AbstractAgent {
    private List<AgentID> targets;

    public PingAgent(String name) {
        super(name);
        targets = new LinkedList<AgentID>();
    }

    @Override
    public void execute() {
        retrieveNewMessages();

        for (IMessage message: getInbox()) {
            if (message.getPerformative().equals(IMessage.REQUEST)) {
                if (message.getContent().equals("ping")) {
                    StringMessage response = StringMessage.newInstance();
                    response.setPerformative(IMessage.INFORM);
                    response.setContent("pong");
                    response.getReceivers().add(message.getSender());
                    response.setSender(this.getAgentID());
                    send(response);
                }
            } else if (message.getPerformative().equals(IMessage.INFORM)) {
                if (message.getContent().equals("pong")) {
                    System.out.println("Received Pong from: " + FIPAHelper.toFIPAString(message.getSender()));
                }
            }
        }

        // Send ping to all target agents
        for (AgentID target: targets) {
            StringMessage response = StringMessage.newInstance();
            response.setPerformative(IMessage.REQUEST);
            response.setContent("ping");
            response.getReceivers().add(target);
            response.setSender(this.getAgentID());
            send(response);
        }
	
        this.endOfIteration();
    }

    @Override
    public String getType() {
        return null;
    }

    @Override
    public void initialise(String data) {
        targets.add(FIPAHelper.fromFIPASL(FIPAContent.newInstance(data)));
    }

    @Override
    public void update(Observable arg0, Object arg1) {
    }
}

Deploying an Example

Details of the deployment process can be found in the Application Deployment Guide. For the purposes of this guide, we focus on the creation of the initial community of agents as this is where the creation and initialization of the agents occurs. To create the agents, you use the following line of code:

ams.createAgent("test1", PingAgent.class);
IAgent agent = ams.createAgent("test2", PingAgent.class);

This creates two ping agents, called "test1" and "test2" respectively. To give the second agent some initial targets, you use the initialise() method:

agent.initialise("(agent-identifier :name test1 :addresses (sequence local:test.ucd.ie))");

Here, "test.ucd.ie" is the full name of the local agent platform and the "local:" is used to indicate that the agent can be contacted on the Local Message Transport Service. This statement has the effect of configuring agent "test2" to start pinging agent "test1".

Interacting with a Platform Service

Platform Services are shared resources that are available to all agents on a given platform. Examples of platform services include:

In this section, we show how to create a simple agent that is able to bind to the Agent Management Service and use the service to create an agent programmatically. For details of how to deploy a platform service please read the Platform Service Developerment Guide.

To use a platform service, you must first bind to that service. To do this, you simply invoke the bindToPlatformService() method, giving the identifier of the platform service you want to bind to. For example, to bind to the Agent Management Service, you use the following code:

this.bindToPlatformService(AgentManagementService.NAME);

To get an object reference for a platform service, you use the getPlatformService() method, again passing in the services identifier:

AgentManagementService ams = (AgentManagementService) this.getPlatformService(AgentManagementService.NAME);

If you are not bound to the platform service, then this method returns null. This facilitates lazy binding to the service:

AgentManagementService ams = (AgentManagementService) this.getPlatformService(AgentManagementService.NAME);
if (ams == null) {
    this.bindToPlatformService(AgentManagementService.NAME);
    ams = (AgentManagementService) this.getPlatformService(AgentManagementService.NAME);
}

Once you have a reference to the platform service, you can use any associated method. The Agent Management Service has the following key methods:

  • createAgent(String name, Object design): this is the basic agent creation method that you use when deploying the initial agent community of an agent platform (see the Application Deployment Guide). The name is the name of the agent, and the design is a reference to the type of agent you want to create.
  • terminateAgent(String name): this terminates the agent with the given name
  • suspendAgent(String name): this suspends execution of the agent with the given name
  • resumeAgent(String name): this resumes execution of the agent with the given name
  • getAgentByName(String name): this returns an object reference corresponding to the agent with the given name
  • getAgentName(): this returns a list of the names of the agents that have been deployed on the platform
  • getBoundAgents(): this returns a list of agents that are bound to the platform service

To create an agent programmatically, we simply use the createAgent(...) method:

ams.createAgent("test1", PingAgent.class);

This causes the agent to be created (an exception is thrown if an agent called "test1" already exists). To start the agent, you use the resumeAgent(...) method:

ams.resumeAgent("test1");

The complete code is as follows:

AgentManagementService ams = (AgentManagementService) this.getPlatformService(AgentManagementService.NAME);
if (ams == null) {
    this.bindToPlatformService(AgentManagementService.NAME);
    ams = (AgentManagementService) this.getPlatformService(AgentManagementService.NAME);
}
ams.createAgent("test1", PingAgent.class);
ams.resumeAgent("test1");

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.