Advanced Behaviours

There are more complex types of behaviours that you can use in SPADE. Let’s see some of them.

Periodic Behaviour

This behaviour runs its run() body at a scheduled period. This period is set in seconds. You can also delay the startup of the periodic behaviour by setting a datetime in the start_at parameter.

Warning

Remember to change the example’s jids and passwords by your own accounts. These accounts do not exist and are only for demonstration purposes.

Let’s see an example:

import datetime
import getpass

import spade
from spade.agent import Agent
from spade.behaviour import CyclicBehaviour, PeriodicBehaviour
from spade.message import Message


class PeriodicSenderAgent(Agent):
    class InformBehav(PeriodicBehaviour):
        async def run(self):
            print(f"PeriodicSenderBehaviour running at {datetime.datetime.now().time()}: {self.counter}")
            msg = Message(to=self.get("receiver_jid"))  # Instantiate the message
            msg.body = "Hello World"  # Set the message content

            await self.send(msg)
            print("Message sent!")

            if self.counter == 5:
                self.kill()
            self.counter += 1

        async def on_end(self):
            # stop agent from behaviour
            await self.agent.stop()

        async def on_start(self):
            self.counter = 0

    async def setup(self):
        print(f"PeriodicSenderAgent started at {datetime.datetime.now().time()}")
        start_at = datetime.datetime.now() + datetime.timedelta(seconds=5)
        b = self.InformBehav(period=2, start_at=start_at)
        self.add_behaviour(b)


class ReceiverAgent(Agent):
    class RecvBehav(CyclicBehaviour):
        async def run(self):
            print("RecvBehav running")
            msg = await self.receive(timeout=10)  # wait for a message for 10 seconds
            if msg:
                print("Message received with content: {}".format(msg.body))
            else:
                print("Did not received any message after 10 seconds")
                self.kill()

        async def on_end(self):
            await self.agent.stop()

    async def setup(self):
        print("ReceiverAgent started")
        b = self.RecvBehav()
        self.add_behaviour(b)

async def main():
    receiver_jid = input("Receiver JID> ")
    passwd = getpass.getpass()
    receiveragent = ReceiverAgent(receiver_jid, passwd)

    sender_jid = input("Sender JID> ")
    passwd = getpass.getpass()
    senderagent = PeriodicSenderAgent(sender_jid, passwd)

    await receiveragent.start(auto_register=True)

    senderagent.set("receiver_jid", receiver_jid)  # store receiver_jid in the sender knowledge base
    await senderagent.start(auto_register=True)

    await spade.wait_until_finished(receiveragent)
    await senderagent.stop()
    await receiveragent.stop()
    print("Agents finished")


if __name__ == "__main__":
    spade.run(main())

The output of this code would be similar to:

$ python periodic.py
ReceiverAgent started
RecvBehav running
PeriodicSenderAgent started at 17:40:39.901903
PeriodicSenderBehaviour running at 17:40:45.720227: 0
Message sent!
Message received with content: Hello World
RecvBehav running
PeriodicSenderBehaviour running at 17:40:46.906229: 1
Message sent!
Message received with content: Hello World
RecvBehav running
PeriodicSenderBehaviour running at 17:40:48.906347: 2
Message sent!
Message received with content: Hello World
RecvBehav running
PeriodicSenderBehaviour running at 17:40:50.903576: 3
Message sent!
Message received with content: Hello World
RecvBehav running
PeriodicSenderBehaviour running at 17:40:52.905082: 4
Message sent!
Message received with content: Hello World
RecvBehav running
PeriodicSenderBehaviour running at 17:40:54.904886: 5
Message sent!
Message received with content: Hello World
RecvBehav running
Did not received any message after 10 seconds
Agents finished

TimeoutBehaviour

You can also create a TimeoutBehaviour which is run once (like OneShotBehaviours) but its activation is triggered at a specified datetime just as in PeriodicBehaviours.

Let’s see an example:

import getpass
import datetime
import spade
from spade.agent import Agent
from spade.behaviour import CyclicBehaviour, TimeoutBehaviour
from spade.message import Message


class TimeoutSenderAgent(Agent):
    class InformBehav(TimeoutBehaviour):
        async def run(self):
            print(f"TimeoutSenderBehaviour running at {datetime.datetime.now().time()}")
            msg = Message(to=self.get("receiver_jid"))  # Instantiate the message
            msg.body = "Hello World"  # Set the message content

            await self.send(msg)

        async def on_end(self):
            await self.agent.stop()

    async def setup(self):
        print(f"TimeoutSenderAgent started at {datetime.datetime.now().time()}")
        start_at = datetime.datetime.now() + datetime.timedelta(seconds=5)
        b = self.InformBehav(start_at=start_at)
        self.add_behaviour(b)


class ReceiverAgent(Agent):
    class RecvBehav(CyclicBehaviour):
        async def run(self):
            msg = await self.receive(timeout=10)  # wait for a message for 10 seconds
            if msg:
                print("Message received with content: {}".format(msg.body))
            else:
                print("Did not received any message after 10 seconds")
                self.kill()

        async def on_end(self):
            await self.agent.stop()

    async def setup(self):
        b = self.RecvBehav()
        self.add_behaviour(b)


async def main():
    receiver_jid = input("Receiver JID> ")
    passwd = getpass.getpass()
    receiveragent = ReceiverAgent(receiver_jid, passwd)

    sender_jid = input("Sender JID> ")
    passwd = getpass.getpass()
    senderagent = TimeoutSenderAgent(sender_jid, passwd)

    await receiveragent.start(auto_register=True)

    senderagent.set("receiver_jid", receiver_jid)  # store receiver_jid in the sender knowledge base
    await senderagent.start(auto_register=True)

    await spade.wait_until_finished(receiveragent)
    await senderagent.stop()
    await receiveragent.stop()
    print("Agents finished")

