AFAS::Planning

From Agent Factory

Jump to: navigation, search

This is lesson 2 of the AF-AgentSpeak language guide.

Contents

Introduction

This lesson introduces the range of plan operators that are provided in AF-AS. Similarly to the extensions provided in Jason, these 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.

#agent assigner

+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, AF-AS allows explicit binding of variables to values via an assignment operator. The example below illustrates this:

#agent assigner

+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.

NOTE: later, we will explore more complex uses of the assignment operator, including the creation of lists by example and functional actions (see the acting and sensing lesson)

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:

#agent iffy

+initialized : true <-
    !say(hello),
    !say(goodbye);
 
+!say(?x) : true <-
    if (?x == hello) {
        .println("Hello World")
    } else {
        .println("Goodbye World")
    };

This program compares the value stored in ?x and prints either "Hello World" or "Goodbye World".

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

#agent fibonacci

+initialized : true <-
    !fibonacci([1, 0], 10);

+fibonacci(?sequence) : true <-
    .println("Fibonacci Sequence found is: " + ?sequence);
 
+!fibonacci(?sequence, ?count) : true <-
    if (?count == 0) {
        +fibonacci(?sequence)
    } else {
        ?f1 = head(?sequence),
        ?f2 = head(tail(?sequence)),
        !fibonacci(merge([?f1 + ?f2], ?sequence), ?count-1)
    };

This program uses a list (defined by square brackets []) to generate the first 10 Fibonacci numbers. It does this using a recursive definition (the third rule). In this rule, 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 raises a new achievement goal (this is the recursive call) that adds the next number to the list and reduces ?count by 1.

The first 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 rule prints out the result of the sequence.

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:

#agent hello10

+initialized : true <-
    ?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:

#agent helloinf

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

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

#agent player

+initialized : true <-
    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:

#agent fibonacci2

+initialized : true <-
    !fibonacci(10);

+fibonacci(?sequence) : true <-
    .println("Fibonacci Sequence found is: " + ?sequence);
 
+!fibonacci(?count) : true <-
    ?x = 0,
    ?seq = [1, 0],
    while (?x < ?count) {
        ?f1 = head(?seq),
        ?f2 = head(tail(?seq)),
        ?seq = merge([?f1 + ?f2], ?seq),
        ?x = ?x + 1
    },
    +fibonacci(?seq);

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.

#agent hello10-2

+initialized : true <-
    ?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:

#agent monitor

+initialized : true <-
    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))
    };

+monitoring(?name, ?addr) : true <-
    +active(?name);

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

#agent monitor

+message(request, ?sender, status) : true <-
    .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 4 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.

The second rule of the program basically states that when the agent starts monitoring another agent, then it should initially assume that the monitored agent is active.

The third 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.aspeak");
IAgent B = ams.createAgent("B", "monitored.aspeak");
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'

For All Statements

In addition to the foreach statements, which operate on bindings that match a given guard, AF-AgentSpeak also supports forall statements which operate over lists. The format for these statements is:

forall (?variable : ?list) statement,

Inside the guard, the term before the colon must always be an unbound variable name and the term after the colon may be a list or a variable. The list variable must be bound before entering the loop.

The forall operator is a plan expansion operator in that it replaces the forall statement with N occurrences of statement (where N is the number of items in the list).

Lets examine a simple example:

#agent example

+initialized : true <-
    ?list = [0, 1, 2, 3, 4, 5],
    forall(?value : ?list) {
        .println("value: " + ?value)
    };

This program will print out the following to the console:

value: 0
value: 1
value: 2
value: 3
value: 4
value: 5

As can be seen, the order of expansion is from the head of the list to the tail of the list.

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 : true <-
   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

The ability to handle the failure of actions is supported through a try {...} recover {...} operator that is similar to the try {...} catch (Exception) {...} operator of Java. The intentions of an agent are encoded as a set of plan stacks - one for each intention of the agent. The agent attempts to achieve the intention by executing all of the elements pushed onto the plan stack. Each element is labelled a plan step, and can be any of the operators listed in this lesson or any of the primitive operators (belief update, goal invocation, or actions).

When an intention fails (because one of the steps in the plan fails), the agent rolls back the plan, popping steps from the plan stack until either there are no more plan steps on the stack or it hits a recovery point. If there is not recovery point, then the intention is considered to have failed. If there is a recovery point, the agent applies an associated recovery plan to attempt to continue its efforts to achieve the intention. Recovery points are declared through the try-recover plan operator.

Lets explore how this works through a simple worked example:

#agent test

@initialization
+initialized : true <-
    try {
        .println("in try"),
        .fail,
        .println("after fail")
    } recover {
        .println("in recover")
    };

In the agent program above, on initialization, the agent attempt to perform a plan. The try part of the try-recover operator defines the normal activity to be executed. In this case, the agent performs the first print action followed by the fail action. This second action causes the plan to fail. As a result, the second print action is not executed. Instead, the plan is rolled back to the try-recover statement, and the recovery plan is invoked, resulting in the single print statement being performed. The expected output of this plan is:

in try
in recover

For completeness, let us consider what happens if we remove the failure action:

#agent test

@initialization
+initialized : true <-
    try {
        .println("in try"),
        .println("after fail")
    } recover {
        .println("in recover")
    };

Here, the expected output is:

in try
after fail

Notice that the recovery plan is no longer executed because the plan no longer fails.

When Statements

The when plan operator blocks until some statement becomes true. Unlike the wait operator it does bind values to any variables that match, with these being available for the remainder of the plan.

when(have(?items)) {
   .println("I have " + ?items + " item(s)")
}