Chimera - Details

This page describes the main features of Chimera, which has been designed and implemented from scratch to simplify the process of writing and running agents in WIN-PROLOG. We'll begin by exploring an illustrated example, and then go on to describe the components of Chimera in more detail.

An Example: The Brains of the Matter

Chimera is based on a very clean architecture, in which agents react to events and respond by posting further events, and where these events may be generated or posted locally or across a network.

Let's look at a simple example, "Brains"; we can set up this agent with the following code:

        % create agent "brains" on a dynamic port and display details

        brains :-
           agent_create( brains, thinker, Port ),
           write( `Welcome to Brains!~M~J` ),
           write( `==================~M~J~M~J` ),
           write( `Brains is running at Port: ` ),
           write( Port ),
           write( `~M~J~M~J` ).

The call to agent_create/3 names the agent ("brains"), and specifies its arity-3 event handler ("thinker", in other words, thinker/3); in this case, we have specified the TCP/IP port as a variable ("Port"), so Chimera will automatically allocate a dynamic port and return its number, which will be displayed by the calls to write/1.

Agent Brains is designed to handle just five events, whose meaning and interpretation is entirely up to us:

        calculate(Maths)
        english(English)
        french(French)
        result(Maths=Result)
        translation(English,French)

Consider the first case, "calculate(Maths)": let's assume this is a request from some agent, somewhere on the Internet, to perform a mathematical calculation. In response to this event, we will simply perform the arithmetic, and post a new event, "result(Maths=Result)", to whoever generated the original event. Here's the Prolog code that does this:

        % "brains" has been asked to calculate some maths

        thinker( Name, Link, calculate(Maths) ) :-
           write( `~M~JBrains is calculating ` ),
           write( Maths ),
           write( `~M~J` ),
           Result is Maths,
           agent_post( Name, Link, result(Maths=Result) ).

All this event handler does is write some information to the WIN-PROLOG console window, before performing the arithmetic using is/2, and posting the result back to the sender with a call to agent_post/3. The two arguments, "Name" and "Link", uniquely identify the connection to the remote agent.

It is not necessary to post events in every case; here's how Brains reacts to the "result(Maths=Result)" event:

        % "brains" has been given the result of some maths

        thinker( Name, Link, result(Maths=Result) ) :-
           write( `~M~JBrains says:` ),
           write( `~M~J~IThe result of ` ),
           write( Maths ),
           write( ` is ` ),
           write( Result ),
           write( `~M~J` ).

All this event handler does is to display the result of a mathematical calculation, which was performed elsewhere, in the WIN-PROLOG console window, using a series of calls to write/1.

Let's look at two other cases, "english(English)" and "french(French)": Brains treats these events as requests to translate English into French and vice versa respectively. Both event handlers perform the appropriate translation, before posting a "translation(English,French)" event back to the sender:

        % "brains" has been asked to translate english to french

        thinker( Name, Link, english(English) ) :-
           write( `~M~JBrains is translating the English, ` ),
           write( English ),
           write( `, into French~M~J` ),
           (  translate( English, French )
           -> agent_post( Name, Link, translation(English,French) )
           ;  agent_post( Name, Link, translation(English,???) )
           ).

        % "brains" has been asked to translate french to english

        thinker( Name, Link, french(French) ) :-
           write( `~M~JBrains is translating the French, ` ),
           write( French ),
           write( `, into English~M~J` ),
           (  translate( English, French )
           -> agent_post( Name, Link, translation(English,French) )
           ;  agent_post( Name, Link, translation(???,French) )
           ).

Again, calls to write/1 are used to display information in the WIN-PROLOG console window, after which a program, translate/2, is called to perform the translation. If successful, the result is posted back to the sender as a "translation(English,French)" event; otherwise, the same event is posted, but with the atom, "???", in place of the desired text.

The translation program itself is about as simple as possible, and really shouldn't need any explanation:

        % a really simple english/french translator (no grammar!)

        translate( [], [] ).

        translate( [Word|Words], [Mot|Mots] ) :-
           vocabulary( Word, Mot ),
           translate( Words, Mots ).

        % an equally simple english/french vocabulary (twelve words!)

        vocabulary( hello, bonjour ).
        vocabulary( thankyou, merci ).
        vocabulary( please, svp ).
        vocabulary( goodbye, au_revoir ).
        vocabulary( sir, monsieur ).
        vocabulary( madam, madame ).
        vocabulary( and, et ).
        vocabulary( i, je ).
        vocabulary( want, veux ).
        vocabulary( hate, deteste ).
        vocabulary( wine, le_vin ).
        vocabulary( cabbage, le_chou ).

