Designing ABLE Applications

This document describes how to use ABLE to add intelligent agent functions to applications. It illustrates several options for using AbleAgents and AbleBeans in applications ranging from using synchronous method calls to synchronous or asynchronous action events.

The ABLE framework provides considerable flexibility to the developer of custom application specific agents. The agent can use the provided AbleBean and AbleAgent components to implement its functions. These beans can be tightly integrated using method calls and run on the application's thread of control. Or the beans could be loosely connected using event passing and some or all of the beans could have their own thread of control. Data can be shared between beans by accessing bean properties held in the container agent, or data can be passed between beans using the data flow (buffer) connections.

You can use one or more forms of data connection within your AbleAgent, and use different connection mechanisms between the agent and the rest of your application. For example, you may create a custom AbleAgent and use data flow and synchronous method calls between AbleBeans inside of the agent, but you may decide to integrate it with your application by sending asynchronous action events. It all depends on your application requirements.

The major design decisions involve:

  1. Threading:
    You can create an AbleAgent that runs entirely on the application's thread.
    You can create an AbleAgent that runs on its own thread and handles work requests from the application through events on that thread
    Your AbleAgent can itself contain 0, 1, or N threads of control
  2. Data flow between beans:
    You can pass buffers of data between beans using the data flow connections
    You can share data by using the container agent properties as global data
    You can share specific data members by using property connections
    You can pass data between beans as arguments on notification or action events
  3. Processing flow between beans:
    You can wire up beans using data flow connections and let the default agent process() method sequence the beans
    You can invoke the beans in any desired order by hard coding the logic in your agent process() method
    You can let the bean processing be driven by events generated external to the agent or internal to the agent

AbleBeans

The techniques described in this section apply to all JavaBeans that implement the AbleBean interface. This includes all core AbleBeans provided with the ABLE framework (based on the AbleObject base class) and all core AbleAgents (based on the AbleDefaultAgent class). These functions are also supported by any custom AbleBeans that extend AbleObject or AbleDefaultAgent.

Initializing a bean

The standard sequence for instantiating and using an AbleBean (as implemented by AbleObject) is shown below:

// Instantiate and configure the bean 
AbleObject bean = new AbleObject() ; // create the bean
bean.setName("myName") ;    // set some configuration parameter 
bean.init() ;    // configure it, start thread(s) if any, set state, dataflow on 

The init() method will set up the behavior of the bean in two respects. First, it will configure the AbleEventQueue for posting AbleEvents. Second, it will set up an asychronous thread to handle a timer event loop and for processing AbleEvents. AbleEvents have two aspects to consider. You can tell the event queue to queue new events (post them) or not, and you can tell the event queue to dequeue events (process them) or not. For example, you may want the bean to accept all incoming events but not start processing them until some other condition applies. Or you may want the event queue to not even accept new events if the bean is not ready to process them.

In the following example of a reset() method , the bean's event queue is set up with no timer processing and no asynchronous event processing enabled. No thread is created in the bean's event queue and this is the lightest weight bean you can create. It must be called synchronously by other beans from their thread. Processing options are typically set in the reset method, and also in the bean constructor.

setSleepTime(0);
setTimerEventProcessingEnabled(false);
setAbleEventProcessingEnabled(Able.ProcessingDisabled_PostingDisabled);
setDataFlowEnabled(false) ;    

In the next example, we set up the bean's event queue to wake up every 5 seconds. The bean's processTimerEvent() method is called each time the timer pops. It is to accept new events and process asynchronous events on its own thread.

setSleepTime(5000);   // call processTimerEvent() every 5 seconds
setTimerEventProcessingEnabled(true);
setAbleEventProcessingEnabled(Able.ProcessingEnabled_PostingEnabled);

