Interaction Protocols
When two agents interact, a single message is rarely enough to ensure that the interaction will result in a desired behaviour. Instead, agent interaction typically involves the exchange of a sequence of messages whose ordering and types are based on some form of interaction protocol.
An interaction protocol is the common sense equivalent of a protocol in networking. It defines a sequence of message exchanges between two (or more) agents. The sequence of message exchanges is based on the types of messages that are exchanged, which participant should send each message, and the order in which they should be exchanged. FIPA has defined a number of standard protocols.
This section builds on the messaging features (send(...) and @message(...) plans) described in Agent Communication and the Multi-Agent Communication with ASTRA tutorial. In it, we will explore how to implement one of the simpler FIPA protocols, known as the FIPA Request Protocol. The sequence of messages in this protocol is shown below.
Initiator Participant
| |
|------------- request ------------>|
| |
|<------------- refuse -------------| (the Participant refuses: end)
| or |
|<------------- agree --------------| (the Participant agrees)
| |
|<------------- failure ------------| (the task could not be done)
| or |
|<------------- inform -------------| (the task is done, or its result)
The FIPA Request Protocol is designed to allow an agent to ask another agent to perform a task. The Initiator is the agent that needs the task to be performed and the Participant is the agent that it is asking to perform the task. The flow of the protocol is fairly straightforward: the Initiator requests that the Participant perform a task. The Participant decides whether or not to perform the task and either agrees or refuses to perform it. If the Participant agrees to perform the task, then, once the agreed task is performed, it should inform the Initiator that either the task is completed or (if necessary) the outcome (result) of the task. In the event that the Participant fails to complete the agreed task, it is required to inform the Initiator of the failure of that task.
To illustrate this, let’s consider an example of an Authentication agent that is able to authenticate username / password tuples when requested to by trusted agents.
An Authentication Agent
To implement our example, let us first look at the Authentication agent, as this agent implements the task that is to be performed. To implement this agent, we need to make use of two types of belief:
trusted(string X): a belief that indicates that agentXis trusted.credentials(string U, string P): a belief that holds user credentials: a username (U) and password (P).
These beliefs, and the formulae used in the messages that the agents exchange, are declared in a types block. Next, let’s consider the core behaviour: the Authentication agent receives a request message from another agent asking that it authenticate a username/password tuple. This can be modelled using the following rule:
agent Authentication {
types auth {
formula trusted(string);
formula credentials(string, string);
formula validate(string, string);
formula validate(string);
formula validated(string);
formula invalid(string);
}
plan @message(request, string X, validate(string U, string P)) {
}
}
The first step of the protocol involves deciding whether or not to accept the request. In the description of our proposed solution, the Authentication agent will agree to authentication requests from agents that it trusts and will refuse requests from agents that it does not trust. To implement this, we can use the trusted(...) belief as follows:
plan @message(request, string X, validate(string U, string P)) {
if (trusted(X)) {
send(agree, X, validate(U));
} else {
send(refuse, X, validate(U));
}
}
Now, the final step involved in implementing our authentication protocol is to add additional code to actually validate the user. For this, we can use a query statement:
plan @message(request, string X, validate(string U, string P)) {
if (trusted(X)) {
send(agree, X, validate(U));
query(credentials(U, P));
send(inform, X, validated(U));
} else {
send(refuse, X, validate(U));
}
}
Note that the above code makes an assumption that the user credentials will be validated. Also, the behaviour of the query statement is that failure of the query means failure of the plan. This means that we need to cater for the possibility that the credentials will be incorrect. To do this, we change from using a query statement to using an if statement. The complete Authentication agent, with some initial beliefs to test it with, is:
agent Authentication {
types auth {
formula trusted(string);
formula credentials(string, string);
formula validate(string, string);
formula validate(string);
formula validated(string);
formula invalid(string);
}
initial trusted("main");
initial credentials("rem", "password");
plan @message(request, string X, validate(string U, string P)) {
if (trusted(X)) {
send(agree, X, validate(U));
if (credentials(U, P)) {
send(inform, X, validated(U));
} else {
send(inform, X, invalid(U));
}
} else {
send(refuse, X, validate(U));
}
}
}
This completes the core implementation of the Authentication agent. Now, we need to implement an agent to test this behaviour. The test agent creates the Authentication agent, sends it two requests (one with the correct password and one without), and handles each of the possible replies:
agent AuthenticationTest {
module Console C;
module System S;
types auth {
formula validate(string, string);
formula validate(string);
formula validated(string);
formula invalid(string);
}
plan +!main(list args) {
S.createAgent("authenticator", "Authentication");
send(request, "authenticator", validate("rem", "password"));
send(request, "authenticator", validate("rem", "letmein"));
}
plan @message(refuse, string X, validate(string U)) {
C.println(X + " has refused to validate: " + U);
}
plan @message(agree, string X, validate(string U)) {
C.println(X + " has agreed to validate: " + U);
}
plan @message(inform, string X, validated(string U)) {
C.println(U + " has been validated");
}
plan @message(inform, string X, invalid(string U)) {
C.println(U + " has invalid credentials");
S.exit();
}
}
Running mvn -Dastra.main=AuthenticationTest produces:
[main] authenticator has agreed to validate: rem
[main] authenticator has agreed to validate: rem
[main] rem has been validated
[main] rem has invalid credentials
Integrating Timeouts into Conversations
Sometimes you need to develop a solution that allows an agent to send out a number of messages and then wait a fixed amount of time for responses. This scenario requires the implementation of a timeout mechanism. The code below shows a Master agent, which asks a Slave agent to add two numbers, and gives up if no answer arrives within a second:
package examples;
agent Master {
module Console C;
module System S;
types master {
formula state(string);
formula slave(string);
formula add(int, int);
formula added(int, int, int);
formula timeout(string);
}
initial !init();
plan +!init() {
S.createAgent("rem", "examples.Slave");
S.sleep(5000);
!add(3, 5);
}
plan @message(inform, string sender, state(string X)) {
C.println(sender + " is alive!");
+slave(sender);
}
plan +!add(int X, int Y) : slave(string slave) {
send(request, slave, add(X, Y));
!!timeout("add", 1000); // timeout set for 1 second...
wait(added(X, Y, int Z) | timeout("add"));
if (added(X, Y, int Z2)) {
C.println(X + "+" + Y + "=" + Z2);
} else {
C.println("No response from " + slave);
}
-timeout("add"); // remove the timeout flag
}
plan @message(inform, string sender, added(int X, int Y, int Z)) {
+added(X, Y, Z);
}
plan +!timeout(string id, int length) {
S.sleep(length);
+timeout(id);
}
}
Here, the timeout is implemented through a new !timeout(...) goal, which takes an id and a timeout duration as parameters. This goal is adopted whenever the agent wishes to have a timeout, and the wait(...) statement is satisfied by either the original condition (the added(...) belief) or the timeout belief that is generated by the !timeout(...) rule. Notice that the timeout goal is adopted using !! as a spawned goal instead of a subgoal: this means that the timeout runs as a separate intention that is executed in parallel to the “conversation” intention.
Prewritten Protocols
The astra-protocols library contains ready-made implementations of some common interaction protocols. To use it, add it to your project’s pom.xml:
<dependencies>
<dependency>
<groupId>com.astralanguage</groupId>
<artifactId>astra-protocols</artifactId>
<version>2.0.13</version>
</dependency>
</dependencies>
Each protocol is an agent program in the astra.protocol package. You use one by inheriting from it (with extends) and then overriding the plans that decide what your agent does at each step of the protocol. Each protocol program contains the plans for both roles: the agent that starts the conversation (the initiator) and the agent that responds to it (the participant). The protocols are written using encapsulated goals, so the plans that handle the replies to a conversation are only active while that conversation is going on.
Protocol |
Program |
Initiator goal |
Participant goal |
|---|---|---|---|
FIPA Request |
|
|
|
FIPA Subscribe |
|
|
|
First-price auction |
|
|
|
Vickrey (second-price) auction |
|
|
|
FIPA Request
FipaRequest implements the protocol described at the top of this page. Actions are represented as funct terms, such as greet("world").
The participant adopts !monitor_action_requests() and overrides two plans:
Plan |
Purpose |
|---|---|
|
Set |
|
Perform |
The initiator adopts !request_action(target, act) for each request, and can override +!agreed_request(funct act), +!failed_request(funct act) (the request was refused) and +!succeeded_request(funct act, funct outcome) to react to the replies.
In this example, a Requester asks a Responder to greet the world:
package demo;
agent Responder extends astra.protocol.FipaRequest {
initial !monitor_action_requests();
plan +!evaluate_request(string name, greet(string who), boolean result) {
result = true;
}
plan +!execute_request(string name, greet(string who), funct outcome, boolean result) {
console.println("Hello, " + who + "!");
outcome = greeted(who);
result = true;
}
}
package demo;
agent Requester extends astra.protocol.FipaRequest {
plan +!main(list args) {
system.createAgent("responder", "demo.Responder");
!request_action("responder", greet("world"));
}
plan +!agreed_request(funct act) {
console.println("responder agreed to: " + act);
}
plan +!succeeded_request(funct act, funct outcome) {
console.println(act + " succeeded with outcome: " + outcome);
system.exit();
}
}
[main] responder agreed to: greet("world")
[responder] Hello, world!
[main] greet("world") succeeded with outcome: greeted("world")
The Responder’s plans only match greet(...) actions; any other request is handled by the default plans, and refused.
Note
In ASTRA 2.0.13, the !request_action(...) goal does not return to the plan that adopted it: the +!succeeded_request(...) or +!failed_request(...) plan runs, but the statements after !request_action(...) are never reached. Until this is fixed, put whatever should happen next in those plans, as the Requester above does with system.exit().
FIPA Subscribe
FipaSubscribe lets agents subscribe to channels that another agent publishes information on.
The participant (the publisher) adopts
!monitor_subscriptions(), and has achannel(name)belief for each channel it offers. It can override+!evaluate_subscription(string channel, boolean result)to decide whether to accept a subscription (by default, all are accepted), and adopts!publish(channel, message)to send afunctmessage to every subscriber of a channel.The initiator (the subscriber) adopts
!subscribe(target, channel). It overrides+!handle_content(string channel, funct message)to process the messages it receives, and can override+!subscription_accepted(...),+!subscription_refused(...)and+!subscription_cancelled(...). It adopts!cancel_subscription(target, channel)to unsubscribe.
Auctions
FirstPriceAuction and VickreyAuction implement sealed-bid auctions: the auctioneer sends a call for proposals (cfp) to every participant, waits for a fixed time, and then accepts the best bid. In a first-price auction, the winner pays the amount it bid; in a Vickrey auction, it pays the second-highest bid.
The auctioneer has a
participant(name)belief for each agent that should be invited, and adopts!auction(id, description, configuration(timeout))(for a Vickrey auction, the last argument isconfigure(timeout)). It can override+!compare_bids(...)to change how bids are compared; by default, the highest bid wins.Each participant adopts
!monitor_for_auctions()and overrides+!generate_bid(string id, funct description, int bid)to set its bid. It can also override+!successful_bid(...)and+!failed_bid(...).
package demo;
agent Auctioneer extends astra.protocol.FirstPriceAuction {
initial participant("alice"), participant("bob");
plan +!main(list args) {
system.createAgent("alice", "demo.Bidder");
system.createAgent("bob", "demo.Bidder");
system.sleep(200);
!auction("lot1", painting("sunflowers"), configuration(500));
system.sleep(200);
system.exit();
}
}
package demo;
agent Bidder extends astra.protocol.FirstPriceAuction {
initial !monitor_for_auctions();
plan +!generate_bid(string id, funct description, int bid) {
if (system.name() == "alice") bid = 30;
else bid = 25;
}
}
[main] Starting Auction of: lot1
[main] Waiting for bids
[main] received bid: 25 from: bob
[main] received bid: 30 from: alice
[main] Evaluating bids
[main] winner of: lot1 is: alice with bid: 30
[alice] I AM WINNER of: lot1 with bid: 30
The order in which the bids arrive may vary. The protocols can be tested using the unit testing library; the astra-protocols project includes a test for the FIPA Request protocol.