The remaining event, "translation(English,French)", is once more very simple, displaying the results that have been posted back:

        % "brains" has been given the result of a translation

        thinker( Name, Link, translation(English,French) ) :-
           write( `~M~JBrains says:` ),
           write( `~M~J~IThe English phrase: ` ),
           write( English ),
           write( `~M~J~Iis like the French: ` ),
           write( French ),
           write( `~M~J` ).

Just as with the "result(Maths=Result)" case, no further events are posted; all it does is to display the result of a translation, again which was performed elsewhere, in the WIN-PROLOG console window, using a series of calls to write/1:

Running Brains

To see Brains running, we'll load up two copies of WIN-PROLOG on a single computer (though this example will work just as easily across a local area network, or even over the Internet). Let's arrange them one above the other, as shown here:

Chimera: Two copies of WIN-PROLOG

Into each one, we'll load the BRAINS.PL file, using this command:

        | ?- consult( examples('chimera/brains') ). <enter>

Once Brains is loaded in both copies of WIN-PROLOG, we can run it in both locations, with the command:

        | ?- brains. <enter>

What we should now see is the following message in each instance of WIN-PROLOG:

        Welcome to Brains!
        ==================

        Brains is running at Port: XXXXX

        yes

The value, "XXXXX", will be a "dynamic" TCP/IP port number that was allocated automatically by Chimera. Dynamic ports are defined as starting at 49152, so assuming the first two such ports were available, we'll see the following result:

Chimera: Two copies Agent Brains

Linking Brains

Before our two copies of Brains can interact, they must be connected; to do this, we use the agent_create/4 predicate. For example, in the bottom instance of WIN-PROLOG, we could use the command:

        | ?- agent_create( brains, Link, `localhost`, 49152 ). <enter>

The first argument, "brains", is simply the name of our agent: Chimera allows any number of agents to run within a single instance of WIN-PROLOG; the second, "Link", will return a unique integer identifying this link. The third argument, "`localhost`", is simply shorthand for a connection on the local machine: normally it would be an IP address, such as "`192.168.1.2`", or domain name, for example, "`www.lpa.co.uk`". The final argument, "49152", should whichever be the dynamic TCP/IP port was reported by the other, remote instance of Brains.

Assuming the connection worked, we will see a response such as this:

Chimera: Brains makes a connection

The returned number, here "0" (zero), is what we must use to communicate with the remote agent. Let's see it working; enter the following command into the lower instance of WIN-PROLOG:

        | ?- agent_post( brains, 0, english([hello,sir]) ). <enter>

This call to agent_post/3 posts the event, "english([hello,sir])" to link "0" of agent "brains"; the link number we use here should be whatever value was returned from the call to agent_create/4. In response to this event, the top instance of Brains will display some information, before performing the translation, and posting back a "translation(English,French)" event containing the result. The lower instance of Brains will react to that event, displaying the result, as shown here:

Chimera: Brains translates English to French

Hither and Thither

The link between the two instances of Brains is entirely symmetrical, so the top instance can initiate interactions just as easily as the lower one. In the top instance, let's post a "calculate(Maths)" event:

        | ?- agent_post( brains, 0, calculate(22/7) ). <enter>

The lower instance of Brains reacts to this event by displaying some text, before performing the calculation and posting the appropriate "result(Maths=Result)" event back to the sender; this is received by the top instance of brains, which displays the final result, as shown here:

Chimera: Brains performes some maths

More About Chimera

What we've just seen is a very simple example of a Chimera agent, running in two locations with a single link. The beauty of Chimera's design is that all you need do is define events you want to handle, and post events that you want someone else to handle. There is no agent "loop"; no special modes; no explicit polling for input: in fact, Chimera can run within WIN-PROLOG while the latter is being used interactively for any other purpose. Now let's look at Chimera in more detail...

Chimera Heuristic Intelligence Modelling Enhanced Relational Agents

An "agent", so far as Chimera is concerned, is a named software entity which reacts to events, generated both within itself and from elsewhere on a distributed network. Each agent comprises:

  • A predicate, of arity 3, whose name is specified at creation
  • A socket, whose name is the same as that of the agent

The agent predicate is typically made up of clauses of the form:

        <pred>( Name, Link, Event ) :-
           <appropriate action>.

The Name is the atom which names an agent (the first argument specified in agent_create/3 when the agent was created), while Link will either be an integer (for a link to another agent) or an empty list (for messages posted locally to the agent).

The "Event" parameter will be one of two data types: for system events, it is a conjunction (comma pair) of the form:

        (<atom>,Params)

