Designing Agent Programs in ASTRA
This guide aims to walk you through the process of designing and implementing an agent. The type of agent we will develop is purely mental, in that everything that happens occurs in the “mind” of the agent. Specifically, we will build a mental model of a light switch and the attached light. The scenario is taken from the Belief Update example in ASTRA Concepts. In this guide, we will develop rules that allow the agent to manipulate the light, turning it on and off through the use of the lightswitch. The starting point for this guide is the LightSwitch agent program:
agent LightSwitch {
types lightExample {
formula switch(string);
formula light(string);
}
initial switch("up"), light("off");
plan +!light(string X) : light(X) { }
plan +!light("on") {
-light("off");
+light("on");
}
plan +!light("off") {
-light("on");
+light("off");
}
}
As was described at the end of that section, this solution is not really optimal as it does not consider the state of the switch. An alternative solution is to replace the rules to handle the !light(...) goals with a different rule:
plan +!light("on") {
!flipSwitch();
}
Unlike the !light(...) goal, this subgoal is not a declarative goal. We could have used the declarative goal !switch("down"), but this assumes that the down position on the switch means “off”, but depending on the wiring, it could also mean on, or could be non-deterministic if there are two light switches… Flipping the switch is a more appropriate abstraction of the goal as it means that the switch should change from one state to the other.
Before moving on, its worth noting that, when we put the two rules together, we can simplify them a little:
plan +!light("on") : light("on") {}
plan +!light("on") {
!flipSwitch();
}
Notice that we have dropped the context on the second rule. This is because ASTRA (and AgentSpeak(L)) use rule order to select which rule to apply to the given event. When overloading rules in this way, you can think of the last rule as representing the default behaviour and earlier rules representing specific cases that override the default behaviour.
For the flipped switch goal, what we want to do is to capture the following: if the switch is down, then move it up. Alternatively, if the switch is up, move it down. We can capture this through two rules in the same way that we did with the light on:
plan +!flipSwitch() : switch("down") {
!switch("up");
}
plan +!flipSwitch() {
!switch("down");
}
This is the ASTRA equivalement of an if-else statement where we have one rule for each switch state. An alternative approach would be to model the transitions of the switch and to write a single general rule. This requires a little more work, but it is a nice solution. First, we need to add a belief to model a transition. Lets use:
types lightExample {
...
formula switchTransition(string, string); // first arg is current state, second arg is target state.
}
Now, we can use this belief template to create beliefs that map the transition:
initial switchTransition("down", "up"), switchTransition("up", "down");
This leads to a new !flipSwitch(...) rule:
plan +!flipSwitch() : switch(string X) & switchTransition(X, string Y) {
!switch(Y);
}
This new rule basically says: if we want to flip the switch and we know it is in position X and we know that X transitions to Y, then adopt a goal to set the switch to position Y.
The last part then is to deal with the !switch(...) goal. This goal is similar to the flip switch goal, but instead of resulting in a subgoal, results in a changing of the agents state:
// if the switch is already in state X, do nothing
plan +!switch(string X) : switch(X) {}
// if the switch is not is state X, change it to state X
plan +!switch(string X): switch(string Y) & switchTransition(Y, X) {
-switch(Y);
+switch(X);
}
This rule causes the switch belief to be updated to reflect the changing of the switch state, but there is an indirect impact on the state of the light - it must turn on or off respectively. We can use a similar rule to theone we used to handle the !flipSwitch() goal:
types {
...
formula lightTransition(string, string);
}
initial lightTransition("off", "on"), lightTransition("on", "off");
plan +switch(string X) : light(string Y) & lightTransition(Y, string Z) {
-light(Y);
+light(Z);
}
Informally, the rule states: if the state of the switch changes and the state of the light is Y, and you know that Y transitions to Z, update the state of the switch from Y to Z.
Finally, lets put all this together into a single program:
agent LightSwitch {
types lightExample {
formula switch(string); // the argument should be "up" or "down"
formula light(string); // the argument should be "on" or "off"
formula switchTransition(string, string); // first arg is current state, second arg is target state.
formula lightTransition(string, string);
}
initial lightTransition("off", "on"), lightTransition("on", "off");
initial switchTransition("down", "up"), switchTransition("up", "down");
initial switch("up"), light("off");
plan +!light(string X) : light(X) { }
plan +!light(string X) {
!flipSwitch();
}
plan +!flipSwitch() : switch(string X) & switchTransition(X, string Y) {
!switch(Y);
}
// if the switch is already in state X, do nothing
plan +!switch(string X) : switch(X) {}
// if the switch is not is state X, change it to state X
plan +!switch(string X): switch(string Y) & switchTransition(Y, X) {
-switch(Y);
+switch(X);
}
plan +switch(string X) : light(string Y) & lightTransition(Y, string Z) {
-light(Y);
+light(Z);
}
}
There are some obvious questions about the above code:
How does the agent know when to change the light?
The agent changes its belief about the lights state, but how do we link it to an actual light?
Both of these questions relate to the same issue - how to link the agent to external systems. In ASTRA, we do this through the use of Modules. In fact, we would not normally model the relationship between the switch and the light. This information would be hidden behind the module. Normally, an agent performs actions that affect their environment. They then observe the effect of that action on the environment. How to implement modules is discussed in the section on creating your own modules.