In some cases, the init() method and event queue configuration is called directly from the bean constructor. This works well for relatively simple beans. However, in many cases, the user must first set bean parameters from the bean's customizer or set a property before the bean can be initialized. In this case, init() is called after all the property 'set' methods or by a Configure button action in a customizer. Any user parameters are set and the bean is configured and then ready to process timer or asynchronous events.

Synchronous method calls

You can use the AbleBean function as any ordinary Java object or JavaBean. You call methods when you want the bean to do some processing for you. You can call getter/setter methods to set bean properties and state.

You can also use input and output data flow buffers to pass data between AbleBeans if they support it. If you want to pass data for use in the process() method, you first set the input buffer values, then call the process() method, and get the results from the output buffer.


AbleBackPropagation bean = new AbleBackPropagation() ;  
bean.setNetArchitecture("1 2 0 0 1");    
bean.init() ;  // allocates network weight arrays
// Set the input buffer 
double[] inputData = (double[])bean.getInputBuffer() ; 
inputData[0] = 0.2 ;   // input value 1
inputData[1] = 0.8 ;   // target output value 1

bean.process() ;  //  process the input buffer data

double[] outData = (double[])bean.getOutputBuffer(); // retrieve results 

Synchronous action events

You can also invoke methods less directly by sending action events. These are AbleEvents whose arguments include the method (action) name and an argument object.

AbleNeuralClassifierAgent  agent = 
    (AbleNeuralClassifierAgent)AbleObject.restoreFromSerializedFile("agent.ser") ;

// assume the agent was trained and serialized out in run mode
// create a synchronous action event  
AbleEvent ev1a = new AbleEvent(this, null, "process", false);
// invoke process() method, read record, load output buffer
agent.handleAbleEvent(ev1a) ; // processed on my thread
// this agent is designed to put its output in the outputbuffer array
String[] outData = agent.getOutputBufferAsStringArray();

Asynchronous action events

You can also create asynchronous action events, which will be queued up and processed by the receiving AbleBean or AbleAgent on the bean's own thread. This function allows any AbleBean to act as a server object.

This code snippet shows how to create an import bean, configure it, and post an event on its event queue. When the event is removed from the queue, a data record will be read asynchronously.


AbleImport  import = new AbleImport() ;
import.setFileName( "aFileName") ; 
import.setAbleEventProcessingEnabled(Able.ProcessingEnabled_PostingEnabled);
import.init() ;  
// create an asynchronous action event
AbleEvent ev1a = new AbleEvent(this, null, "process", true) ;
// when dequeued, will invoke process() method, read record, load output buffer
import.handleAbleEvent(ev1a);

Note there is no implementation in the AbleObject default process(object) method. AbleImport extends AbleAbstractImport whose process(object) method simply calls process, ignoring the null parameter passed.

If you'd like your bean or agent to register with another master agent who would continuously send your agent events to handle:

// SubAgent: a class extended from AbleDefaultAgent, with its own myEventProcess(Object parm) method
// configure your subagent for event posting and processing like: 
subagent = new SubAgent();
subagent.setAbleEventProcessingEnabled(Able.ProcessingEnabled_PostingEnabled);

// create the event queue thread and start processing events
subagent.startEnabledEventProcessing(); // or call init which should call the super.init() method

// your container application wants to have subagents register and receive all 
// the events it generates, then register the subagent
addAbleEventListener(subagent); // this listener is transient, 
// or
new AbleEventConnection(this,subagent); // if this listener is to be recreated when reserialized

// your application creates an event it wants handled asynchronously
AbleEvent event = new AbleEvent(this,someObject,"myEventProcess");

// if your agent knows which agent will process it, 
subagent.handleAbleEvent(event); // will read the asynchronous flag
subagent.processAbleEvent(event); // will process synchronously regardless of flag
// either of the above will cause the following method invocation
//subagent.myEventProcess(someObject); 

// or if your agents are registered, notify all registered agents 
notifyAbleEventListeners(event); // will read the asynch flag

