AFAPL

From Agent Factory

Jump to: navigation, search

Contents

Introduction

The Agent Factory Agent Programming Language (AFAPL) is the original programming language that was developed for Agent Factory. The version currently available is a re-imagining of this language that has been built using the Common Language Framework. Specifically, changes include:

  • the modification of the language grammar to more closely reflect the default grammar of the Common Language Framework.
  • the restructuring of the commitment structure as a plan stack based structure.
  • the introduction of goals and implicit plans (which are actually part of the AFAPL2 language).

The purpose of this version of AFAPL is to provide an additional Agent Programming Language that is sufficiently similar in syntax to the other Common Language Framework languages to reduce the learning curve and enable transfer of skills between Agent Programming Languages. Also, it will be used as a research tool for cross-language research work.

In order to further reduce the learning curve, this guide has been designed to mirror the equivalent guides for AF-AgentSpeak and AF-TeleoReactive.

From a tool perspective, the language comes with a basic inspection tool that is designed to be integrated with the Agent Factory Debugger together with a basic code editor that are provided as part of the Agent Factory Eclipse Plugin.

IMPORTANT: To use AFAPL, we recommend that you use Eclipse together with the Agent Factory Eclipse Plugin. However, this is not required. If you wish to use another editor, simply write the programs in that text editor (making sure files have the right extensions), and create the Main class as is outlined in Lesson 1 (part 2) below. Any syntax errors will be reported (in a less friendly way) when you run the Main class.

Language Overview