There are eight pre-defined system events:

    Link Event Meaning
    [] (close,Port) an agent has been closed on the given Port
    <int> (close,Host,Port) the connection on the given link <int> has been closed, on the given Host and Port
    [] (create,Port) an agent has been created on the given Port
    <int> (create,Host,Port) a connection has been created on the given link <int>, to the given Host and Port
    <int> (error,Error,Event) an given Error has taken place during the given socket Event
    <int> (open,Host,Port) a connection has been opened on the given link <int>, from the given Host and Port
    <int> (read,Host,Port,Term) the given Term has been received by the given link <int> from the given Host and Port
    <int> (write,Host,Port,Term) the given Term has been written by the given link <int> to the given Host and Port

None of the above needs to be handled unless desired for special purposes (eg, tracing, filtering connections, setting timeouts, etc).

For user-generated events, it is always a tuple of the form:

        <atom>(Param1,...,ParamN)

whose values are entirely the choice and responsibility of the application program.

Being event-driven, there is no concept of "agent loop": an application is free to get on with whatever business it wants, whether that's waiting for keyboard input, computing Pi to 1,000,000 places, or just displaying events as they take place.

The Predicates

The entire functionality of Chimera is handled by comparatively few, very easy to use predicates. Remember that an agent is nothing more than a combination of an arity 3 predicate and a listening socket of the same name. To illustrate the simplicity, the predicate documentation will assume that we've written the following agent called "fred":

        fred( Name, Link, Event ) :-
           nl,
           writeq( Name - Agent - Event ),
           nl.

All this does is echo events to the console as they occur. For the sake of argument, let's also assume that "the other end" of any connection has an identical handler, which will ne used by another agent, "mary". With respect to these agents, the predicates are outlined here...

agent_create( +Name, +Pred, ?Port )

This creates an agent of the given Name (atom), using the specified handler Pred (atom), on the given Port (integer between 0..65535). If the Port is a variable, the first available private port (a port in the range 49152..65535) is used and returned. Effectively, this sets up a listening socket of the same name as the agent, and causes all event traffic relating to the socket to be sent to specified arity 3 predicate. For example, call:

        ?- agent_create( fred, fred, 2000 ).

would initiate agent "fred", listening for incoming connection requests on port 2000, which will be accepted and opened automatically. Any events relating to this agent will be passed to fred/3, so that (for starters) we would see:

        fred - [] - (create,2000)

displayed in the console. Agents created with this predicate can be destroyed by agent_close/1. Any existing agent of the given name will be closed and recreated, causing the automatic closure of all links.

agent_close( +Name )

This closes the agent of the given Name (atom), as well as any links that have been set up to or from the agent, and is effectively the reverse action of agent_create/3 with or without automatic calls to agent_close/2 as needed. For example, suppose we had previously created the agent, "fred", as shown above; then the call:

        ?- agent_close( fred ).

would shut down any links related to fred, and then shut down fred itself. Any remaining events, including those caused by the shutdown, will be passed to fred/2, so that (for example) we would see:

        fred - [] - (close,2000)

displayed in the console.

agent_create( +Name, ?Link, +Host, +Port )

This predicate is used to create a proactive connection (ie, client) between an existing agent Name (atom) (see agent_create/3) and the given Host (string) and Port (integer between 0..65535), using the given Link number (any integer). If the Link is a variable, a unique small integer is used and returned; otherwise, any connection with the same name and link number is closed before the new link is created. An event is generated, so assuming agent "fred" exists, the call (for example):

        ?- agent_create( fred, X, `192.168.1.2`, 123 ).

        X = 0

will attempt to connect an agent at IP address 192.168.1.2, on port 123. An event will be generated and displayed:

        fred - 0 - (create,`192.168.1.2`,123)

Agent links (connections) created with this predicate can be closed (disconnected) by agent_close/2. Because the link number is generated automatically, it is not possible to inadvertantly close an existing link when calling agent_create/4.

agent_close( +Name, +Link )

This closes the connection for agent Name (atom) and Link (integer): this connection may have been created proactively in client mode (see agent_create/4) or reactively (automatically) in server mode. For example, assuming a connection, (fred,0), exists on port 123, the following call:

        ?- agent_close( fred, 0 ).

will disconnected it. Our simple agent will display the following event notification:

        fred - 0 - (close,`192.168.1.2`,123)

agent_post( +Name, +Link, +Term )

This predicate is used to post an arbitrary compound term (tuple) to whichever agent is connected to the other end of the given agent Name (atom) and Link (integer). The term is rendered into text and added to a circular send buffer, which is automatically forwarded as quickly as the network connection allows. Assuming that we still have a connection, "(fred,0)" in example agent, the call:

        ?- agent_post( fred, 0, hello(there,world) ).