if __name__ == "__main__":
    spade.run(main())

This would produce the following output:

$python timeout.py
TimeoutSenderAgent started at 18:12:09.620316
TimeoutSenderBehaviour running at 18:12:14.625403
Message received with content: Hello World
Did not received any message after 10 seconds
Agents finished

Finite State Machine Behaviour

SPADE agents can also have more complex behaviours which are a finite state machine (FSM) which has registered states and transitions between states. This kind of behaviour allows SPADE agents to build much more complex and interesting behaviours in our agent model.

The FSMBehaviour class is a container behaviour (subclass of CyclicBehaviour) that implements the methods add_state(name, state, initial) and add_transition(source, dest). Every state of the FSM must be registered in the behaviour with a string name and an instance of the State class. This State class represents a node of the FSM and (since it’s a subclass of OneShotBehaviour) you must override the run coroutine just as in a regular behaviour. Since a State is a regular behaviour, you can also override the on_start and on_end coroutines, and, of course, use the send and receive coroutines to be able to interact with other agents via SPADE messaging.

Note

To mark a State as initial state of the FSM set initial parameter to True when calling add_state (add_state(name, state, initial=True)). A FSM can only have ONE initial state, so the initial state will be the last one registered.

Transitions in a FSMBehaviour define from which state to which state it is allowed to transit. A State defines its transit to another state by using the set_next_state method in its run coroutine. By using the set_next_state method a state dinamically expresses to which state it transits when it finishes. After running a state, the FSM reads this next_state value and, if the transition is valid, it transits to that state.

Warning

If the transition is not registered it raises a NotValidTransition exception and the FSM behaviour is finished.

Warning

set_next_state must be called with the same string name with which that state was registered. If the state is not registered a NotValidState exception is raised and the FSM behaviour is finished.

A FSMBehaviour has a unique template, which is shared with all the states of the FSM. You must take this into account when you describe your FSM states, because they will share the same message queue.

Next, we are going to see an example where a very simple FSM is defined, with three states, which transitate from one state to the next one in order. It also sends a message to itself at the first initial state, which is received at the third (and final) state. Also note that the third state is a final state because it does not set a next_state to transit to:

import spade
from spade.agent import Agent
from spade.behaviour import FSMBehaviour, State
from spade.message import Message

STATE_ONE = "STATE_ONE"
STATE_TWO = "STATE_TWO"
STATE_THREE = "STATE_THREE"


class ExampleFSMBehaviour(FSMBehaviour):
    async def on_start(self):
        print(f"FSM starting at initial state {self.current_state}")

    async def on_end(self):
        print(f"FSM finished at state {self.current_state}")
        await self.agent.stop()


class StateOne(State):
    async def run(self):
        print("I'm at state one (initial state)")
        msg = Message(to=str(self.agent.jid))
        msg.body = "msg_from_state_one_to_state_three"
        await self.send(msg)
        self.set_next_state(STATE_TWO)


class StateTwo(State):
    async def run(self):
        print("I'm at state two")
        self.set_next_state(STATE_THREE)


class StateThree(State):
    async def run(self):
        print("I'm at state three (final state)")
        msg = await self.receive(timeout=5)
        print(f"State Three received message {msg.body}")
        # no final state is setted, since this is a final state


class FSMAgent(Agent):
    async def setup(self):
        fsm = ExampleFSMBehaviour()
        fsm.add_state(name=STATE_ONE, state=StateOne(), initial=True)
        fsm.add_state(name=STATE_TWO, state=StateTwo())
        fsm.add_state(name=STATE_THREE, state=StateThree())
        fsm.add_transition(source=STATE_ONE, dest=STATE_TWO)
        fsm.add_transition(source=STATE_TWO, dest=STATE_THREE)
        self.add_behaviour(fsm)


async def main():
    fsmagent = FSMAgent("fsmagent@your_xmpp_server", "your_password")
    await fsmagent.start()

    await spade.wait_until_finished(fsmagent)
    await fsmagent.stop()
    print("Agent finished")

if __name__ == "__main__":
    spade.run(main())

Waiting a Behaviour

Sometimes you may need to wait for a behaviour to finish. In order to make this easy, behaviours provide a method called join. Using this method you can wait for a behaviour to be finished. Be careful, since this is a blocking operation. Example:

import asyncio
import getpass

import spade
from spade.agent import Agent
from spade.behaviour import OneShotBehaviour


class DummyAgent(Agent):
    class LongBehav(OneShotBehaviour):
        async def run(self):
            await asyncio.sleep(5)
            print("Long Behaviour has finished")

    class WaitingBehav(OneShotBehaviour):
        async def run(self):
            await self.agent.behav.join()  # this join must be awaited
            print("Waiting Behaviour has finished")

    async def setup(self):
        print("Agent starting . . .")
        self.behav = self.LongBehav()
        self.add_behaviour(self.behav)
        self.behav2 = self.WaitingBehav()
        self.add_behaviour(self.behav2)


async def main():
    jid = input("JID> ")
    passwd = getpass.getpass()

    dummy = DummyAgent(jid, passwd)
    await dummy.start()

    await dummy.behav2.join()
    print("Stopping agent.")
    await dummy.stop()


if __name__ == "__main__":
    spade.run(main())