Codecs

Most of the codec related code is taken and adapted from aiomas: https://gitlab.com/sscherfke/aiomas/

Codecs enable the container to encode and decode known data types to send them as messages. Mango already contains two codecs: A json serializer that can (recursively) handle any json serializable object and a protobuf codec that will wrap an object into a generic protobuf message. Other codecs can be implemented by inheriting from the Codec base class and implementing its encode and decode methods. Codecs will only handle types explicitely known to them. New known types can be added to a codec with the add_serializer method. This method expects a type together with a serialization method and a deserialization method that translate the object into a format the codec can handle (for example a json-serializable string for the json codec).

Warning

When using the json codec certain types can not be exactly serialized and deserialized between containers. One example are tuple and classes derived from it like namedtuple. The core of the json codec uses pythons json encoder [1] for any type that this encoder can handle by itself. Tuples are translated to json arrays without any further information by this encoder. Consequently, a receiving container will only see a json array and deserialize it to a python list.

[1]: https://docs.python.org/3/library/json.html#json.JSONEncoder

Quickstart

general use

Consider a simple example class we wish to encode as json:

class MyClass:
    def __init__(self, x, y):
        self.x = x
        self._y = y

    @property
    def y(self):
        return self._y

    def __asdict__(self):
        return {"x": self.x, "y": self.y}

    @classmethod
    def __fromdict__(cls, attrs):
        return cls(**attrs)

    @classmethod
    def __serializer__(cls):
        return (cls, cls.__asdict__, cls.__fromdict__)

If we try to encode an object of MyClass without adding a serializer we get an SerializationError:

codec = codecs.JSON()

my_object = MyClass("abc", 123)
encoded = codec.encode(my_object)
python main.py
...
mango.messages.codecs.SerializationError: No serializer found for type "<class '__main__.MyClass'>"

We have to make the type known to the codec to use it:

codec = codecs.JSON()
codec.add_serializer(*MyClass.__serializer__())

my_object = MyClass("abc", 123)
encoded = codec.encode(my_object)
decoded = codec.decode(encoded)

print(my_object.x, my_object.y)
print(decoded.x, decoded.y)
python main.py
abc 123
abc 123

All that is left to do now is to pass our codec to the container. This is done during container creation in the create_container method.

class SimpleReceivingAgent(Agent):
    def __init__(self, container):
        super().__init__(container)

    def handle_message(self, content, meta):
        print(f"{self.aid} received a message with content {content} and meta f{meta}")
        if isinstance(content, MyClass):
            print(content.x)
            print(content.y)


async def main():
    codec = codecs.JSON()
    codec.add_serializer(*MyClass.__serializer__())

    # codecs can be passed directly to the container
    # if no codec is passed a new instance of JSON() is created
    sending_container = await create_container(addr=("localhost", 5556), codec=codec)
    receiving_container = await create_container(addr=("localhost", 5555), codec=codec)
    receiving_agent = SimpleReceivingAgent(receiving_container)

    # agents can now directly pass content of type MyClass to each other
    my_object = MyClass("abc", 123)
    await sending_container.send_acl_message(
        content=my_object, receiver_addr=("localhost", 5555), receiver_id="agent0"
    )

    await receiving_container.shutdown()
    await sending_container.shutdown()


if __name__ == "__main__":
    asyncio.run(main())
python main.py
agent0 received a message with content <__main__.MyClass object at 0x7f42c930edc0> and meta f{'sender_id': None, 'sender_addr': ['localhost', 5556], 'receiver_id': 'agent0', 'receiver_addr': ['localhost', 5555], 'performative': None, 'conversation_id': None, 'reply_by': None, 'in_reply_to': None, 'protocol': None, 'language': None, 'encoding': None, 'ontology': None, 'reply_with': None, 'network_protocol': 'tcp', 'priority': 0}
abc
123

@json_serializable decorator

In the above example we explicitely defined methods to (de)serialize our class. For simple classes, especially data classes, we can achieve the same result (for json codecs) via the @json_serializable decorator. This creates the __asdict__, __fromdict__ and __serializer__ functions in the class:

from mango.messages.codecs import serializable

@json_serializable
class DecoratorData:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

def main():
    codec = codecs.JSON()
    codec.add_serializer(*DecoratorData.__serializer__())

    my_data = DecoratorData(1,2,3)
    encoded = codec.encode(my_data)
    decoded = codec.decode(encoded)

    print(my_data.x, my_data.y, my_data.z)
    print(decoded.x, decoded.y, decoded.z)
python main.py
1 2 3
1 2 3

fast json

Besides the normal full features json codec, which is able to serialize and deserialize messages under preservation of the type information, mango provides the codecs.FastJson codec. This codec usese msgspec and does not provide any type safety. Therefore are also no custom serializer.

proto codec and ACLMessage

Serialization methods for the proto codec are expected to encode the object into a protobuf message object with the SerializeToString method. The codec then wraps the message into a generic message wrapper, containing the serialized protobuf message object and a type id. This is necessary because in general the original type of a protobuf message can not be infered from its serialized form.

The ACLMessage class is encouraged to be used for fipa compliant agent communication. For ease of use it gets specially handled in the protobuf codec: Its content field may contain any proto object known to the codec and gets encoded with the associated type id just like a non-ACL message would be encoded into the generic message wrapper.

Here is an example class implementing a proto serializer for a proto message containing the same fields as the example class:

from msg_pb2 import MyOtherMsg
from mango.messages.message import ACLMessage

class SomeOtherClass:
    def __init__(self, x=1, y='abc', z=None) -> None:
        self.x = x
        self.y = y
        if z is None:
            self.z = {}
        else:
            self.z = z

    def __toproto__(self):
        msg = MyOtherMsg()
        msg.x = self.x
        msg.y = self.y
        msg.z = str(self.z)
        return msg

    @classmethod
    def __fromproto__(cls, data):
        msg = MyOtherMsg()
        msg.ParseFromString(data)
        return cls(msg.x, msg.y, eval(msg.z))

    @classmethod
    def __protoserializer__(cls):
        return cls, cls.__toproto__, cls.__fromproto__

def main():
    codec = codecs.PROTOBUF()
    codec.add_serializer(*SomeOtherClass.__protoserializer__())

    my_object = SomeOtherClass()
    decoded = codec.decode(codec.encode(my_object))

    wrapper = ACLMessage()
    wrapper.content = my_object
    w_decoded = codec.decode(codec.encode(wrapper))

    print(my_object.x, my_object.y, my_object.z)
    print(decoded.x, decoded.y, decoded.z)
    print(
        wrapper_decoded.content.x,
        wrapper_decoded.content.y,
        wrapper_decoded.content.z,
    )
python main.py
1 2 abc123 {1: 'test', 2: 'data', 3: 123}
1 2 abc123 {1: 'test', 2: 'data', 3: 123}
1 2 abc123 {1: 'test', 2: 'data', 3: 123}

In case you want to directly pass proto objects as content to the codec (or as content to the containers send_message) you can shorten this process by making the proto type known to the codec using the register_proto_type function as in this example:

from msg_pb2 import MyMsg

def main():
codec = codecs.PROTOBUF()
codec.register_proto_type(MyMsg)

my_obj = MyMsg()
my_obj.content = b"some_bytes"
encoded = codec.encode(my_obj)
decoded = codec.decode(encoded)

print(my_obj)
print(encoded)
print(decoded)
python main.py
content: "some_bytes"

b'\x08\x01\x12\x0c\x12\nsome_bytes'
content: "some_bytes"