home » user manual » agent services

Writing and configuring Nuin agent services

In the Nuin architecture, a service is an optional capability that is added in to the agent when it is configured. It does not refer to a web service, though it is quite conceivable that a Nuin agent service could be written for the purposes of communicating with other online web-services. Perhaps plugin would have been an alternative term, but that terminology is also now very overloaded. Some pre-defined services are provided as part of the Nuin installation, but the principal purpose for introducing services into the architecture is to provide a flexible extension point to allow agent designers to augment an agent's capabilities. For some tasks, it makes more sense to program a capability using Java than to attempt to recreate that capability in the agent's plan. This has the added advantage that it keeps the plan actions focussed more on the high-level behaviour of the agent. This view of services as extension points is similar to the view propounded in the FIPA abstract architecture.

Below, we cover the two principal topics relating to Nuin services: how to write a new service in Java, and how to interact with a service from the agent script. First we sketch the organisation of Nuin services to give the necessary context.

Overview of Nuin agent services

Firstly, it should be noted that agents do not need any services in order to operate effectively. However, many common agent tasks, such as sending and receiving messages or interacting with users, are implemented as services. Most agents, other than very simple agents, will make use of at least one service.

A service is implemented as a Java class, conforming to the AgentService interface. Each service has a name and a service type. Either the name or the type can be used to access the service from the script. For example, a script action can invoke an action on a service named NS, or invoke an action on the first service to be discovered that has type TS.

Each service declares a set of entry points, or service actions, that are directly invoked from the agent script. The actions can take parameters, and bind variables to pass information back to the script.

Each agent maintains a table of registered services. The registered services are defined by the configuration document that the agent is constructed with. In addition, a global service registry provides a means of registering services that are not particular to an individual agent.

Developing new agent services

A Nuin agent service must implement the AgentService Java interface. Most of the methods in this interface provide access to the basic state variables of the service, such as its name and type. While it is possible and reasonable to implement the AgentService inteface directly, it is considerably easier to extend the built-in Java class AbstractService. This provides default implementations for most of the information-access methods from the AgentService interface, together with convenience methods for registering actions to invoke on the service, and for passing configuration information to the service instance when it is created.

To create a new service based on AbstractService, the service needs minimally to call the constructor of AbstractService, and provide an implementation for the getAdapters() method. Service action adapters are not a mandatory part of the design of Nuin agent services, they simply provide a convenient way to add new actions to a standard service based on AbstractService. A ServiceActionAdapter provides a standard method for invoking the methods on a service object via action objects created in NuinScript. A ServiceActionAdapter has the following method signature:

/**
 * Perform the named action, using the given arguments as parameters.
 * @param service The service that will perform the action
 * @param action Encapsulates the operation name, and fixed and open argument sets
 * @param intention The intention in which the operation is being invoked.
 * @return The action status: succeeded, failed or ongoing
 */
public ActionResult perform( AgentService service, KsAction action, Intention intention ); 

Figure 1: signature for service action adapters

Note that implementing service action adapters is discussed further below. The return result from the getAdapters() method is a set of pairs of a symbol denoting the name of the action to be invoked, and the corresponding service action adapter. Given Java's limited type declarations, this is simply represented as an array: Object[][]. The reason for using an array type is to allow the symbol—adapter pairs to be declared statically. Thus, an example minimal service, based on AbstractService, would be:

public class ExampleService
  extends AbstractService
{
  /** The adapters for the actions of this service */
  private static Object[][] ADAPTERS = new Object[][] {
    { The.valueFactory().newSymbol( "exampleAction" ), 
      new ServiceActionAdapter() {
        public ActionResult perform( AgentService s, KsAction a, Intention i ) {
          System.out.println( "invoked " + a );
          return ActionResult.SUCCESS;
        }
      }
    }
  };
  
  /** Constructor */
  protected ExampleService( KsSymbol name, KsSymbol type, Agent owner, Resource cRoot ) {
    super( name, type, owner, cRoot );
    addServiceType( The.valueFactory().newSymbol( "exampleService" ), owner );
  }

  /** Return the declared service adapters */
  protected Object[][] getAdapters() {
    return ADAPTERS;
  }
}

Figure 2: minimal example service

There is one standard extension to this minimal pattern, which allows services to be created by the default ServiceFactory. This is the addition of a static method factory(), which returns an instance of a ServiceMaker object. The service maker simply creates a new instance of a service when requested, given some standard parameters. Adding the factory method to the minimal service we get:

