5. Agent-oriented programming

A Turberfield application is made up of cohesive programs called Controllers. Each controller contains specialised objects which run on their own as autonomous agents.

When an agent has to share the data it creates, it’s known as an Expert. In Turberfield, experts:

  • can be configured with global settings like file paths and host names
  • listen on a queue for messages from another expert
  • listen on a queue for messages from another controller
  • operate autonomously within an event loop
  • publish certain attributes to other experts within the same controller
  • publish certain attributes to other controllers via RSON

Turberfield provides the expert module to standardise this pattern. Subclasses of Expert inherit these behaviours.

5.1. Using an Expert subclass

Turberfield defines a four-stage process for setting up Expert objects, running them, and checking their results. There are conventions for each of these interactions:

5.1.1. Configuration

Suppose for example that a certain Expert subclass requires an output argument to tell it where to save application data files. Typically that would be known to the controller process as a command line argument or configuration file setting.

To configure a new object of this Expert class, you would first call the options method of the class:

options = SomeExpertSubclass.options(output="/var/experts")

The return value of the options call is a Python dictionary compatible with the keyword argument parameters of the Expert subclass. The keys of this dictionary are the names of attributes which your object will publish publicly. The corresponding value of a key shows the way the attribute will be published, as follows:

Expert.Attribute (name). A regular public attribute.
Expert.Event (name). A public attribute which is an asyncio.Event.
Expert.HATEOAS (name, attr, dst). A sequence of items in a JSON-formatted web page, publicly readable as a local file.
Expert.RSON (name, attr, dst). A sequence of items in RSON format, publicly readable as a local file.

5.1.2. Instantiation

Some Experts will define positional parameters specific to their operation. Aside from those, you can also pass unnamed positional arguments and the keyword arguments you got from the configuration step.

Unnamed positional arguments must be objects compatible with the asyncio.Queue interface. The Expert will watch for messages you pass into them.

wiring = (asyncio.Queue(), PipeQueue.pipequeue("/tmp/pq.fifo"))
expert = SomeExpertSubclass(*wiring, **options)

5.1.3. Invocation

Experts are active objects which run in an event loop. They support Python call semantics. The result of calling an Expert is a coroutine you can pass to asyncio for use as a Task:

loop = asyncio.get_event_loop()
task = asyncio.Task(expert(loop=loop))
loop.run_until_complete(asyncio.wait(asyncio.Task.all_tasks(loop)))

5.1.4. Inspection

Experts either publish the data they generate to a local file, or to the public attribute of their class. Exactly what goes where will depend on the class and can be discovered from the options call.

For example, should SomeExpertSubclass define an Event called someEvent, you’d use it like this:

yield from SomeExpertSubclass.public.someEvent.wait()

5.2. Subclassing Expert

class turberfield.utils.expert.Expert(*args, **kwargs)

A base class for Information Experts.

static options()

Subclasses must override the base class implementation.

The method returns an ordered dictionary. Each key is the name of a piece of public data to be managed by the Expert class. The object mapped to that key configures how the data is to be published. See the declare method for details.

__init__(*args, **kwargs)

Subclasses must begin by invoking the superclass initialiser:

super().__init__(*args, **kwargs)

Unnamed positional arguments must be compatible with asyncio.Queue. Keyword arguments should have been generated by the options method.

__call__(loop=None)

Subclasses must override the base class implementation.

This method makes the object callable. It is a coroutine to be launched as an asyncio.Task.

declare(data, loop=None)
Parameters:data (a dictionary) – data to be published

Invoke this method from within __call__ to publish data via the class-defined interface.

The keyword arguments previously supplied to __init__ determine what attributes are published, according to the following mechanisms:

Interface type Publishing mechanism
Attribute <Subclass>.public.<name> mirrors the data value.
Event <Subclass>.public.<name> is set or cleared by the data value.
RSON RSON.dst is the file path to the data in RSON format.
HATEOAS HATEOAS.dst is the file path to the data as a JSON web page.
watch(q, **kwargs)

Subclasses may override the base class implementation.

This method is used to create one coroutine for each queue of input. The job of the method is to watch the queue for messages and dispatch them appropriately.