Skip to content

Commit

Permalink
Develop (#11)
Browse files Browse the repository at this point in the history
* fix: import sys.path not including folder of yaml config file

* chore: bump version 1.0.7 -> 1.0.8

* chore: add folders to gitignore

* fix: xmpp server docker

* fix: add docker folders to gitignore

* fix: logger in yaml config parser

* fix: receive method in behaviours from returning instantly when timeout is None

* docs: corrected example 2

* chore: format using black and isort

* chore: bump version 1.0.8 -> 1.0.9

* docs: add readme to docker folder

* docs: simplify main readme example

* feat: add logging module to the framework to facilitate logging

* docs: corrected uncorrected merge
  • Loading branch information
brunus-reberes authored May 24, 2023
1 parent e405d31 commit 89b5748
Show file tree
Hide file tree
Showing 14 changed files with 95 additions and 63 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,5 @@ test/
.obsidian/
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
output.json
docker/mysql/
docker/server/
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -89,11 +88,11 @@ To alert an issue or a bug please post in the <a href="https://github.com/gecad-
## Roadmap

This are some functionalities that are being developed and will be released in a near future:
- [ ] Create a Docker for XMPP server and PEAK.
- [ ] Integrate FIPA ACL messages in PEAK.
- [ ] Add dynamic speed option to PEAK's internal clock.
- [ ] Add multi-threading option to the execution configurations.
- [ ] Implement Yellow Page Service in DF agent.
- [ ] Implement Data Analysis section in the Dashboard.
- [ ] Add multi-threading option to the Command Line Interface.
- [ ] Implement Yellow Page Service in the Directory Facilitator agent.
- [ ] Implement physical mobility in the agents.

## Scientific Publications

Expand Down
4 changes: 4 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Docker for the XMPP server

This docker is only for the XMPP server and the Directory Facilitator agent. The only files missing are the certificates that must be in the folder `server/config/certs`.
To create self-assign certificates you can use OpenSSL to do it. The name of the certificates must be `localhost.key` and `localhost.crt`.
2 changes: 2 additions & 0 deletions docker/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ services:
depends_on:
- server
build: .
links:
- server:localhost
ports:
- 10000:10000
restart: unless-stopped
22 changes: 12 additions & 10 deletions docker/server/config/prosody.cfg.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use_libevent = true

-- Paths for community models:
plugin_paths = {
"/usr/local/lib/prosody/modules";
"/usr/lib/prosody/modules-community";
}

-- This is the list of modules Prosody will load on startup.
Expand All @@ -27,16 +27,16 @@ modules_enabled = {
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
"dialback"; -- s2s dialback support
--"dialback"; -- s2s dialback support
"disco"; -- Service discovery

-- Not essential, but recommended
"carbons"; -- Keep multiple clients in sync
"pep"; -- Enables users to publish their avatar, mood, activity, playing music and more
--"carbons"; -- Keep multiple clients in sync
--"pep"; -- Enables users to publish their avatar, mood, activity, playing music and more
"private"; -- Private XML storage (for room bookmarks, etc.)
"blocklist"; -- Allow users to block communications with other users
"vcard4"; -- User profiles (stored in PEP)
"vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard
--"vcard4"; -- User profiles (stored in PEP)
--"vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard

-- Nice to have
"version"; -- Replies to server version requests
Expand Down Expand Up @@ -79,10 +79,12 @@ authentication = "internal_hashed"
-- Logs errors to syslog also

log = {
-- Log files (change 'info' to 'debug' for debug logs):
{ levels = { "info" }; to = "console"; };
-- Syslog:
{ levels = { "error" }; to = "syslog"; };
{levels = { min = "info" }, to="console"};
-- Log all error messages to prosody.err
{ levels = { min = "error" }, to = "file", filename = "/var/log/prosody/prosody.err" };
-- Log everything of level "info" and higher (that is, all except "debug" messages)
-- to prosody.log
{ levels = { min = "debug" }, to = "file", filename = "/var/log/prosody/prosody.log" };
}

certificates = "certs"
Expand Down
21 changes: 0 additions & 21 deletions docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,30 +108,18 @@ Let's create two agents one that sends the a message, the `sender.py`, and one t
$ peak start mas.yaml
```
<<<<<<< HEAD
So, what happened? Two agents were created. One called `john@localhost` and the other called `harry@localhost`. `john` sent a `Hello World` to `harry` and `harry` printed it out.

The way it works is simple. You can only define two root variables, the `defaults` and the `agents`. The `defaults` is used to define parameters to be applied to all agents. The `agents` variable defines the list of agents to be executed and their individual parameters. Type `peak start -h` on the terminal to see the list of available parameters.

In this case we are defining, in the `defaults`, the default domain as `localhost` for all agents. In `agents` variable, we are defining two different types of agents, the `john` and the `harry`. In both agents we are defining their source file. The `agents` parameters will override the `defaults` parameters if they are the same.
=======
So, what happened? Two agents were created. One called `john@localhost` and the other called `harry@localhost`. `john` sent a message `Hello World` to `harry` and `harry` printed it out. The log file of `jonh` was in logging level `DEBUG`, and `harry`'s file was in level `INFO`.

The way it works is simple. You can only define two root variables, the `defaults` and the `agents`. The `defaults` is used to define parameters to be applied to all agents. The `agents` variable defines the list of agents to be executed and their respective parameters. The parameters available in `defaults` and in the agents of the variable `agents` can be seen using the `-h` argument in the `peak run` command.

In this case we are defining, in the `defaults`, the default domain as `localhost` and the default logging level as `debug` for all agents. In `agents` variable, we are defining two different types of agents, the `john` and the `harry`. In `john` we are defining the agents source file. In `harry` we are defining the source file and the logging level, overriding the default value.
>>>>>>> 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
- `domain` - domain of the server to be used for the agent's connection
- `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
Expand All @@ -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.

<img src="peak_communities.png" height="300">
Expand All @@ -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
Expand All @@ -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}")
Expand Down
4 changes: 2 additions & 2 deletions examples/2_multiagent_system/receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]" }]
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/peak/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = "[email protected]"
__version__ = "1.0.7"
__version__ = "1.0.9"
2 changes: 0 additions & 2 deletions src/peak/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
import os
import sys
from argparse import ArgumentParser
from pathlib import Path
Expand All @@ -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)


Expand Down
9 changes: 4 additions & 5 deletions src/peak/bootloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -65,17 +66,15 @@ 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)
handler.setLevel(log_level)

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:
Expand Down
12 changes: 6 additions & 6 deletions src/peak/cli/mas.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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"]
Expand Down
37 changes: 31 additions & 6 deletions src/peak/core.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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."""
21 changes: 21 additions & 0 deletions src/peak/logging.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 89b5748

Please sign in to comment.