public class ExampleService
  extends AbstractService
{
  /** The adapters for the actions of this service */
  private static Object[][] ADAPTERS = new Object[][] {
    { The.valueFactory().newSymbol( "exampleAction" ), 
      new ServiceActionAdapter() {
       public ActionResult perform( AgentService s, KsAction a, Intention i ) {
         System.out.println( "invoked " + a );
         return ActionResult.SUCCESS;
       }
     }
    }
  };
  
  /** Factory to create instances of this service */ 
  private static ServiceMaker s_factory = new ServiceMaker() {
    public AgentService createService( KsSymbol name, Agent owner, Resource configRoot ) {
      return new ExampleService( name,
                                 The.valueFactory().newSymbol( "exampleSvcType" ),
                                 owner, configRoot );
    }
  };
  
  /** Answer the factory that makes new instances of this service */
  public static ServiceMaker factory() {return s_factory;}

  /** Constructor */
  protected ExampleService( KsSymbol name, KsSymbol type, Agent owner, Resource cRoot ) {
    super( name, type, owner, cRoot );
    addServiceType( The.valueFactory().newSymbol( "exampleService" ), owner );
  }

  /** Return the declared service adapters */
  protected Object[][] getAdapters() {
    return ADAPTERS;
  }
}

Figure 3: minimal example service with service factory support

Writing service adapters

To meet the contract of the agent service interface, a service must implement the method

public ActionResult performAction( KsAction action, Intention intent );

That is, a given action expression is performed by the service in the context of some currently executing intention (which provides access to variable bindings, etc). Using the AbstractService implementation, each service entry point is represented by a symbol (i.e. a URI), and imlemented by a ServiceActionAdapter with the following signature:

public ActionResult perform( AgentService service, KsAction action, Intention intention )

Given the way that the service setup occurs, when this method is invoked it is known that the action object passed from the agent script matches this adapter. Thus, while the service can inspect the action type of the given action, it will typically proceed straightaway to extracting the necessary parameters. Since KsAction is a specialisation of KsTerm, the arguments can be accessed directly with action.getArg(n). For added convenience, a set of utility methods are provided in the abstract service, to make this process easier. See, for example, getArgSymbol(), getArgTerm(), or getArgVar().

Once the service action has finished, it must return a success code to the invoking script. The success codes are the same as for script actions, discussed previously.

Example service adapter

The following shows a complete example of a simple action adapter, from the library of built-in script actions (which is implemented as a service). This service action unifies a given variable, passed as a parameter, with the identity of the current intention. This will succeed, in a way that does not prevent the intention from backtracking (hence the _NO_COMMIT) either if the variable is unbound, or if it is bound but is equal to the intention ID. In the former case, the current bindings for the intention will be updated with the new value for the variable, which is how information is passed out from executing service actions.

{ LibVocab.GET_INTENTION_ID, 
  new ServiceActionAdapter() {
    public ActionResult perform( AgentService s, KsAction a, Intention i ) {
      KsVar var = getArgVar( a, 0 );
      return new Unifier().unify( var, i.getUniqueID(), i.getBindings() ) ?
                 ActionResult.SUCCESS_NO_COMMIT : ActionResult.FAILURE;
    }
  }
}

Figure 4: example service adapter

Invoking services from agent scripts

To invoke a service from an agent script, the service instance itself must first be identified, and then passed an action term representing the action to invoke. In Nuinscript, this is accomplished by the @ syntax, as shown by the following examples:

 @ [type=svc:MessageService] action:bind()
 @ [svc:RssService] rss:poll()

Following the keyword @, the service is identified by name or by type. The service name is simply a symbol in square brackets. The service type is a symbol, also enclosed in square brackets, but preceded by type=. In the first example, the service invocation action will look for a registered service of type svc:MessageService, first in the agent's local registry, and then in the global registry. In the second case, the service invocation action will look for a service named svc:RssService first locally, then globally. Generally speaking, the service type may be regarded as more flexible, as the same script will work without changes when the agent is configured with different services that correspond to the service type (for example, a SOAP message service and a Jade message service might be alternative instantiations of svc:MessageService).

If both the service name and type are omitted, the service name defaults to the builtin library of actions. This provides a convenient short syntax for invoking builtin actions, e.g:

@lib:getIntentionId( ?id )

After the service identification term, if specified, is a term that specifies the service entry point and arguments. This is a standard term, whose functor is the identity of the action to be performed, and whose arguments are the parameters to be passed to the action. Note that variable substitutions will be performed automatically, so service actions do not have to check whether any variables they are passed should be further grounded. The first parameter has index zero.

« prev: agent configuration | agent services | next: jena & joseki »