Writing Agents

Introduction

Agents communicate with each other using a logic-based language called the Interagent Communication Language (ICL). Typically, agents use the ICL to register their capabilities with a Facilitator, and then to make requests of the agent community to accomplish tasks, read or write data, and install distributed triggers or monitors.

Creating an OAA agent is very simple. Every agent must:

During the callback functions, an agent may need to make requests of the community to perform tasks, read or write data, or install distributed triggers, and the agent library provides primitives for each of these functions.


Publishing Capabilities

In OAA, the main component of an agent capability definition is a Prolog-style declaration such as  send(email, Person, Msg, AdditionalParams). Incoming requests will be unified with the declaration, and if unification succeeds, the request will be forwarded to the agent who published the capability.

Agents generally create a list of all their capabilities declarations (also called "Solvables") and pass them to the oaa_Register() function during the initialization of their agent:

// Java: Register an agent with a Facilitator agent.
//   oaa_Register(ConnectionId, AgentName, Solvables, Params)
//     name = 'email', 2 capabilities (forward & send), empty parameter list
oaa_Register("parent", "email", 
	IclUtils.icl("[send(email,Person,Msg),forward(email,Person,Msg)]"),
	new IclList());
// Prolog: Register an agent with a Facilitator agent.
oaa_Register(parent, email, [send(email,Person,Msg),forward(email,Person,Msg)], [])

Solvable declarations can be dynamically added, changed, or removed during the course of the agent's lifetime using the functions oaa_Declare(Solvable, Params), oaa_Redeclare(Solvable, Params), and oaa_Undeclare(Solvable, Params).

In addition to "simple" solvable declarations as expressed above, programmers may also include special attributes and permissions associated with a solvable.  For instance, a programmer might specify certain preconditions which must be true before it will be able to handle the result.  Or perhaps a programmer might choose to give an indication of how good the agent is expected to be at solving the task associated with a solvable.  For any of these needs, the solvable specification can be written in a more longhand form solvable(SimpleSpec, ParamList,PermisionList). For instance, here is a solvable which says that the agent can return the location of person on a given day and time only if the day is tuesday.  However, if it returns a solution, it is expected that the solution will be better than average (a score of 7 out of 10).

solvable(where(Person,Day,Time, Place), [utility(7),test(Day = tuesday)],[])

The complete list of attribute parameters and permissions that may be associated with a solvable declaration can be found in the OAA reference manual or developer's guide, but here are a few of them:

As indicated by the type() parameter, solvables may be of type "procedure", for which an agent programmer will define a callback event to handle the request, or of type "data", for which the agent library will automatically manage a data store corresponding to the declaration.  Data solvables are registered with a Facilitator agent in the same way as procedure solvables, and and will be used during automated delegation of data among agents. For data solvables, here are some of the relevant parameters::

  OAA capabilities declarations are always made using the style of a relational declaration, returning solutions in a separate argument, not as a function value. For instance:
// Good: given a Child, return Parent as answer in a separate arg
parent(Child, Parent)

// NOT
Parent = parent(Child)

Writing handlers for Capabilities

For each capability declaration defined in an agent's solvable list (of type "procedure"), the agent must define code to handle the declaration.  The code is written in a callback which must initially be registered using the oaa_RegisterCallback() function.  You must at least define the callback oaa_AppDoEvent(), which is the default handler for incoming oaa_Solve() requests, but you can additionally define callbacks in the same manner for specific solvables using the callback() parameter, as described in the previous section.
// In Java, register a callback corresponding the default request handler
// oaa_AppDoEvent() that calls my function myOAADoEvent to process the request.
oaa.oaa_RegisterCallback("oaa_AppDoEvent",  new OAAEventListener() {
     public boolean doOAAEvent(IclTerm goal, IclList params, IclList answers) {
        return myOAADoEvent(goal, params, answers);
     }
});
// In Prolog, register the function myAppDoEvent to define the default request
// handler oaa_AppDoEvent.
oaa_RegisterCallback(oaa_AppDoEvent, myAppDoEvent).

Once the callbacks are registered, you should actually write the code to handle the events.  The handler you've defined will be called with the incoming request and parameters associated with the request.  Typically, you will pull out the arguments from the request, call an API to process the request, and then return a list of "solutions" to the request indicating success, failure, or multiple answers that are solutions to the request.  

Example: An agent has published the capability declaration:  send(email, Person, Msg, Params) and here we define how matching requests should be handled.

Prolog:

// send a message to a person by asking the agent community if anyone
//   knows the email address for person, and if so, uses the UNIX mail
//   command to send the message.  MailParams can contain a subject line
//   for the message.
myAppDoEvent(send(email, Person, Msg, MailParams), InParams) :-
   oaa_Solve(email(Person,EmailAddr), []),
   (memberchk(subject(Subj), MailParams) ; Subj = 'No Subject'),
   sprintf(Cmd, 'mail -s ~p ~p < ~p', [Subj, EmailAddr, Msg]),
   system(unix(Cmd)), !.

Java:

// Callback to handle my solvable declarations
IclList myAppDoEvent(IclTerm request, IclList params) {

   // Empty solution list means request has FAILED
   IclList solutionList = new IclList();
   
   // send(email, PersonName, Msg, MailParams)
   if (request.iclStr().equals("send") && (request.iclNumTerms() = 4)) {
      IclTerm personName = request.iclNthTerm(2);
      IclTerm msg = request.iclNthTerm(3);
      IclTerm mailParams = request.iclNthTerm(4);
      IclList ans = new IclList();
      boolean ok = false;
      // Look up the email address for PersonName by asking the community
      if (oaa_Solve(new IclStruct("email", personName, new IclVar("Addr")),
	        new IclList(), ans) {
         // ans will be in form [email(Person,EmailAddr)] so pull out EmailAddr
         IclTerm emailAddr = ans.iclNthTerm(1).iclNthTerm(2);

         IclTerm subject = icl_ParamValue("subject", null, mailParams);
         String subj = "No Subject";
	 if (subject != null) subj = subject.iclStr();
         // call a function to send the mail:  
	 //   callMailAPI(String addr, String subject, String Msg) {...}
	 ok = callMailAPI(emailAddr.iclStr(), subj, msg.iclStr());
      }
      if (ok)
         solutionList.iclAddToList(request, true);
   }

   return solutionList;
}

In non-Prolog languages, oaa_AppDoEvent() callbacks must return a list of solutions, where the empty list signifies failure and a list containing one element matching the complete original request (with all variables filled in) signifies success.  Many Java or C programmers have the tendency to want to return only the answer, not the full request. 

Example: An agent wants to publish the min() function, returning the lesser of two values.  This should be declared in a relational style as   min(X, Y, Min), with Min returning the minimum.  oaa_AppDoEvent() must return a solution list:

Request:      min(7, 2, X)
WRONG:    solutionList = 2
WRONG:    solutionList = [2]
RIGHT!:      solutionList = [min(7,2,2)]

If there are multiple solutions to a problem, they can be returned in the solution list, each as a different instance of the original request:

request:   	squareroot(4, X)
solutionList =	[squareroot(4, 2), squareroot(4, -2)]

 

Tasking the Agent Community

Agents may make delegated requests of the agent community using the library function oaa_Solve(). oaa_ Solve() takes a goal to be executed and a list of control parameters as arguments, and returns success, failure, or multiple solutions for the request.  For instance, given a person's name, an agent may ask if any other agent knows the email address for the person:
Prolog:  oaa_Solve(email('Adam Cheyer', X), [])
Java:    IclList params = new IclList();   // Empty parameter list
	 IclList answers = new IclList();  // Answers will be added in this list
         if (oaa_Solve(IclUtils.icl("email('Adam Cheyer', X)"), params, answers) {
	    // Here we have one or more solutions to the request, stored in answers
	 }

The default behavior oaa_Solve() (parameter list is empty) is to send the request in parallel to all agents who have declared solvables that match the request, gather their answers, and return them all at once in the answers list.  This behavior can be adjusted using combinations of parameters, which provide interaction control at either a high level ("This is a problem of type XXX") or at a low leverl ("Use these specific agents and routing pattern").  Again, please see the developer's guide or reference manual for a complete list of parameters, but here are a few :

New in OAA 2.0 is the possibility to have parameters return values from an oaa_Solve() request.  Examples of these special parameters are:

Examples (expressed in Prolog, but only slight syntactic adjustment for other languages):

   % ask agent community for the FaxNumber of Adam Cheyer, sending request to all
   %   relevant databases in parallel, filtering out duplicate answers, returning them
   %   in a list.
   solve("fax_number('Adam Cheyer', FaxNumber)", "[strategy(query)]").

   % send a fax to the Fax number: strategy=action so only one fax is sent even if
   %    many fax agents are connected.
   solve(send(fax, FaxNumber, Document, [from('Bob Jones')]), [strategy(action)]).
   % Broadcast to anyone that cares that the fax was sent.  
   %   No response required from community.
   solve(inform(fax, status(sent)), [strategy(inform)]).

Data Management

In OAA 1.0, data management was limited to using the Facilitator agent as a blackboard to store data that is shared among the agent community.  In OAA 2.0, data is delegated among agents much in the same way that tasks are with the oaa_Solve() function.  Agents register data solvables in exactly the same way they register procedure solvables, and the oaa_AddData(DataElement,Params) is used to route the data for storage on the appropriate agents.  Many of the parameters described in the previous section are valid for data as well.

The primary data manipulation functions are oaa_AddData(DataElement, Params), oaa_RemoveData(DataElement, Params), and oaa_ReplaceData(DataElement, Params).

Examples (expressed in Prolog, but only slight syntactic adjustment for other languages):

% Write status information on the Facilitator (parent), using it like a blackboard
%   Other agents can read it using oaa_Solve(status(phone, S), []), or set triggers
%   on it to be proactively informed when the status changes.
oaa_AddData(status(phone, busy), [address(parent)])
% A database agent declares that the word "boss" is a new noun meaning "manager".
%    This information will be routed to all natural language agents who will use 
%    this information.
oaa_AddData(noun(manager, boss, [language(english)]), [])

 

OAA Triggers

Triggers can be used to monitor communication events (type=comm), data changes (type=data), domain-specific tasks (type=task) such as email arriving, webpage changing, and so forth, and if the alarm agent is connected, time (type=time).  The library functions used for adding, removing and modifying triggers are oaa_AddTrigger(Type,Cond,Action,Params), oaa_RemoveTrigger(Type,Cond,Action,Params), and oaa_ReplaceTrigger(Type,Cond,Action,Params) respectively.

A trigger must have a type, a condition to be tested, and an action.  Optional parameters for triggers include:

Many parameters  from oaa_Solve() and oaa_AddData() can also be used for oaa_AddTrigger(), such as address(A), block(TrueOrFalse), reply(TrueOrNone), get_address(L), and so forth.  Please see OAA documentation for the complete list of parameters that can be used for trigger commands.

Examples (in Java, but similar in other languages):

// DATA TRIGGER: spy on changes to Facilitator data
// Local agent receives update_agent_info() solvable whenever a new agent
// connects to Facilitator (because the agent library
// writes the agent_data() fact on connection/disconnection).
oaa.oaa_AddTrigger(new IclStr("data"),       	  // Type = DATA trigger
  IclUtils.icl("agent_data(Id,ready,Sv,Name)"),   // Cond = agent_data()
  // Action: send to ME a message containing info about new agent
  IclUtils.icl("oaa_Solve(update_agent_info(add,[Id,ready,Sv,Name]),
     [reply(none),address(" + me.toString() + ")])"),
  // Params: do this forever (whenever), but only as new agents connect op(add)
  new IclList(new IclStruct("recurrence", new IclStr("whenever")),
              new IclStruct("on", new IclStr("add"))));

 

// COMM TRIGGER: look for low-level event arriving from Facilitator
// When an agent disconnects, the Facilitator broadcasts the event
// ev_agent_disconnected(Addr) to all agents.  This trigger captures that
// event and call one of my oaa_AppDoEvent() functions using oaa_Interpret()
oaa.oaa_AddTrigger(new IclStr("comm"), 		  // Type = COMM trigger
   // Condition: Event we are looking for: ev_agent_disconnected
   IclUtils.icl("event(_FromId,ev_agent_disconnected(AgtId),_P)"),
   // Action: call myself with update_agent_status solvable
   IclUtils.icl("oaa_Interpret(update_agent_status(removed,AgtId),[])"),
   // Params: recurrence forever, trigger installed on myself
   (IclList)IclUtils.icl("[recurrence(whenever), address(self)]"));


A Sample Agent

Here is complete source code to a very simple agent in various programming languages:

NL & ICL Interfaces

When publishing ICL capability declarations with a Facilitator agent, is there any standard convention to what the published declaration should be like?  Agent programmers are, of course, allowed to register solvable declarations however they want, as long as they are legal ICL expressions.  However, we can give you a few suggestions about how to have good "style" when publishing agent capabilities.

Tips for writing "good" solvable declarations:


MultiAgent Communication

  Agents are free to communicate as they please, but there are several styles of communication that are useful in different situations.  Consider creating a phone agent who controls a phone line and can determine whether it  is busy or free.  Agents in the community will want to ask the phone agent about the status of the phone line.  Here are four ways that this information could be communicated among agents.

  1. One way to model this would be for the phone agent to declare a solvable status(phone, S).  However in this case, a requesting agent would execute a request: oaa_Solve(status(phone, S),[]), the request would be routed by the Facilitator to the phone agent who return the status.  Every curious agent in the community would cause the phone agent to interrupt what it was doing in order to return the status information.  Not very efficient in this case.
  2. A second solution would be for the phone agent to simply broadcast the status changes to all interested agents, who would maintain the latest state themselves.  This could be accomplished by the phone agent sending a message oaa_Solve(status(phone,busy),[strategy(inform)]) during each state change.  Agents interested in receiving this information would register solvables of the form  status(phone, S), and the messages would flow to them as the phone agent sends them out.
  3. Perhaps a better solution would be for the phone agent to disseminate the information using oaa_AddData(status(phone, busy), []), where it would be added on the Facilitator's data store.  Interested agents issuing the oaa_Solve() request would have their responses come directly from the Facilitator, without the request being forwarded to the phone agent.  In addition, by adding a trigger on the data fact using oaa_AddTrigger(data, status(phone, S), oaa_Solve(contact_me(status(phone,S))), []), agents would be automatically notified when the status changes, eliminating the need to poll for the status.  Interested agents would then have better control over when they want to be contacted or not contacted with the information.
  4. Agents who really care about this information could also opt to themselves publish a data solvable for the information, using  oaa_Declare(solvable(status(phone, S),[type(data)],[]), []).  Then, as the phone agent updates the status information using oaa_AddData() as discussed in the previous bullet, a copy of this information will automatically be stored locally on the interested agent, whose oaa_Solve() request will now find the information locally without having to go over the net at all.

This example should suggest some of the rich ways that OAA agents can use the services of the architecture to optimize the types of interactions they have with each other.