diff --git a/.gitignore b/.gitignore index ea59b02..e36aa47 100644 --- a/.gitignore +++ b/.gitignore @@ -318,3 +318,5 @@ test/ .obsidian/ # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode output.json +docker/mysql/ +docker/server/ diff --git a/README.md b/README.md index 158434e..d081fb3 100644 --- a/README.md +++ b/README.md @@ -52,16 +52,15 @@ One thing that was added in PEAK was the way the user executes the agents. PEAK In this example we will show you how to execute a single agent. Save the following code in a file called `agent.py`. ```python -from peak import Agent -from peak.behaviours import OneShotBehaviour +from peak import Agent, OneShotBehaviour class agent(Agent):     class HelloWorld(OneShotBehaviour): -        async def run(self) -> None: +        async def run(self):             print("Hello World")             await self.agent.stop() -    async def setup(self) -> None: +    async def setup(self):         self.add_behaviour(self.HelloWorld()) ``` It is necessary that the name of the file is the same as the name of the agent's class so PEAK can do the proper parsing. This agent only has a behavior that prints to the terminal the "Hello World" message. To execute the agent just type the following command: @@ -89,11 +88,11 @@ To alert an issue or a bug please post in the >>>>>> main There is the list of options that you can define in the configuration file, inside each agent and in the `defaults` variable: - `file` - source file of the agent @@ -128,10 +120,6 @@ There is the list of options that you can define in the configuration file, insi - `resource` - resource to be used in the JID - `log_level` - logging level of the log file - `clones` - number of clones to be executed -<<<<<<< HEAD -======= -- `properties` - source file of the agent's properties (more on that later) ->>>>>>> main - `verify_security` - if present verifies the SSL certificates ### Thread vs. Process @@ -140,7 +128,6 @@ This section will talk about how to run agents as different threads of the same ## PEAK Communities -<<<<<<< HEAD In PEAK, communities can be seen as groups of agents that share similar goals. Communities are a very useful and efficient way to make communication between three or more agents. What makes this usefull is that for each message sent to the community every member will receive the message. @@ -150,9 +137,6 @@ For this examples you will need to execute a pre-defined PEAK agent called Direc ### Creating a community ([Example 3](https://github.com/gecad-group/peak-mas/tree/main/examples/3_simple_community)) To create a community is very simple. There is a pre defined behavior that enables the agent join communities. For only this functionality you don't need DF, but it is recommended. -======= -The groups are a very useful way to make the communication between more than two agents. To create a group is very simple. There is a pre defined behavior that enables the agent to create and join groups. ->>>>>>> main ```python #agent.py @@ -163,13 +147,8 @@ from peak import Agent, JoinCommunity, LeaveCommunity, Message, OneShotBehaviour class agent(Agent): class HelloWorld(OneShotBehaviour): -<<<<<<< HEAD async def on_start(self): await self.wait_for(JoinCommunity("group1", f"conference.{self.agent.jid.domain}")) -======= - async def on_start(self) -> None: - await self.wait_for(JoinGroup("group1", f"conference.{self.agent.jid.domain}")) ->>>>>>> main async def run(self): msg = Message(to=f"group1@conference.{self.agent.jid.domain}") diff --git a/examples/2_multiagent_system/receiver.py b/examples/2_multiagent_system/receiver.py index 6f9e427..d0e517e 100644 --- a/examples/2_multiagent_system/receiver.py +++ b/examples/2_multiagent_system/receiver.py @@ -4,8 +4,8 @@ class receiver(Agent): class ReceiveHelloWorld(OneShotBehaviour): async def run(self): - while msg := await self.receive(10): - print(f"{msg.sender} sent me a message: '{msg.body}'") + msg = await self.receive() + print(f"{msg.sender} sent me a message: '{msg.body}'") await self.agent.stop() async def setup(self): diff --git a/pyproject.toml b/pyproject.toml index df86d22..ddde98c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "peak-mas" -version = "1.0.7" +version = "1.0.9" description = "Python-based framework for heterogeneous agent communities" readme = "README.md" authors = [{ name = "Bruno Ribeiro", email = "brgri@isep.ipp.pt" }] @@ -42,7 +42,7 @@ Github = "https://github.com/gecad-group/peak-mas" peak = "peak.__main__:main" [tool.bumpver] -current_version = "1.0.7" +current_version = "1.0.9" version_pattern = "MAJOR.MINOR.PATCH" commit_message = "chore: bump version {old_version} -> {new_version}" commit = true diff --git a/src/peak/__init__.py b/src/peak/__init__.py index 7210e16..3ab9f2a 100644 --- a/src/peak/__init__.py +++ b/src/peak/__init__.py @@ -15,9 +15,10 @@ from peak.core import * from peak.agents import * from peak.behaviours import * +from peak.logging import * logging.getLogger(__name__).addHandler(logging.NullHandler()) __author__ = "Bruno Ribeiro" __email__ = "brgri@isep.ipp.pt" -__version__ = "1.0.7" +__version__ = "1.0.9" diff --git a/src/peak/__main__.py b/src/peak/__main__.py index 2a137a7..fa9f924 100644 --- a/src/peak/__main__.py +++ b/src/peak/__main__.py @@ -1,5 +1,4 @@ import logging -import os import sys from argparse import ArgumentParser from pathlib import Path @@ -9,7 +8,6 @@ from peak import __version__ as version from peak.cli import df, mas -sys.path.append(os.getcwd()) _logger = logging.getLogger(peak_name) diff --git a/src/peak/bootloader.py b/src/peak/bootloader.py index 4b21963..d9769f8 100644 --- a/src/peak/bootloader.py +++ b/src/peak/bootloader.py @@ -7,6 +7,7 @@ from multiprocessing import Process from pathlib import Path from typing import List, Type +from peak.logging import FORMATTER from aioxmpp import JID from spade import quit_spade @@ -45,7 +46,7 @@ def boot_agent( file: Path, jid: JID, cid: int, - log_level: int, + log_level: str, verify_security: bool, ): """Configures logging system and boots the agent. @@ -65,9 +66,8 @@ def boot_agent( sys.stdout = open(log_file, "a", buffering=1) sys.stderr = sys.stdout handler = logging.FileHandler(log_file) - handler.setFormatter( - logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") - ) + handler.setFormatter(FORMATTER) + log_level = log_level.upper() logger.parent.handlers = [] logger.parent.addHandler(handler) logger.parent.setLevel(log_level) @@ -75,7 +75,6 @@ def boot_agent( logger.info("Creating agent from file") agent_class = _get_class(file) - os.chdir(file.parent.absolute()) agent_instance = agent_class(jid, cid, verify_security) try: diff --git a/src/peak/cli/mas.py b/src/peak/cli/mas.py index 574a5e7..0b6c40b 100644 --- a/src/peak/cli/mas.py +++ b/src/peak/cli/mas.py @@ -1,6 +1,6 @@ +import sys from argparse import ArgumentTypeError -from logging import getLevelName, getLogger -from os import chdir +from logging import getLogger from pathlib import Path import yaml @@ -14,8 +14,8 @@ def agent_exec( file: Path, jid: JID, - clones: int = 1, - log_level: int = getLevelName("INFO"), + clones: int, + log_level: str, verify_security: bool = False, *args, **kargs, @@ -51,7 +51,7 @@ def agent_exec( bootloader(agents) -def multi_agent_exec(file: Path, log_level, *args, **kargs): +def multi_agent_exec(file: Path, log_level: str, *args, **kargs): """Executes agents using a YAML configuration file. Args: @@ -71,7 +71,7 @@ def multi_agent_exec(file: Path, log_level, *args, **kargs): with file.open() as f: yml = yaml.full_load(f) - chdir(file.parent) + sys.path.append(str(file.parent.absolute())) if "defaults" in yml: defaults = defaults | yml["defaults"] diff --git a/src/peak/core.py b/src/peak/core.py index 561375a..a06131c 100644 --- a/src/peak/core.py +++ b/src/peak/core.py @@ -1,6 +1,7 @@ +import asyncio import logging as _logging from abc import ABCMeta as _ABCMeta -from typing import Dict, List +from typing import Dict, List, Optional import aioxmpp as _aioxmpp import spade as _spade @@ -52,7 +53,7 @@ async def _hook_plugin_after_connection(self): ) -class _Behaviour: +class _BehaviourMixin: """Adds XMPP functinalities to SPADE's base behaviours. Acts as Mixin in the SPADE's behaviours. @@ -63,6 +64,28 @@ class _Behaviour: agent: Agent _logger = _module_logger.getChild("_Behaviour") + async def receive( + self, timeout: Optional[float] = None + ) -> Optional[_spade.message.Message]: + """ + Receives a message for this behaviour and waits `timeout` seconds. + If timeout is `None`, it will wait until it receives a message. + + Note: Redefinition of the receive method of SPADE Behaviours + + Args: + timeout (float, optional): number of seconds until return + + Returns: + Message | None + """ + coro = self.queue.get() + try: + msg = await asyncio.wait_for(coro, timeout=timeout) + except asyncio.TimeoutError: + msg = None + return msg + async def join_community(self, jid: str): """Joins a community. @@ -180,21 +203,23 @@ async def wait_for( class OneShotBehaviour( - _spade.behaviour.OneShotBehaviour, _Behaviour, metaclass=_ABCMeta + _BehaviourMixin, _spade.behaviour.OneShotBehaviour, metaclass=_ABCMeta ): """This behaviour is only executed once.""" class PeriodicBehaviour( - _spade.behaviour.PeriodicBehaviour, _Behaviour, metaclass=_ABCMeta + _BehaviourMixin, _spade.behaviour.PeriodicBehaviour, metaclass=_ABCMeta ): """This behaviour is executed periodically with an interval.""" -class CyclicBehaviour(_spade.behaviour.CyclicBehaviour, _Behaviour, metaclass=_ABCMeta): +class CyclicBehaviour( + _BehaviourMixin, _spade.behaviour.CyclicBehaviour, metaclass=_ABCMeta +): """This behaviour is executed cyclically until it is stopped.""" -class FSMBehaviour(_spade.behaviour.FSMBehaviour, _Behaviour, metaclass=_ABCMeta): +class FSMBehaviour(_BehaviourMixin, _spade.behaviour.FSMBehaviour, metaclass=_ABCMeta): """A behaviour composed of states (oneshotbehaviours) that may transition from one state to another.""" diff --git a/src/peak/logging.py b/src/peak/logging.py new file mode 100644 index 0000000..67eb317 --- /dev/null +++ b/src/peak/logging.py @@ -0,0 +1,21 @@ +import logging +import sys +from typing import Optional + +FORMATTER = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +_logger = logging.getLogger(__name__) +_logger.setLevel(logging.DEBUG) +_handler = logging.StreamHandler(sys.stdout) +_handler.setFormatter(FORMATTER) +_logger.addHandler(_handler) + +def getLogger(name: Optional[str], level: int = logging.INFO): + logger = logging.getLogger(name) + logger.setLevel(level) + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(FORMATTER) + logger.addHandler(handler) + return logger + +def log(message: str, level: int = logging.INFO): + _logger.log(level, message) \ No newline at end of file