When you did the 'startEnabledEventProcessing' a thread was created for managing the subagent's event queue. The asynchronous event will be posted on that queue, the event queue thread will remove it, and handleAbleEvent will call the action method you specified from the event queue's thread. The action "myEventProcess" is a method implemented in your SubAgent class that takes a single parameter. It will be passed the AbleEvent's argument object when invoked.

The handleAbleEvent method is required by the AbleEventListener interface. The processAbleEvent method is simply an arbitrarily-named method which is called in the base implementation for synchronous processing. It can be called directly to handle an event is handled synchronously, without regard to the asynchronous flag setting.

If the listener is to be serializable, add an AbleEventConnection; if transient, add an AbleEventListener. Beans within are typically wired up so they are serializable. Remember to remove transient listeners before exiting.

Data events

Data events are created when no action is provided, or with an int parameter as a data event identifier. Handle data events by overriding the handleAbleEvent or processAbleEvent methods. Call the super method in the case your method does not handle the event or if an action is specified. Like Action events, data events may be processed either synchronously or asynchronously. Data events are passed to registered listeners when the notifyAbleEventListeners method is called.

// Create a synchronous datachanged event
AbleEvent dataEvent = new AbleEvent(this,someObject,AbleEvent.DATACHANGED,false);

Your overrridden handleAbleEvent might look like this:

public void handleAbleEvent(AbleEvent theAbleEvent) throws AbleException {
   if (theAbleEvent.getId() == AbleEvent.DATACHANGED) {
      process();
   } else {
      super.handleAbleEvent(theAbleEvent);
   }
}

The AbleEvent class defines these data events; check the JavaDoc for additions:

Timer action events

AbleObjects also have a timer capability, which can be optionally used to provide processing at defined intervals. When the timer expires, this calls the AbleObject's processTimerEvent method.

AbleDefaultAgent agent1 = new AbleDefaultAgent("agent1") ;
// Enable timer processing, but not asynch events
agent1.setTimerEventProcessingEnabled(true);
agent1.setAbleEventProcessingEnabled(Able.ProcessingDisabled_PostingDisabled);
agent1.setSleepTime(5000); // call processTimerEvent() every 5 seconds
// create/resume the event queue thread
agent1.startEnabledEventProcessing(); 

Processing can be suspended, resumed, or quit as well as started.

agent1.suspendEnabledEventProcessing(); // suspend thread, don't call processTimerEvent()
agent1.resumeEnabledEventProcessing(); // resume thread, processTimerEvent() gets called
agent1.quitEnabledEventProcessing();  // shut down the event queue thread

Property change events

Property change events follow the standard Java pattern: register, send, and receive. An application registers with an AbleBean by adding itself as a property change listener. If the listener is to be serializable, add a PropertyConnection; if transient, add a PropertyChangeListener.

myBean.addPropertyChangeListener(this); // register for property changes
myBean.setColor("Blue"); // change a property

The application provides a method to be called when a property change occurs:

public void propertyChange(PropertyChangeEvent e) {
System.out.println("received PropertyChangeEvent ") ;
System.out.println(e.getSource().getClass().getName() +": Property '" + e.getPropertyName() 
+ "' changed from '" + e.getOldValue() + "' to '" + e.getNewValue()+"'");
}

The method setColor in myBean sends the property change:

public void setColor(String newBeanColor) throws RemoteException {
String oldBeanColor = simpleBeanColor;
simpleBeanColor = newBeanColor ;
firePropertyChange("simpleBeanColor", oldBeanColor, newBeanColor);
}

When the application is no longer interested in property change events, it should deregister.

myBean.removePropertyChangeListener(this);

AbleAgents

