AFAS::Protocols
From Agent Factory
This is lesson 8 of the AF-AgentSpeak language guide.
Implementing Protocols
The default Agent Communication model in Agent Factory is based on the FIPA Agent Communication Language (ACL). As such, the types of message that can be sent by an agent are those that are listed as FIPA Communicative Acts. As was seen in the first lesson, agent communication is supported through the inclusion of a sensor and action in the Core API of AF-AgentSpeak:
- .inbox: sensor that generates beliefs/belief events about messages received. Beliefs take the form: message(?perf, ?agentID, ?content). Here, ?agentID is the agent identifier of the sender.
- .send(?perf, ?agentID, ?content): action that constructs and sends a FIPA ACL message. Here ?agentID is the agent identifier of the receiver.
Typically, an interaction between two or more agents involves a sequence of messages being passed between those participating agents. Such a sequence of messages is commonly known as a protocol. The FIPA Standards specify a number of standard protocols for agent interaction. In the sections below, we will outline how to implement some of these protocols.
WARNING: This lesson is incomplete and the agent programs may not accurately represent the corresponding Agent UML Diagrams
Request Protocol
The FIPA Request Protocol is one of the simpler protocols in the FIPA Standards. It encodes the sequence of messages should be used when an agent wishes to request that another agent perform some activity on its behalf. As can be seen in the diagram, the Initiator sends a request to a Participant asking for some activity to be performed. Based on the protocol, the Participant should return one of two messages:
- a refuse message should be sent if the Participant does not wish to perform the requested activity;
- an agree message should be sent if the Participant agrees to perform the requested activity.
If the participant agrees to perform the activity, then a second message should be sent once the activity has been completed. This second message may take one of three forms depending on the outcome of the activity:
- a failure message should be sent if the Participant fails to perform the agreed activity;
- an inform message should be sent once the activity has been completed successfully informing the Initiator that the activity has been done;
- in the event that the activity requires that some answer be sent back to the Initiator, then this inform message should contain that information.
The second and third messages are variants on the same theme: both involve informing the Initiator that the activity has been completed, but the latter form includes any information generated by the activity (e.g. if the activity was a calculation, then the inform response should include the result of that calculation).
Lets explore this protocol with a simple example that uses two agents: a calculator agent (who will be the Participant) and a test user agent (who will be the Initiator). The test user agent will send requests for various calculations to be performed (for simplicity we will consider only addition and subtraction). These requests will be designed as a "unit test" in that they will evaluate all possible outcomes from the protocol.
The best way to start implementing the protocol is to outline the calculator agent code first:
#agent calculator
+message(request, ?sender, calculate(?op, ?a, ?b)) : true <-
// agree to all calculate requests
.send(agree, ?sender, calculate(?op, ?a, ?b)),
// do the activity or return failure
if (?op == add) {
.send(inform, ?sender, result(calculate(?op, ?a, ?b), ?a+?b))
} else if (?op == subtract) {
.send(inform, ?sender, result(calculate(?op, ?a, ?b), ?a-?b))
} else {
.send(failure, ?sender, calculate(?op, ?a, ?b))
};
+message(request, ?sender, ?activity) : true <-
.send(refuse, ?sender, ?activity);
So, the above code waits for a request message to be received asking for an activity to be performed. If the activity is a calculate activity, then the first rule is triggered, resulting in the calculator agreeing to perform the activity, and then attempting to perform the activity. If the operation is addition or subtraction, then the Initiator is informed of the result of the operation. Otherwise, the Initiator is told that the activity failed because it is not supported. If the calculator receives any other request to perform an activity, then the second rule is triggered, resulting in the calculator refusing to perform the activity (as it is not supported).
To test our calculator implementation, we create a test user agent that sends three requests:
- first it asks the calculator to calculate 2 plus 2 (which it does successfully)
- next, it asks the calculator to calculate 2 times 2 (which it agrees to do, but fails)
- finally, it asks the calculator to print out "hello" (which it refuses to do)
The code for the test user agent is below:
#agent user
+initialized : name(?name) & agentID(?name, ?addr) <-
.send(request, agentID(calc, ?addr), calculate(add, 2, 2));
+message(inform, ?sender, result(calculate(add, 2, 2), ?x)) : true <-
.println("Result for 2 plus 2 is: " + ?x),
.send(request, ?sender, calculate(multiply, 2, 2));
+message(failure, ?sender, calculate(?op,?x,?y)) : true <-
.println("Failed to calculate 2 times 2"),
.send(request, ?sender, print(hello));
+message(refuse, ?sender, print(hello)) : true <-
.println("refused to print hello");
Each rule in this program is based on the outcome of the previous step (i.e. if the calculator fails to calculate the result of 2 plus 2, then no other request will be sent). To run this program, simple create a Main class with a AgentSpeakDebugConfiguration, and start the agents using the debugger as is shown below.
public class Main {
public static void main(String[] args) {
Map<String,String> designs = new HashMap<String, String>();
designs.put("calc", "calculator.aspeak");
designs.put("user", "user.aspeak");
new AgentSpeakDebugConfiguration("test", designs).configure();
}
}
The expected output is:
Result for 2 plus 2 is: 4 Failed to calculate 2 times 2 refused to print hello
Now, lets extend the functionality offered by the calculator to include support for multiplication:
#agent calculator
+message(request, ?sender, calculate(?op, ?a, ?b)) : true <-
// agree to all calculate requests
.send(agree, ?sender, calculate(?op, ?a, ?b)),
// do the activity or return failure
if (?op == add) {
.send(inform, ?sender, result(calculate(?op, ?a, ?b), ?a+?b))
} else if (?op == subtract) {
.send(inform, ?sender, result(calculate(?op, ?a, ?b), ?a-?b))
} else if (?op == multiply) {
.send(inform, ?sender, result(calculate(?op, ?a, ?b), ?a*?b))
} else {
.send(failure, ?sender, calculate(?op, ?a, ?b))
};
+message(request, ?sender, ?activity) : true <-
.send(refuse, ?sender, ?activity);
If we run our test user agent against the new calculator program, we get the following output:
Result for 2 plus 2 is: 4
This is because the multiply activity no longer returns failure, but instead returns a result. So, we must update our test user agent as follows:
#agent user
+initialized : name(?name) & agentID(?name, ?addr) <-
.send(request, agentID(calc, ?addr), calculate(add, 2, 2));
+message(inform, ?sender, result(calculate(add, 2, 2), ?x)) : true <-
.println("Result for 2 plus 2 is: " + ?x),
.send(request, ?sender, calculate(multiply, 2, 2));
+message(inform, ?sender, result(calculate(multiple, 2, 2), ?x)) : true <-
.println("Result for 2 times 2 is " + ?x),
.send(request, ?sender, print(hello));
+message(refuse, ?sender, print(hello)) : true <-
.println("refused to print hello");
Now the output generated is:
Result for 2 plus 2 is: 4 Result for 2 times 2 is 4 refused to print hello
Finally, to introduce support for additional activities, such as the print activity, we simply introduce new rules to the calculator agent to handle the activity:
#agent calculator
+message(request, ?sender, calculate(?op, ?a, ?b)) : true <-
// agree to all calculate requests
.send(agree, ?sender, calculate(?op, ?a, ?b)),
// do the activity or return failure
if (?op == add) {
.send(inform, ?sender, result(calculate(?op, ?a, ?b), ?a+?b))
} else if (?op == subtract) {
.send(inform, ?sender, result(calculate(?op, ?a, ?b), ?a-?b))
} else if (?op == multiply) {
.send(inform, ?sender, result(calculate(?op, ?a, ?b), ?a*?b))
} else {
.send(failure, ?sender, calculate(?op, ?a, ?b))
};
+message(request, ?sender, print(?string)) : name(?name) <-
// agree to all print requests
.send(agree, ?sender, print(?string)),
.println(?name + ": " + ?string),
.send(inform, ?sender, done(calculate(?op, ?a, ?b)));
+message(request, ?sender, ?activity) : true <-
.send(refuse, ?sender, ?activity);
Notice that the new rule is added before the general rule for handling requests. This is important because of the order in which AgentSpeak evaluates rules: it chooses the first rule whose triggering event matches the event currently being handled for which the context is true. The expected output when running the test user agent is:
Result for 2 plus 2 is: 4 Result for 2 times 2 is 4 calc: hello
Here, the third line is generated by the calculator instead of the test user agent...
Before concluding this example, lets consider some issues relating to the above design:
- the implementation assumes (naively) that all request messages are associated with the FIPA Request Protocol. If a request message is used in another protocol, it may be difficult to distinguish between the protocols and the generic refuse response may no longer work.
- each activity is declared through a rule that handles the initial request, and the example agent assumes infinite resources for its activities. In cases where the resources available are finite, a more complete implementation may be required (you will need to track resource usage).
- reuse of code is difficult because the implementation is activity specific and rule ordering is central to the activity handling strategy (the general rule must be last), so the protocol will have to be re-coded every time it is used.
- the full version of the protocol includes support for the protocol being "not understood" and also for cancellation of activities that were previously requested. the former cannot be implemented in the current communication model (see ACRE) and the latter requires a more complex model on the Participant side of the implementation (we must track activities).
English Auction Protocol
English Auctions are a first-price, open-cry, ascending bid negotiation strategy. An Agent UML Protocol Diagram outlining how an English Auction works can be seen on the right-hand side.
#agent auctioneer
// Add a belief indicating that the auction is about to start
+initialized : true <-
+start(auction);
// Main Auctioneer Algorithm
+auctioning(?item, ?startPrice, ?increment) : true <-
// Remove the auctioning belief and update the state of the auction
-auctioning(?item, ?startPrice, ?increment),
if (start(auction)) {
// this is the first iteration of the auction, so do nothing (apart
// from removing the belief that the auction has just started).
-start(auction)
} else {
// Check which participants returned proposals in the last round
// of bidding, and remove any participants who did not bid.
foreach(participant(?name, ?addr)) {
if (~proposal(agentID(?name, ?addr2), ?item, ?price)) {
-participant(?name, ?addr)
}
}
},
// remove the previous rounds proposals (if there were any)
foreach(proposal(?agentID, ?item, ?price)) {
-proposal(?agentID, ?item, ?price)
},
// indicate the start of the next round of the auction
foreach(participant(?name, ?addr)) {
.send(cfp, agentID(?name, ?addr), auction(?item, ?startPrice))
},
// Wait for a second to allow the participants to reply (this is the
// deadline)
durative(.sleep(1000)),
// count how many responses we got
?count = 0,
foreach(proposal(?agentID, ?item, ?startPrice)) {
?count = ?count + 1
},
// if there was more than one proposal, reject them all and start the next
// round of the auction with the price increased
if (?count > 1) {
foreach(proposal(?agentID, ?item, ?startPrice)) {
.send(reject-proposal, ?agentID, auction(?item, ?price))
},
+auctioning(?item, ?startPrice+?increment, ?increment)
},
// if there was only one proposal, accept it as the winning proposal
if (?count == 1) {
foreach(proposal(?agentID, ?item, ?startPrice)) {
.send(accept-proposal, ?agentID, auction(?item, ?price))
}
},
// If there were no proposals, the first responder from the
// previous round is the winner, so accept their last proposal
if (?count == 0) {
foreach(lastwinner(?agentID, ?item, ?price)) {
.send(accept-proposal, ?agentID, auction(?item, ?price))
}
},
// drop any beliefs about who won this round of the auction
foreach(lastwinner(?agentID, ?item, ?price)) {
-lastwinner(?agentID, ?item, ?price)
};
// Record the receipt of the first proposal and believe that the sender is the winner of
// the round
+message(propose, ?sender, auction(?item, ?price)) : ~lastwinner(?aid, ?item, ?price) <-
+lastwinner(?sender, ?item, ?price),
+proposal(?sender, ?item, ?price);
// Record the receipt of the proposal
+message(propose, ?sender, auction(?item, ?price)) : lastwinner(?aid, ?item, ?price) <-
+proposal(?sender, ?item, ?price);
Participant Code:
#agent bidder
action randomDecision(?item) -> RandomDecision;
+message(cfp, ?sender, auction(?item, ?price)) : ~responding(?item) <-
+responding(?item),
!decide(?item),
wait(decision(?item, ?choice)),
if (decision(?item, accept)) {
.send(propose, ?sender, auction(?item, ?price))
},
foreach (decision(?item, ?choice)) {
-decision(?item, ?choice)
},
-responding(?item);
+!decide(?item) : true <-
randomDecision(?item);
Random Decision Action:
public class RandomDecision extends Action{
@Override
public boolean execute(Predicate activity) {
String item = Utilities.presenter.toString(activity.termAt(0));
if (Math.random() > 0.5) {
addBelief("decision(" + item + ", accept)");
} else {
addBelief("decision(" + item + ", reject)");
}
return true;
}
}