AFAPL is a re-imagining of the original agent programming language that was developed for Agent Factory. To gain a basic understanding of the original language, we recommend that you read the following article [Modelling and Programming with Commitment Rules in Agent Factory. Additionally, the new version of AFAPL combines some of the goal-based reasoning support provided in AFAPL2, and we recommend that you also familiarise yourself with this language before reading on.

At their core, AFAPL programs consist of a set of commitment rules that define situations in which the agent should act. These rules drive the core behaviour of the agent, via the adoption of goals (representing states of the world to be realised) and commitments (representing plans of action that the agent has decided to follow - possibly in order to fulfill one or more goals).

The key concepts of the language are:

Beliefs: In AFAPL, beliefs take the form of grounded predicate formulae. For example, the belief that Socrates is mortal would take the form: is(Socrates, mortal).

Goals: Goals represent future states of the world that the agent would like to bring about, for example, if you are a soccer playing agent and your team is attacking, then you may have a goal to score. To achieve this, the agent would need to have a goal to bring about the belief that the team has scored a goal: scored(goal). In AFAPL, two kinds of goal are supported: achievement goals and maintenance goals. Achievement goals represent a one shot goal - once it has been achieved, the goal is dropped (for example, the achievement goal to have scored a goal is represented as !scored(goal)) represents the goal that the agent scores a goal, and once achieved, the agent drops the goal. Maintenance goals, on the other hand, are persistent goals that are not dropped once the are achieved, but persist. This means that, should the agent stop believing that the goal is achieved, then the agent will automatically attempt to re-achieve the goal (for example, the maintenance goal to have scored a goal would be represented as $scored(goal)). The key difference between these goal types is that achievement goals only require that the agent bring about the corresponding belief for one iteration, whereas maintenance goals require that the agent continues to believe the corresponding belief so long as the goal exists. Agents attempt to realise goals by identifying and executing plans whose post conditions match the goal.

Plans: In AFAPL, plans are represented as both part of a commitment rule (see later) or as partial plans that can be invoked via rules or in an effort to bring about a goal. Partial plans are specified via the plan statement (see below). Details of the various plan operators will be provided later in lesson 2.

plan myPlan(?param) {
    pre true; // no pre conditions
    post scored(goal); // used for matching to goals

    body {
        while (!near(ball)) {
            moveTo(ball)
        }
        if (has(?x, ball) & is(?x, opposition)) {
            tackle(?x)
        }
        control(ball),
        dribbleTo(oppositionArea),
        shoot
    };
}

Commitment Rules: these rules identify a situation in which some sequence of actions that should be performed. As such, commitment rules can be seen as the more reactive element of the language as they define only responses to changes in the environment. Typically, commitment rules take the form: ctxt <- Act1, Act2, Act3; where ctxt is the situation; and Ac1, Ac2, and Ac3 represent the sequence of actions that should be performed whenever the specified triggering event occurs and the associated context is satisfied.

// if an opposition player has the ball and they are close to you, then
// keep tackle them until the ball is free.
// once the ball is free, goto the ball and control it
has(?x, ball) & is(?x, opposition) & close(?x) <-
    while (~ball(free)) {
        tackle(?x)
    }
    goto(ball),
    control(ball);

// if the game has started and the agent is an attacker, then adopt
// the maintenance goal to have scored a goal.
game_state(started) & is(self, attacker) <-
    +$scored(goal);

An iteration of the agent interpreter cycle contains the following steps:

  1. gather perceptions
  2. check goals
  3. check commitment rules
  4. execute commitments (perform actions)

The remainder of this guide will introduce the language in more detail via a set of simple examples.

Lesson 1: The Core Language

To get you started, in the language, we will present a number of simple examples that illustrate both how to write an AFAPL program, and how to use the Core API (basic actions and sensors) that is available by default for all AF-AS agents (in fact, this Core API is available to all agent programming languages implemented using the Common Language Framework). In later lessons, we will explain both how to use other APIs and how to develop your own APIs.

Printing to the Console

AFAPL provides two basic actions for outputting to the console: .print(...) and .println(...). These actions are basically wrappers for the Java System.out.print(...) and the System.out.println(...) methods. The simplest program demonstrates how to use these actions is the "hello world" program that prints out hello world to the console, which is given below:

state(initialized) <-
    .println("Hello World");

This program prints the text "Hello World" to the console once. It works because the belief state(initialized) is a special belief that all newly created agents are given when they are created. This belief persists only for the first cycle of the agent interpreter and is then dropped. The result is that, on the first iteration of the agent interpreter, the situation part of the above commitment rule is satisfied, resulting in the rule being fired and the corresponding plan (the right-hand side) is adopted as a commitment. The agent then realises this commitment, resulting in the .println(...) action being performed. On subsequent iterations, the agent no longer believes state(initialized) and so the rule is not fired.

Writing and Running an AFAPL Agent Program

To write and run an AFAPL agent program, you need to install the Agent Factory Eclipse Plugin. Once you have the plugin installed, you should perform the following steps:

  1. Create a new Agent Factory Project: The first step is to create an Agent Factory project. To do this select "File->New->Project..." and select "New Agent Factory Project" from the "Agent Factory" category. Click on the "Next" button and enter a name for the project (here enter "HelloWorld"). Click "Finish" and you are done!
  2. Create an AFAPL Program File: Now, you need to create the source code file in which you will write your AFAPL program. To do this, select "File->New->Other" and select "New AFAPL file" from the "Agent Factory" category. Click on the "Next" button and enter a name for the file (here, enter "helloworld.afapl" as the filename, and "/HelloWorld/src" as the container). Now, click "Finish" and you are done.
  3. Write the AFAPL Program: Next, you need to write your agent program. For this step, let's use the example agent program from the previous section. Save the file, and Eclipses "autobuild" process will take care of the rest (it will do a syntax check on the file, and report any syntax errors).
  4. Create a main class: A main class (one that has a main method) is responsible with setting up the platform. Two examples, a debug variant and a deployment variant, appear in the following sections. As with any Java application in Eclipse, say "Run as -> Java Application" to set up a Run Configuration. (Warning: Some versions of the Eclipse plugin generate a class called Main that cannot be used---you still need to create a main class as described below.)

Writing/Instantiating a Debugger Run Configuration

The Agent Factory Debugger is a generic component of Agent Factory that can be customised for different agent languages and architectures. Basic support for using the debugger is provided via the DebuggerRunConfiguration class. To debug an application, you simply extend this class, override the configure() method and then write a very simple main method. We illustrate this with the example code below:

import com.agentfactory.afapl.debugger.AFAPLInspectorFactory;
import com.agentfactory.afapl.debugger.AFAPLStateManagerFactory;
import com.agentfactory.afapl.interpreter.AFAPLArchitectureFactory;
import com.agentfactory.visualiser.DebuggerRunConfiguration;

public class HelloDebugConfiguration extends DebuggerRunConfiguration {
    public String getName() {
        return "helloworld";
    }

    public String getDomain() {
        return "ucd.ie";
    }

    public void configure() {
        addLanguageSupport(new AFAPLInspectorFactory(), new AFAPLStateManagerFactory());
        super.configure();
        addArchitectureFactory(new AFAPLArchitectureFactory());

        addAgent("Bob", "helloworld.afapl");
    }

    public static void main(String[] args) {
        new HelloDebugConfiguration().configure();
    }
}

The getName() and getDomain() methods are used by the superclasses as part of the platform configuration, removing them will result in a default platform name and domain being applied. The core of the platform configuration is done in the configure() method. The first line of this method configures AFAPL support for the Agent Factory Debugger. The second line causes the agent platform to be created. The third line installs architectural support for the AFAPL language (basically it links the AFAPL interpreter to the Run-Time Environment). Finally, the fourth line creates a helloworld.afapl agent with name "Bob".

The main method simply creates an instance of the class and calls the configure() method.

When you run this class (this is just a standard Java class with a main method), the debugger should be loaded, which looks something like this:

To open the agent inspector shown above, you double-click on the agent name in the tree view on the left-hand side. You are then able to start, stop, and step an agent by clicking on the buttons at the top of the corresponding inspector. The various views expose different aspects of the agents internal state.

Using the AFAPLDebugConfiguration class

For applications that use only a single agent programming language and do not require the installation of any additional platform services, it is also possible to make use of pre-written debugger run configurations. For example, the code below uses the AFAPLDebugConfiguration:

public class Examples {
    public static void main(String[] args) {
        Map<String, String> agents = new HashMap<String, String>();
        agents.put("Bob", "helloworld.afapl");
        new AFAPLDebugConfiguration("alive", agents).configure();
    }
}

It does exactly the same as the previous example, but requires only that you pass a map containing the agent names and designs.

Writing a Deployment Run Configuration

Deployment Run Configurations are very similar to Debugger Run Configurations. In fact, the only changes you need to make are:

  • Extend DefaultRunConfiguration instead of DebuggerRunConfiguration.
  • Remove any debugger specific code from the configuration (this is anything that appears before the super.configure() line.

The Deployment Run Configuration that is equivalent to the Debugger Run Configuration code given above is:

import com.agentfactory.agentspeak.interpreter.AFAPLArchitectureFactory;
import com.agentfactory.platform.impl.DefaultRunConfiguration;

public class HelloRunConfiguration extends DefaultRunConfiguration {
    public String getName() {
        return "helloworld";
    }

    public String getDomain() {
        return "ucd.ie";
    }

    public void configure() {
        super.configure();
        addArchitectureFactory(new AFAPLArchitectureFactory());

        addAgent("Bob", "helloworld.afapl");
    }

    public static void main(String[] args) {
        new HelloDebugConfiguration().configure();
    }
}

Self Sensing

AFAPL provides a default sensor, called ".self" that provides basic beliefs about the agent. In its current form, the sensor provides 2 beliefs:

  • name(?x): is a belief about the name of the agent (the variable ?x is replaced by the name)
  • agentID(?x, ?addr): is a belief about the FIPA Agent Identifier of the agent. These identifiers are used for inter-agent communication.

To illustrate how to use these beliefs, we extend the hello world program to include the name of the agent in the string that is printed to the console:

state(initialized) & name(?name) <-
    .println("Hello World from " + ?name);

The output of this program depends on the name of the agent you create. For example, the previous section, described how to create and deploy a "helloworld" agent called "Bob". If you modified the .afapl file to match the above program, then running the program would cause the text "Hello World from Bob" to be printed to the console once.

Communicating

The syntactic structure of the messages that are sent between agents, and the underlying transport mechanisms have been developed in compliance with the existing FIPA Standards[1]. Rather than give a detailed overview of these standards, we will start here with simple examples and add further information in later lessons.

Support for communication in AFAPL takes the form of a core sensor, called .inbox that generates beliefs corresponding to messages that are received and the .send(...) action that enables agents to send messages. The format of the beliefs generated is:

message(?performative, ?senderAgentID, ?content)

where ?performative is a message type that is compliant with the communicative acts specified in FIPA ACL; ?senderAgentID is a representation of the senders agent identifier, and is the same as the agent identifier belief outlined in the previous section; and ?content is the content of the message. Similarly, the .send(...) action takes 3 parameters: the communicative act, the receiver agent identifier and the content of the message.

The two most basic types of messages that can be sent between agents are inform and request messages. These messages capture the sharing of beliefs / information, and asking for help / actions to be performed respectively. To illustrate how to use the communication infrastructure, we will create a simple "monitoring" agent that repeatedly checks to see if an agent that it is monitoring is "alive" (active). To implement this, we need to write the following programs:

// Monitor agent code:
monitoring(?name, ?addr) <-
    .send(request, agentID(?name, ?addr), status);

message(inform, agentID(?name, ?addr), status(alive)) <-
    .send(request, agentID(?name, ?addr), status);

// Monitored agent code:
message(request, ?sender, status) <-
    .send(inform, ?sender, status(alive));

For simplicity, we have implemented this behaviour as one program. In real world systems, the code may be separated over two different agent programs.

The first rule in this program states that, if you have an belief that you have started to monitor a given agent, then send a message to that agent requesting its status. The receipt of this message by the monitored agent is encoded in the third rule, which states that, if the agent has received a message requesting its status, it should respond by informing the requester that its status is "alive". The receipt of the response is encoded in the second rule, where the agent responds to the receipt of the message by again requesting the status of the monitored agent.

To run this program, create a new AFAPL source file called "monitor.afapl" and copy the code into it. Next, you simply replace the following line of code in the Run Configuration class:

addAgent("Bob", "helloworld.afapl");

with:

addAgent("A", "monitor.afapl");
addAgent("B", "monitor.afapl");
initAgent("A", "monitoring(B, addresses(local:helloworld.ucd.ie))");

The first two lines create 2 agents A and B, and the third line gives A an initial belief that it should be monitoring B (this causes A to start monitoring B via the first rule in the agent program). The address is composed from the name (helloworld) and the domain (ucd.ie) of the platform on which agent B is running.

Failing

The actions of an agent can succeed or fail depending on whether the action code executed correctly. The failure action, .fail always fails. An example of this action is use is the following rule:

state(initialized) & name(?name) <-
    .println("Hello World from " + ?name),
    .fail,
    .println("Goodbye World from " + ?name);

This plan rule defines a plan that involves a sequence of three steps:

  1. the previous print statement;
  2. the .fail action; and
  3. a second print statement that prints out "Goodbye World from ..."

In this example, the agent will execute the first step of the plan, printing out the first string. The second step of the plan will also be performed, but this step will fail (because it is the fail action) resulting in the plan being dropped. As a result, the second string will never be printed out.

Remembering and Forgetting

In AFAPL, you are able to add and remove beliefs using the + and - operators. For example, the following plan adds a belief about the agent's name:

state(initialized) & name(?name) <-
    +iam(?name);

Similarly, to remove beliefs, you use the - operator, for example the following program prints "Hello World from XXX" followed by "Goodbye World from XXX". The first string is printed after the addition of the iam(...) belief and the second string is printed after its removal:

state(initialized) & name(?name) <-
    +iam(?name);

iam(?name) <-
    .println("Hello World from " + ?name),
    -iam(?name),
    .println("Goodbye World from " + ?name);

NOTE: If you are reading this after reading the equivalent part of the AF-AgentSpeak guide, you will realise that the equivalent AF-AgentSpeak program is much more elegant. This is due to the event-based model used in AF-AgentSpeak as it makes it easy to detect changes in the belief base of the agent. In a later lesson, we will introduce some mechanisms by which AFAPL can replicate (to an extent) the event-based model of AF-AgentSpeak.

The + and - operators only work on ground beliefs (i.e. beliefs that have no variables). Sometimes we want to create plans that remove multiple beliefs matching some ungrounded formula. To do this, we can apply the .abolishAll(...) action.

state(initialized) <-
    +know(agent1),
    +know(agent2),
    .abolishAll(know(?x));

know(?name) <-
    .println("I know " + ?name);

~know(?name) <-
    .println("I know nobody");

The output generated when you run this program is:

I know nobody
I know agent1
I know agent1
I know agent2
I know nobody
I know nobody
...

Working with Goals

Goals in AFAPL are more complex than in AF-AgentSpeak. Specifically, AFAPL supports both achievement and maintenance goals, which are written as predicate statements prefixed by an exclamation mark (!) and dollar sign ($) respectively. These goals define future states of the world that the agent should try to bring about. That is, a goal is satisfied when the agent has a belief that corresponds to the goal. For example, if the agent has an achievement goal to score a goal, denoted !scored(goal), then it would only consider the goal to be satisfied when it has the belief scored(goal). This is different to the way goals are treated in languages like AF-AgentSpeak, where the goal formula does not necessarily have to map onto a future state of the world.

Goals are adopted (or dropped) via the + (or -) update plan operator, however, the effect of this operator is the adoption of the goal, and not the satisfaction of the goal (that is, the plan does not wait for the goal to be satisfied before continuing, but continues to be executed independently of the goal). The example program below illustrates the adoption of goals:

state(initialized) & name(?name) <-
    +!iam(?name);

plan iAmPlan {
    pre name(?name);
    post iam(?name);

    body {
        +iam(?name)
    };
}

iam(?name) <-
    -iam(?name),
    .println("Hello World from " + ?name);


The first thing to note is that goals are satisfied through the adoption of plans whose post conditions include the goal formula (in the above example, this is iam(?name)). For a given goal, the set of plans whose post conditions include the goal formula are known as options. As part of the generation of the options of a goal, the goal formula (which must contain no variables), is unified with the relevant part of the post-condition. Any variable bindings resulting from this unification are applied globally to the plan. For example, in the above program, if the agent's name was bob, then the resultant plan option would be:

plan iAmPlan {
    pre name(bob);
    post iam(bob);

    body {
        +iam(bob)
    };
}

The options are then assessed in the order in which the plans appeared in the program. The first option whose pre-condition is satisfied is then selected and the agent adopts a commitment to the corresponding plan. In this sense, the pre-condition performs a role that is very similar to the context component of a plan rule in AF-AgentSpeak in that, it is the pre-condition that provides the context for choosing the plan to realise the goal.

Running the above example causes the following sequence:

  1. the achievement goal !iam(?name) is adopted (where ?name is bound to the name of the agent)
  2. the iAmPlan plan is identified as an option for this goal, and subsequently selected because its pre-conditions are satisfied.
  3. This plan is committed to and subsequently executed, resulting in the belief iam(?name) being adopted.
  4. This causes the achievement goal to be satisfied which results in the goal being (successfully) dropped.
  5. At the same time, the second commitment rule is triggered, causing its plan to be committed to.
  6. The result of this commitment is that the iam(?name) belief is dropped and the "Hello World..." statement is printed

From this point onwards, the agent does nothing more as it has no more goals, and none of the commitment rules are ever triggered.

To demonstrate the difference between achievement goals and maintenance goals all that we need to do is to modify the above program to adopt an maintenance goal instead of an achievement goal:

state(initialized) & name(?name) <-
    +$iam(?name);

The result of this change is that the goal is no longer dropped when it is satisfied. This means that, when the second commitment rule causes the iam(?name) belief to be dropped, then the goal is not satisfied, and the agent re-attempts to satisfy the goal (the same as if the achievement goal was re-adopted). This causes the agent to continually re-adopt the belief iam(?name) which causes the second commitment rule to be fired repeatedly, resulting in the "Hello World..." text being printed repeatedly (until the agent is terminated).

The above model of goals supports the adoption of primary goals, that is, the top level goals of the agent. Some behaviours may also require sub-(secondary) goals. These goals are typically adopted at key decision points in an existing plan, where the agent must make a decision about how to achieve part of that plan (the subgoal). As such, they represent expansion points in an existing plan/commitment rather than motivators for a new plan/commitment. AFAPL supports the introduction of achievement subgoals (maintenance subgoals are not supported). This is achieved by specifying the achievement goal directly in the plan (basically, no + prefix). Below, we modify the running example to use an achievement subgoal:

state(initialized) & name(?name) <-
    !iam(?name);

Running the program, you will see that the primary difference is that the commitment in which the goal was adopted is not dropped, and the plan for achieving the goal is added to this commitment rather than a new commitment being added. Also, you will notice that the plan does not progress until the goal is satisfied.

In order to maintain correspondence with the AF-AgentSpeak guide, we also adapt the example agent program that adapts our hello world program to use the goal !say("Hello World"):

state(initialized) <-
    !say("Hello World");

plan speak(?text) {
    pre true;
    post say(?text);

    body {
        .println(?text),
        +say(?text),
        -say(?text)
    };
}

You will notice that this is not as elegant as the AF-AgentSpeak code because we must actually generate a belief that corresponds to the goal in order for the goal to be considered successful - unlike AgentSpeak(L), it is not enough just to have performed some plan for the goal to be considered achieved - the plan must actually modify the environment so that the agent believes that the goal is achieved. The result of this we need two update steps in the plan - one to generate the belief and one to remove it (because non-perception beliefs persist by default). However, this additional complexity arises from the forced nature of the above example.

For AFAPL, the more appropriate solution is not to use a goal, but to explicitly invoke the plan:

state(initialized) <-
    speak("Hello World");

plan speak(?text) {
    pre true;
    post true;

    body {
        .println(?text),
    };
}

This solution is closer to the AF-AgentSpeak solution in terms of conciseness but cannot be invoked by a goal because the post condition contains no beliefs that can be matched to goals.

As with AgentSpeak(L), AFAPL achievement subgoals are a lot like method or function calls in Object-Oriented or Procedural languages. The agent handles the adoption of the goal by matching it to the post conditions of the plans. Once a plan has been selected, the actions of that plan are added to the existing commitment stack in which the goal was raised. This is the same as occurs with the method/program call stack more traditional programming languages. Once the goal has been completed, the agent continues execution of the plan in which the initial goal was raised.

state(initialized) <-
    !say("Hello World"),
    .println("You are a wonderful place."),
    !say("Goodbye World"),
    .println("I am sorry to leave");

plan speak(?text) {
    pre true;
    post say(?text);

    body {
        .println(?text),
        +say(?text),
        -say(?text)
    };
}

It is interesting to note that, apart from the removal of the event information, the plan part of the rule does not change - all that we do is replace the second rule with an AFAPL plan. As before, the example program above generates the following output:

Hello World
You are a wonderful place.
Goodbye World
I am sorry to leave

As this shows, the agent completes the first !say(...) goal before moving on to the second line of the first plan.

One of the main differences between goals and method / function calls is the lack of a return value. In AFAPL, as with AF-AgentSpeak, return values are realised indirectly through the addition of beliefs that represent the outcome of a given plan.

Sleeping and Durative Actions

The final primitive action that is provided as part of the Core API of AFAPL is the .sleep(?timeInMS) action. This action basically exposes the Thread.sleep method, where the parameter provided to the action is treated as a time in milliseconds. This action can be used in two ways:

  • If the action is invoked directly by the agent, it will cause the interpreter to stop for the specified time (no perception or deliberation will take place).
  • If the action is wrapped within the durative(...) plan operator, then the plan is suspended for the allotted time period, but the agent interpreter cycle continues to execute (allowing other intentions to be achieved).

In general, it is recommended that you use the durative plan operator with the sleep action (unless you explicitly want the interpreter to stop). To illustrate how you should use it, the following program prints out the string "Hello World" after 10 seconds have passed:

state(initialized) <-
    durative(.sleep(10000)),
    .println("Hello World");

Lesson 2: Working with the Planning Language

This lesson introduces the range of plan operators that are provided in AFAPL. The set of operators provided is the same as those provided for the AF-AgentSpeak language. This is because both languages are based on the Common Language Framework and use the planning component provided by this framework. The plan operators have been designed to be as close as possible to the types of statement that are typically found within procedural programming languages.

Assignment

The most common way for variables to be assigned to values is through matching of predicates containing variables with beliefs. For example, in the following rule, the variable ?name is bound by matching the predicate name(?name) to a corresponding belief in the agents belief base.

state(initialized) & name(?name) <-
    +iam(?name);

In this example, the variable is bound during rule selection and then applied to the body of the plan. Similar binding can be achieved when matching the the current event under consideration to the triggering event of the plan.

In some cases, this implicit binding of variables to values is not appropriate or easy, for example, the problem of initialising a variable 1. To deal with such cases, AFAPL allows explicit binding of variables to values via an assignment operator. The example below illustrates this:

state(initialized) & name(?name) <-
    ?x = ?name,
    +iam(?x);

As can be seen, the plan involves 2 steps. In the first step, the variable ?x is bound to the value currently associated with the variable ?name; and in the second step, the variable ?x is used to create a belief.

If statements

If statements are analogous with their commonsense counterparts in procedural programming languages. The basically allow the agent to choose (or not) a course of action within the scope of a larger plan. The syntax of an if statement is similar to that used in the Java programming language:

if (<guard>) {
    // do something
}

if (<guard>) {
    // do something
} else {
    // do something else
}

What differs is the form of the guard. This may be any logical sentence written in AFL (the underlying logic), which includes positive and negative belief literals (isa(rem, man), ~available(rem)), conjunctions of literals ( isa(rem, man) & isa(man, mortal) ), and comparison operators (==, <, >). Guards may also introduce variable (the scope of the variable is the positive branch of the if statement (there will be no additional bindings for the negative branch because a match could not be found).

Lets consider the standard programs we have been developing to print out hello and goodbye:

state(initialized) <-
    !say(hello),
    !say(goodbye);
 
plan sayPlan(?x) {
    pre true;
    post say(?x);

    body {
        if (?x == hello) {
            .println("Hello World")
        } else {
            .println("Goodbye World")
        }
        +say(?x),
        -say(?x)
    };
}

This program compares the value stored in ?x and prints either "Hello World" or "Goodbye World". Again, the use of goals complicates the program, as the more appropriate AFAPL solution is to directly invoke the plan and not to use goals.

A more interesting problem is to create a program that generates Fibonacci numbers:

state(initialized) <-
    fibonacci(10);

fibonacci(?sequence, ?count) <-
    -fibonacci(?sequence, ?count),
    .println("fibonacci sequence: " + ?sequence);

plan fibonacci(?count) {
    pre true;
    post fibonacci(?sequence, ?count);

    body {
        if (?count == 1) {
            +fibonacci([0], 1)
        } else if (?count > 1) {
            fibonacci([1,0], ?count-2)
        }
    };
}
    
plan fibonacci(?sequence, ?count, ?offset) {
    pre true;
    post fibonacci(?sequence, ?count);

    body {
        if (?count == 0) {
            +fibonnacci(?sequence, ?offset)
        } else {
            ?f1 = head(?sequence),
            ?f2 = head(tail(?sequence)),
            fibonacci(merge([?f1 + ?f2], ?sequence), ?count-1, ?offset+1)
        }
    };
}

This program uses a list (defined by square brackets []) to generate the first 10 Fibonacci numbers. It does this using a recursive plan (the second plan). In this plan, the if statement is used to check for the base case (here, when ?count equals 0). If the base case arises, a belief is added that refers to the sequence of numbers generated. If the base case has not arisen, the recursive case is performed. This gets the top two values of the list, ?f1, and ?f2, using the head(...) (returns the head of the list), and tail(...) (returns the tail list created by removing the head). It then recursively calls the plan with modified arguments that result in the next number being added to the list, reducing ?count by 1, and increasing the offset by 1.

The first plan either deals with the special case of a single length fibonacci sequence or makes the initial recursive call to the second plan.

The first commitment rule triggers the generation of the sequence giving seed values 0 and 1 and identifying 10 as the number of numbers to be generated.

The second commitment rule prints out the result of the sequence.

NOTE: The above program is interesting in terms of comparison with AF-AgentSpeak: ths AFAPL program is not goal based, while the AF-AgentSpeak program is goal based. This is because AF-AgentSpeak does not allow direct invocation of plans, but forces all plan invocations to be done through the creation of (goal) events.

More examples of if statements will be presented later in conjunction with other plan operators.

While Loops

While loops also function in a similar manner to their common sense counterparts in procedural languages. The guard of the while loop is checked repeatedly, and so long as it is evaluated to true, the associated plan block is adopted. Once the guard is evaluated as false, the statement is considered to have completed successfully. Any variable bindings used in the evaluation of the guard are not applied to the associated plan block.

To illustrate the basic operation of the while statement, we start with a program the prints out "Hello World" 10 times:

state(initialized) <-
    ?x = 0,
    while (?x < 10) {
        .println("Hello World"),
        ?x = ?x + 1
    };

An interesting variation is the standard while(true) loop. The program below prints out "Hello World" continuously until the agent is terminated:

state(initialized) <-
    while (true) {
        .println("Hello World")
    };

Predicate statements can also be used in the guard, for example:

state(initialized) <-
    while (see(ball) & team(attacking)) {
        follow(ball)
    };

This program (while a bit simplistic) states that the agent keeps following the ball so long as it can see the ball and it believes that its team is attacking.

A more complex example is a modified version of the Fibonacci program from the previous section. Here, we show how a while loop can be used to construct an iterative version of the program:

state(initialized) <-
    fibonacci(10);

+fibonacci(?sequence, ?count) : true <-
    .println("Fibonacci Sequence found is: " + ?sequence);
 
plan fibonacci(?count) {
    pre true;
    post fibonacci(?sequence, ?count);

    body {
        ?x = 2,
        ?seq = [1, 0],
        while (?x < ?count) {
            ?f1 = head(?seq),
            ?f2 = head(tail(?seq)),
            ?seq = merge([?f1 + ?f2], ?seq),
            ?x = ?x + 1
        },
        +fibonacci(?seq, ?count);
    };
}

This program is simpler than its recursive counterpart, and closely matches the equivalent program that you would expect to see written in a procedural language.

For Each Statements

For each statements are a form of plan expansion operator, that does NOT operate similarly to for-loops in procedural languages. The syntax of the statement is:

foreach (guard) {
    act1,
    act2,
    ...
}

Basically, the guard of the statement is matched against the beliefs of the agent, and the associated sub plan is instantiated once for each variable binding generated. For people who are less knowledgeable about logics, this can seem a little peculiar, so we will illustrate this with the simple example program below.

state(initialized) <-
    ?x = 0,
    while (?x < 10) {
        +value(?x),
        ?x = ?x + 1
    }

    foreach (value(?y)) {
        .println("Hello World number " + ?y)
    };

This program prints out "Hello World number X" ten times, where X is a number between 0 and 9. This is achieved by splitting the program into 2 parts. The first part generates 10 beliefs about the numbers 0 to 9, with each belief having the form value(?x), where ?x is a number. The second part used these beliefs to "expand" the plan. Basically, the guard on the foreach statement is matched against each value(?x) belief (resulting in 10 possible variable bindings for ?x). The associated block is then enumerated once for each variable binding (in this case, the plan generates 10 .println(...) statements). These statements are then executed as normal plan steps.

When running the above program, it is printed in descending order, however the foreach() statement does not guarantee any order - it is simply the order in which the bindings are generated. Lets explore a more complex example that expands on the monitoring agent program from lesson 1, which was used to introduce the idea of agent communication through an agent that sends periodic status check messages to any agents that it has been told to monitor. The actual program shown is very basic as all that happens is that the monitoring agent sends an initial status check message to the monitored agent, and then resends this status check message each time the monitored agent responds. This means that the monitoring stops when the monitored agent does not respond, but the program does not capture this and the agent is not really aware that the monitored agent failed to respond.

The example programs below present a more elegant solution, where the monitoring agent sends the status check message periodically, and only to those agents that it believes are currently active. In this sense, it captures the notion of which of the monitored agents are currently active, and which are inactive:

// Monitor.afapl
state(initialized) <-
    foreach(monitoring(?name, ?addr)) {
        +active(?name)
    }

    while (true) {
        foreach(monitoring(?name, ?addr) & active(?name)) {
            .send(request, agentID(?name, ?addr), status)
        }

        durative(.sleep(10000)),

        foreach(monitoring(?name, ?addr) & active(?name)) {
            if (~status(?name, alive)) {
                -active(?name),
                +inactive(?name)
            }
        }

        .abolishAll(status(?name, alive))
    };

message(inform, agentID(?name, ?addr), status(alive)) & alive(?name, ?addr) <-
    +status(?name, alive);

// Monitored.afapl
message(request, ?sender, status) <-
    .send(inform, ?sender, status(alive));

As before, this problem actually requires two agent programs - one for the monitor agent, and one for the agents that are being monitored. The second of these programs has not changed from the program outlined in lesson 1. The program that has changed is the monitor agent program - let's examine this in more detail.

The core plan used in this program is adopted in response to the initialized event. In essence, the plan is an infinite loop that is broken into 5 parts:

  • Part 1: Sending the Status Check Message: This is the first foreach(...) statement, which sends a message to every agent that is being monitored, and which the monitor agent believes to still be active.
  • Part 2: Waiting for Responses: This is the durative sleep action (see lesson 1 for more details), which causes the agent to wait for 10 seconds (10000 ms)
  • Part 3: Updating the Status information: This is the second foreach(...) statement, and it basically checks to see if any of the active agents have not responded. If a monitored agent has not responded, then the active(...) belief is dropped and an inactive(...) belief is created. The recognition of whether or not the agent responded is based on the existence of a status(...) belief, which is generated by the third rule when the agent receives the response.
  • Part 4: Clearing the statuses: This is the .abolishAll(...) action, and it is responsible for dropping all the status(...) beliefs in preparation for the next iteration of the while loop. This needs to be done because the status(...) beliefs represent the knowledge about whether or not the monitored agent responded to the last status check message.

Prior to this infinite loop, there is a foreach(...) statement that adds initial beliefs that each monitored agent is active.

The second rule deals with the receipt of the response from the monitored agent, and generates the corresponding status(...) belief that us ised by part 3 of the first rule.

To run this program, you need to create at least one Monitor agent and one Monitored agent. This can be done by modifying the main() method to include the following lines:

IAgent A = ams.createAgent("A", "monitor.afapl");
IAgent B = ams.createAgent("B", "monitored.afapl");
A.initialise("monitoring(B, addresses(local:ping.ucd.ie))");

The best way to understand the program is to use the debugger. Start agent B running, as this is not so interesting, and then step the execution of the monitor agent. By examining the beliefs of the agent after each step, you should be able to see how the program is executed.

Specific things to watch for are:

  • In the beliefs, you will see that the agent has a monitoring(...) belief and at a later point, there is a corresponding active(...) belief (caused by the second rule).
  • Periodically, you will see a status(...) belief that corresponds to the agent. This belief is held for a small number of iterations, and then dropped (this corresponds to receipt of the response message from the monitored agent.
  • If you stop agent B and continue to step agent A, at some point, you will see that the active(...) belief has been dropped and the inactive(...) belief has been added. This is because agent B does not respond in the 10 second window, and as a result agent A now believes that agent B is no longer active.

NOTE: If you take too long to step agent A, then it is possible that it will not process the response from agent B in time, and so A will believe agent B is no longer active'

Waiting

The wait plan operator causes the execution of a plan to block until a particular belief is present in the agent's belief base. e.g.

+initialized <-
   wait(friend(?agentID)),
   .println("I have a friend");

The above example will wait until the agent has a belief of the form friend(?agentID). The .println action will not be executed until this belief becomes true. Variables used in a wait operator are not bound to the values they match. Therefore, although any value may exist within the friend(...) predicate, the ?agentID variable will not be bound to the value it matches for use later.

Failure handling

Lesson 3: Using Other APIs

This lesson is currently being adapted to AFAPL

In addition to the Core API, which comes as a standard part of the AF-AgentSpeak language, it is also possible to use other pre-written APIs that provide additional capabilities to the agent. Typically, agent programming language APIs provide an interface to some external system or component. APIs are written in Java as a special type of agent component, known as a Module. Within the implementation, the developer creates various actions and sensors, which provide the acting and sensing capabilities that the agent will need to use the API.

Examples of APIs include:

  • Platform Service APIs: Here, the API "agentifies" the platform service, identifying what information it can garner from the service and what it can do to the service.
  • GUI APIs: Here, the API provides an interface through which the agent can monitor user activity, push content to the user and, where appropriate, adapt the interface to changes in its environment.
  • Remote application APIs: Here, the API provides an interface to some remote application, such as a physical robot or a wireless sensor network, or a virtual environment.
  • Data Processing APIs: Logic is not always the most appropriate was to represent and manipulate information. Sometimes it is more appropriate to use Java. In these cases, modules can be used to implement more appropriate data structures for storing the data, and algorithms can be provided to perform data analysis. The API exposes a view of the data stored in the data structures and provides actions that allow the agent to apply the associated data analysis algorithms.

This lesson illustrates how to use an existing API, while the next lesson will focus on how to build your own APIs. Later lessons will introduce other pre-written APIs for specific components / applications.

The Agent Management Service API

The API that the lesson will focus on is the Agent Management Service API. This API provides run-time support for the creation, suspension, resumption, and termination of agents resident on an agent platform. The actual functionality is provided as part of the Agent Management Service (AMS), a core platform service (the is mandatory in the FIPA Specifications) that is deployed on all agent platforms.

To use the AMS API, all you need to do is add a single line to the agent program:

module ams -> com.agentfactory.clf.interpreter.AMSModule;

This sets up an association between the agent and the module, and allows the agent to access the actions and sensors associated with that module / API.

The API comes with one sensor, called "agents", which generates beliefs about what agents are running on their platform and their current state. The specific beliefs generated by the sensor are:

  • localAgent(?name): One instance of this belief is generated per agent on the platform - each belief holds the name of one of the agents currently residing on the agent platform.
  • agentType(?name, ?type): One instance of this belief is generated per agent on the platform. Here, ?type refers to the type of agent (e.g. AF-AgentSpeak, AF-APL, AF-TR, AF-RMA, ...).
  • agentState(?name, ?state): One instance of this belief is generated per agent on the platform. Here, ?state refers to the current state of the agent, and may be one of: {active, initiated, suspended, terminated, transit, waiting}. These states have been chosen in compliance with the FIPA standards.
  • agentID(?name, ?addr): One instance of this belief is generated per agent on the platform. This is the unique identifier of the agent.

In addition to these perceptions, the AMS API also provides a number of actions:

  • create(?name, ?design): Creates an agent whose name is ?name that is based on the specified design, given by ?design.
  • resume(?name): Causes the execution of the agent identified by ?name to be resumed. If the agent is currently active, then this action has no effect.
  • suspend(?name): Causes the execution of the agent identified by ?name to be suspended. If the agent is currently suspended, then this action has no effect.
  • terminate(?name): Causes the execution of the agent identified by ?name to be terminated. This results in the agent being suspended and then removed from the agent container.

To access the API actions, you must use an extended action syntax:

<module-name> . <action>

For example, to resume an existing agent called "rem", you would specify the following action:

ams.resume(rem)

The prefix to the action changes depending on the identifier you have associated with the module when you declared the module (in the examples here, we use the identifier "ams" in compliance with our declaration of the module above).

The remaining sections of this lesson will explore how to use this API in more detail through a number of simple examples. In later lessons, we will explore some other API's that have been built for AF-AgentSpeak.

Creating Agents Dynamically

Perhaps the most powerful feature of the AMS API is the ability to create agents dynamically (i.e. at run-time). The program below outlines how to do this based on a simple scenario in which 10 agents are created. Once started, each agent then creates the next agent in the sequence.

module ams -> com.agentfactory.clf.interpreter.AMSModule;

+initialized : true <-
    ams.setup,
    ?next = 0,
    ?count = 1,
    while (?count < 11) {
        if (~localAgent(agent+?count) & ~name(agent+?count)) {
            ?next = ?count,
            ?count = 11
        } else {
            ?count = ?count + 1
        }
    },
    if (?next > 0) {
        ams.create(agent+?next, launcher.aspeak),
        .println("Agent: agent" + ?count + " has been created."),
        ams.resume(agent+?next)
    };

To run the platform, we create a single agent, called "initial" that is an instance of the above program (lets call it launcher.aspeak). When run, the overall system goal is to create 10 agents labelled agent1-agent10 respectively. Each agent that is created at runtime is responsible for creating the next agent in the sequence. The plan that is used to achieve this can be broken into 3 parts:

  • line 1: this first part of the plan sets up the AMS API by performing the ams.setup action. This links the API to the underlying platform service.
  • lines 2-11: this second part of the plan uses the localAgent(...)' belief of the AMS API to work out which agent should be created next. Basically, this is a while loop in which the ?count variable is incremented repeatedly until there is a value for ?count such that there is no local agent with the name "agent+?count" and also, this is not the name of the agent itself (as there will not be a belief that the agent is itself a localAgent). Once such a name is identified, the ?next variable is set to the corresponding value of ?count. If no name is found (i.e. agent10 has been created), then ?next remains bound to 0 (which indicates that no agent should be created).
  • lines 12-15: the final part of the plan creates the next agent (if required). It uses the ams.create(...) action to create the next agent, and the ams.resume(...) action to start the agent once it has been created.

Suspending Agents

To illustrate how to suspend agents at runtime, we can extend the example used in the previous section to include a fourth step to the plan in which the agent suspends itself once it has created and resumed the next agent.

module ams -> com.agentfactory.clf.interpreter.AMSModule;

+initialized : true <-
    ams.setup,
    ?next = 0,
    ?count = 1,
    while (?count < 11) {
        if (~localAgent(agent+?count) & ~name(agent+?count)) {
            ?next = ?count,
            ?count = 11
        } else {
            ?count = ?count + 1
        }
    },
    if (?next > 0) {
        ams.create(agent+?next, launcher.aspeak),
        .println("Agent: agent" + ?count + " has been created."),
        ams.resume(agent+?next)
    },
    foreach(name(?name)) {
        ams.suspend(?name)
    };

Here, the name(...) belief is used with a foreach(...) plan operator to bind the variable ?name to the agents name. This is then used in the ams.suspend(...) action.

Terminating Agents

For this final example, we show a simple agent program that is designed initially to be used by an agent with name A. Agent A its name, creates an agent called agent B, and terminates itself. Agent B, once started, checks its name, creates an agent with name A, and terminates itself. This continues on until the agent platform is stopped. The program is called "switcher.aspeak"

module ams -> com.agentfactory.clf.interpreter.AMSModule;

+initialized : name(?name) <-
    ams.setup,
    durative(.sleep(1000)),
    if (?name == A) {
        ams.create(B, switcher.aspeak),
        .println("Creating B"),
        ams.resume(B)
    } else {
        ams.create(A, switcher.aspeak),
        .println("Creating A"),
        ams.resume(A)
    },
    ams.terminate(?name);

The Queue API

The Queue API is an example of a Data Structure API that has been implemented for the Common Language Framework. This API provides support for the creation and manipulation of Queues.

The API consists of 4 actions:

  • create(?id): This action creates a new queue with the given identifier.
  • destroy(?id): This action destroys an existing queue with the given identifier.
  • enqueue(?id, ?value): This action adds the given value to the queue specified by the given identifier. If no such queue exists, then the action fails.
  • dequeue(?id): This action removes the head of the queue specified by the given identifier. If no such queue exists, then the action fails. If the queue is empty, then the action fails.

Additionally, the API provides 1 sensor, called state that generates beliefs about the states of any queues that have been created using the API. Specifically, the sensor generates the following beliefs:

  • empty(?id): the queue specified by the identifier is empty
  • front(?id, ?val): ?val is the value at the that is currently at the head of the queue specified by the identifier.
  • size(?id, ?x): the number of items currently held in the queue specified by the identifier.

To use the API, add the following line to your agent program:

module queues -> com.agentfactory.clf.interpreter.QueueAPI;

Below, we illustrate how to use this API with a simple example:

module queues -> com.agentfactory.clf.interpreter.QueueAPI;

+initialized : true <-
    queues.create(test);

+empty(test) : true <-
    queues.enqueue(test, token);

+front(test, token) : true <-
    queues.dequeue(test);

With this program, the agent will continuously add and remove a token value from the test queue.

References

  1. Template:Cite web