Agents are containers; that is, they typically contain one or more beans who pass data to each other or to the agent itself. This is an example of an AbleNeuralAgentClassifier instantiating and configuring several beans with data connections in its init() method. It creates an AbleImport bean as a data source, and instantiates two filter beans generated from that import bean. Next it instantiates an AbleBackPropagation neural network bean, and creates data connections from the AbleImport bean to the first AbleFilter bean to the AbleBackPropagation bean and to the second AbleFilter bean. Finally it configures the agent for timer event processing which is used for training the neural network.


public void init() { 

    try {
      removeAllBeans() ;   // make sure container agent is empty

      imp1 = new AbleImport("Import") ;
      addBean(imp1) ;
      imp1.setBufferSize(bufferSize) ;
      imp1.setDataFileName(dataFileName) ;
      imp1.init() ;
      imp1.generateTranslateTemplate() ;

      // now import the two filters
      filt1 = (AbleBean)new AbleFilter("InFilter");
      filt1 = filt1.restoreFromFile( imp1.getDataFileName()+".xlt") ;
      addBean(filt1) ;

      filt2 = (AbleBean) new AbleFilter("OutFilter");
      filt2 = filt2.restoreFromFile( imp1.getDataFileName()+"b.xlt") ;
      addBean(filt2) ;

      // create a neural network
      net = new AbleBackPropagation("Network" ) ;
      addBean(net) ;

      int numInUnits = ((AbleFilter)filt1).getNumInUnits() ;
      int numOutUnits = ((AbleFilter)filt1).getNumOutUnits()  ; // net dups output
      // Note: netArch contains the hidden unit specs (usually "n 0 0")
      String arch = numInUnits + " " + netArch + " " + numOutUnits ;
      net.setNetArchitecture(arch);

      // create the data connections
      new AbleBufferConnection(imp1, (AbleObject)filt1 ) ; // training
      new AbleBufferConnection((AbleObject)filt1, net ) ;
      new AbleBufferConnection((AbleObject)net, (AbleObject)filt2 ) ;

      inputBuffer  = ((AbleFilter)filt1).getInputBuffer();
      outputBuffer = ((AbleFilter)filt2).getOutputBuffer();
      imp1.setDataFlowEnabled(true) ;  // training
      filt1.setDataFlowEnabled(true) ;
      filt2.setDataFlowEnabled(true) ;
      net.setDataFlowEnabled(true) ;
      // create/resume asynch thread and event queue thread
      startEnabledEventProcessing();
    } catch (IOException e1) {
      throw new RemoteException(e1.toString());
    } catch (ClassNotFoundException e2) {
      throw new RemoteException(e2.toString());
    }
}

Application Integration Scenarios

Here are several descriptions of how to integrate an AbleAgent into an application.

Transaction Server

In this case the user fills out an HTML form or a Wizard and specifies a set of parameters for a transaction request. This could be to book a trip, order a product, or perform a document search. The application receives the user input and packages it into an object or objects.

One way to do this is to call the AbleAgent process() method passing the object as an argument. The application will wait until the agent completes the transaction.

A second way to do this is to create an AbleEvent and then to pass the object as an argument and specify "process" as the action. This can be done either synchronously or asynchronously. If synchronously, then the application will wait until the agent completes the transaction. If asynchronously, then the application can continue processing, while the AbleAgent processes the request on an asynchronous thread.

A third way is to have the AbleAgent wake up occasionally and look to see if there is any work for it to do on an input queue shared with the application. It could take each transaction request, process them on its own thread, and then post the results to an output queue shared with the application.

Notification

In this case the application sets conditions on which it wants to be notified. The purpose of the agent is to monitor some system or application, watch changes in the system or application state, and send an event to the application when the specified condition occurs. This could be when a stock exceeds a threshold, when a message comes from a certain agent, or when urgent e-mail comes from the boss.

The standard way to achieve this would be for the AbleAgent to run asynchronously. The application will send events or call methods to register its interest in certain data and to set the trigger conditions (rules). Once the rules are set, the agent will take over, monitor the data, and send notification events to the application when any of the rules fire. The application would register as a listener on the agent.