=================== 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 dynamically 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 set, 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())