will send the term, "hello(there,world)" to whatever is connected at this link. The resulting local event is shown by fred/2:

        fred - 0 - (write,`192.168.1.2`,123,hello(there,world))

Once transmission is complete, assuming (for example) that "mary" is at the other end of the connection, the latter will display the events:

        mary - 1 - (read,`192.168.1.4`,1024,hello(there,world))

        mary - 1 - hello(there,world)

Note that the first of these is a "system" event (a conjunction); the second is a "user" event (a tuple). Most agent applications can ignore the system events, and concentrate on processing user events.

This predicate can also be used to post an arbitrary term (tuple) to the local agent with the given Name (atom), by specifying a link of "[]", rather than using an integer, in very much the same way as if it had been sent by some remote agent. The only difference is that that it is the agent itself, rather than a specific link, that receive the event, and because no actual writing and reading has taken place, the system "write" and "read" events do not occur. For example, continuing with our "fred" agent, the call:

        ?- agent_post( fred, [], hello(there,world) ).

will post the term, "hello(there,world)" directly to "fred", resulting in the following local output:

        fred - [] - hello(there,world)

agent_stream( +Name, ?Pred )

All agent reads and writes (see agent_post/3) must be converted between Prolog terms and an ASCII respresentation for physically sending. Unless you specify otherwise, this is done automatically by a predicate calls agent_stream/1. The present predicate, agent_stream/2, allows you to specify your own I/O predicate for an agent of Name (atom). The Pred (atom) refers to an arity 1 predicate that must be able to handle symmetric reading and writing of Prolog terms. For example:

        ?- agent_stream( fred, X ).
        X = agent_stream

        ?- agent_stream( fred, my_reader_writer ).

declares that something called my_reader_writer/1 will be used to handle input (when its argument is a variable) and output (when not) relating to agent fred.

agent_stream( ?Term )

This is the default predicate used to read and write terms, and is called indirectly by agent_post/3 and the event-driven handler that receives incoming data. When given a variable, it attempts to read an encoded term from the current input stream; when given a non-variable term, it writes the encoded term to the current output stream. For example:

        ?- agent_stream( hello(there,world) ) ~> S,
           agent_stream( X ) <~ S.
        S = `~M~J(hello there world) `
        X = hello(there,world)

Moreover, the predicate is designed to fail, and not raise an end of file error, when an incomplete term is given:

        ?- agent_stream( X ) <~ `~M~J(hello there` .
        no

This gentle failure is essential to the event-driven read loop, allowing terms to be read and dispatched in a piecemeal fashion.

agent_dict( +Flag, -List )

This returns a List of all currently active agent names (atoms) of the type indicated by the given flag (ie, 0=visible, 1=hidden, -1=all). For example, assuming "fred" is up and running:

        ?- agent_dict( 0, X ).
        X = [fred]

agent_data( +Name, -Pred, -Port, -List )

This returns the Pred (atom, naming the arity 3 agent handler predicate), Port (integer between 0..65535) and a List of all connections (small unique integers) for the agent specified by Name (atom). For example, if agent "fred" has a single connection (see agent_create/4) with a Link of "0", then the call:

        ?- agent_data( fred, H, P, L ).
        H = fred
        P = 2000
        L = [0]

agent_data( +Name, +Link, -Sock, -Host, -Port )

This returns the socket handle Sock (atom), remote Host (string), Port (integer between 0..65535) and Data (either an empty list, or something else) for the given agent Name (atom) and Link (integer). Assuming "fred"'s link "0" is using a socket, "abcdefgh", and is connected to "192.168.1.4" at port "123", then the call:

        ?- agent_data( fred, 0, S, H, P ).
        S = abcdefgh
        H = `192.168.1.4`
        P = 123

agent_version( +Mode )

This displays a version banner, in the given Mode (integer):

    Mode Displays
    0 Product, Copyright
    1 Product, Copyright, Tramlines
    2 Product, Copyright, Authorship
    3 Product, Copyright, Authorship, Tramlines

For example, the call:

        ?- agent_version( 3 ).

        ---------------------------------------------------
        Chimera 1.000 - Agents for WIN-PROLOG - 22 Jul 2005
        Copyright (c) 2005 Logic Programming Associates Ltd
        designed and created by Brian D Steel - 05 Apr 2005
        ---------------------------------------------------

The Chimera Manual: An Illustrated Tutorial

To find out even more about Chimera, click on the "Downloads" tab at the top of this page, and choose "Documentation Files". From there, you can download the entire Chimera reference manual in .PDF format: this document is written as a tutorial, with plenty of explanations and screen shots, and should make an interesting read!